@tridion-sites/extensions-cli 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/LICENSE.md +322 -0
  3. package/dist/addon/template/addonId.config.json.hbs +6 -0
  4. package/dist/addon/template/extension/.browserslistrc.hbs +5 -0
  5. package/dist/addon/template/extension/.editorconfig.hbs +10 -0
  6. package/dist/addon/template/extension/.eslintrc.json.hbs +30 -0
  7. package/dist/addon/template/extension/.gitignore.hbs +1 -0
  8. package/dist/addon/template/extension/.npmrc.hbs +1 -0
  9. package/dist/addon/template/extension/.prettierrc.hbs +10 -0
  10. package/dist/addon/template/extension/babel.config.js.hbs +8 -0
  11. package/dist/addon/template/extension/devServer.js.hbs +43 -0
  12. package/dist/addon/template/extension/package.json.hbs +64 -0
  13. package/dist/addon/template/extension/src/globals.ts.hbs +5 -0
  14. package/dist/addon/template/extension/src/index.css.hbs +0 -0
  15. package/dist/addon/template/extension/src/index.tsx.hbs +18 -0
  16. package/dist/addon/template/extension/tsconfig.json.hbs +28 -0
  17. package/dist/addon/template/extension/types/css.d.ts.hbs +4 -0
  18. package/dist/addon/template/extension/webpack.dev.config.js.hbs +94 -0
  19. package/dist/addon/template/extension/webpack.prod.config.js.hbs +85 -0
  20. package/dist/addonManifest-30c25a45.js +225 -0
  21. package/dist/cli.js +281 -0
  22. package/dist/extensionPoints/primaryNavigation/template/PageComponent/PageComponent.module.css.hbs +4 -0
  23. package/dist/extensionPoints/primaryNavigation/template/PageComponent/PageComponent.tsx.hbs +9 -0
  24. package/dist/extensionPoints/primaryNavigation/template/PageComponent/index.ts.hbs +1 -0
  25. package/dist/extensionPoints/primaryNavigation/template/extensionPoint.hbs +15 -0
  26. package/dist/index.d.ts +34 -0
  27. package/dist/index.js +83 -0
  28. package/dist/tsdoc-metadata.json +11 -0
  29. package/package.json +65 -0
package/dist/cli.js ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import spawn from 'cross-spawn';
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, createWriteStream } from 'node:fs';
6
+ import { resolve, dirname, basename } from 'node:path';
7
+ import Handlebars from 'handlebars';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { r as readJsonFile, A as AddonManifest } from './addonManifest-30c25a45.js';
10
+ import inquirer from 'inquirer';
11
+ import isValidFilename from 'valid-filename';
12
+ import archiver from 'archiver';
13
+
14
+ const installDependencies = (cwd) => {
15
+ spawn.sync('npm', ['install'], {
16
+ stdio: 'inherit',
17
+ cwd,
18
+ });
19
+ };
20
+
21
+ const runLinters = (cwd) => {
22
+ spawn.sync('npm', ['run', 'lint'], {
23
+ stdio: 'inherit',
24
+ cwd,
25
+ });
26
+ };
27
+
28
+ const createFolder = (parentFolder, folderName) => {
29
+ const path = resolve(parentFolder, folderName);
30
+ if (!existsSync(path)) {
31
+ mkdirSync(path);
32
+ }
33
+ return path;
34
+ };
35
+
36
+ const createFileFromTemplate = (templateFilePath, destinationFilePath, templateParams) => {
37
+ const templateFile = readFileSync(templateFilePath, 'utf-8');
38
+ const template = Handlebars.compile(templateFile);
39
+ const file = template(templateParams);
40
+ mkdirSync(dirname(destinationFilePath), { recursive: true });
41
+ writeFileSync(destinationFilePath, file);
42
+ };
43
+
44
+ /**
45
+ * Return the path to currently executing binary (in node_modules)
46
+ * @note This function is going to return an incorrect path if
47
+ * a build does not bundle all files into a single javascript file!
48
+ */
49
+ const getBinaryPath = () => {
50
+ const filepath = fileURLToPath(import.meta.url);
51
+ const binaryPath = dirname(filepath);
52
+ return binaryPath;
53
+ };
54
+
55
+ const getPackageJson = () => {
56
+ const binaryPath = getBinaryPath();
57
+ const packageJsonPath = resolve(binaryPath, '../package.json');
58
+ const packageJson = readJsonFile(packageJsonPath);
59
+ return packageJson;
60
+ };
61
+
62
+ const copyTemplate = ({ addonId, addonRootFolderPath, author, extensionDescription, extensionName, extensionRootFolderPath, sitesUrl, }) => {
63
+ const binaryPath = getBinaryPath();
64
+ const templatePath = resolve(binaryPath, 'addon/template');
65
+ const packageJson = getPackageJson();
66
+ createFileFromTemplate(resolve(templatePath, 'addonId.config.json.hbs'), resolve(addonRootFolderPath, `${addonId}.config.json`), {
67
+ extensionName,
68
+ });
69
+ [
70
+ '.browserslistrc',
71
+ '.editorconfig',
72
+ '.eslintrc.json',
73
+ '.gitignore',
74
+ '.prettierrc',
75
+ 'babel.config.js',
76
+ 'devServer.js',
77
+ 'tsconfig.json',
78
+ 'src/globals.ts',
79
+ 'src/index.css',
80
+ 'src/index.tsx',
81
+ 'types/css.d.ts',
82
+ ].forEach(fileName => {
83
+ createFileFromTemplate(resolve(templatePath, `extension/${fileName}.hbs`), resolve(extensionRootFolderPath, fileName));
84
+ });
85
+ createFileFromTemplate(resolve(templatePath, 'extension/package.json.hbs'), resolve(extensionRootFolderPath, 'package.json'), {
86
+ extensionName,
87
+ extensionDescription,
88
+ author,
89
+ addonId,
90
+ sitesUrl,
91
+ extensionsApiVersion: packageJson.dependencies['@tridion-sites/extensions'],
92
+ modelsVersion: packageJson.dependencies['@tridion-sites/models'],
93
+ openApiClientVersion: packageJson.dependencies['@tridion-sites/open-api-client'],
94
+ extensionsCliVersion: packageJson.version,
95
+ });
96
+ createFileFromTemplate(resolve(templatePath, 'extension/webpack.dev.config.js.hbs'), resolve(extensionRootFolderPath, 'webpack.dev.config.js'), {
97
+ extensionName,
98
+ });
99
+ createFileFromTemplate(resolve(templatePath, 'extension/webpack.prod.config.js.hbs'), resolve(extensionRootFolderPath, 'webpack.prod.config.js'), {
100
+ extensionName,
101
+ });
102
+ };
103
+
104
+ const createAddonPrompt = async () => {
105
+ return inquirer.prompt([
106
+ {
107
+ type: 'input',
108
+ name: 'addonId',
109
+ message: 'Provide id of the addon:',
110
+ default: 'my-addon',
111
+ validate: (input) => {
112
+ if (/\s+/g.test(input)) {
113
+ return "Addon id can't have whitespace";
114
+ }
115
+ if (!isValidFilename(input)) {
116
+ return "Addon id can't have reserved symbols";
117
+ }
118
+ return true;
119
+ },
120
+ },
121
+ {
122
+ type: 'input',
123
+ name: 'addonName',
124
+ message: 'Provide name of the addon:',
125
+ default: 'My addon',
126
+ },
127
+ {
128
+ type: 'input',
129
+ name: 'addonDescription',
130
+ message: 'Provide description of the addon:',
131
+ default: 'My addon for Tridion Experience Space',
132
+ },
133
+ {
134
+ type: 'input',
135
+ name: 'author',
136
+ message: `Provide author's name:`,
137
+ default: 'RWS',
138
+ },
139
+ {
140
+ type: 'input',
141
+ name: 'extensionName',
142
+ message: 'Provide the name of the frontend extension:',
143
+ default: 'my-extension',
144
+ validate: (input) => {
145
+ if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(input)) {
146
+ return 'Extension name should adhere to package.json name requirements!';
147
+ }
148
+ return true;
149
+ },
150
+ },
151
+ {
152
+ type: 'input',
153
+ name: 'extensionDescription',
154
+ message: 'Provide description of the extension:',
155
+ default: 'My first extension',
156
+ },
157
+ {
158
+ type: 'input',
159
+ name: 'url',
160
+ message: `Provide the url to Tridion Sites:`,
161
+ validate: (input) => {
162
+ if (!/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}(\.[a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/gm.test(input)) {
163
+ return `Make sure it's a valid URL`;
164
+ }
165
+ return true;
166
+ },
167
+ },
168
+ ]);
169
+ };
170
+
171
+ const createAddonRootFolder = ({ path, addonDescription, addonId, addonName, author, extensionName, }) => {
172
+ const addonFolderPath = createFolder(path, addonId);
173
+ const manifest = new AddonManifest({
174
+ version: '1.0.0',
175
+ id: addonId,
176
+ name: addonName,
177
+ description: addonDescription,
178
+ author: author,
179
+ });
180
+ manifest.addFrontendExtension(extensionName, [`dist\\${extensionName}\\main.js`, `dist\\${extensionName}\\main.css`], `dist\\${extensionName}\\main.js`);
181
+ manifest.writeFile(addonFolderPath);
182
+ return addonFolderPath;
183
+ };
184
+
185
+ const createAddon = async (path) => {
186
+ const answers = await createAddonPrompt();
187
+ const addonRootFolderPath = createAddonRootFolder({
188
+ path,
189
+ addonId: answers.addonId,
190
+ addonName: answers.addonName,
191
+ addonDescription: answers.addonDescription,
192
+ author: answers.author,
193
+ extensionName: answers.extensionName,
194
+ });
195
+ const extensionRootFolderPath = createFolder(addonRootFolderPath, answers.extensionName);
196
+ copyTemplate({
197
+ addonRootFolderPath,
198
+ extensionRootFolderPath,
199
+ addonId: answers.addonId,
200
+ author: answers.author,
201
+ extensionDescription: answers.extensionDescription,
202
+ extensionName: answers.extensionName,
203
+ sitesUrl: answers.url,
204
+ });
205
+ return {
206
+ addonFolderPath: addonRootFolderPath,
207
+ extensionFolderPath: extensionRootFolderPath,
208
+ };
209
+ };
210
+
211
+ const configureHandlebars = () => {
212
+ Handlebars.registerHelper('camelCase', (text) => {
213
+ return text.replace(/^./gm, match => match.toLowerCase());
214
+ });
215
+ Handlebars.registerHelper('kebabCase', (text) => {
216
+ return text.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
217
+ });
218
+ };
219
+
220
+ configureHandlebars();
221
+ const createCommand = async () => {
222
+ const currentProcessPath = process.cwd();
223
+ console.info(chalk.green(`Creating a new addon at: ${currentProcessPath}`));
224
+ const { extensionFolderPath } = await createAddon(currentProcessPath);
225
+ console.info(chalk.green(`Installing dependencies`));
226
+ installDependencies(extensionFolderPath);
227
+ console.info(chalk.green(`Running linters`));
228
+ runLinters(extensionFolderPath);
229
+ console.info(chalk.green('🎉️️ All done! Happy hacking!'));
230
+ console.info(chalk.green(`Your extension is available at: ${extensionFolderPath}`));
231
+ };
232
+
233
+ const packCommand = ({ inputFilesPath, manifestPath, outputPath }) => {
234
+ console.info(chalk.green(`Creating an addon package from folder: ${inputFilesPath}`));
235
+ console.info(chalk.green(`Manifest file path: ${manifestPath}`));
236
+ const manifest = AddonManifest.fromFile(manifestPath);
237
+ const packageName = `${manifest.id}-${manifest.version}.zip`;
238
+ const archive = archiver('zip');
239
+ const zipPath = resolve(outputPath, packageName);
240
+ const stream = createWriteStream(zipPath);
241
+ stream.on('close', () => {
242
+ console.info(chalk.green(`Addon package "${packageName}" has been created at: ${zipPath}.`));
243
+ console.info(chalk.green('It can be uploaded into Addon Manager now.'));
244
+ });
245
+ archive.on('error', error => {
246
+ console.warn('We were unable to create a package.');
247
+ throw error;
248
+ });
249
+ archive.pipe(stream);
250
+ archive.file(manifestPath, { name: basename(manifestPath) });
251
+ archive.directory(inputFilesPath, basename(resolve(inputFilesPath)));
252
+ archive.finalize();
253
+ };
254
+
255
+ const packageJson = getPackageJson();
256
+ const program = new Command();
257
+ const programName = Object.keys(packageJson.bin)[0];
258
+ program
259
+ .name(programName)
260
+ .description('Provides helpers to build, run and deploy addons for Tridion Experience Space')
261
+ .version(packageJson.version);
262
+ program
263
+ .command('create')
264
+ .description('Create new Tridion Experience Space addon')
265
+ .usage(`${programName} create`)
266
+ .action(createCommand);
267
+ program
268
+ .command('pack')
269
+ .description('Creates a package ready to be used in Addon Manager')
270
+ .usage(`${programName} pack`)
271
+ .requiredOption('-i, --input <string>', 'path to the directory with addon files')
272
+ .requiredOption('-m, --manifest <string>', 'path to the addon manifest file')
273
+ .requiredOption('-o, --output <string>', 'path where to output the package')
274
+ .action(args => {
275
+ packCommand({
276
+ inputFilesPath: args.input,
277
+ manifestPath: args.manifest,
278
+ outputPath: args.output,
279
+ });
280
+ });
281
+ program.parse(process.argv);
@@ -0,0 +1,4 @@
1
+ .{{kebabCase componentName}} {
2
+ padding: 60px;
3
+ background: #ccc;
4
+ }
@@ -0,0 +1,9 @@
1
+ import { useUserProfile } from '@tridion-sites/extensions';
2
+
3
+ import styles from './{{componentName}}.module.css';
4
+
5
+ export const {{componentName}} = () => {
6
+ const { userProfile } = useUserProfile();
7
+
8
+ return <div className={styles.{{camelCase componentName~}} }>Hi {userProfile.displayName}!</div>;
9
+ };
@@ -0,0 +1 @@
1
+ export * from './{{componentName}}';
@@ -0,0 +1,15 @@
1
+ builder.header.navigation.register(() => ({
2
+ id: '{{id}}',
3
+ routePath: '{{routePath}}',
4
+ routeComponent: {{routeComponentName}},
5
+ useNavigationItem: () => {
6
+ return {
7
+ isAvailable: true,
8
+ isInitialized: true,
9
+ label: '{{label}}',
10
+ }
11
+ }
12
+
13
+ }));
14
+
15
+ builder.header.navigation.config.add('{{id}}');
@@ -0,0 +1,34 @@
1
+ import type { FrontendAddon } from '@tridion-sites/open-api-client';
2
+
3
+ export declare const extensionsRequestBasePath = "/api/uiExtensionsRepository";
4
+
5
+ export declare const getLocalFrontendAddon: ({ manifestPath, addonConfigPath }: GetLocalFrontendAddonParams) => FrontendAddon;
6
+
7
+ export declare interface GetLocalFrontendAddonParams {
8
+ manifestPath: string;
9
+ addonConfigPath?: string;
10
+ }
11
+
12
+ declare interface Request_2 extends Record<string, any> {
13
+ }
14
+
15
+ declare type RequestListener = (request: Request_2, response: Response_2) => void;
16
+
17
+ declare interface Response_2 extends Record<string, any> {
18
+ json: (result: any) => void;
19
+ }
20
+
21
+ export declare const setupExtensionsResponse: ({ app, webAppPath, manifestPath, addonConfigPath, }: SetupExtensionsResponseParams) => void;
22
+
23
+ declare interface SetupExtensionsResponseApp extends Record<string, any> {
24
+ get: (path: string, listener: RequestListener) => void;
25
+ }
26
+
27
+ export declare interface SetupExtensionsResponseParams {
28
+ app: SetupExtensionsResponseApp;
29
+ webAppPath: string;
30
+ manifestPath: string;
31
+ addonConfigPath?: string;
32
+ }
33
+
34
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { resolve, basename } from 'node:path';
3
+ import { w as writeJsonFile, r as readJsonFile, A as AddonManifest } from './addonManifest-30c25a45.js';
4
+ import 'node:fs';
5
+
6
+ class AddonConfiguration {
7
+ constructor(fileName, configPerExtension) {
8
+ Object.defineProperty(this, "fileName", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: void 0
13
+ });
14
+ Object.defineProperty(this, "configPerExtension", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
20
+ Object.defineProperty(this, "writeFile", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: (path) => {
25
+ const configPath = resolve(path, this.fileName);
26
+ writeJsonFile(configPath, {
27
+ configuration: this.configPerExtension,
28
+ });
29
+ }
30
+ });
31
+ this.fileName = fileName;
32
+ this.configPerExtension = configPerExtension;
33
+ }
34
+ }
35
+ Object.defineProperty(AddonConfiguration, "fromFile", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: (configurationFilePath) => {
40
+ const configurationFile = readJsonFile(configurationFilePath);
41
+ const configuration = new AddonConfiguration(basename(configurationFilePath), configurationFile.configuration);
42
+ return configuration;
43
+ }
44
+ });
45
+
46
+ const extensionsRequestBasePath = '/api/uiExtensionsRepository';
47
+ const getLocalFrontendAddon = ({ manifestPath, addonConfigPath }) => {
48
+ const manifest = AddonManifest.fromFile(manifestPath);
49
+ const addonConfiguration = addonConfigPath ? AddonConfiguration.fromFile(addonConfigPath) : undefined;
50
+ const localAddon = {
51
+ Hash: Date.now().toString(),
52
+ Id: 'LocalAddon',
53
+ Configuration: {},
54
+ Extensions: [],
55
+ };
56
+ const localExtensions = manifest.getFrontendExtensions();
57
+ localExtensions.forEach(extension => {
58
+ var _a;
59
+ localAddon.Configuration[extension.name] = (addonConfiguration === null || addonConfiguration === void 0 ? void 0 : addonConfiguration.configPerExtension[extension.name]) || {};
60
+ const extensionFiles = extension.files.map(filePath => `${extensionsRequestBasePath}/${extension.name}/${filePath}?hash=${Date.now()}`);
61
+ (_a = localAddon.Extensions) === null || _a === void 0 ? void 0 : _a.push({
62
+ Name: extension.name,
63
+ MainFile: `${extensionsRequestBasePath}/${extension.name}/${extension.main}`,
64
+ Contents: extensionFiles,
65
+ });
66
+ });
67
+ return localAddon;
68
+ };
69
+
70
+ const setupExtensionsResponse = ({ app, webAppPath, manifestPath, addonConfigPath, }) => {
71
+ const basePath = webAppPath.endsWith('/') ? webAppPath : `${webAppPath}/`;
72
+ app.get(`${basePath}api/v2.0/extensions`, (request, response) => {
73
+ const localAddon = getLocalFrontendAddon({
74
+ manifestPath,
75
+ addonConfigPath,
76
+ });
77
+ const liveAddons = /*await ExtensionsService.getExtensions()*/ [];
78
+ const result = [...liveAddons, localAddon];
79
+ response.json(result);
80
+ });
81
+ };
82
+
83
+ export { extensionsRequestBasePath, getLocalFrontendAddon, setupExtensionsResponse };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.20.1"
9
+ }
10
+ ]
11
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@tridion-sites/extensions-cli",
3
+ "version": "0.3.2",
4
+ "description": "CLI to develop, build and package extensions for Tridion Experience Space",
5
+ "author": "RWS",
6
+ "homepage": "https://www.rws.com",
7
+ "license": "SEE LICENSE IN LICENSE.md",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "typings": "./dist/index.d.ts",
12
+ "bin": {
13
+ "sites-extensions": "./dist/cli.js"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://stash.sdl.com/scm/tdx/tridion-sites.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://jira.sdl.com/projects/DXUI/issues/"
21
+ },
22
+ "files": [
23
+ "dist/**/*"
24
+ ],
25
+ "scripts": {
26
+ "clean": "rimraf dist",
27
+ "build": "rollup -c && yarn run docs:extract --local && rimraf dist/dts",
28
+ "build:ci": "rollup -c && yarn run docs:extract && rimraf dist/dts",
29
+ "lint": "eslint . --fix && prettier --write \"src/**/*\"",
30
+ "prepublish": "yarn build",
31
+ "test": "sites-extensions"
32
+ },
33
+ "dependencies": {
34
+ "@tridion-sites/extensions": "0.5.0",
35
+ "@tridion-sites/models": "0.1.0",
36
+ "@tridion-sites/open-api-client": "1.0.4",
37
+ "archiver": "5.3.1",
38
+ "chalk": "5.2.0",
39
+ "commander": "10.0.0",
40
+ "cross-spawn": "7.0.3",
41
+ "decamelize": "6.0.0",
42
+ "fs-extra": "11.1.0",
43
+ "handlebars": "4.7.7",
44
+ "inquirer": "9.1.4",
45
+ "valid-filename": "4.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@rollup/plugin-commonjs": "24.0.1",
49
+ "@rollup/plugin-node-resolve": "15.0.1",
50
+ "@rollup/plugin-typescript": "11.0.0",
51
+ "@types/archiver": "5.3.1",
52
+ "@types/compression": "1.7.2",
53
+ "@types/cross-spawn": "6.0.2",
54
+ "@types/fs-extra": "11.0.1",
55
+ "@types/inquirer": "9.0.3",
56
+ "@types/node": "18.11.18",
57
+ "@web/rollup-plugin-copy": "0.3.0",
58
+ "rimraf": "4.1.2",
59
+ "rollup": "3.11.0",
60
+ "rollup-plugin-delete": "2.0.0",
61
+ "rollup-plugin-node-externals": "5.1.0",
62
+ "typescript": "4.9.4",
63
+ "typescript-transform-paths": "3.4.6"
64
+ }
65
+ }