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..fea7d6ea 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_';
+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,11 @@ module.exports = class CreateProjectAction extends BaseAction {
});
}
+ _createMyCustomFolder(params) {
+ const customFolderPath = path.join(params[COMMAND_OPTIONS.PARENT_DIRECTORY], 'mycustomfolder');
+ this._fileSystemService.createFolderFromAbsolutePath(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