@strapi/strapi 4.13.4 → 4.13.6

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.
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs/promises');
4
+ const boxen = require('boxen');
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+ const { createLogger } = require('../../../utils/logger');
8
+ const { notifyExperimentalCommand } = require('../../../utils/helpers');
9
+ const {
10
+ loadPkg,
11
+ validatePkg,
12
+ validateExportsOrdering,
13
+ getExportExtensionMap,
14
+ } = require('../../../utils/pkg');
15
+ const { createBuildContext, createBuildTasks } = require('../../../builders/packages');
16
+ const { buildTaskHandlers } = require('../../../builders/tasks');
17
+
18
+ /**
19
+ *
20
+ * @param {object} args
21
+ * @param {boolean} args.force
22
+ * @param {boolean} args.debug
23
+ */
24
+ module.exports = async ({ force, debug }) => {
25
+ const logger = createLogger({ debug, timestamp: false });
26
+ try {
27
+ /**
28
+ * Notify users this is an experimental command and get them to approve first
29
+ * this can be opted out by setting the argument --yes
30
+ */
31
+ await notifyExperimentalCommand({ force });
32
+
33
+ const cwd = process.cwd();
34
+
35
+ /**
36
+ * Load the closest package.json and then verify the structure against what we expect.
37
+ */
38
+ const packageJsonLoader = ora('Verifying package.json \n').start();
39
+
40
+ const rawPkg = await loadPkg({ cwd, logger }).catch((err) => {
41
+ packageJsonLoader.fail();
42
+ logger.error(err.message);
43
+ logger.debug(`Path checked – ${cwd}`);
44
+ process.exit(1);
45
+ });
46
+
47
+ const validatedPkg = await validatePkg({
48
+ pkg: rawPkg,
49
+ }).catch((err) => {
50
+ packageJsonLoader.fail();
51
+ logger.error(err.message);
52
+ process.exit(1);
53
+ });
54
+
55
+ /**
56
+ * Validate the exports of the package incl. the order of the
57
+ * exports within the exports map if applicable
58
+ */
59
+ const packageJson = await validateExportsOrdering({ pkg: validatedPkg, logger }).catch(
60
+ (err) => {
61
+ packageJsonLoader.fail();
62
+ logger.error(err.message);
63
+ process.exit(1);
64
+ }
65
+ );
66
+
67
+ packageJsonLoader.succeed('Verified package.json');
68
+
69
+ /**
70
+ * We create tasks based on the exports of the package.json
71
+ * their handlers are then ran in the order of the exports map
72
+ * and results are logged to see gradual progress.
73
+ */
74
+
75
+ const buildContextLoader = ora('Creating build context \n').start();
76
+
77
+ const extMap = getExportExtensionMap();
78
+
79
+ const ctx = await createBuildContext({
80
+ cwd,
81
+ extMap,
82
+ logger,
83
+ pkg: packageJson,
84
+ }).catch((err) => {
85
+ buildContextLoader.fail();
86
+ logger.error(err.message);
87
+ process.exit(1);
88
+ });
89
+
90
+ logger.debug('Build context: \n', ctx);
91
+
92
+ const buildTasks = await createBuildTasks(ctx);
93
+
94
+ buildContextLoader.succeed('Created build context');
95
+
96
+ /**
97
+ * If the distPath already exists, clean it
98
+ */
99
+ try {
100
+ logger.debug(`Cleaning dist folder: ${ctx.distPath}`);
101
+ await fs.rm(ctx.distPath, { recursive: true, force: true });
102
+ logger.debug('Cleaned dist folder');
103
+ } catch {
104
+ // do nothing, it will fail if the folder does not exist
105
+ logger.debug('There was no dist folder to clean');
106
+ }
107
+
108
+ for (const task of buildTasks) {
109
+ /**
110
+ * @type {import('../../../builders/tasks').TaskHandler<any>}
111
+ */
112
+ const handler = buildTaskHandlers[task.type];
113
+ handler.print(ctx, task);
114
+
115
+ await handler.run(ctx, task).catch((err) => {
116
+ if (err instanceof Error) {
117
+ logger.error(err.message);
118
+ }
119
+
120
+ process.exit(1);
121
+ });
122
+ }
123
+ } catch (err) {
124
+ logger.error(
125
+ 'There seems to be an unexpected error, try again with --debug for more information \n'
126
+ );
127
+ console.log(
128
+ chalk.red(
129
+ boxen(err.stack, {
130
+ padding: 1,
131
+ align: 'left',
132
+ })
133
+ )
134
+ );
135
+ process.exit(1);
136
+ }
137
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const { forceOption } = require('../../../utils/commander');
4
+ const { getLocalScript } = require('../../../utils/helpers');
5
+
6
+ /**
7
+ * `$ strapi plugin:build`
8
+ * @param {import('../../../../types/core/commands').AddCommandOptions} options
9
+ */
10
+ module.exports = ({ command }) => {
11
+ command
12
+ .command('plugin:build')
13
+ .description('Bundle your strapi plugin for publishing.')
14
+ .addOption(forceOption)
15
+ .option('-d, --debug', 'Enable debugging mode with verbose logs', false)
16
+ .action(getLocalScript('plugin/build-command'));
17
+ };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const browserslistToEsbuild = require('browserslist-to-esbuild');
5
+
6
+ const { parseExports } = require('../utils/pkg');
7
+
8
+ /**
9
+ * @typedef {Object} BuildContextArgs
10
+ * @property {string} cwd
11
+ * @property {import('../utils/pkg').ExtMap} extMap
12
+ * @property {import('../utils/logger').Logger} logger
13
+ * @property {import('../utils/pkg').PackageJson} pkg
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} Targets
18
+ * @property {string[]} node
19
+ * @property {string[]} web
20
+ * @property {string[]} *
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} BuildContext
25
+ * @property {string} cwd
26
+ * @property {import('../utils/pkg').Export[]} exports
27
+ * @property {string[]} external
28
+ * @property {import('../utils/pkg').ExtMap} extMap
29
+ * @property {import('../utils/logger').Logger} logger
30
+ * @property {import('../utils/pkg').PackageJson} pkg
31
+ * @property {Targets} targets
32
+ */
33
+
34
+ const DEFAULT_BROWSERS_LIST_CONFIG = [
35
+ 'last 3 major versions',
36
+ 'Firefox ESR',
37
+ 'last 2 Opera versions',
38
+ 'not dead',
39
+ 'node 16.0.0',
40
+ ];
41
+
42
+ /**
43
+ * @description Create a build context for the pipeline we're creating,
44
+ * this is shared among tasks so they all use the same settings for core pieces
45
+ * such as a target, distPath, externals etc.
46
+ *
47
+ * @type {(args: BuildContextArgs) => Promise<BuildContext>}
48
+ */
49
+ const createBuildContext = async ({ cwd, extMap, logger, pkg }) => {
50
+ const targets = {
51
+ '*': browserslistToEsbuild(pkg.browserslist ?? DEFAULT_BROWSERS_LIST_CONFIG),
52
+ node: browserslistToEsbuild(['node 16.0.0']),
53
+ web: ['esnext'],
54
+ };
55
+
56
+ const exports = parseExports({ extMap, pkg }).reduce((acc, x) => {
57
+ const { _path: exportPath, ...exportEntry } = x;
58
+
59
+ return { ...acc, [exportPath]: exportEntry };
60
+ }, {});
61
+
62
+ const external = [
63
+ ...(pkg.dependencies ? Object.keys(pkg.dependencies) : []),
64
+ ...(pkg.peerDependencies ? Object.keys(pkg.peerDependencies) : []),
65
+ ];
66
+
67
+ const outputPaths = Object.values(exports)
68
+ .flatMap((exportEntry) => {
69
+ return [exportEntry.import, exportEntry.require].filter(Boolean);
70
+ })
71
+ .map((p) => path.resolve(cwd, p));
72
+
73
+ const distPath = findCommonDirPath(outputPaths);
74
+
75
+ if (distPath === cwd) {
76
+ throw new Error(
77
+ 'all output files must share a common parent directory which is not the root package directory'
78
+ );
79
+ }
80
+
81
+ if (!distPath) {
82
+ throw new Error("could not detect 'dist' path");
83
+ }
84
+
85
+ return {
86
+ logger,
87
+ cwd,
88
+ pkg,
89
+ exports,
90
+ external,
91
+ distPath,
92
+ targets,
93
+ extMap,
94
+ };
95
+ };
96
+
97
+ /**
98
+ * @type {(containerPath: string, itemPath: string) => boolean}
99
+ */
100
+ const pathContains = (containerPath, itemPath) => {
101
+ return !path.relative(containerPath, itemPath).startsWith('..');
102
+ };
103
+
104
+ /**
105
+ * @type {(filePaths: string[]) => string | undefined}
106
+ */
107
+ const findCommonDirPath = (filePaths) => {
108
+ /**
109
+ * @type {string | undefined}
110
+ */
111
+ let commonPath;
112
+
113
+ for (const filePath of filePaths) {
114
+ let dirPath = path.dirname(filePath);
115
+
116
+ if (!commonPath) {
117
+ commonPath = dirPath;
118
+ // eslint-disable-next-line no-continue
119
+ continue;
120
+ }
121
+
122
+ while (dirPath !== commonPath) {
123
+ dirPath = path.dirname(dirPath);
124
+
125
+ if (dirPath === commonPath) {
126
+ break;
127
+ }
128
+
129
+ if (pathContains(dirPath, commonPath)) {
130
+ commonPath = dirPath;
131
+ break;
132
+ }
133
+
134
+ if (dirPath === '.') return undefined;
135
+ }
136
+ }
137
+
138
+ return commonPath;
139
+ };
140
+
141
+ /**
142
+ * @typedef {import('./tasks/vite').ViteTask | import('./tasks/dts').DtsTask} BuildTask
143
+ */
144
+
145
+ /**
146
+ * @description Create the build tasks for the pipeline, this
147
+ * comes from the exports map we've created in the build context.
148
+ * But handles each export line uniquely with space to add more
149
+ * as the standard develops.
150
+ *
151
+ * @type {(args: BuildContext) => Promise<BuildTask[]>}
152
+ */
153
+ const createBuildTasks = async (ctx) => {
154
+ /**
155
+ * @type {BuildTask[]}
156
+ */
157
+ const tasks = [];
158
+
159
+ /**
160
+ * @type {import('./tasks/dts').DtsTask}
161
+ */
162
+ const dtsTask = {
163
+ type: 'build:dts',
164
+ entries: [],
165
+ };
166
+
167
+ /**
168
+ * @type {Record<string, import('./tasks/vite').ViteTask>}
169
+ */
170
+ const viteTasks = {};
171
+
172
+ const createViteTask = (format, runtime, { output, ...restEntry }) => {
173
+ const buildId = `${format}:${output}`;
174
+
175
+ if (viteTasks[buildId]) {
176
+ viteTasks[buildId].entries.push(restEntry);
177
+
178
+ if (output !== viteTasks[buildId].output) {
179
+ ctx.logger.warn(
180
+ 'Multiple entries with different outputs for the same format are not supported. The first output will be used.'
181
+ );
182
+ }
183
+ } else {
184
+ viteTasks[buildId] = {
185
+ type: 'build:js',
186
+ format,
187
+ output,
188
+ runtime,
189
+ entries: [restEntry],
190
+ };
191
+ }
192
+ };
193
+
194
+ const exps = Object.entries(ctx.exports).map(([exportPath, exportEntry]) => ({
195
+ ...exportEntry,
196
+ _path: exportPath,
197
+ }));
198
+
199
+ for (const exp of exps) {
200
+ if (exp.types) {
201
+ const importId = path.join(ctx.pkg.name, exp._path);
202
+
203
+ dtsTask.entries.push({
204
+ importId,
205
+ exportPath: exp._path,
206
+ sourcePath: exp.source,
207
+ targetPath: exp.types,
208
+ });
209
+ }
210
+
211
+ /**
212
+ * @type {keyof Target}
213
+ */
214
+ // eslint-disable-next-line no-nested-ternary
215
+ const runtime = exp._path.includes('strapi-admin')
216
+ ? 'web'
217
+ : exp._path.includes('strapi-server')
218
+ ? 'node'
219
+ : '*';
220
+
221
+ if (exp.require) {
222
+ /**
223
+ * register CJS task
224
+ */
225
+ createViteTask('cjs', runtime, {
226
+ path: exp._path,
227
+ entry: exp.source,
228
+ output: exp.require,
229
+ });
230
+ }
231
+
232
+ if (exp.import) {
233
+ /**
234
+ * register ESM task
235
+ */
236
+ createViteTask('es', runtime, {
237
+ path: exp._path,
238
+ entry: exp.source,
239
+ output: exp.import,
240
+ });
241
+ }
242
+ }
243
+
244
+ tasks.push(dtsTask, ...Object.values(viteTasks));
245
+
246
+ return tasks;
247
+ };
248
+
249
+ module.exports = {
250
+ createBuildContext,
251
+ createBuildTasks,
252
+ };
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const ts = require('typescript');
7
+
8
+ /**
9
+ * @description Load a tsconfig.json file and return the parsed config
10
+ *
11
+ * @internal
12
+ *
13
+ * @type {(args: { cwd: string; path: string }) => Promise<ts.ParsedCommandLine>)}
14
+ */
15
+ const loadTsConfig = async ({ cwd, path }) => {
16
+ const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, path);
17
+
18
+ if (!configPath) {
19
+ throw new TSConfigNotFoundError(`could not find a valid '${path}'`);
20
+ }
21
+
22
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
23
+
24
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, cwd);
25
+ };
26
+
27
+ class TSConfigNotFoundError extends Error {
28
+ // eslint-disable-next-line no-useless-constructor
29
+ constructor(message, options) {
30
+ super(message, options);
31
+ }
32
+
33
+ get code() {
34
+ return 'TS_CONFIG_NOT_FOUND';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @description
40
+ *
41
+ * @internal
42
+ *
43
+ * @type {(args: { cwd: string; logger: import('../../utils/logger').Logger; outDir: string; tsconfig: ts.ParsedCommandLine }) => Promise<void>}
44
+ */
45
+ const buildTypes = ({ cwd, logger, outDir, tsconfig }) => {
46
+ const compilerOptions = {
47
+ ...tsconfig.options,
48
+ declaration: true,
49
+ declarationDir: outDir,
50
+ emitDeclarationOnly: true,
51
+ noEmit: false,
52
+ outDir,
53
+ };
54
+
55
+ const program = ts.createProgram(tsconfig.fileNames, compilerOptions);
56
+
57
+ const emitResult = program.emit();
58
+
59
+ const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
60
+
61
+ for (const diagnostic of allDiagnostics) {
62
+ if (diagnostic.file && diagnostic.start) {
63
+ const { line, character } = ts.getLineAndCharacterOfPosition(
64
+ diagnostic.file,
65
+ diagnostic.start
66
+ );
67
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
68
+
69
+ const file = path.relative(cwd, diagnostic.file.fileName);
70
+
71
+ const output = [
72
+ `${chalk.cyan(file)}:${chalk.cyan(line + 1)}:${chalk.cyan(character + 1)} - `,
73
+ `${chalk.gray(`TS${diagnostic.code}:`)} ${message}`,
74
+ ].join('');
75
+
76
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
77
+ logger.error(output);
78
+ }
79
+
80
+ if (diagnostic.category === ts.DiagnosticCategory.Warning) {
81
+ logger.warn(output);
82
+ }
83
+
84
+ if (diagnostic.category === ts.DiagnosticCategory.Message) {
85
+ logger.info(output);
86
+ }
87
+
88
+ if (diagnostic.category === ts.DiagnosticCategory.Suggestion) {
89
+ logger.info(output);
90
+ }
91
+ } else {
92
+ logger.info(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
93
+ }
94
+ }
95
+
96
+ if (emitResult.emitSkipped) {
97
+ const errors = allDiagnostics.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
98
+
99
+ if (errors.length) {
100
+ throw new Error('Failed to compile TypeScript definitions');
101
+ }
102
+ }
103
+ };
104
+
105
+ /**
106
+ * @typedef {Object} DtsTaskEntry
107
+ * @property {string} exportPath
108
+ * @property {string} sourcePath
109
+ * @property {string} targetPath
110
+ */
111
+
112
+ /**
113
+ * @typedef {Object} DtsTask
114
+ * @property {"build:dts"} type
115
+ * @property {DtsTaskEntry[]} entries
116
+ */
117
+
118
+ /**
119
+ * @type {import('./index').TaskHandler<DtsTask>}
120
+ */
121
+ const dtsTask = {
122
+ _spinner: null,
123
+ print(ctx, task) {
124
+ const entries = [
125
+ ' entries:',
126
+ ...task.entries.map((entry) =>
127
+ [
128
+ ` – `,
129
+ chalk.green(`${entry.importId} `),
130
+ `${chalk.cyan(entry.sourcePath)} ${chalk.gray('→')} ${chalk.cyan(entry.targetPath)}`,
131
+ ].join('')
132
+ ),
133
+ '',
134
+ ];
135
+
136
+ this._spinner = ora(`Building type files:\n`).start();
137
+
138
+ ctx.logger.log([...entries].join('\n'));
139
+ },
140
+ async run(ctx, task) {
141
+ try {
142
+ await Promise.all(
143
+ task.entries.map(async (entry) => {
144
+ const config = await loadTsConfig({
145
+ /**
146
+ * TODO: this will not scale and assumes all project sourcePaths are `src/index.ts`
147
+ * so we can go back to the "root" of the project...
148
+ */
149
+ cwd: path.join(ctx.cwd, entry.sourcePath, '..', '..'),
150
+ path: 'tsconfig.build.json',
151
+ }).catch((err) => {
152
+ if (err instanceof TSConfigNotFoundError) {
153
+ return undefined;
154
+ }
155
+
156
+ throw err;
157
+ });
158
+
159
+ if (config) {
160
+ ctx.logger.debug(`TS config for '${entry.sourcePath}': \n`, config);
161
+ } else {
162
+ ctx.logger.warn(
163
+ `You've added a types entry but no tsconfig.json was found for ${entry.targetPath}. Skipping...`
164
+ );
165
+
166
+ return;
167
+ }
168
+
169
+ const { outDir } = config.raw.compilerOptions;
170
+
171
+ if (!outDir) {
172
+ throw new Error("tsconfig.json is missing 'compilerOptions.outDir'");
173
+ }
174
+
175
+ await buildTypes({
176
+ cwd: ctx.cwd,
177
+ logger: ctx.logger,
178
+ outDir: path.relative(ctx.cwd, outDir),
179
+ tsconfig: config,
180
+ });
181
+ })
182
+ );
183
+
184
+ await this.success(ctx, task);
185
+ } catch (err) {
186
+ this.fail(ctx, task, err);
187
+ }
188
+ },
189
+ async success() {
190
+ this._spinner.succeed('Built type files');
191
+ },
192
+ async fail(ctx, task, err) {
193
+ this._spinner.fail('Failed to build type files');
194
+
195
+ throw err;
196
+ },
197
+ };
198
+
199
+ module.exports = { dtsTask };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { dtsTask } = require('./dts');
4
+ const { viteTask } = require('./vite');
5
+
6
+ /**
7
+ * @template Task
8
+ * @param {Task}
9
+ * @returns {Task}
10
+ *
11
+ * @typedef {Object} TaskHandler
12
+ * @property {(ctx: import("../packages").BuildContext, task: Task) => import('ora').Ora} print
13
+ * @property {(ctx: import("../packages").BuildContext, task: Task) => Promise<void>} run
14
+ * @property {(ctx: import("../packages").BuildContext, task: Task) => Promise<void>} success
15
+ * @property {(ctx: import("../packages").BuildContext, task: Task, err: unknown) => Promise<void>} fail
16
+ * @property {import('ora').Ora | null} _spinner
17
+ */
18
+
19
+ /**
20
+ * @type {{ "build:js": TaskHandler<import("./vite").ViteTask>; "build:dts": TaskHandler<import("./dts").DtsTask>; }}}
21
+ */
22
+ const buildTaskHandlers = {
23
+ 'build:js': viteTask,
24
+ 'build:dts': dtsTask,
25
+ };
26
+
27
+ module.exports = {
28
+ buildTaskHandlers,
29
+ };