From 4df4edbd4c6f749bee82cb8ecfde177b9a97d4bc Mon Sep 17 00:00:00 2001 From: "Arambillete, Wilman" Date: Wed, 21 May 2025 13:54:45 -0300 Subject: [PATCH 1/3] Added a new feature to the CLI workflow that allows users to create a scaffold for SPAs Signed-off-by: Arambillete, Wilman --- package-lock.json | 42 +-- packages/node-cli/messages.json | 17 +- .../project/create/CreateProjectAction.js | 232 +++++++++++++- .../create/CreateProjectInputHandler.js | 32 +- .../node-cli/src/services/TranslationKeys.js | 13 +- .../actionresult/CreateProjectActionResult.js | 22 ++ packages/node-cli/src/suitecloud.js | 4 +- .../node-cli/src/templates/TemplateKeys.js | 23 +- .../custspa_projectname.xml.template | 13 + .../spaproject/eslint.config.mjs.template | 297 ++++++++++++++++++ .../spaproject/gulpfile.mjs.template | 229 ++++++++++++++ .../spaproject/helloworld.tsx.template | 15 + .../spaproject/package.json.template | 45 +++ .../spaproject/spaclient.tsx.template | 6 + .../spaproject/spaserver.ts.template | 6 + .../spaproject/tsconfig.json.template | 29 ++ .../spaproject/tsconfig.test.json.template | 6 + 17 files changed, 975 insertions(+), 56 deletions(-) create mode 100644 packages/node-cli/src/templates/spaproject/custspa_projectname.xml.template create mode 100644 packages/node-cli/src/templates/spaproject/eslint.config.mjs.template create mode 100644 packages/node-cli/src/templates/spaproject/gulpfile.mjs.template create mode 100644 packages/node-cli/src/templates/spaproject/helloworld.tsx.template create mode 100644 packages/node-cli/src/templates/spaproject/package.json.template create mode 100644 packages/node-cli/src/templates/spaproject/spaclient.tsx.template create mode 100644 packages/node-cli/src/templates/spaproject/spaserver.ts.template create mode 100644 packages/node-cli/src/templates/spaproject/tsconfig.json.template create mode 100644 packages/node-cli/src/templates/spaproject/tsconfig.test.json.template diff --git a/package-lock.json b/package-lock.json index 76681568..fcfd0730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -308,23 +308,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", - "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.5" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1487,9 +1487,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1498,13 +1498,13 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1528,9 +1528,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", - "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" diff --git a/packages/node-cli/messages.json b/packages/node-cli/messages.json index 57c689d7..28ff169e 100644 --- a/packages/node-cli/messages.json +++ b/packages/node-cli/messages.json @@ -44,13 +44,6 @@ "COMMAND_CREATEFILE_SELECT_FOLDER": "Select the folder where you want to create the SuiteScript file", "COMMAND_CREATEFILE_SELECT_SUITESCRIPT_MODULES": "Select the SuiteScript modules you want to add to the SuiteScript file", - "COMMAND_CREATEPROJECT_QUESTIONS_CHOOSE_PROJECT_TYPE": "Select the project type you want to create.", - "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_ID": "Enter the project ID.", - "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_NAME": "Enter the project name.", - "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_VERSION": "Enter the project version.", - "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PUBLISHER_ID": "Enter the publisher ID.", - "COMMAND_CREATEPROJECT_QUESTIONS_INCLUDE_UNIT_TESTING": "Do you want to include unit testing with the Jest testing framework? ***This will install NPM module dependencies.", - "COMMAND_CREATEPROJECT_QUESTIONS_OVERWRITE_PROJECT": "Do you want to overwrite the {0} project? All the files and folders from the former project will be deleted and replaced by the new project.", "COMMAND_CREATEPROJECT_MESSAGES_CREATING_PROJECT_STRUCTURE": "Creating the project structure...", "COMMAND_CREATEPROJECT_MESSAGES_INIT_NPM_DEPENDENCIES": "Initializing npm dependencies for the testing environment...", "COMMAND_CREATEPROJECT_MESSAGES_INIT_NPM_DEPENDENCIES_FAILED": "There was an error when installing npm dependencies. Check the npm log and run \"npm install\" again inside of the project you created.", @@ -60,7 +53,17 @@ "COMMAND_CREATEPROJECT_MESSAGES_PROJECT_CREATED": "The {0} project was created successfully.", "COMMAND_CREATEPROJECT_MESSAGES_PROJECT_CREATION_CANCELED": "The creation process has been canceled.", "COMMAND_CREATEPROJECT_MESSAGES_SAMPLE_UNIT_TEST_ADDED": "A sample unit test has been added in the \"__tests__\" folder of your project.", + "COMMAND_CREATEPROJECT_MESSAGES_SETUP_SPA_PROJECT": "Setting up the SPA project...", "COMMAND_CREATEPROJECT_MESSAGES_SETUP_TEST_ENV": "Setting up the testing environment...", + "COMMAND_CREATEPROJECT_QUESTIONS_CHOOSE_PROJECT_TYPE": "Select the project type you want to create.", + "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_ID": "Enter the project ID.", + "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_NAME": "Enter the project name.", + "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_VERSION": "Enter the project version.", + "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PUBLISHER_ID": "Enter the publisher ID.", + "COMMAND_CREATEPROJECT_QUESTIONS_ENTER_SPA_PROJECT_NAME": "Enter the SPA project name.", + "COMMAND_CREATEPROJECT_QUESTIONS_CREATE_SPA": "Do you want to create an SPA project? ***This will install NPM module dependencies and create needed files and folders.", + "COMMAND_CREATEPROJECT_QUESTIONS_INCLUDE_UNIT_TESTING": "Do you want to include unit testing with the Jest testing framework? ***This will install NPM module dependencies.", + "COMMAND_CREATEPROJECT_QUESTIONS_OVERWRITE_PROJECT": "Do you want to overwrite the {0} project? All the files and folders from the former project will be deleted and replaced by the new project.", "COMMAND_DEPLOY_ERRORS_APPLY_INSTALLATION_PREFERENCES_IN_ACP": "Installation preferences cannot be applied in an Account Customization Project.", "COMMAND_DEPLOY_ERRORS_WRONG_ACCOUNT_SPECIFIC_VALUES_OPTION": "You have specified an invalid value for the \"--accountspecficvalues\" option. Enter either WARNING or ERROR.", diff --git a/packages/node-cli/src/commands/project/create/CreateProjectAction.js b/packages/node-cli/src/commands/project/create/CreateProjectAction.js index f452dce5..ef2a9d74 100644 --- a/packages/node-cli/src/commands/project/create/CreateProjectAction.js +++ b/packages/node-cli/src/commands/project/create/CreateProjectAction.js @@ -41,8 +41,41 @@ const PACKAGE_JSON_DEFAULT_VERSION = '1.0.0'; const PACKAGE_JSON_REPLACE_STRING_VERSION = '{{version}}'; const SOURCE_FOLDER = 'src'; +const OBJECTS_FOLDER = 'Objects'; const UNIT_TEST_TEST_FOLDER = '__tests__'; +const SPA_SUITEAPPS_FOLDER = 'SuiteApps'; +const SPA_ASSETS_FOLDER = 'assets'; +const SPA_PROJECT_NAME_REPLACE_STRING = '{{projectName}}'; +const SPA_PROJECT_PATH_REPLACE_STRING = '{{projectPath}}'; +const SPA_SPA_CLIENT_TEMPLATE_KEY = 'spaclient'; +const SPA_SPA_CLIENT_FILENAME = 'SpaClient'; +const SPA_SPA_CLIENT_EXTENSION = 'tsx'; +const SPA_SPA_SERVER_TEMPLATE_KEY = 'spaserver'; +const SPA_SPA_SERVER_FILENAME = 'SpaServer'; +const SPA_SPA_SERVER_EXTENSION = 'ts'; +const SPA_HELLO_WORLD_TEMPLATE_KEY = 'helloworld'; +const SPA_HELLO_WORLD_FILENAME = 'HelloWorld'; +const SPA_HELLO_WORLD_EXTENSION = 'tsx'; +const SPA_CUSTSPA_TEMPLATE_KEY = 'custspa'; +const SPA_CUSTSPA_FILENAME = 'custspa_'; // this is the prefix for the file name +const SPA_CUSTSPA_EXTENSION = 'xml'; +const SPA_GULPFILE_TEMPLATE_KEY = 'gulpfile'; +const SPA_GULPFILE_FILENAME = 'gulpfile'; +const SPA_GULPFILE_EXTENSION = 'mjs'; +const SPA_ESLINT_TEMPLATE_KEY = 'eslint'; +const SPA_ESLINT_FILENAME = 'eslint.config'; +const SPA_ESLINT_EXTENSION = 'mjs'; +const SPA_TS_CONFIG_TEMPLATE_KEY = 'tsconfig'; +const SPA_TS_CONFIG_FILENAME = 'tsconfig'; +const SPA_TS_CONFIG_EXTENSION = 'json'; +const SPA_TS_CONFIG_TEMPLATE_KEY_TEST = 'tsconfigtest'; +const SPA_TS_CONFIG_FILENAME_TEST = 'tsconfig.test'; +const SPA_TS_CONFIG_EXTENSION_TEST = 'json'; +const SPA_PACKAGE_TEMPLATE_KEY = 'package'; +const SPA_PACKAGE_FILENAME = 'package'; +const SPA_PACKAGE_EXTENSION = 'json'; + const CLI_CONFIG_TEMPLATE_KEY = 'cliconfig'; const GITIGNORE_TEMPLATE_KEY = 'gitignore'; const CLI_CONFIG_FILENAME = 'suitecloud.config'; @@ -65,15 +98,17 @@ const UNIT_TEST_JSCONFIG_FILENAME = 'jsconfig'; const UNIT_TEST_JSCONFIG_EXTENSION = 'json'; const COMMAND_OPTIONS = { + CREATE_SPA: 'createspa', + INCLUDE_UNIT_TESTING: 'includeunittesting', OVERWRITE: 'overwrite', PARENT_DIRECTORY: 'parentdirectory', + PROJECT_FOLDER_NAME: 'projectfoldername', PROJECT_ID: 'projectid', PROJECT_NAME: 'projectname', PROJECT_VERSION: 'projectversion', PUBLISHER_ID: 'publisherid', + SPA_PROJECT_NAME: 'spaprojectname', TYPE: 'type', - INCLUDE_UNIT_TESTING: 'includeunittesting', - PROJECT_FOLDER_NAME: 'projectfoldername', }; module.exports = class CreateProjectAction extends BaseAction { @@ -138,27 +173,42 @@ module.exports = class CreateProjectAction extends BaseAction { const projectName = params[COMMAND_OPTIONS.PROJECT_NAME]; const includeUnitTesting = this._getIncludeUnitTestingBoolean(params[COMMAND_OPTIONS.INCLUDE_UNIT_TESTING]); + const createSpa = this._getCreateSpaBoolean(params[COMMAND_OPTIONS.CREATE_SPA]); + const spaProjectName = params[COMMAND_OPTIONS.SPA_PROJECT_NAME]; + //fixing project name for not interactive output before building results const commandParameters = { ...createProjectParams, [`${COMMAND_OPTIONS.PROJECT_NAME}`]: params[COMMAND_OPTIONS.PROJECT_NAME] }; return createProjectActionData.operationResult.status === SdkOperationResultUtils.STATUS.SUCCESS ? CreateProjectActionResult.Builder.withData(createProjectActionData.operationResult.data) - .withResultMessage(createProjectActionData.operationResult.resultMessage) - .withProjectType(projectType) - .withProjectName(projectName) - .withProjectDirectory(createProjectActionData.projectDirectory) - .withUnitTesting(includeUnitTesting) - .withNpmPackageInitialized(createProjectActionData.npmInstallSuccess) - .withCommandParameters(commandParameters) - .build() + .withResultMessage(createProjectActionData.operationResult.resultMessage) + .withProjectType(projectType) + .withProjectName(projectName) + .withProjectDirectory(createProjectActionData.projectDirectory) + .withUnitTesting(includeUnitTesting) + .withSpaProject(createSpa) + .withSpaProjectName(spaProjectName) + .withNpmPackageInitialized(createProjectActionData.npmInstallSuccess) + .withCommandParameters(commandParameters) + .build() : CreateProjectActionResult.Builder.withErrors(createProjectActionData.operationResult.errorMessages) - .withCommandParameters(commandParameters) - .build(); + .withCommandParameters(commandParameters) + .build(); } catch (error) { return CreateProjectActionResult.Builder.withErrors([unwrapExceptionMessage(error)]).build(); } } + withIncludeSpa(includeSpa) { + this.includeSpa = includeSpa; + return this; + } + + withSpaProjectName(spaProjectName) { + this.spaProjectName = spaProjectName; + return this; + } + createProject(executionContextCreateProject, params, projectAbsolutePath, projectFolderName, manifestFilePath) { return async (resolve, reject) => { try { @@ -185,6 +235,17 @@ module.exports = class CreateProjectAction extends BaseAction { } this._fileSystemService.replaceStringInFile(manifestFilePath, SOURCE_FOLDER, params[COMMAND_OPTIONS.PROJECT_NAME]); let npmInstallSuccess; + + //SPA + const createSpa = this._getCreateSpaBoolean(params[COMMAND_OPTIONS.CREATE_SPA]); + if (createSpa) { + this._log.info(NodeTranslationService.getMessage(MESSAGES.SETUP_SPA_PROJECT)); + await this._createSpaFiles(params[COMMAND_OPTIONS.SPA_PROJECT_NAME], projectAbsolutePath); + // this._log.info(NodeTranslationService.getMessage(MESSAGES.INIT_NPM_DEPENDENCIES)); + // npmInstallSuccess = await this._runNpmInstall(this._getSpaProjectFolderSource(projectAbsolutePath)); + } + + //Unit Testing let includeUnitTesting = this._getIncludeUnitTestingBoolean(params[COMMAND_OPTIONS.INCLUDE_UNIT_TESTING]); if (includeUnitTesting) { this._log.info(NodeTranslationService.getMessage(MESSAGES.SETUP_TEST_ENV)); @@ -238,6 +299,143 @@ module.exports = class CreateProjectAction extends BaseAction { } } + _getCreateSpaBoolean(createSpaParam) { + return typeof createSpaParam === 'string' ? createSpaParam.toLowerCase() === 'true' : Boolean(createSpaParam); + } + + _getSpaProjectFolderSource(projectAbsolutePath) { + return path.join(projectAbsolutePath, SOURCE_FOLDER); + } + + _getSpaProjectFolderSourceObjects(projectAbsolutePath) { + return path.join(projectAbsolutePath, SOURCE_FOLDER, OBJECTS_FOLDER); + } + + _getSpaProjectFolderSuiteApps(projectAbsolutePath) { + return path.join(projectAbsolutePath, SOURCE_FOLDER, SPA_SUITEAPPS_FOLDER); + } + + _getSpaProjectFolderProjectName(projectAbsolutePath, projectName) { + return path.join(projectAbsolutePath, SOURCE_FOLDER, SPA_SUITEAPPS_FOLDER, projectName); + } + + async _createSpaFiles(projectName, projectAbsolutePath) { + const spaProjectFolderSource = this._getSpaProjectFolderSource(projectAbsolutePath); + const spaProjectFolderProjectName = this._getSpaProjectFolderProjectName(projectAbsolutePath, projectName); + const spaProjectFolderSourceObjects = this._getSpaProjectFolderSourceObjects(projectAbsolutePath); + + await this._createSpaFolders(projectName, projectAbsolutePath); + await this._createSpaClientFile(spaProjectFolderProjectName); + await this._createSpaServerFile(spaProjectFolderProjectName); + await this._createHelloWorldFile(spaProjectFolderProjectName); + await this._createCustspaFile(projectName, spaProjectFolderSourceObjects); + await this._createGulpFile(spaProjectFolderSource); + await this._createEslintFile(spaProjectFolderSource); + await this._createTsConfigFile(spaProjectFolderSource); + await this._createTsConfigTestFile(spaProjectFolderSource); + await this._createPackageFile(projectName, spaProjectFolderSource); + } + + async _createSpaFolders(projectName, projectAbsolutePath) { + const spaProjectFolderSource = this._getSpaProjectFolderSource(projectAbsolutePath); + const spaProjectFolderSuiteApps = this._getSpaProjectFolderSuiteApps(projectAbsolutePath); + const spaProjectFolderProjectName = this._getSpaProjectFolderProjectName(projectAbsolutePath, projectName); + + await this._fileSystemService.createFolder(spaProjectFolderSource, SPA_SUITEAPPS_FOLDER); //SuiteApps folder + await this._fileSystemService.createFolder(spaProjectFolderSuiteApps, projectName); //Project Name folder + await this._fileSystemService.createFolder(spaProjectFolderProjectName, SPA_ASSETS_FOLDER); //Assets folder + } + + async _createSpaClientFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_SPA_CLIENT_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_SPA_CLIENT_FILENAME, + fileExtension: SPA_SPA_CLIENT_EXTENSION, + }); + } + + async _createSpaServerFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_SPA_SERVER_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_SPA_SERVER_FILENAME, + fileExtension: SPA_SPA_SERVER_EXTENSION, + }); + } + + async _createHelloWorldFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_HELLO_WORLD_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_HELLO_WORLD_FILENAME, + fileExtension: SPA_HELLO_WORLD_EXTENSION, + }); + } + + async _createCustspaFile(projectName, projectAbsolutePath) { + const spaProjectFolderProjectName = '/' + SPA_SUITEAPPS_FOLDER; + + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_CUSTSPA_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_CUSTSPA_FILENAME + projectName, + fileExtension: SPA_CUSTSPA_EXTENSION, + }); + + const custSpaFilePath = path.join(projectAbsolutePath, SPA_CUSTSPA_FILENAME + projectName + '.' + SPA_CUSTSPA_EXTENSION); + await this._fileSystemService.replaceStringInFile(custSpaFilePath, SPA_PROJECT_NAME_REPLACE_STRING, projectName); + await this._fileSystemService.replaceStringInFile(custSpaFilePath, SPA_PROJECT_PATH_REPLACE_STRING, spaProjectFolderProjectName); + } + + async _createGulpFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_GULPFILE_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_GULPFILE_FILENAME, + fileExtension: SPA_GULPFILE_EXTENSION, + }); + } + + async _createEslintFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_ESLINT_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_ESLINT_FILENAME, + fileExtension: SPA_ESLINT_EXTENSION, + }); + } + + async _createTsConfigFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_TS_CONFIG_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_TS_CONFIG_FILENAME, + fileExtension: SPA_TS_CONFIG_EXTENSION, + }); + } + + async _createTsConfigTestFile(projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_TS_CONFIG_TEMPLATE_KEY_TEST], + destinationFolder: projectAbsolutePath, + fileName: SPA_TS_CONFIG_FILENAME_TEST, + fileExtension: SPA_TS_CONFIG_EXTENSION_TEST, + }); + } + + async _createPackageFile(projectName, projectAbsolutePath) { + await this._fileSystemService.createFileFromTemplate({ + template: TemplateKeys.SPA_PROJECT[SPA_PACKAGE_TEMPLATE_KEY], + destinationFolder: projectAbsolutePath, + fileName: SPA_PACKAGE_FILENAME, + fileExtension: SPA_PACKAGE_EXTENSION, + }); + + const packageJsonFilePath = path.join(projectAbsolutePath, SPA_PACKAGE_FILENAME + '.' + SPA_PACKAGE_EXTENSION); + await this._fileSystemService.replaceStringInFile(packageJsonFilePath, SPA_PROJECT_NAME_REPLACE_STRING, projectName); + } + async _createUnitTestFiles(type, projectName, projectVersion, projectAbsolutePath) { await this._createUnitTestCliConfigFile(projectAbsolutePath); await this._createUnitTestPackageJsonFile(type, projectName, projectVersion, projectAbsolutePath); @@ -324,6 +522,16 @@ module.exports = class CreateProjectAction extends BaseAction { }); } + _createMyCustomFolder(params) { + this._log.info('Creating custom folder...'); + this._log.info('parent directory: ' + params[COMMAND_OPTIONS.PARENT_DIRECTORY]); + // if (params.withcustomfolder) { + const customFolderPath = path.join(params[COMMAND_OPTIONS.PARENT_DIRECTORY], 'mycustomfolder'); + this._fileSystemService.createFolderFromAbsolutePath(customFolderPath); + this._log.info('✅ Created custom folder: ' + customFolderPath); + // } + } + async _runNpmInstall(projectAbsolutePath) { try { await NpmInstallRunner.run(projectAbsolutePath); diff --git a/packages/node-cli/src/commands/project/create/CreateProjectInputHandler.js b/packages/node-cli/src/commands/project/create/CreateProjectInputHandler.js index 22fbf0fd..41b8a4b2 100644 --- a/packages/node-cli/src/commands/project/create/CreateProjectInputHandler.js +++ b/packages/node-cli/src/commands/project/create/CreateProjectInputHandler.js @@ -30,16 +30,18 @@ const { } = require('../../../validation/InteractiveAnswersValidator'); const COMMAND_OPTIONS = { + CREATE_SPA: 'createspa', + INCLUDE_UNIT_TESTING: 'includeunittesting', OVERWRITE: 'overwrite', PARENT_DIRECTORY: 'parentdirectory', + PROJECT_ABSOLUTE_PATH: 'projectabsolutepath', + PROJECT_FOLDER_NAME: 'projectfoldername', PROJECT_ID: 'projectid', PROJECT_NAME: 'projectname', PROJECT_VERSION: 'projectversion', PUBLISHER_ID: 'publisherid', + SPA_PROJECT_NAME: 'spaprojectname', TYPE: 'type', - INCLUDE_UNIT_TESTING: 'includeunittesting', - PROJECT_ABSOLUTE_PATH: 'projectabsolutepath', - PROJECT_FOLDER_NAME: 'projectfoldername', }; const ACP_PROJECT_TYPE_DISPLAY = 'Account Customization Project'; @@ -118,6 +120,28 @@ module.exports = class CreateObjectInputHandler extends BaseInputHandler { { name: NodeTranslationService.getMessage(NO), value: false }, ], }, + { + type: CommandUtils.INQUIRER_TYPES.LIST, + name: COMMAND_OPTIONS.CREATE_SPA, + message: NodeTranslationService.getMessage(QUESTIONS.CREATE_SPA), + default: 0, + choices: [ + { name: NodeTranslationService.getMessage(YES), value: true }, + { name: NodeTranslationService.getMessage(NO), value: false }, + ], + }, + { + when: function (response) { + return response[COMMAND_OPTIONS.CREATE_SPA]; + }, + type: CommandUtils.INQUIRER_TYPES.INPUT, + name: COMMAND_OPTIONS.SPA_PROJECT_NAME, + message: NodeTranslationService.getMessage(QUESTIONS.ENTER_SPA_PROJECT_NAME), + validate: (fieldValue) => + showValidationResults(fieldValue, validateFieldIsNotEmpty, validateFieldHasNoSpaces, (fieldValue) => + validateFieldIsLowerCase(COMMAND_OPTIONS.SPA_PROJECT_NAME, fieldValue) + ), + }, ]); const projectFolderName = this._getProjectFolderName(answers); @@ -156,7 +180,7 @@ module.exports = class CreateObjectInputHandler extends BaseInputHandler { _getProjectFolderName(params) { switch (params[COMMAND_OPTIONS.TYPE]) { case ApplicationConstants.PROJECT_SUITEAPP: - return (params[COMMAND_OPTIONS.PUBLISHER_ID] && params[COMMAND_OPTIONS.PROJECT_ID]) + return params[COMMAND_OPTIONS.PUBLISHER_ID] && params[COMMAND_OPTIONS.PROJECT_ID] ? params[COMMAND_OPTIONS.PUBLISHER_ID] + '.' + params[COMMAND_OPTIONS.PROJECT_ID] : 'not_specified'; case ApplicationConstants.PROJECT_ACP: diff --git a/packages/node-cli/src/services/TranslationKeys.js b/packages/node-cli/src/services/TranslationKeys.js index 25b48f91..ae5f77d0 100644 --- a/packages/node-cli/src/services/TranslationKeys.js +++ b/packages/node-cli/src/services/TranslationKeys.js @@ -79,6 +79,8 @@ module.exports = { ENTER_PROJECT_NAME: 'COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_NAME', ENTER_PROJECT_VERSION: 'COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PROJECT_VERSION', ENTER_PUBLISHER_ID: 'COMMAND_CREATEPROJECT_QUESTIONS_ENTER_PUBLISHER_ID', + ENTER_SPA_PROJECT_NAME: 'COMMAND_CREATEPROJECT_QUESTIONS_ENTER_SPA_PROJECT_NAME', + CREATE_SPA: 'COMMAND_CREATEPROJECT_QUESTIONS_CREATE_SPA', INCLUDE_UNIT_TESTING: 'COMMAND_CREATEPROJECT_QUESTIONS_INCLUDE_UNIT_TESTING', OVERWRITE_PROJECT: 'COMMAND_CREATEPROJECT_QUESTIONS_OVERWRITE_PROJECT', }, @@ -92,6 +94,7 @@ module.exports = { PROJECT_CREATED: 'COMMAND_CREATEPROJECT_MESSAGES_PROJECT_CREATED', PROJECT_CREATION_CANCELED: 'COMMAND_CREATEPROJECT_MESSAGES_PROJECT_CREATION_CANCELED', SAMPLE_UNIT_TEST_ADDED: 'COMMAND_CREATEPROJECT_MESSAGES_SAMPLE_UNIT_TEST_ADDED', + SETUP_SPA_PROJECT: 'COMMAND_CREATEPROJECT_MESSAGES_SETUP_SPA_PROJECT', SETUP_TEST_ENV: 'COMMAND_CREATEPROJECT_MESSAGES_SETUP_TEST_ENV', }, }, @@ -273,8 +276,8 @@ module.exports = { COMMAND_REFRESH_AUTHORIZATION: { MESSAGES: { AUTHORIZATION_REFRESH_COMPLETED: 'COMMAND_REFRESH_AUTHORIZATION_MESSAGES_AUTHORIZATION_REFRESH_COMPLETED', - CREDENTIALS_NEED_TO_BE_REFRESHED: 'COMMAND_REFRESH_AUTHORIZATION_MESSAGES_CREDENTIALS_NEED_TO_BE_REFRESHED' - } + CREDENTIALS_NEED_TO_BE_REFRESHED: 'COMMAND_REFRESH_AUTHORIZATION_MESSAGES_CREDENTIALS_NEED_TO_BE_REFRESHED', + }, }, COMMAND_SETUPACCOUNT: { @@ -296,9 +299,9 @@ module.exports = { SELECT_CONFIGURED_AUTHID: 'COMMAND_SETUPACCOUNT_MESSAGES_SELECT_CONFIGURED_AUTHID', }, ERRORS: { - BROWSER_BASED_NOT_ALLOWED : 'COMMAND_MANAGE_ACCOUNT_ERROR_BROWSER_BASED_NOT_ALLOWED', - MACHINE_TO_MACHINE_NOT_ALLOWED : 'COMMAND_MANAGE_ACCOUNT_ERROR_MACHINE_TO_MACHINE_NOT_ALLOWED', - NON_CONSISTENT_AUTH_STATE : 'COMMAND_MANAGE_ACCOUNT_ERROR_NON_CONSISTENT_AUTH_STATE', + BROWSER_BASED_NOT_ALLOWED: 'COMMAND_MANAGE_ACCOUNT_ERROR_BROWSER_BASED_NOT_ALLOWED', + MACHINE_TO_MACHINE_NOT_ALLOWED: 'COMMAND_MANAGE_ACCOUNT_ERROR_MACHINE_TO_MACHINE_NOT_ALLOWED', + NON_CONSISTENT_AUTH_STATE: 'COMMAND_MANAGE_ACCOUNT_ERROR_NON_CONSISTENT_AUTH_STATE', }, OUTPUT: { NEW_OAUTH: 'COMMAND_SETUPACCOUNT_OUTPUT_NEW_OAUTH', diff --git a/packages/node-cli/src/services/actionresult/CreateProjectActionResult.js b/packages/node-cli/src/services/actionresult/CreateProjectActionResult.js index 93296d10..33d30e79 100644 --- a/packages/node-cli/src/services/actionresult/CreateProjectActionResult.js +++ b/packages/node-cli/src/services/actionresult/CreateProjectActionResult.js @@ -13,6 +13,8 @@ class CreateProjectActionResult extends ActionResult { this._projectName = parameters.projectName; this._projectDirectory = parameters.projectDirectory; this._includeUnitTesting = parameters.includeUnitTesting; + this._createSpa = parameters.createSpa; + this._spaProjectName = parameters.spaProjectName; this._npmPackageInitialized = parameters.npmPackageInitialized; } @@ -40,6 +42,14 @@ class CreateProjectActionResult extends ActionResult { return this._includeUnitTesting; } + get createSpa() { + return this._createSpa; + } + + get spaProjectName() { + return this._spaProjectName; + } + get npmPackageInitialized() { return this._npmPackageInitialized; } @@ -74,6 +84,16 @@ class CreateProjectActionResultBuilder extends ActionResultBuilder { return this; } + withSpaProject(createSpa) { + this.createSpa = createSpa; + return this; + } + + withSpaProjectName(spaProjectName) { + this.spaProjectName = spaProjectName; + return this; + } + withNpmPackageInitialized(npmPackageInitialized) { this.npmPackageInitialized = npmPackageInitialized; return this; @@ -89,6 +109,8 @@ class CreateProjectActionResultBuilder extends ActionResultBuilder { ...(this.projectName && { projectName: this.projectName }), ...(this.projectDirectory && { projectDirectory: this.projectDirectory }), ...(this.includeUnitTesting && { includeUnitTesting: this.includeUnitTesting }), + ...(this.createSpa && { createSpa: this.createSpa }), + ...(this.spaProjectName && { spaProjectName: this.spaProjectName }), ...(this.npmPackageInitialized && { npmPackageInitialized: this.npmPackageInitialized }), ...(this.projectFolder && { projectFolder: this.projectFolder }), ...(this.commandParameters && { commandParameters: this.commandParameters }), diff --git a/packages/node-cli/src/suitecloud.js b/packages/node-cli/src/suitecloud.js index f8466db8..eb56392b 100755 --- a/packages/node-cli/src/suitecloud.js +++ b/packages/node-cli/src/suitecloud.js @@ -5,6 +5,8 @@ */ 'use strict'; +console.log('⚡ Running LOCAL linked version of SuiteCloud CLI'); + const CLI = require('./CLI'); const CommandsMetadataService = require('./core/CommandsMetadataService'); const CommandActionExecutor = require('./core/CommandActionExecutor'); @@ -26,7 +28,7 @@ const cliInstance = new CLI({ cliConfigurationService: new CLIConfigurationService(), commandsMetadataService: commandsMetadataServiceSingleton, log: NodeConsoleLogger, - sdkPath: sdkPath + sdkPath: sdkPath, }), }); diff --git a/packages/node-cli/src/templates/TemplateKeys.js b/packages/node-cli/src/templates/TemplateKeys.js index ae73c3d4..b84443ee 100644 --- a/packages/node-cli/src/templates/TemplateKeys.js +++ b/packages/node-cli/src/templates/TemplateKeys.js @@ -1,7 +1,7 @@ /* -** Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. -** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. -*/ + ** Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + */ 'use strict'; module.exports = { @@ -13,13 +13,24 @@ module.exports = { }, PROJECTCONFIGS: { cliconfig: require.resolve('./projectconfigs/suitecloud.config.js'), - gitignore: require.resolve('./projectconfigs/default_gitignore') + gitignore: require.resolve('./projectconfigs/default_gitignore'), }, UNIT_TEST: { cliconfig: require.resolve('./unittest/suitecloud.config.js.template'), jestconfig: require.resolve('./unittest/jest.config.js.template'), packagejson: require.resolve('./unittest/package.json.template'), sampletest: require.resolve('./unittest/sample-test.js.template'), - jsconfig: require.resolve('./unittest/jsconfig.json.template') - } + jsconfig: require.resolve('./unittest/jsconfig.json.template'), + }, + SPA_PROJECT: { + spaclient: require.resolve('./spaproject/spaclient.tsx.template'), + spaserver: require.resolve('./spaproject/spaserver.ts.template'), + helloworld: require.resolve('./spaproject/helloworld.tsx.template'), + custspa: require.resolve('./spaproject/custspa_projectname.xml.template'), + eslint: require.resolve('./spaproject/eslint.config.mjs.template'), + gulpfile: require.resolve('./spaproject/gulpfile.mjs.template'), + package: require.resolve('./spaproject/package.json.template'), + tsconfig: require.resolve('./spaproject/tsconfig.json.template'), + tsconfigtest: require.resolve('./spaproject/tsconfig.test.json.template'), + }, }; diff --git a/packages/node-cli/src/templates/spaproject/custspa_projectname.xml.template b/packages/node-cli/src/templates/spaproject/custspa_projectname.xml.template new file mode 100644 index 00000000..82fd2c1e --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/custspa_projectname.xml.template @@ -0,0 +1,13 @@ + + {{projectName}} + This is an SPA template project + {{projectName}} + [{{projectPath}}/{{projectName}}/] + [{{projectPath}}/{{projectName}}/SpaClient.js] + [{{projectPath}}/{{projectName}}/SpaServer.js] + [{{projectPath}}/{{projectName}}/assets/] + ERROR + F + + + diff --git a/packages/node-cli/src/templates/spaproject/eslint.config.mjs.template b/packages/node-cli/src/templates/spaproject/eslint.config.mjs.template new file mode 100644 index 00000000..e92cb690 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/eslint.config.mjs.template @@ -0,0 +1,297 @@ +import eslintPluginImport from 'eslint-plugin-import'; +import eslintPluginReact from 'eslint-plugin-react'; +import eslintJs from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import globals from 'globals'; +import eslintPluginJest from 'eslint-plugin-jest'; +import eslintPluginJestFormatting from 'eslint-plugin-jest-formatting'; +import eslintPluginTypeScript from '@typescript-eslint/eslint-plugin'; +import eslintTypeScriptParser from '@typescript-eslint/parser'; + +const languageOptionsJs = { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.amd, + ...globals.node, + ...globals.browser, + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + } +}; + +const pluginsJs = { + import: eslintPluginImport, + react: eslintPluginReact, +}; +const rulesJs = { + 'no-var': 'error', + 'no-multi-str': 'error', + 'no-prototype-builtins': 'off', + 'no-duplicate-imports': 'error', + 'no-self-compare': 'error', + 'no-sequences': [ + 'error', + { + allowInParentheses: false, + }, + ], + 'no-template-curly-in-string': 'error', + 'no-unused-private-class-members': 'error', + curly: 'error', + camelcase: [ + 'error', + { + properties: 'never', + }, + ], + 'no-extend-native': 'error', + 'max-depth': 'error', + 'dot-notation': 'error', + eqeqeq: 'error', + 'comma-dangle': ['error', 'only-multiline'], + 'no-constant-condition': [ + 'error', + { + checkLoops: false, + }, + ], + 'no-unused-vars': [ + 'error', + { + args: 'none', + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + 'react/jsx-uses-vars': 2, +}; + +const pluginsTs = { + ...pluginsJs, + '@typescript-eslint': eslintPluginTypeScript, +} + +const rulesTs = { + 'block-scoped-var': 'error', + 'prefer-arrow-callback': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + eqeqeq: ['error', 'always', {null: 'ignore'}], + 'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs'], + '@typescript-eslint/no-non-null-assertion': 'warn', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + '{}': false, + }, + extendDefaults: true, + }, + ], + '@typescript-eslint/no-base-to-string': 'warn', + '@typescript-eslint/no-unsafe-enum-comparison': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-inferrable-types': 'error', + '@typescript-eslint/prefer-namespace-keyword': 'error', + 'no-extra-semi': 'off', + '@typescript-eslint/no-extra-semi': 'error', + 'require-atomic-updates': [ + 'error', + { + allowProperties: true, + }, + ], + '@typescript-eslint/no-extraneous-class': [ + 'error', + { + allowEmpty: true, + allowStaticOnly: true, + }, + ], + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'never', + }, + ], + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + minimumDescriptionLength: 10, + }, + ], + '@typescript-eslint/require-await': 'off', + 'no-return-await': 'off', + '@typescript-eslint/return-await': ['error', 'always'], + '@typescript-eslint/promise-function-async': ['error'], + '@typescript-eslint/strict-boolean-expressions': [ + 'error', + { + allowString: true, + allowNumber: false, + allowNullableObject: true, + allowNullableBoolean: false, + allowNullableString: false, + allowNullableNumber: false, + allowNullableEnum: false, + allowAny: false, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + }, + ], + 'import/no-unresolved': [ + 'warn', { ignore: ['@uif-js/.'] } + ], + 'import/extensions': [ + 'error', + 'ignorePackages', + { + '': 'never', + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], +}; + +const languageOptionsTs = { + parser: eslintTypeScriptParser, + parserOptions: { + project: ['./tsconfig.json'], + } +} + +const settingsTs = { + 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], + 'import/parsers': { + '@typescript-eslint/parser': ['.js', '.ts', '.tsx'], + }, + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + typescript: { + alwaysTryTypes: true, + project: './tsconfig.json', + }, + }, +} + +const pluginsJsJest = { + ...pluginsJs, + jest: eslintPluginJest, + 'jest-formatting': eslintPluginJestFormatting, +}; + +const rulesJsJest = { + 'jest/consistent-test-it': [ + 'error', + { + fn: 'test', + withinDescribe: 'test', + }, + ], + 'jest-formatting/padding-around-all': 'warn', + 'jest/max-expects': [ + 'warn', + { + max: 5, + }, + ], + + 'jest/max-nested-describe': [ + 'warn', + { + max: 5, + }, + ], + 'jest/no-conditional-in-test': 'warn', + 'jest/no-duplicate-hooks': 'error', + 'jest/no-restricted-matchers': [ + 'warn', + { + toBeTruthy: + "For boolean checks is preferred to use toBe(true). Use .toBeTruthy() when you don't care what the value is, there are six falsy values: false, 0, '', null, undefined, and NaN. Everything else is truthy!", + toBeFalsy: + "For boolean checks is preferred to use toBe(false), there are six falsy values: false, 0, '', null, undefined, and NaN. Everything else is truthy!", + }, + ], + 'jest/prefer-comparison-matcher': 'warn', + 'jest/prefer-equality-matcher': 'warn', + 'jest/prefer-hooks-on-top': 'warn', + 'jest/prefer-lowercase-title': [ + 'warn', + { + ignoreTopLevelDescribe: true, // We want only uppercase for top level Describe + }, + ], + 'jest/prefer-mock-promise-shorthand': 'warn', + 'jest/prefer-spy-on': 'warn', + 'jest/prefer-todo': 'error', + 'jest/require-to-throw-message': 'warn', + 'jest/valid-title': [ + 'error', + { + mustNotMatch: [ + '(%#)|(\\$#)', + "Parameterized tests shouldn't contain test case indexes. It's an anti-pattern which also causes problems on TC and in executing individual tests in IDEs. More info: https://jestjs.io/docs/api#1-testeachtablename-fn-timeout", + ], + }, + ], +}; + +const languageOptionsJsJest = { + globals: { + ...globals.jest, + }, +}; + +export default [ + { + ignores: ['*', '!src', '!src/SuiteApps', '!src/SuiteApps/*', '!test', '!test/unit', '!test/unit/*'], + languageOptions: languageOptionsJs, + }, + { + files: ['src/SuiteApps/**/*.ts', 'src/SuiteApps/**/*.tsx'], + ignores: ['src/**/SpaClient.tsx', 'src/**/SpaServer.ts'], + plugins: pluginsTs, + rules: { + ...eslintJs.configs.recommended.rules, + ...rulesJs, + ...pluginsJs.import.configs.recommended.rules, + ...pluginsJs.import.configs.typescript.rules, + ...eslintPluginTypeScript.configs['eslint-recommended'].rules, + ...eslintPluginTypeScript.configs.recommended.rules, + ...eslintPluginTypeScript.configs.stylistic.rules, + ...rulesTs, + ...eslintConfigPrettier.rules, + }, + languageOptions: languageOptionsTs, + settings: settingsTs, + }, + { + files: ['test/unit/**/*.test.ts', 'test/unit/**/*.test.tsx'], + plugins: pluginsJsJest, + rules: { + + ...eslintJs.configs.recommended.rules, + ...rulesJs, + ...rulesJsJest, + ...eslintConfigPrettier.rules, + }, + languageOptions: languageOptionsJsJest, + }, +]; \ No newline at end of file diff --git a/packages/node-cli/src/templates/spaproject/gulpfile.mjs.template b/packages/node-cli/src/templates/spaproject/gulpfile.mjs.template new file mode 100644 index 00000000..d2cb7aaf --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/gulpfile.mjs.template @@ -0,0 +1,229 @@ +import fs from 'fs/promises'; +import path from 'path'; +import gulp from 'gulp'; +import ts from 'gulp-typescript'; +import {rollup} from 'rollup'; +import terser from '@rollup/plugin-terser'; + +/** + * Enable or disable script concatenation. When enabled all code will be concatenated into a single file for each + * entry point. This can greatly reduce the number of requests and loading time. + */ +const concatenateScripts = true; +/** + * Enable/disable minification of scripts to save bandwidth + */ +const minifyScripts = true; + +const tsBuildDir = 'build'; +const srcSuiteAppDir = path.join('src', 'SuiteApps'); +const buildSuiteAppDir = path.join(tsBuildDir, 'src', 'SuiteApps'); +const fileCabinetSuiteAppDir = path.join('src', 'FileCabinet', 'SuiteApps'); +const rollupTerserPlugin = terser(); + +export const clean = gulp.series( + cleanBuild, + cleanBundles, +); + +export const build = gulp.series( + cleanBuild, + compileTs, +); + +export const bundle = gulp.series( + build, + cleanBundles, + bundleScripts, + bundleAssets, +); + +/** + * Transpile TypeScript files into JavaScript. The output is saved in the build directory. + */ +function compileTs() { + const tsProject = ts.createProject('tsconfig.json'); + return tsProject.src() + .pipe(tsProject()) + .pipe(gulp.dest(tsBuildDir)); +} + +/** + * Find all scripts that are entry points and bundle them using Rollup. + * - Converts imports into define/require + * - Scripts are optionally concatenated and minified based on the concatenateScripts and minifyScripts settings + * - The input is taken from the build directory containing the transpiled sources + * - The output is saved into src/FileCabinet/SuiteApps + */ +async function bundleScripts() { + const spaRoots = await findSpaRoots(); + for (const root of spaRoots){ + const entryPoints = await findEntryPoints(root); + for (const input of entryPoints) { + const scriptType = input.metadata.match(scriptTypeRegex)[0]; + const result = await rollup({ + input: path.resolve(input.filePath), + external: ['@uif-js/core', '@uif-js/core/jsx-runtime', '@uif-js/component', /^N$/, /^N\//], + plugins: [rollupScriptTypePlugin()], + }); + await result.write(rollupOutputConfig(input.filePath)); + if (scriptType.length > 0 && !scriptType.includes('SpaClient')) { + await appendScriptType(input); + } + } + } +} + +/** + * Copies all files that are not source files from src/SuiteApps into src/FileCabinet/SuiteApps + */ +async function bundleAssets() { + await visitDir(srcSuiteAppDir, async (filePath) => { + if (isSourceFile(filePath)) { + return; + } + const targetPath = path.join(fileCabinetSuiteAppDir, path.relative(srcSuiteAppDir, filePath)); + await fs.mkdir(path.dirname(targetPath), { + recursive: true, + }); + await fs.copyFile(filePath, targetPath); + }); +} + +/** + * Cleans the build directory removing all transpiled sources + */ +async function cleanBuild() { + await cleanDir(tsBuildDir); +} + +/** + * Cleans the src/FileCabinet/SuiteApps directory removing all bundled sources and assets for any SPA projects + */ +async function cleanBundles() { + const ep = await findEntryPoints(tsBuildDir); + for (const entry of ep){ + if (entry.metadata.includes("SpaServer")){ + await cleanDir(path.dirname(getOutputFile(entry.filePath))); + } + } +} + +/** + * Finds source files that are entry points for backend scripts or SPAs + */ +async function findEntryPoints(dir) { + const result = []; + await visitDir(dir, async (filePath) => { + const entryPoint = await checkEntryPoint(filePath); + if (entryPoint) { + result.push(entryPoint); + } + }); + return result; +} + +/** + * Determine if file path is a source file or an asset + */ +function isSourceFile(filePath) { + const extensions = ['.js', '.jsx', '.ts', '.tsx']; + return extensions.some((ext) => filePath.endsWith(ext)); +} + +/** + * Determines if file path is an entry point for a backend script or an SPA + */ +async function checkEntryPoint(filePath) { + if (filePath.endsWith('SpaClient.js')) { + return {filePath, metadata: '@NScriptType SpaClient'}; + } + const content = (await fs.readFile(filePath)).toString(); + const array = content.match(scriptMetadataRegex); + return array ? {filePath, metadata: array[0]} : null; +} + +/** + * Get Rollup output config for an input file based on the concatenateScripts and minifyScripts options + */ +function rollupOutputConfig(input) { + const common = { + plugins: minifyScripts ? [rollupTerserPlugin] : [], + format: 'amd', + }; + + if (concatenateScripts) { + return { + file: getOutputFile(input), + ...common, + }; + } else { + return { + dir: fileCabinetSuiteAppDir, + preserveModules: true, + preserveModulesRoot: buildSuiteAppDir, + ...common, + }; + } +} + +/** + * For a file in the build folder get a corresponding file in the FileCabinet folder + */ +function getOutputFile(input) { + return path.join(fileCabinetSuiteAppDir, path.relative(buildSuiteAppDir, input)); +} + +/** + * Append the NScriptType annotation to the top of the bundled file + */ +async function appendScriptType(input) { + const outputFile = getOutputFile(input.filePath); + const content = (await fs.readFile(outputFile)).toString(); + await fs.writeFile(outputFile, `${input.metadata}\n${content}`); +} + +/** + * Removes a directory + */ +async function cleanDir(dirPath) { + return fs.rm(dirPath, { + recursive: true, + force: true, + }); +} + +/** + * Visits all files in a directory recursively + */ +async function visitDir(dirPath, process) { + for (const entryName of await fs.readdir(dirPath)) { + const entryPath = path.join(dirPath, entryName); + if ((await fs.lstat(entryPath)).isFile()) { + await process(entryPath); + } else { + await visitDir(entryPath, process); + } + } +} + +async function findSpaRoots(){ + const roots = []; + await visitDir(tsBuildDir, async (filePath) => { + const entryPoint = await checkEntryPoint(filePath); + if (entryPoint && entryPoint.metadata.includes("SpaServer")) { + roots.push(path.dirname(entryPoint.filePath)); + } + }); + return roots; +} + +/** + * Rollup plugin that removes the NScriptType annotation before processing + */ +function rollupScriptTypePlugin() { + return {transform: (code) => code.replace(scriptMetadataRegex, '')}; +} + +const scriptMetadataRegex = /\/\*\*[\s\S]*?NScriptType[\s\S]*?\*\//gm; +const scriptTypeRegex = /(?<=@NScriptType )\w+/gm \ No newline at end of file diff --git a/packages/node-cli/src/templates/spaproject/helloworld.tsx.template b/packages/node-cli/src/templates/spaproject/helloworld.tsx.template new file mode 100644 index 00000000..61365d32 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/helloworld.tsx.template @@ -0,0 +1,15 @@ +import {ContentPanel, Heading, ThemeSelector} from '@uif-js/component'; +import {JSX, Theme} from '@uif-js/core'; + +export default function HelloWorld(): JSX.Element { + return ( + + + Hello World! + + + ); +} diff --git a/packages/node-cli/src/templates/spaproject/package.json.template b/packages/node-cli/src/templates/spaproject/package.json.template new file mode 100644 index 00000000..f4bc2f11 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/package.json.template @@ -0,0 +1,45 @@ +{ + "name": "{{projectName}}", + "version": "1.0.0", + "description": "{{projectName}}", + "devDependencies": { + "@hitc/netsuite-types": "^2024.2.2", + "@oracle/netsuite-uif-types": "7.0.1", + "@rollup/plugin-terser": "^0.4.0", + "@types/jest": "^29.5.14", + "gulp": "^4.0.0", + "gulp-typescript": "^5.0.0", + "jest": "^29.0.0", + "prettier": "^2.8.1", + "rollup": "^3.26.0", + "ts-jest": "^29.0.0", + "typescript": "^5.1.0" + }, + "peerDependencies": { + "@eslint/js": "^8.54.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jasmine": "^4.1.0", + "eslint-plugin-jest": "^27.6.0", + "eslint-plugin-jest-formatting": "^3", + "eslint-plugin-react": "^7.33.2", + "globals": "^13.23.0" + }, + "scripts": { + "clean": "gulp clean", + "build": "gulp build", + "bundle": "gulp bundle", + "deploy": "npm run bundle && suitecloud project:deploy", + "test": "jest", + "eslint-inspection": "eslint .", + "eslint-fix": "eslint --fix .", + "prettier-inspection": "prettier . --check", + "prettier-fix": "prettier . --write", + "inspections": "npm run eslint-inspection && npm run prettier-inspection", + "lint": "npm run eslint-fix && npm run prettier-fix" + } +} diff --git a/packages/node-cli/src/templates/spaproject/spaclient.tsx.template b/packages/node-cli/src/templates/spaproject/spaclient.tsx.template new file mode 100644 index 00000000..b60c4ef8 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/spaclient.tsx.template @@ -0,0 +1,6 @@ +import HelloWorld from './HelloWorld.tsx.template'; + +export const run = (context) => { + context.setLayout('application'); // Make the application fill the entire viewport + context.setContent(); +}; diff --git a/packages/node-cli/src/templates/spaproject/spaserver.ts.template b/packages/node-cli/src/templates/spaproject/spaserver.ts.template new file mode 100644 index 00000000..c9f95a87 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/spaserver.ts.template @@ -0,0 +1,6 @@ +/** + * @NApiVersion 2.1 + * @NScriptType SpaServerScript + */ + +export const initializeSpa = (context) => {}; diff --git a/packages/node-cli/src/templates/spaproject/tsconfig.json.template b/packages/node-cli/src/templates/spaproject/tsconfig.json.template new file mode 100644 index 00000000..8ce1a993 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/tsconfig.json.template @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "es2022", + "moduleResolution": "node", + "esModuleInterop": true, + "allowJs": true, + "newLine": "LF", + "rootDir": "./", + "outDir": "build", + "lib": ["es2022", "dom"], + "jsx": "react-jsx", + "jsxImportSource": "@uif-js/core", + "sourceMap": false, + "strictNullChecks": true, + "typeRoots": [ + "node_modules/@types", + "node_modules/@oracle/netsuite-uif-types" + ], + "paths": { + "N": ["./node_modules/@hitc/netsuite-types/N"], + "N/*": ["./node_modules/@hitc/netsuite-types/N/*"] + }, + }, + "include": [ + "./src/SuiteApps/**/*", + "./test/unit/**/*" + ] +} diff --git a/packages/node-cli/src/templates/spaproject/tsconfig.test.json.template b/packages/node-cli/src/templates/spaproject/tsconfig.test.json.template new file mode 100644 index 00000000..68d6c114 --- /dev/null +++ b/packages/node-cli/src/templates/spaproject/tsconfig.test.json.template @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "sourceMap": true + }, + "extends": "./tsconfig.json" +} \ No newline at end of file From 59e5341387d7d750bd6c494c553ba5c9eac2d1cb Mon Sep 17 00:00:00 2001 From: warambillete Date: Wed, 21 May 2025 14:28:49 -0300 Subject: [PATCH 2/3] Removed logs Signed-off-by: warambillete # --- .../src/commands/project/create/CreateProjectAction.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/node-cli/src/commands/project/create/CreateProjectAction.js b/packages/node-cli/src/commands/project/create/CreateProjectAction.js index ef2a9d74..59194cde 100644 --- a/packages/node-cli/src/commands/project/create/CreateProjectAction.js +++ b/packages/node-cli/src/commands/project/create/CreateProjectAction.js @@ -523,13 +523,8 @@ module.exports = class CreateProjectAction extends BaseAction { } _createMyCustomFolder(params) { - this._log.info('Creating custom folder...'); - this._log.info('parent directory: ' + params[COMMAND_OPTIONS.PARENT_DIRECTORY]); - // if (params.withcustomfolder) { const customFolderPath = path.join(params[COMMAND_OPTIONS.PARENT_DIRECTORY], 'mycustomfolder'); this._fileSystemService.createFolderFromAbsolutePath(customFolderPath); - this._log.info('✅ Created custom folder: ' + customFolderPath); - // } } async _runNpmInstall(projectAbsolutePath) { From ba20acaee641c35e943048158a6f62ca868904cb Mon Sep 17 00:00:00 2001 From: warambillete Date: Fri, 23 May 2025 06:59:35 -0300 Subject: [PATCH 3/3] Removed comments Signed-off-by: warambillete --- .../node-cli/src/commands/project/create/CreateProjectAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-cli/src/commands/project/create/CreateProjectAction.js b/packages/node-cli/src/commands/project/create/CreateProjectAction.js index 59194cde..fea7d6ea 100644 --- a/packages/node-cli/src/commands/project/create/CreateProjectAction.js +++ b/packages/node-cli/src/commands/project/create/CreateProjectAction.js @@ -58,7 +58,7 @@ const SPA_HELLO_WORLD_TEMPLATE_KEY = 'helloworld'; const SPA_HELLO_WORLD_FILENAME = 'HelloWorld'; const SPA_HELLO_WORLD_EXTENSION = 'tsx'; const SPA_CUSTSPA_TEMPLATE_KEY = 'custspa'; -const SPA_CUSTSPA_FILENAME = 'custspa_'; // this is the prefix for the file name +const SPA_CUSTSPA_FILENAME = 'custspa_'; const SPA_CUSTSPA_EXTENSION = 'xml'; const SPA_GULPFILE_TEMPLATE_KEY = 'gulpfile'; const SPA_GULPFILE_FILENAME = 'gulpfile';