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,37 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('type-fest').PackageJson} PackageJson
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isPluginPackageName } from './plugin-name.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {PackageJson} pkg
|
|
12
|
+
* @param {Set<string>} loaded
|
|
13
|
+
* @param {string} packageJsonPath
|
|
14
|
+
*/
|
|
15
|
+
export async function loadPlugins(pkg, loaded, packageJsonPath) {
|
|
16
|
+
const resolveFromPackage = createRequire(packageJsonPath).resolve;
|
|
17
|
+
return await Promise.all(
|
|
18
|
+
[
|
|
19
|
+
...new Set([
|
|
20
|
+
...Object.keys(pkg.devDependencies || {}),
|
|
21
|
+
...Object.keys(pkg.dependencies || {}),
|
|
22
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
23
|
+
]),
|
|
24
|
+
]
|
|
25
|
+
.filter(packageName => isPluginPackageName(packageName) && !loaded.has(packageName))
|
|
26
|
+
.map(async packageName => {
|
|
27
|
+
loaded.add(packageName);
|
|
28
|
+
try {
|
|
29
|
+
const pluginPath = resolveFromPackage(packageName);
|
|
30
|
+
const pluginFactory = await import(pathToFileURL(pluginPath).href);
|
|
31
|
+
return await pluginFactory.create();
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
throw new Error(`Failed to load Build plugin ${JSON.stringify(packageName)} from ${packageJsonPath}`, { cause });
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getTimeDiff } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} sourceDir
|
|
5
|
+
* @param {string} dir
|
|
6
|
+
* @param {number} configsCount
|
|
7
|
+
* @param {number} startingTime
|
|
8
|
+
* @param {number} [finishedCount]
|
|
9
|
+
*/
|
|
10
|
+
export const mainLoggerText =
|
|
11
|
+
(sourceDir, dir, configsCount, startingTime, finishedCount = 0) =>
|
|
12
|
+
(final = false) =>
|
|
13
|
+
`${sourceDir} → ${dir} ${final ? configsCount : finishedCount++} / ${configsCount}${final ? ` in ${getTimeDiff(startingTime)}` : ''}`;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').PackageJson} PackageJson */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @returns {value is PackageJson}
|
|
6
|
+
*/
|
|
7
|
+
export function isPackageJson(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const obj = /** @type {Record<string, unknown>} */ (value);
|
|
12
|
+
if (obj.private !== undefined && typeof obj.private !== 'boolean') return false;
|
|
13
|
+
if (obj.version !== undefined && typeof obj.version !== 'string') return false;
|
|
14
|
+
if (obj.name !== undefined && typeof obj.name !== 'string') return false;
|
|
15
|
+
if (obj.main !== undefined && typeof obj.main !== 'string') return false;
|
|
16
|
+
if (obj.license !== undefined && typeof obj.license !== 'string') return false;
|
|
17
|
+
if (obj.readme !== undefined && typeof obj.readme !== 'string') return false;
|
|
18
|
+
if (obj.description !== undefined && typeof obj.description !== 'string') return false;
|
|
19
|
+
if (obj.bugs !== undefined && typeof obj.bugs !== 'string') return false;
|
|
20
|
+
if (obj.homepage !== undefined && typeof obj.homepage !== 'string') return false;
|
|
21
|
+
if (obj.bin !== undefined && typeof obj.bin !== 'string' && (typeof obj.bin !== 'object' || obj.bin === null || Array.isArray(obj.bin)))
|
|
22
|
+
return false;
|
|
23
|
+
if (
|
|
24
|
+
obj.author !== undefined &&
|
|
25
|
+
typeof obj.author !== 'string' &&
|
|
26
|
+
(typeof obj.author !== 'object' || obj.author === null || Array.isArray(obj.author))
|
|
27
|
+
)
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} value
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
function CommaSeparatedString(value) {
|
|
37
|
+
if (value === '') return [];
|
|
38
|
+
return value.split(',').map((/** @type {string} */ arg) => arg.trim());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} value
|
|
43
|
+
* @returns {true | string[]}
|
|
44
|
+
*/
|
|
45
|
+
function CommaSeparatedStringOrBoolean(value) {
|
|
46
|
+
if (value === '') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return CommaSeparatedString(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const cliFlagsDefaults = {
|
|
53
|
+
formats: /** @type {string[]} */ (['es', 'cjs']),
|
|
54
|
+
umd: /** @type {string[]} */ ([]),
|
|
55
|
+
compress: /** @type {string[]} */ (['umd']),
|
|
56
|
+
sourcemaps: /** @type {string[]} */ (['umd']),
|
|
57
|
+
preprocess: /** @type {string[]} */ ([]),
|
|
58
|
+
dest: 'dist',
|
|
59
|
+
src: 'src',
|
|
60
|
+
bin: /** @type {string[] | undefined} */ (undefined),
|
|
61
|
+
includeExternals: /** @type {boolean | string[]} */ (false),
|
|
62
|
+
imports: false,
|
|
63
|
+
conditions: /** @type {string[]} */ ([]),
|
|
64
|
+
eject: false,
|
|
65
|
+
tsConfig: false,
|
|
66
|
+
updatePackageJson: true,
|
|
67
|
+
commonjsPattern: '[name].cjs',
|
|
68
|
+
esmPattern: '[name].mjs',
|
|
69
|
+
umdPattern: '[name].umd.js',
|
|
70
|
+
formatPackageJson: false,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {{
|
|
75
|
+
* type: 'string' | 'boolean' | ((value: string) => unknown);
|
|
76
|
+
* description: string;
|
|
77
|
+
* default?: string | boolean | string[] | boolean[];
|
|
78
|
+
* optionalValue?: boolean;
|
|
79
|
+
* }} CliFlag
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** @type {Record<string, CliFlag>} */
|
|
83
|
+
export const cliFlags = {
|
|
84
|
+
umd: {
|
|
85
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
86
|
+
description: 'Package subpath exports in UMD format',
|
|
87
|
+
},
|
|
88
|
+
compress: {
|
|
89
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
90
|
+
description: 'Compress formats using terser',
|
|
91
|
+
},
|
|
92
|
+
sourcemaps: {
|
|
93
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
94
|
+
description: 'Emit sourcemaps for the specified formats',
|
|
95
|
+
},
|
|
96
|
+
formats: {
|
|
97
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
98
|
+
description: 'Formats to emit',
|
|
99
|
+
},
|
|
100
|
+
preprocess: {
|
|
101
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
102
|
+
description: 'Preprocess entry points / subpath exports',
|
|
103
|
+
},
|
|
104
|
+
dest: {
|
|
105
|
+
type: /** @type {'string'} */ ('string'),
|
|
106
|
+
description: 'Output directory',
|
|
107
|
+
default: cliFlagsDefaults.dest,
|
|
108
|
+
},
|
|
109
|
+
src: {
|
|
110
|
+
type: /** @type {'string'} */ ('string'),
|
|
111
|
+
description: 'Source directory',
|
|
112
|
+
default: cliFlagsDefaults.src,
|
|
113
|
+
},
|
|
114
|
+
bin: {
|
|
115
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
116
|
+
description: 'Executable files',
|
|
117
|
+
},
|
|
118
|
+
includeExternals: {
|
|
119
|
+
type: /** @type {(value: string) => true | string[]} */ (CommaSeparatedStringOrBoolean),
|
|
120
|
+
description: 'Include all/specified externals into the result bundle(s)',
|
|
121
|
+
optionalValue: true,
|
|
122
|
+
},
|
|
123
|
+
imports: {
|
|
124
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
125
|
+
description: 'Build and preserve package imports',
|
|
126
|
+
default: cliFlagsDefaults.imports,
|
|
127
|
+
},
|
|
128
|
+
conditions: {
|
|
129
|
+
type: /** @type {(value: string) => string[]} */ (CommaSeparatedString),
|
|
130
|
+
description: 'Additional conditions for resolving bundled dependencies',
|
|
131
|
+
},
|
|
132
|
+
eject: {
|
|
133
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
134
|
+
description: 'Eject config',
|
|
135
|
+
default: cliFlagsDefaults.eject,
|
|
136
|
+
},
|
|
137
|
+
tsConfig: {
|
|
138
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
139
|
+
description: 'Create / update tsconfig.json',
|
|
140
|
+
default: cliFlagsDefaults.tsConfig,
|
|
141
|
+
},
|
|
142
|
+
updatePackageJson: {
|
|
143
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
144
|
+
description: 'Create / update package.json',
|
|
145
|
+
default: cliFlagsDefaults.updatePackageJson,
|
|
146
|
+
},
|
|
147
|
+
commonjsPattern: {
|
|
148
|
+
type: /** @type {'string'} */ ('string'),
|
|
149
|
+
description: 'CommonJS output file name pattern',
|
|
150
|
+
default: cliFlagsDefaults.commonjsPattern,
|
|
151
|
+
},
|
|
152
|
+
esmPattern: {
|
|
153
|
+
type: /** @type {'string'} */ ('string'),
|
|
154
|
+
description: 'ES output file name pattern',
|
|
155
|
+
default: cliFlagsDefaults.esmPattern,
|
|
156
|
+
},
|
|
157
|
+
umdPattern: {
|
|
158
|
+
type: /** @type {'string'} */ ('string'),
|
|
159
|
+
description: 'UMD output file name pattern',
|
|
160
|
+
default: cliFlagsDefaults.umdPattern,
|
|
161
|
+
},
|
|
162
|
+
formatPackageJson: {
|
|
163
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
164
|
+
description: 'Format package.json',
|
|
165
|
+
default: cliFlagsDefaults.formatPackageJson,
|
|
166
|
+
},
|
|
167
|
+
pack: {
|
|
168
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
169
|
+
description: 'Pack',
|
|
170
|
+
default: true,
|
|
171
|
+
},
|
|
172
|
+
exports: {
|
|
173
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
174
|
+
description: 'Add exports field to package.json',
|
|
175
|
+
default: true,
|
|
176
|
+
},
|
|
177
|
+
clean: {
|
|
178
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
179
|
+
description: 'Clean the output directory',
|
|
180
|
+
default: true,
|
|
181
|
+
},
|
|
182
|
+
bundle: {
|
|
183
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
184
|
+
description: 'Bundle',
|
|
185
|
+
default: true,
|
|
186
|
+
},
|
|
187
|
+
removeLegalComments: {
|
|
188
|
+
type: /** @type {'boolean'} */ ('boolean'),
|
|
189
|
+
description: 'Remove legal comments',
|
|
190
|
+
default: false,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const packageJsonFieldsOrder = new Set([
|
|
195
|
+
'private',
|
|
196
|
+
'type',
|
|
197
|
+
'version',
|
|
198
|
+
'name',
|
|
199
|
+
'scope', // custom
|
|
200
|
+
'description',
|
|
201
|
+
'license',
|
|
202
|
+
'author',
|
|
203
|
+
'contributors',
|
|
204
|
+
'funding',
|
|
205
|
+
'bin',
|
|
206
|
+
'main',
|
|
207
|
+
'browser',
|
|
208
|
+
'unpkg',
|
|
209
|
+
'module',
|
|
210
|
+
'svelte',
|
|
211
|
+
'exports',
|
|
212
|
+
'imports',
|
|
213
|
+
'types',
|
|
214
|
+
'typings',
|
|
215
|
+
'typesVersions', // non standard but required for typescript with resolution other than nodenext
|
|
216
|
+
'files',
|
|
217
|
+
'packageManager',
|
|
218
|
+
'sideEffects',
|
|
219
|
+
'engines',
|
|
220
|
+
'os',
|
|
221
|
+
'cpu',
|
|
222
|
+
'man',
|
|
223
|
+
'directories',
|
|
224
|
+
'repository',
|
|
225
|
+
'bugs',
|
|
226
|
+
'homepage',
|
|
227
|
+
'readme',
|
|
228
|
+
'keywords',
|
|
229
|
+
'scripts',
|
|
230
|
+
'config',
|
|
231
|
+
'dependencies',
|
|
232
|
+
'devDependencies',
|
|
233
|
+
'peerDependencies',
|
|
234
|
+
'peerDependenciesMeta',
|
|
235
|
+
'bundleDependencies',
|
|
236
|
+
'bundledDependencies',
|
|
237
|
+
'optionalDependencies',
|
|
238
|
+
'overrides',
|
|
239
|
+
'publishConfig',
|
|
240
|
+
'workspaces',
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {PackageJson} pkg
|
|
245
|
+
* @param {(key: string) => boolean} needTreatment
|
|
246
|
+
* @param {(key: string) => unknown} treatKey
|
|
247
|
+
* @returns {PackageJson}
|
|
248
|
+
*/
|
|
249
|
+
export function processPackageJson(pkg, needTreatment, treatKey) {
|
|
250
|
+
/** @type {PackageJson} */
|
|
251
|
+
const newPkg = {};
|
|
252
|
+
|
|
253
|
+
for (const key of packageJsonFieldsOrder) {
|
|
254
|
+
if (needTreatment(key)) {
|
|
255
|
+
/** @type {Record<string, unknown>} */ (newPkg)[key] = treatKey(key);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const key in pkg) {
|
|
260
|
+
if (!packageJsonFieldsOrder.has(key)) {
|
|
261
|
+
/** @type {Record<string, unknown>} */ (newPkg)[key] = /** @type {Record<string, unknown>} */ (pkg)[key];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return newPkg;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @template {object | null | number | string | boolean} T
|
|
270
|
+
* @param {T} json
|
|
271
|
+
* @param {string | null} [current] Existing file contents used to detect indentation.
|
|
272
|
+
* @returns {string}
|
|
273
|
+
*/
|
|
274
|
+
export function toFormattedJson(json, current) {
|
|
275
|
+
const indent = current?.match(/^[ \t]+(?=\S)/m)?.[0] ?? 2;
|
|
276
|
+
return `${JSON.stringify(json, null, indent)}\n`;
|
|
277
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* private?: boolean;
|
|
4
|
+
* version?: string;
|
|
5
|
+
* name?: string;
|
|
6
|
+
* bin?: string | Record<string, string>;
|
|
7
|
+
* main?: string;
|
|
8
|
+
* license?: string;
|
|
9
|
+
* readme?: string;
|
|
10
|
+
* author?: string | { name?: string; email?: string; url?: string; };
|
|
11
|
+
* description?: string;
|
|
12
|
+
* scripts?: { [key: string]: string; };
|
|
13
|
+
* repository?: { type?: string; url?: string; };
|
|
14
|
+
* files?: string[];
|
|
15
|
+
* bugs?: string;
|
|
16
|
+
* homepage?: string;
|
|
17
|
+
* dependencies?: Record<string, string>;
|
|
18
|
+
* devDependencies?: Record<string, string>;
|
|
19
|
+
* peerDependencies?: Record<string, string>;
|
|
20
|
+
* exports?: Record<string, string | Record<string, string>>;
|
|
21
|
+
* }} PackageJson
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { BuildEntryError, sourceFileExtensions } from './build-entries.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./types.js').BuildConfiguration} BuildConfiguration
|
|
9
|
+
* @typedef {import('./types.js').BuildEntryIssue} BuildEntryIssue
|
|
10
|
+
* @typedef {import('./types.js').ImportTarget} ImportTarget
|
|
11
|
+
* @typedef {import('./types.js').BuildFormat} BuildFormat
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const javascriptExtensions = new Set(['.js', '.mjs', '.cjs']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find every local JavaScript leaf of an authored package imports map.
|
|
18
|
+
* Non-JavaScript and package-specifier leaves remain the responsibility of
|
|
19
|
+
* their runtime resolver or producer.
|
|
20
|
+
*
|
|
21
|
+
* @param {unknown} imports
|
|
22
|
+
* @param {BuildConfiguration} configuration
|
|
23
|
+
* @param {unknown} packageType
|
|
24
|
+
* @returns {Promise<ImportTarget[]>}
|
|
25
|
+
*/
|
|
26
|
+
export async function collectPackageImportTargets(imports, configuration, packageType) {
|
|
27
|
+
if (imports === undefined || !configuration.resolution.imports) return [];
|
|
28
|
+
|
|
29
|
+
/** @type {BuildEntryIssue[]} */
|
|
30
|
+
const issues = [];
|
|
31
|
+
/** @type {Map<string, ImportTarget>} */
|
|
32
|
+
const targets = new Map();
|
|
33
|
+
const leaves = parseImportMap(imports, issues);
|
|
34
|
+
for (const leaf of leaves) {
|
|
35
|
+
for (const target of await expandLocalTarget(leaf, configuration, packageType, issues)) {
|
|
36
|
+
if (!targets.has(target.outputPath)) targets.set(target.outputPath, target);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (issues.length > 0) throw new BuildEntryError(issues);
|
|
41
|
+
return [...targets.values()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk the authored map without selecting runtime conditions. Every local
|
|
46
|
+
* branch may need its own output, while package specifiers remain mappings.
|
|
47
|
+
* @param {unknown} imports
|
|
48
|
+
* @param {BuildEntryIssue[]} issues
|
|
49
|
+
*/
|
|
50
|
+
function parseImportMap(imports, issues) {
|
|
51
|
+
/** @type {{ value: string; issuePath: string; wildcardKey: boolean }[]} */
|
|
52
|
+
const leaves = [];
|
|
53
|
+
if (!isRecord(imports)) {
|
|
54
|
+
issues.push({ code: 'INVALID_IMPORT_MAP', path: 'package.imports', message: 'must be an object' });
|
|
55
|
+
return leaves;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [key, value] of Object.entries(imports)) {
|
|
59
|
+
const issuePath = `package.imports[${JSON.stringify(key)}]`;
|
|
60
|
+
const stars = key.match(/\*/g)?.length ?? 0;
|
|
61
|
+
if (!key.startsWith('#') || key === '#' || key.endsWith('/') || stars > 1) {
|
|
62
|
+
issues.push({ code: 'INVALID_IMPORT_KEY', path: issuePath, message: `Invalid package import key ${JSON.stringify(key)}` });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
visit(value, issuePath, stars > 0);
|
|
66
|
+
}
|
|
67
|
+
return leaves;
|
|
68
|
+
|
|
69
|
+
/** @param {unknown} value @param {string} issuePath @param {boolean} wildcardKey */
|
|
70
|
+
function visit(value, issuePath, wildcardKey) {
|
|
71
|
+
if (value === null) return;
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
for (const [index, leaf] of value.entries()) visit(leaf, `${issuePath}[${index}]`, wildcardKey);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isRecord(value)) {
|
|
77
|
+
for (const [condition, leaf] of Object.entries(value)) {
|
|
78
|
+
if (isArrayIndex(condition)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
code: 'INVALID_IMPORT_CONDITION',
|
|
81
|
+
path: `${issuePath}.${condition}`,
|
|
82
|
+
message: `Integer condition key ${JSON.stringify(condition)} is invalid in a package imports map`,
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
visit(leaf, `${issuePath}.${condition}`, wildcardKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== 'string') {
|
|
91
|
+
issues.push({ code: 'INVALID_IMPORT_TARGET', path: issuePath, message: 'must be a string, null, array, or condition object' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!value.startsWith('./')) {
|
|
95
|
+
if (value.startsWith('.') || value.startsWith('/') || value.includes('\\') || value.length === 0 || URL.canParse(value)) {
|
|
96
|
+
issues.push({
|
|
97
|
+
code: 'INVALID_IMPORT_TARGET',
|
|
98
|
+
path: issuePath,
|
|
99
|
+
message: `Invalid package import target ${JSON.stringify(value)}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
leaves.push({ value, issuePath, wildcardKey });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {string} key */
|
|
109
|
+
function isArrayIndex(key) {
|
|
110
|
+
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate a local URL target, then expand its output pattern against source
|
|
115
|
+
* names. Final source-extension selection happens in resolveBuildEntries.
|
|
116
|
+
* @param {{ value: string; issuePath: string; wildcardKey: boolean }} leaf
|
|
117
|
+
* @param {BuildConfiguration} configuration
|
|
118
|
+
* @param {unknown} packageType
|
|
119
|
+
* @param {BuildEntryIssue[]} issues
|
|
120
|
+
* @returns {Promise<ImportTarget[]>}
|
|
121
|
+
*/
|
|
122
|
+
async function expandLocalTarget({ value, issuePath, wildcardKey }, configuration, packageType, issues) {
|
|
123
|
+
const pathPart = value.split(/[?#]/, 1)[0];
|
|
124
|
+
const rawSegments = pathPart.slice(2).split('/');
|
|
125
|
+
let outputPath;
|
|
126
|
+
try {
|
|
127
|
+
if (
|
|
128
|
+
pathPart.includes('\\') ||
|
|
129
|
+
rawSegments.some(segment => {
|
|
130
|
+
const decoded = decodeURIComponent(segment);
|
|
131
|
+
return (
|
|
132
|
+
decoded === '' ||
|
|
133
|
+
decoded === '.' ||
|
|
134
|
+
decoded === '..' ||
|
|
135
|
+
decoded.toLowerCase() === 'node_modules' ||
|
|
136
|
+
decoded.includes('/') ||
|
|
137
|
+
decoded.includes('\\')
|
|
138
|
+
);
|
|
139
|
+
})
|
|
140
|
+
) {
|
|
141
|
+
throw new Error('invalid path segment');
|
|
142
|
+
}
|
|
143
|
+
outputPath = fileURLToPath(new URL(value, pathToFileURL(path.join(process.cwd(), 'package.json'))));
|
|
144
|
+
} catch {
|
|
145
|
+
issues.push({
|
|
146
|
+
code: 'INVALID_IMPORT_TARGET',
|
|
147
|
+
path: issuePath,
|
|
148
|
+
message: `Invalid local package import target ${JSON.stringify(value)}`,
|
|
149
|
+
});
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const extension = path.extname(outputPath);
|
|
154
|
+
if (!javascriptExtensions.has(extension)) return [];
|
|
155
|
+
|
|
156
|
+
const outputDir = path.resolve(configuration.paths.outputDir);
|
|
157
|
+
if (!outputPath.startsWith(`${outputDir}${path.sep}`) || (pathPart.includes('*') && !wildcardKey)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
code: 'INVALID_IMPORT_TARGET',
|
|
160
|
+
path: issuePath,
|
|
161
|
+
message: `Local JavaScript target ${JSON.stringify(value)} must be a safe path beneath ${configuration.paths.outputDir}`,
|
|
162
|
+
});
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const format = /** @type {BuildFormat} */ (
|
|
167
|
+
extension === '.mjs' ? 'es' : extension === '.cjs' ? 'cjs' : packageType === 'module' ? 'es' : 'cjs'
|
|
168
|
+
);
|
|
169
|
+
if (!configuration.outputs.formats.includes(format)) {
|
|
170
|
+
issues.push({
|
|
171
|
+
code: 'EXCLUDED_IMPORT_FORMAT',
|
|
172
|
+
path: issuePath,
|
|
173
|
+
message: `Local target ${JSON.stringify(value)} requires ${format}, which is excluded by outputs.formats`,
|
|
174
|
+
});
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const relativeOutput = path.relative(outputDir, outputPath).replaceAll('\\', '/');
|
|
179
|
+
const sourceName = relativeOutput.slice(0, -extension.length);
|
|
180
|
+
if (pathPart.includes('*')) {
|
|
181
|
+
const sourceNames = await findMatchingSourceNames(configuration.paths.sourceDir, sourceName);
|
|
182
|
+
if (sourceNames.length === 0) {
|
|
183
|
+
issues.push({
|
|
184
|
+
code: 'SOURCE_NOT_FOUND',
|
|
185
|
+
path: issuePath,
|
|
186
|
+
message: `Import target ${JSON.stringify(value)} has no supported source file`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return sourceNames.map(matchedName => ({
|
|
190
|
+
sourceName: matchedName,
|
|
191
|
+
outputPath: `./${configuration.paths.outputDir}/${matchedName}${extension}`,
|
|
192
|
+
format,
|
|
193
|
+
issuePath,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
return [{ sourceName, outputPath: `./${configuration.paths.outputDir}/${relativeOutput}`, format, issuePath }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
200
|
+
function isRecord(value) {
|
|
201
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @param {string} sourceDir @param {string} pattern */
|
|
205
|
+
async function findMatchingSourceNames(sourceDir, pattern) {
|
|
206
|
+
const parts = pattern.split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
207
|
+
const matcher = new RegExp(
|
|
208
|
+
`^${parts[0]}(.+?)${parts[1]}${parts
|
|
209
|
+
.slice(2)
|
|
210
|
+
.map(part => `\\1${part}`)
|
|
211
|
+
.join('')}$`
|
|
212
|
+
);
|
|
213
|
+
/** @type {Set<string>} */
|
|
214
|
+
const names = new Set();
|
|
215
|
+
await walk(sourceDir, '');
|
|
216
|
+
return [...names].sort();
|
|
217
|
+
|
|
218
|
+
/** @param {string} directory @param {string} relative */
|
|
219
|
+
async function walk(directory, relative) {
|
|
220
|
+
let entries;
|
|
221
|
+
try {
|
|
222
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT') return;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const nextRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
await walk(path.join(directory, entry.name), nextRelative);
|
|
231
|
+
} else if (entry.isFile()) {
|
|
232
|
+
const extension = path.posix.extname(nextRelative).slice(1);
|
|
233
|
+
if (sourceFileExtensions.includes(/** @type {typeof sourceFileExtensions[number]} */ (extension))) {
|
|
234
|
+
const name = nextRelative.slice(0, -(extension.length + 1));
|
|
235
|
+
if (matcher.test(name)) names.add(name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|