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.
@@ -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
+ }