pkgbld 1.36.0 → 2.1.0
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.
- package/README.md +71 -89
- package/index.js +2 -2
- package/package.json +31 -27
- package/src/build-configuration.js +406 -0
- package/src/build-entries.js +271 -0
- package/src/build-plugin-lifecycle.js +100 -0
- package/src/builtin-plugins/binify.js +31 -0
- package/src/builtin-plugins/clean.js +30 -0
- package/src/builtin-plugins/commonjs.js +14 -0
- package/src/builtin-plugins/externals.js +108 -0
- package/src/builtin-plugins/json.js +14 -0
- package/src/builtin-plugins/package-imports.js +79 -0
- package/src/builtin-plugins/preprocess.js +37 -0
- package/src/builtin-plugins/resolve.js +22 -0
- package/src/builtin-plugins/terser.js +50 -0
- package/src/eject.js +151 -0
- package/src/get-json.js +16 -0
- package/src/get-plugins.js +44 -0
- package/src/get-rollup-configs.js +261 -0
- package/src/helpers.js +229 -0
- package/src/index.js +133 -0
- package/src/load-plugins.js +37 -0
- package/src/messages.js +13 -0
- package/src/options/index.js +277 -0
- package/src/options/types.js +24 -0
- package/src/package-imports.js +240 -0
- package/src/plugin-name.js +6 -0
- package/src/priorities.js +11 -0
- package/src/process-pkg.js +244 -0
- package/src/process-ts-config.js +80 -0
- package/src/rollup-plugin-preprocess.d.ts +1 -0
- package/src/types.js +175 -0
- package/src/write-json.js +21 -0
- package/types/index.d.ts +310 -0
- package/dist/index.d.ts +0 -79
- package/dist/index.mjs +0 -1626
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { parseArgsPlus } from '@niceties/node-parseargs-plus';
|
|
2
|
+
import { camelCase } from '@niceties/node-parseargs-plus/camel-case';
|
|
3
|
+
import { customValue } from '@niceties/node-parseargs-plus/custom-value';
|
|
4
|
+
import { help } from '@niceties/node-parseargs-plus/help';
|
|
5
|
+
|
|
6
|
+
import { cliFlags, cliFlagsDefaults as defaults } from './options/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('type-fest').PackageJson} PackageJson
|
|
10
|
+
* @typedef {import('./types.js').BuildFormat} BuildFormat
|
|
11
|
+
* @typedef {import('./types.js').BuildConfiguration} BuildConfiguration
|
|
12
|
+
* @typedef {import('./types.js').BuildConfigurationDraft} BuildConfigurationDraft
|
|
13
|
+
* @typedef {import('./types.js').ParsedOptions} ParsedOptions
|
|
14
|
+
* @typedef {ReturnType<typeof import('./build-plugin-lifecycle.js').createBuildPluginLifecycle>} BuildPluginLifecycle
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const formats = new Set(['es', 'cjs', 'umd']);
|
|
18
|
+
|
|
19
|
+
export class BuildConfigurationError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {{ code: string; path: string; message: string; value?: unknown }[]} issues
|
|
22
|
+
* @param {{ cause?: unknown }} [options]
|
|
23
|
+
*/
|
|
24
|
+
constructor(issues, options) {
|
|
25
|
+
super(`Invalid Build configuration:\n${issues.map(issue => `- ${issue.path}: ${issue.message}`).join('\n')}`, options);
|
|
26
|
+
this.name = 'BuildConfigurationError';
|
|
27
|
+
this.issues = issues;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the immutable Build configuration for one build.
|
|
33
|
+
*
|
|
34
|
+
* @param {{
|
|
35
|
+
* argv?: readonly string[];
|
|
36
|
+
* packageJson: PackageJson;
|
|
37
|
+
* pluginLifecycle: BuildPluginLifecycle;
|
|
38
|
+
* }} input
|
|
39
|
+
* @returns {BuildConfiguration}
|
|
40
|
+
*/
|
|
41
|
+
export function resolveBuildConfiguration({ argv = process.argv.slice(2), packageJson, pluginLifecycle }) {
|
|
42
|
+
const draft = createDefaultDraft();
|
|
43
|
+
const defaultSource = deepFreeze(structuredClone(draft));
|
|
44
|
+
applyPackageMetadata(draft, packageJson);
|
|
45
|
+
|
|
46
|
+
const cli = parseCli(argv, packageJson);
|
|
47
|
+
applyCli(draft, cli.values, cli.provided);
|
|
48
|
+
|
|
49
|
+
const sources = deepFreeze({
|
|
50
|
+
defaults: defaultSource,
|
|
51
|
+
package: structuredClone(packageJson),
|
|
52
|
+
cli: {
|
|
53
|
+
values: structuredClone(cli.values),
|
|
54
|
+
provided: { ...cli.provided },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
pluginLifecycle.configure(draft, sources);
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new BuildConfigurationError(
|
|
62
|
+
[{ code: 'PLUGIN_CONFIGURATION_FAILED', path: 'plugins', message: 'Build plugin configuration failed' }],
|
|
63
|
+
{ cause }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const structuralIssues = validateStructure(createDefaultDraft(), draft);
|
|
68
|
+
if (structuralIssues.length > 0) {
|
|
69
|
+
throw new BuildConfigurationError(structuralIssues);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
normalize(draft);
|
|
73
|
+
const issues = validate(draft, packageJson);
|
|
74
|
+
if (issues.length > 0) {
|
|
75
|
+
throw new BuildConfigurationError(issues);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return /** @type {BuildConfiguration} */ (deepFreeze(draft));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @returns {BuildConfigurationDraft} */
|
|
82
|
+
function createDefaultDraft() {
|
|
83
|
+
return {
|
|
84
|
+
paths: {
|
|
85
|
+
sourceDir: defaults.src,
|
|
86
|
+
outputDir: defaults.dest,
|
|
87
|
+
},
|
|
88
|
+
outputs: {
|
|
89
|
+
formats: /** @type {BuildFormat[]} */ ([...defaults.formats]),
|
|
90
|
+
patterns: {
|
|
91
|
+
es: defaults.esmPattern,
|
|
92
|
+
cjs: defaults.commonjsPattern,
|
|
93
|
+
umd: defaults.umdPattern,
|
|
94
|
+
},
|
|
95
|
+
umdEntries: [...defaults.umd],
|
|
96
|
+
sourcemaps: /** @type {BuildFormat[]} */ ([...defaults.sourcemaps]),
|
|
97
|
+
},
|
|
98
|
+
transforms: {
|
|
99
|
+
compress: /** @type {BuildFormat[]} */ ([...defaults.compress]),
|
|
100
|
+
preprocess: [...defaults.preprocess],
|
|
101
|
+
includeExternals: defaults.includeExternals,
|
|
102
|
+
removeLegalComments: false,
|
|
103
|
+
},
|
|
104
|
+
resolution: {
|
|
105
|
+
imports: defaults.imports,
|
|
106
|
+
conditions: [...defaults.conditions],
|
|
107
|
+
},
|
|
108
|
+
packageJson: {
|
|
109
|
+
update: true,
|
|
110
|
+
format: defaults.formatPackageJson,
|
|
111
|
+
exports: true,
|
|
112
|
+
pack: true,
|
|
113
|
+
executables: { mode: 'infer', values: [] },
|
|
114
|
+
},
|
|
115
|
+
typescript: {
|
|
116
|
+
updateConfig: defaults.tsConfig,
|
|
117
|
+
},
|
|
118
|
+
execution: {
|
|
119
|
+
clean: true,
|
|
120
|
+
bundle: true,
|
|
121
|
+
eject: defaults.eject,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {BuildConfigurationDraft} draft
|
|
128
|
+
* @param {PackageJson} packageJson
|
|
129
|
+
*/
|
|
130
|
+
function applyPackageMetadata(draft, packageJson) {
|
|
131
|
+
if (packageJson.imports !== undefined) {
|
|
132
|
+
draft.resolution.imports = true;
|
|
133
|
+
}
|
|
134
|
+
if (typeof packageJson.umd === 'string') {
|
|
135
|
+
draft.outputs.umdEntries.push('index');
|
|
136
|
+
draft.outputs.formats.push('umd');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {readonly string[]} argv
|
|
142
|
+
* @param {PackageJson} packageJson
|
|
143
|
+
*/
|
|
144
|
+
function parseCli(argv, packageJson) {
|
|
145
|
+
const result = parseArgsPlus(
|
|
146
|
+
{
|
|
147
|
+
args: [...argv],
|
|
148
|
+
name: 'pkgbld',
|
|
149
|
+
version: packageJson.version ?? '<unknown>',
|
|
150
|
+
options: /** @type {any} */ (cliFlags),
|
|
151
|
+
allowNegative: true,
|
|
152
|
+
tokens: true,
|
|
153
|
+
},
|
|
154
|
+
[help, camelCase, customValue]
|
|
155
|
+
);
|
|
156
|
+
/** @type {Record<string, boolean>} */
|
|
157
|
+
const provided = {};
|
|
158
|
+
for (const token of result.tokens ?? []) {
|
|
159
|
+
if (token.kind === 'option') {
|
|
160
|
+
provided[toCamelCase(token.name)] = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { values: /** @type {ParsedOptions} */ (result.values), provided };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {BuildConfigurationDraft} draft
|
|
168
|
+
* @param {ParsedOptions} flags
|
|
169
|
+
* @param {Record<string, boolean>} provided
|
|
170
|
+
*/
|
|
171
|
+
function applyCli(draft, flags, provided) {
|
|
172
|
+
if (provided.formats) {
|
|
173
|
+
draft.outputs.formats = [.../** @type {BuildFormat[]} */ (flags.formats)];
|
|
174
|
+
if (!draft.outputs.formats.includes('umd') && !provided.umd) {
|
|
175
|
+
draft.outputs.umdEntries = [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (provided.umd) {
|
|
179
|
+
draft.outputs.umdEntries = [.../** @type {string[]} */ (flags.umd)];
|
|
180
|
+
if (draft.outputs.umdEntries.length === 0 && !provided.formats) {
|
|
181
|
+
draft.outputs.formats = draft.outputs.formats.filter(format => format !== 'umd');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (provided.compress) draft.transforms.compress = [.../** @type {BuildFormat[]} */ (flags.compress)];
|
|
185
|
+
if (provided.sourcemaps) draft.outputs.sourcemaps = [.../** @type {BuildFormat[]} */ (flags.sourcemaps)];
|
|
186
|
+
if (provided.preprocess) draft.transforms.preprocess = [.../** @type {string[]} */ (flags.preprocess)];
|
|
187
|
+
if (provided.dest) draft.paths.outputDir = /** @type {string} */ (flags.dest);
|
|
188
|
+
if (provided.src) draft.paths.sourceDir = /** @type {string} */ (flags.src);
|
|
189
|
+
if (provided.bin) {
|
|
190
|
+
const values = /** @type {string[]} */ (flags.bin);
|
|
191
|
+
draft.packageJson.executables = { mode: values.length > 0 ? 'explicit' : 'disabled', values: [...values] };
|
|
192
|
+
}
|
|
193
|
+
if (provided.includeExternals) {
|
|
194
|
+
draft.transforms.includeExternals = /** @type {boolean | string[]} */ (flags.includeExternals);
|
|
195
|
+
}
|
|
196
|
+
if (provided.imports) draft.resolution.imports = /** @type {boolean} */ (flags.imports);
|
|
197
|
+
if (provided.conditions) draft.resolution.conditions = [.../** @type {string[]} */ (flags.conditions)];
|
|
198
|
+
if (provided.eject) draft.execution.eject = /** @type {boolean} */ (flags.eject);
|
|
199
|
+
if (provided.tsConfig) draft.typescript.updateConfig = /** @type {boolean} */ (flags.tsConfig);
|
|
200
|
+
if (provided.updatePackageJson) draft.packageJson.update = /** @type {boolean} */ (flags.updatePackageJson);
|
|
201
|
+
if (provided.commonjsPattern) draft.outputs.patterns.cjs = /** @type {string} */ (flags.commonjsPattern);
|
|
202
|
+
if (provided.esmPattern) draft.outputs.patterns.es = /** @type {string} */ (flags.esmPattern);
|
|
203
|
+
if (provided.umdPattern) draft.outputs.patterns.umd = /** @type {string} */ (flags.umdPattern);
|
|
204
|
+
if (provided.formatPackageJson) draft.packageJson.format = /** @type {boolean} */ (flags.formatPackageJson);
|
|
205
|
+
if (provided.pack) draft.packageJson.pack = /** @type {boolean} */ (flags.pack);
|
|
206
|
+
if (provided.exports) draft.packageJson.exports = /** @type {boolean} */ (flags.exports);
|
|
207
|
+
if (provided.clean) draft.execution.clean = /** @type {boolean} */ (flags.clean);
|
|
208
|
+
if (provided.bundle) draft.execution.bundle = /** @type {boolean} */ (flags.bundle);
|
|
209
|
+
if (provided.removeLegalComments) {
|
|
210
|
+
draft.transforms.removeLegalComments = /** @type {boolean} */ (flags.removeLegalComments);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @param {BuildConfigurationDraft} draft */
|
|
215
|
+
function normalize(draft) {
|
|
216
|
+
if (Array.isArray(draft.outputs?.formats)) draft.outputs.formats = unique(draft.outputs.formats);
|
|
217
|
+
if (Array.isArray(draft.outputs?.umdEntries)) draft.outputs.umdEntries = unique(draft.outputs.umdEntries);
|
|
218
|
+
if (Array.isArray(draft.outputs?.sourcemaps)) draft.outputs.sourcemaps = unique(draft.outputs.sourcemaps);
|
|
219
|
+
if (Array.isArray(draft.transforms?.compress)) draft.transforms.compress = unique(draft.transforms.compress);
|
|
220
|
+
if (Array.isArray(draft.transforms?.preprocess)) draft.transforms.preprocess = unique(draft.transforms.preprocess);
|
|
221
|
+
if (Array.isArray(draft.resolution?.conditions)) draft.resolution.conditions = unique(draft.resolution.conditions);
|
|
222
|
+
if (Array.isArray(draft.transforms.includeExternals)) {
|
|
223
|
+
draft.transforms.includeExternals = unique(draft.transforms.includeExternals);
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(draft.packageJson?.executables?.values)) {
|
|
226
|
+
draft.packageJson.executables.values = unique(draft.packageJson.executables.values.filter(Boolean));
|
|
227
|
+
if (draft.packageJson.executables.mode === 'explicit' && draft.packageJson.executables.values.length === 0) {
|
|
228
|
+
draft.packageJson.executables.mode = 'disabled';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
Array.isArray(draft.outputs?.umdEntries) &&
|
|
233
|
+
Array.isArray(draft.outputs?.formats) &&
|
|
234
|
+
draft.outputs.umdEntries.length > 0 &&
|
|
235
|
+
!draft.outputs.formats.includes('umd')
|
|
236
|
+
) {
|
|
237
|
+
draft.outputs.formats.push('umd');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {BuildConfigurationDraft} draft
|
|
243
|
+
* @param {PackageJson} packageJson
|
|
244
|
+
*/
|
|
245
|
+
function validate(draft, packageJson) {
|
|
246
|
+
/** @type {{ code: string; path: string; message: string; value?: unknown }[]} */
|
|
247
|
+
const issues = [];
|
|
248
|
+
validateFormats(draft.outputs.formats, 'outputs.formats', issues);
|
|
249
|
+
validateFormats(draft.outputs.sourcemaps, 'outputs.sourcemaps', issues);
|
|
250
|
+
validateFormats(draft.transforms.compress, 'transforms.compress', issues);
|
|
251
|
+
validateStrings(draft.outputs.umdEntries, 'outputs.umdEntries', issues);
|
|
252
|
+
validateStrings(draft.transforms.preprocess, 'transforms.preprocess', issues);
|
|
253
|
+
validateStrings(draft.resolution.conditions, 'resolution.conditions', issues);
|
|
254
|
+
if (draft.outputs.umdEntries.length > 0 && typeof packageJson.name !== 'string') {
|
|
255
|
+
issues.push({ code: 'PACKAGE_NAME_REQUIRED', path: 'package.name', message: 'a package name is required for UMD entries' });
|
|
256
|
+
}
|
|
257
|
+
for (const [format, pattern] of Object.entries(draft.outputs.patterns)) {
|
|
258
|
+
if (typeof pattern !== 'string' || !pattern.includes('[name]')) {
|
|
259
|
+
issues.push({
|
|
260
|
+
code: 'INVALID_FILE_NAME_PATTERN',
|
|
261
|
+
path: `outputs.patterns.${format}`,
|
|
262
|
+
message: 'must contain [name]',
|
|
263
|
+
value: pattern,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const [path, value] of [
|
|
268
|
+
['paths.sourceDir', draft.paths.sourceDir],
|
|
269
|
+
['paths.outputDir', draft.paths.outputDir],
|
|
270
|
+
]) {
|
|
271
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
272
|
+
issues.push({ code: 'INVALID_PATH', path, message: 'must be a non-empty string', value });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
typeof draft.transforms.includeExternals !== 'boolean' &&
|
|
277
|
+
(!Array.isArray(draft.transforms.includeExternals) ||
|
|
278
|
+
draft.transforms.includeExternals.some(value => typeof value !== 'string' || value.length === 0))
|
|
279
|
+
) {
|
|
280
|
+
issues.push({
|
|
281
|
+
code: 'INVALID_EXTERNALS',
|
|
282
|
+
path: 'transforms.includeExternals',
|
|
283
|
+
message: 'must be a boolean or an array of non-empty strings',
|
|
284
|
+
value: draft.transforms.includeExternals,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (!['infer', 'disabled', 'explicit'].includes(draft.packageJson.executables.mode)) {
|
|
288
|
+
issues.push({
|
|
289
|
+
code: 'INVALID_EXECUTABLE_MODE',
|
|
290
|
+
path: 'packageJson.executables.mode',
|
|
291
|
+
message: 'must be infer, disabled, or explicit',
|
|
292
|
+
value: draft.packageJson.executables.mode,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
validateStrings(draft.packageJson.executables.values, 'packageJson.executables.values', issues);
|
|
296
|
+
return issues;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {object} expected
|
|
301
|
+
* @param {unknown} actual
|
|
302
|
+
* @returns {{ code: string; path: string; message: string; value?: unknown }[]}
|
|
303
|
+
*/
|
|
304
|
+
function validateStructure(expected, actual) {
|
|
305
|
+
/** @type {{ code: string; path: string; message: string; value?: unknown }[]} */
|
|
306
|
+
const issues = [];
|
|
307
|
+
visit(expected, actual, '');
|
|
308
|
+
return issues;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {unknown} expected
|
|
312
|
+
* @param {unknown} actual
|
|
313
|
+
* @param {string} path
|
|
314
|
+
*/
|
|
315
|
+
function visit(expected, actual, path) {
|
|
316
|
+
if (Array.isArray(expected)) {
|
|
317
|
+
if (!Array.isArray(actual)) {
|
|
318
|
+
issues.push({ code: 'INVALID_CONFIGURATION_SHAPE', path, message: 'must be an array', value: actual });
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (typeof expected !== 'object' || expected == null) {
|
|
323
|
+
const isExternalList = path === 'transforms.includeExternals' && Array.isArray(actual);
|
|
324
|
+
if (!isExternalList && typeof actual !== typeof expected) {
|
|
325
|
+
issues.push({
|
|
326
|
+
code: 'INVALID_CONFIGURATION_SHAPE',
|
|
327
|
+
path,
|
|
328
|
+
message: `must be ${typeof expected}`,
|
|
329
|
+
value: actual,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (typeof actual !== 'object' || actual == null || Array.isArray(actual)) {
|
|
335
|
+
issues.push({ code: 'INVALID_CONFIGURATION_SHAPE', path, message: 'must be an object', value: actual });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
for (const key of Object.keys(actual)) {
|
|
339
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
340
|
+
if (!(key in expected)) {
|
|
341
|
+
issues.push({ code: 'UNKNOWN_CONFIGURATION_KEY', path: keyPath, message: 'is not a recognized configuration key' });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const key of Object.keys(expected)) {
|
|
345
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
346
|
+
if (!(key in actual)) {
|
|
347
|
+
issues.push({ code: 'MISSING_CONFIGURATION_KEY', path: keyPath, message: 'is required' });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
visit(/** @type {Record<string, unknown>} */ (expected)[key], /** @type {Record<string, unknown>} */ (actual)[key], keyPath);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {unknown} values
|
|
357
|
+
* @param {string} path
|
|
358
|
+
* @param {{ code: string; path: string; message: string; value?: unknown }[]} issues
|
|
359
|
+
*/
|
|
360
|
+
function validateFormats(values, path, issues) {
|
|
361
|
+
if (!Array.isArray(values)) {
|
|
362
|
+
issues.push({ code: 'INVALID_FORMATS', path, message: 'must be an array', value: values });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const value of values) {
|
|
366
|
+
if (!formats.has(value)) {
|
|
367
|
+
issues.push({ code: 'UNSUPPORTED_FORMAT', path, message: `unsupported format ${JSON.stringify(value)}`, value });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {unknown} values
|
|
374
|
+
* @param {string} path
|
|
375
|
+
* @param {{ code: string; path: string; message: string; value?: unknown }[]} issues
|
|
376
|
+
*/
|
|
377
|
+
function validateStrings(values, path, issues) {
|
|
378
|
+
if (!Array.isArray(values)) {
|
|
379
|
+
issues.push({ code: 'INVALID_LIST', path, message: 'must be an array', value: values });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const value of values) {
|
|
383
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
384
|
+
issues.push({ code: 'INVALID_LIST_VALUE', path, message: 'must contain non-empty strings', value });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @template T @param {T[]} values @returns {T[]} */
|
|
390
|
+
function unique(values) {
|
|
391
|
+
return [...new Set(values)];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {string} value */
|
|
395
|
+
function toCamelCase(value) {
|
|
396
|
+
return value.replace(/-([a-z])/g, (_, character) => character.toUpperCase());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @template T @param {T} value @returns {T} */
|
|
400
|
+
function deepFreeze(value) {
|
|
401
|
+
if (typeof value !== 'object' || value == null || Object.isFrozen(value)) return value;
|
|
402
|
+
for (const nested of Object.values(value)) {
|
|
403
|
+
deepFreeze(nested);
|
|
404
|
+
}
|
|
405
|
+
return Object.freeze(value);
|
|
406
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { isExists } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./types.js').BuildConfiguration} BuildConfiguration
|
|
7
|
+
* @typedef {import('./types.js').BuildEntries} BuildEntries
|
|
8
|
+
* @typedef {import('./types.js').BuildEntry} BuildEntry
|
|
9
|
+
* @typedef {import('./types.js').BuildEntryContribution} BuildEntryContribution
|
|
10
|
+
* @typedef {import('./types.js').BuildEntryContributions} BuildEntryContributions
|
|
11
|
+
* @typedef {import('./types.js').BuildEntryIssue} BuildEntryIssue
|
|
12
|
+
* @typedef {import('./types.js').BuildFormat} BuildFormat
|
|
13
|
+
* @typedef {import('./types.js').ImportTarget} ImportTarget
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const sourceFileExtensions = /** @type {const} */ (['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs']);
|
|
17
|
+
|
|
18
|
+
export class BuildEntryError extends Error {
|
|
19
|
+
/** @param {BuildEntryIssue[]} issues */
|
|
20
|
+
constructor(issues) {
|
|
21
|
+
super(issues.map(issue => `${issue.path}: ${issue.message}`).join('\n'));
|
|
22
|
+
this.name = 'BuildEntryError';
|
|
23
|
+
this.issues = issues;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve package-declared and Build plugin-contributed entry specifications into
|
|
29
|
+
* one immutable catalog.
|
|
30
|
+
*
|
|
31
|
+
* @param {readonly string[]} packageEntryNames
|
|
32
|
+
* @param {readonly ImportTarget[]} importTargets
|
|
33
|
+
* @param {BuildConfiguration} configuration
|
|
34
|
+
* @param {(contributions: BuildEntryContributions) => void} contribute
|
|
35
|
+
* @returns {Promise<BuildEntries>}
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveBuildEntries(packageEntryNames, importTargets, configuration, contribute) {
|
|
38
|
+
/** @type {(BuildEntryContribution & { issuePath: string; origin: 'export' | 'plugin'; manifestPath: string })[]} */
|
|
39
|
+
const specifications = packageEntryNames.map((name, index) => ({
|
|
40
|
+
name,
|
|
41
|
+
issuePath: `package.entries[${index}]`,
|
|
42
|
+
origin: 'export',
|
|
43
|
+
manifestPath: `package.exports[${JSON.stringify(name === 'index' ? '.' : `./${name}`)}]`,
|
|
44
|
+
}));
|
|
45
|
+
let contributionIndex = 0;
|
|
46
|
+
const contributions = {
|
|
47
|
+
/** @param {BuildEntryContribution} contribution */
|
|
48
|
+
add(contribution) {
|
|
49
|
+
const issuePath = `plugins.entries[${contributionIndex}]`;
|
|
50
|
+
specifications.push({ ...contribution, issuePath, origin: 'plugin', manifestPath: issuePath });
|
|
51
|
+
contributionIndex += 1;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
contribute(contributions);
|
|
55
|
+
|
|
56
|
+
/** @type {BuildEntryIssue[]} */
|
|
57
|
+
const issues = [];
|
|
58
|
+
/** @type {BuildEntry[]} */
|
|
59
|
+
const values = [];
|
|
60
|
+
/** @type {Map<string, BuildEntry>} */
|
|
61
|
+
const byName = new Map();
|
|
62
|
+
|
|
63
|
+
for (const specification of specifications) {
|
|
64
|
+
const { issuePath } = specification;
|
|
65
|
+
const name = normalizeName(specification.name, issuePath, issues);
|
|
66
|
+
if (!name) continue;
|
|
67
|
+
if (byName.has(name)) {
|
|
68
|
+
issues.push({
|
|
69
|
+
code: 'DUPLICATE_BUILD_ENTRY',
|
|
70
|
+
path: issuePath,
|
|
71
|
+
name,
|
|
72
|
+
message: `Build entry ${JSON.stringify(name)} is declared more than once`,
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const source = await resolveSource(name, specification.sourcePath, configuration.paths.sourceDir);
|
|
78
|
+
if (!source) {
|
|
79
|
+
issues.push({
|
|
80
|
+
code: 'SOURCE_NOT_FOUND',
|
|
81
|
+
path: issuePath,
|
|
82
|
+
name,
|
|
83
|
+
message: `Build entry ${JSON.stringify(name)} has no supported source file`,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {Partial<Record<BuildFormat, string>>} */
|
|
89
|
+
const outputPaths = {};
|
|
90
|
+
for (const format of configuration.outputs.formats) {
|
|
91
|
+
if (format !== 'umd' || configuration.outputs.umdEntries.includes(name)) {
|
|
92
|
+
outputPaths[format] =
|
|
93
|
+
`./${configuration.paths.outputDir}/${configuration.outputs.patterns[format].replace('[name]', name)}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const entry = Object.freeze({
|
|
97
|
+
name,
|
|
98
|
+
origin: specification.origin,
|
|
99
|
+
manifestPath: specification.manifestPath,
|
|
100
|
+
sourcePath: source.sourcePath,
|
|
101
|
+
extension: source.extension,
|
|
102
|
+
outputPaths: Object.freeze(outputPaths),
|
|
103
|
+
});
|
|
104
|
+
values.push(entry);
|
|
105
|
+
byName.set(name, entry);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const target of importTargets) {
|
|
109
|
+
const name = `@imports/${target.outputPath.slice(2)}`;
|
|
110
|
+
if (byName.has(name)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
code: 'DUPLICATE_BUILD_ENTRY',
|
|
113
|
+
path: target.issuePath,
|
|
114
|
+
name,
|
|
115
|
+
message: `Build entry ${JSON.stringify(name)} is declared more than once`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const source = await resolveSource(target.sourceName, undefined, configuration.paths.sourceDir);
|
|
120
|
+
if (!source) {
|
|
121
|
+
issues.push({
|
|
122
|
+
code: 'SOURCE_NOT_FOUND',
|
|
123
|
+
path: target.issuePath,
|
|
124
|
+
name,
|
|
125
|
+
message: `Import target ${JSON.stringify(target.outputPath)} has no supported source file`,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const entry = Object.freeze({
|
|
130
|
+
name,
|
|
131
|
+
origin: /** @type {const} */ ('import'),
|
|
132
|
+
manifestPath: target.issuePath,
|
|
133
|
+
sourcePath: source.sourcePath,
|
|
134
|
+
extension: source.extension,
|
|
135
|
+
outputPaths: Object.freeze({ [target.format]: target.outputPath }),
|
|
136
|
+
});
|
|
137
|
+
values.push(entry);
|
|
138
|
+
byName.set(name, entry);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
validateSelections(byName, configuration.outputs.umdEntries, 'outputs.umdEntries', issues);
|
|
142
|
+
validateSelections(byName, configuration.transforms.preprocess, 'transforms.preprocess', issues);
|
|
143
|
+
const shared = validateOutputPaths(values, issues);
|
|
144
|
+
|
|
145
|
+
if (issues.length > 0) throw new BuildEntryError(issues);
|
|
146
|
+
|
|
147
|
+
for (const entry of shared) byName.delete(entry.name);
|
|
148
|
+
const frozenValues = Object.freeze(values.filter(entry => !shared.has(entry)));
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
values: frozenValues,
|
|
151
|
+
/** @param {string} name */
|
|
152
|
+
require(name) {
|
|
153
|
+
const entry = byName.get(name);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
throw new BuildEntryError([
|
|
156
|
+
{
|
|
157
|
+
code: 'SELECTED_BUILD_ENTRY_NOT_FOUND',
|
|
158
|
+
path: 'entries',
|
|
159
|
+
name,
|
|
160
|
+
message: `Build entry ${JSON.stringify(name)} was not discovered; available entries: ${frozenValues.map(entry => entry.name).join(', ')}`,
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
return entry;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {unknown} value
|
|
171
|
+
* @param {string} issuePath
|
|
172
|
+
* @param {BuildEntryIssue[]} issues
|
|
173
|
+
*/
|
|
174
|
+
function normalizeName(value, issuePath, issues) {
|
|
175
|
+
if (typeof value !== 'string') {
|
|
176
|
+
issues.push({ code: 'INVALID_BUILD_ENTRY_NAME', path: issuePath, message: 'Build entry name must be a string' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const name = value.replaceAll('\\', '/');
|
|
180
|
+
const segments = name.split('/');
|
|
181
|
+
if (
|
|
182
|
+
name.length === 0 ||
|
|
183
|
+
name.startsWith('/') ||
|
|
184
|
+
name.startsWith('./') ||
|
|
185
|
+
path.isAbsolute(name) ||
|
|
186
|
+
segments.some(segment => segment === '' || segment === '.' || segment === '..')
|
|
187
|
+
) {
|
|
188
|
+
issues.push({
|
|
189
|
+
code: 'INVALID_BUILD_ENTRY_NAME',
|
|
190
|
+
path: issuePath,
|
|
191
|
+
name,
|
|
192
|
+
message: `Invalid Build entry name ${JSON.stringify(value)}`,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} name
|
|
201
|
+
* @param {string | undefined} providedPath
|
|
202
|
+
* @param {string} sourceDir
|
|
203
|
+
* @returns {Promise<{ sourcePath: string; extension: import('./types.js').BuildEntryExtension } | undefined>}
|
|
204
|
+
*/
|
|
205
|
+
async function resolveSource(name, providedPath, sourceDir) {
|
|
206
|
+
if (providedPath != null) {
|
|
207
|
+
if (!(await isExists(providedPath))) return;
|
|
208
|
+
const extension = path.extname(providedPath).slice(1);
|
|
209
|
+
if (!sourceFileExtensions.includes(/** @type {typeof sourceFileExtensions[number]} */ (extension))) return;
|
|
210
|
+
return { sourcePath: providedPath, extension: /** @type {import('./types.js').BuildEntryExtension} */ (extension) };
|
|
211
|
+
}
|
|
212
|
+
for (const extension of sourceFileExtensions) {
|
|
213
|
+
const sourcePath = `./${sourceDir}/${name}.${extension}`;
|
|
214
|
+
if (await isExists(sourcePath)) return { sourcePath, extension };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {Map<string, BuildEntry>} byName
|
|
220
|
+
* @param {readonly string[]} names
|
|
221
|
+
* @param {string} selectionPath
|
|
222
|
+
* @param {BuildEntryIssue[]} issues
|
|
223
|
+
*/
|
|
224
|
+
function validateSelections(byName, names, selectionPath, issues) {
|
|
225
|
+
for (const [index, name] of names.entries()) {
|
|
226
|
+
if (!byName.has(name)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
code: 'SELECTED_BUILD_ENTRY_NOT_FOUND',
|
|
229
|
+
path: `${selectionPath}[${index}]`,
|
|
230
|
+
name,
|
|
231
|
+
message: `Build entry ${JSON.stringify(name)} was selected but not discovered`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {BuildEntry[]} entries
|
|
239
|
+
* @param {BuildEntryIssue[]} issues
|
|
240
|
+
*/
|
|
241
|
+
function validateOutputPaths(entries, issues) {
|
|
242
|
+
/** @type {Map<string, { entry: BuildEntry; format: string }>} */
|
|
243
|
+
const owners = new Map();
|
|
244
|
+
/** @type {Set<BuildEntry>} */
|
|
245
|
+
const shared = new Set();
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
for (const [format, outputPath] of Object.entries(entry.outputPaths)) {
|
|
248
|
+
const normalizedPath = path.resolve(outputPath);
|
|
249
|
+
const owner = owners.get(normalizedPath);
|
|
250
|
+
if (owner) {
|
|
251
|
+
if (
|
|
252
|
+
entry.origin === 'import' &&
|
|
253
|
+
owner.format === format &&
|
|
254
|
+
path.resolve(entry.sourcePath) === path.resolve(owner.entry.sourcePath)
|
|
255
|
+
) {
|
|
256
|
+
shared.add(entry);
|
|
257
|
+
} else {
|
|
258
|
+
issues.push({
|
|
259
|
+
code: 'OUTPUT_PATH_COLLISION',
|
|
260
|
+
path: entry.manifestPath,
|
|
261
|
+
name: entry.name,
|
|
262
|
+
message: `Output path ${JSON.stringify(outputPath)} conflicts with ${owner.entry.manifestPath}`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
owners.set(normalizedPath, { entry, format });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return shared;
|
|
271
|
+
}
|