pkgbld 1.35.1 → 2.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # pkgbld
2
2
 
3
- *Build your libraries with ease*
3
+ _Build your libraries with ease_
4
4
 
5
5
  Rollup-based build tool for building libraries based on package.json config and simple CLI options.
6
6
 
@@ -15,6 +15,7 @@ It is created to easily build libraries that contain multiple subpath exports (e
15
15
  ## Installation
16
16
 
17
17
  Using npm:
18
+
18
19
  ```
19
20
  npm install --save-dev pkgbld
20
21
  ```
@@ -23,7 +24,7 @@ npm install --save-dev pkgbld
23
24
 
24
25
  1. Start by creating package.json using `npm init`
25
26
  2. Add pkgbld `npm install --save-dev pkgbld`
26
- 3. Create `src/index.ts`
27
+ 3. Create `src/index.js`
27
28
  4. Add pkgbld in the 'scripts' field of your package.json like:
28
29
 
29
30
  ```json
@@ -34,6 +35,10 @@ npm install --save-dev pkgbld
34
35
 
35
36
  Run `npm run build`.
36
37
 
38
+ For TypeScript or TSX sources, also install
39
+ [`pkgbld-plugin-swc`](https://github.com/kshutkin/package-build/tree/main/pkgbld-plugin-swc).
40
+ `pkgbld` discovers the plugin from your project dependencies and uses SWC to strip types.
41
+
37
42
  ## package.json
38
43
 
39
44
  `pkgbld` expects the name field to be filled in the package.json file. `exports` field defines what entries/outputs should be built for this package.
@@ -194,76 +199,36 @@ pkgbld --no-exports
194
199
 
195
200
  Do not add exports field in package.json.
196
201
 
197
- ### prune (command)
198
-
199
- ```
200
- pkgbld prune
201
- ```
202
-
203
- prune devDependencies and redundant scripts from package.json
204
-
205
- ### prune --profile=<profile>
206
-
207
- There are two profiles: `library` and `app`. `library` is default.
208
-
209
- Right now it only affects how `prune` command removes entries in the `scripts` field.
210
-
211
- For `library` profile it retains: 'preinstall', 'install', 'postinstall', 'prepublish', 'preprepare', 'prepare', 'postprepare'.
212
-
213
- For `app` profile it retains in addition: 'prestart', 'start', 'poststart', 'prerestart', 'restart', 'postrestart', 'prestop', 'stop', 'poststop', 'pretest', 'test', 'posttest'.
214
-
215
- ### flatten
216
-
217
- ```
218
- pkgbld prune --flatten=<directory>
219
- ```
220
-
221
- Flattens file structure by moving all files from `dist` or other directory to the root directory and updating package.json.
202
+ This also disables entry-point discovery from an existing `exports` field. Only the top-level `src/index` entry point is built
203
+ unless a plugin provides additional inputs.
222
204
 
223
- If the directory is not specified it is guessed from package.json.
205
+ ## Build plugin interface
224
206
 
225
- If files cannot be copied because of name conflicts the command will fail.
226
-
227
- ### removeSourcemaps
228
-
229
- ```
230
- pkgbld prune --remove-sourcemaps
231
- ```
232
-
233
- Removes all sourcemaps from the package. The logic is very simple and removes all files with `.map` extension and references in format `//# sourceMappingURL=<mapFile>`.
234
-
235
- ### optimizeFiles (default)
236
-
237
- ```
238
- pkgbld prune --optimize-files=false
239
- ```
240
-
241
- Optimizes files by removing all files that are not required for pack at the given moment.
242
-
243
- You might want to disable this option in some edge cases.
244
-
245
- ### removeLegalComments
246
-
247
- ```
248
- pkgbld prune --remove-legal-comments --compress=es,cjs
249
- ```
207
+ `pkgbld` loads plugins named `pkgbld-plugin-*` or `@scope/pkgbld-plugin-*` from
208
+ `dependencies`, `devDependencies`, and `peerDependencies`. The package name after
209
+ the optional scope must start with `pkgbld-plugin-`.
250
210
 
251
- Removes all legal comments from the package. Only works with compress.
211
+ Plugins implement one or more lifecycle methods on the object returned by the plugin module's `create()` function.
252
212
 
253
- ## Plugin API
213
+ Build configuration is resolved from defaults, package metadata, and explicit CLI options before `configure` runs. Plugins receive the effective mutable draft and have final authority. After all `configure` hooks finish, `pkgbld` normalizes, validates, and deeply freezes the configuration; every later hook receives that frozen value.
254
214
 
255
- `pkgbld` reads all installed packages named `pkgbld-plugin-*` and assumes they are plugins
215
+ Build entries are resolved after configuration. A plugin that needs to add a source module does so during `contributeEntries`; later phases receive immutable entries containing the canonical name, concrete source path, source extension, and enabled output paths. Configured UMD and preprocessing selections must resolve to discovered Build entries.
256
216
 
257
- Plugins suppose to implement one or more of the following interface methods on an object that returned by `create()` function exported by the plugin module.
217
+ `shared` is mutable state scoped to one build for coordination between plugins. Lifecycle phase boundaries are preserved, but plugin order within one phase is not guaranteed and asynchronous hooks in that phase may run in parallel. Plugins must not depend on the order of same-phase reads and writes. State owned by one plugin should remain in the closure created by `create()`.
258
218
 
259
219
  ```typescript
260
220
  interface PkgbldPlugin {
261
- options(parsedArgs: {[key: string]: string | number}, options: ReturnType<typeof getCliOptions>): void;
262
- processPackageJson(packageJson: PackageJson, inputs: string[], logger: Logger): void;
263
- processTsConfig(config: Json): void;
264
- providePlugins(provider: Provider, config: Record<string, string | string[] | boolean>, inputs: string[]): Promise<void>;
265
- getExtraOutputSettings(format: InternalModuleFormat, inputs: string[]): Partial<OutputOptions>;
266
- buildEnd(): Promise<void>;
221
+ configure(context: {
222
+ draft: BuildConfigurationDraft;
223
+ sources: BuildConfigurationSources;
224
+ shared: Map<unknown, unknown>;
225
+ }): void;
226
+ contributeEntries(context: PluginContributeEntriesContext): void;
227
+ processPackageJson(context: PluginPackageContext): void;
228
+ processTsConfig(context: PluginTsConfigContext): void;
229
+ providePlugins(context: PluginRollupContext): Promise<void>;
230
+ getExtraOutputSettings(context: PluginOutputContext): Partial<OutputOptions>;
231
+ buildEnd(context: PluginBuildEndContext): Promise<void>;
267
232
  }
268
233
  ```
269
234
 
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
- import('./dist/index.mjs');
2
+
3
+ import('./src/index.js');
package/package.json CHANGED
@@ -1,15 +1,23 @@
1
1
  {
2
- "version": "1.35.1",
2
+ "version": "2.0.0",
3
3
  "name": "pkgbld",
4
4
  "license": "MIT",
5
5
  "author": "Konstantin Shutkin",
6
6
  "bin": "./index.js",
7
7
  "type": "module",
8
- "main": "./dist/index.mjs",
9
- "types": "./dist/index.d.ts",
10
- "files": [
11
- "dist"
12
- ],
8
+ "main": "./src/index.js",
9
+ "types": "./types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./types/index.d.ts",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./options": {
16
+ "types": "./types/index.d.ts",
17
+ "default": "./src/options/index.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
13
21
  "engines": {
14
22
  "node": ">=20"
15
23
  },
@@ -28,28 +36,24 @@
28
36
  "rollup"
29
37
  ],
30
38
  "dependencies": {
31
- "@niceties/logger": "^1.1.13",
32
- "@niceties/draftlog-appender": "^1.3.3",
33
- "lodash": "^4.17.21",
34
- "rollup": "^4.34.7",
35
- "rollup-plugin-typescript2": "^0.36.0",
39
+ "@niceties/logger": "^2.1.1",
40
+ "@niceties/draftlog-appender": "^2.1.1",
41
+ "fast-is-equal": "^1.3.3",
42
+ "type-fest": "^5.9.0",
43
+ "rollup": "^4.63.1",
36
44
  "rollup-plugin-preprocess": "^0.0.4",
37
- "@rollup/plugin-commonjs": "^28.0.2",
38
- "@rollup/plugin-terser": "^0.4.4",
45
+ "@rollup/plugin-commonjs": "^29.0.3",
46
+ "@rollup/plugin-terser": "^1.0.0",
39
47
  "@rollup/plugin-json": "^6.1.0",
40
- "@rollup/plugin-node-resolve": "^16.0.0",
41
- "@rollup-extras/plugin-clean": "^1.3.9",
42
- "@rollup-extras/plugin-binify": "^1.1.10",
43
- "@rollup-extras/plugin-externals": "^1.2.2",
44
- "@slimlib/refine-partition": "^1.0.3",
45
- "@slimlib/smart-mock": "^0.1.6",
46
- "is-builtin-module": "^3.2.1",
47
- "terser": "^5.39.0",
48
- "kleur": "^4.1.5",
49
- "cleye": "^1.3.4",
50
- "jsonata": "^2.0.6"
51
- },
52
- "peerDependencies": {
53
- "typescript": ">=5.3.3"
48
+ "@rollup/plugin-node-resolve": "^16.0.3",
49
+ "@rollup-extras/plugin-clean": "^2.0.1",
50
+ "@rollup-extras/plugin-binify": "^2.0.0",
51
+ "@rollup-extras/plugin-externals": "^2.0.0",
52
+ "@slimlib/refine-partition": "^2.0.2",
53
+ "@slimlib/smart-mock": "^1.0.2",
54
+ "is-builtin-module": "^5.0.0",
55
+ "terser": "^5.51.2",
56
+ "@niceties/ansi": "^1.1.2",
57
+ "@niceties/node-parseargs-plus": "^0.6.0"
54
58
  }
55
59
  }
@@ -0,0 +1,395 @@
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
+ packageJson: {
105
+ update: true,
106
+ format: defaults.formatPackageJson,
107
+ exports: true,
108
+ pack: true,
109
+ executables: { mode: 'infer', values: [] },
110
+ },
111
+ typescript: {
112
+ updateConfig: defaults.tsConfig,
113
+ },
114
+ execution: {
115
+ clean: true,
116
+ bundle: true,
117
+ eject: defaults.eject,
118
+ },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * @param {BuildConfigurationDraft} draft
124
+ * @param {PackageJson} packageJson
125
+ */
126
+ function applyPackageMetadata(draft, packageJson) {
127
+ if (typeof packageJson.umd === 'string') {
128
+ draft.outputs.umdEntries.push('index');
129
+ draft.outputs.formats.push('umd');
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param {readonly string[]} argv
135
+ * @param {PackageJson} packageJson
136
+ */
137
+ function parseCli(argv, packageJson) {
138
+ const result = parseArgsPlus(
139
+ {
140
+ args: [...argv],
141
+ name: 'pkgbld',
142
+ version: packageJson.version ?? '<unknown>',
143
+ options: /** @type {any} */ (cliFlags),
144
+ allowNegative: true,
145
+ tokens: true,
146
+ },
147
+ [help, camelCase, customValue]
148
+ );
149
+ /** @type {Record<string, boolean>} */
150
+ const provided = {};
151
+ for (const token of result.tokens ?? []) {
152
+ if (token.kind === 'option') {
153
+ provided[toCamelCase(token.name)] = true;
154
+ }
155
+ }
156
+ return { values: /** @type {ParsedOptions} */ (result.values), provided };
157
+ }
158
+
159
+ /**
160
+ * @param {BuildConfigurationDraft} draft
161
+ * @param {ParsedOptions} flags
162
+ * @param {Record<string, boolean>} provided
163
+ */
164
+ function applyCli(draft, flags, provided) {
165
+ if (provided.formats) {
166
+ draft.outputs.formats = [.../** @type {BuildFormat[]} */ (flags.formats)];
167
+ if (!draft.outputs.formats.includes('umd') && !provided.umd) {
168
+ draft.outputs.umdEntries = [];
169
+ }
170
+ }
171
+ if (provided.umd) {
172
+ draft.outputs.umdEntries = [.../** @type {string[]} */ (flags.umd)];
173
+ if (draft.outputs.umdEntries.length === 0 && !provided.formats) {
174
+ draft.outputs.formats = draft.outputs.formats.filter(format => format !== 'umd');
175
+ }
176
+ }
177
+ if (provided.compress) draft.transforms.compress = [.../** @type {BuildFormat[]} */ (flags.compress)];
178
+ if (provided.sourcemaps) draft.outputs.sourcemaps = [.../** @type {BuildFormat[]} */ (flags.sourcemaps)];
179
+ if (provided.preprocess) draft.transforms.preprocess = [.../** @type {string[]} */ (flags.preprocess)];
180
+ if (provided.dest) draft.paths.outputDir = /** @type {string} */ (flags.dest);
181
+ if (provided.src) draft.paths.sourceDir = /** @type {string} */ (flags.src);
182
+ if (provided.bin) {
183
+ const values = /** @type {string[]} */ (flags.bin);
184
+ draft.packageJson.executables = { mode: values.length > 0 ? 'explicit' : 'disabled', values: [...values] };
185
+ }
186
+ if (provided.includeExternals) {
187
+ draft.transforms.includeExternals = /** @type {boolean | string[]} */ (flags.includeExternals);
188
+ }
189
+ if (provided.eject) draft.execution.eject = /** @type {boolean} */ (flags.eject);
190
+ if (provided.tsConfig) draft.typescript.updateConfig = /** @type {boolean} */ (flags.tsConfig);
191
+ if (provided.updatePackageJson) draft.packageJson.update = /** @type {boolean} */ (flags.updatePackageJson);
192
+ if (provided.commonjsPattern) draft.outputs.patterns.cjs = /** @type {string} */ (flags.commonjsPattern);
193
+ if (provided.esmPattern) draft.outputs.patterns.es = /** @type {string} */ (flags.esmPattern);
194
+ if (provided.umdPattern) draft.outputs.patterns.umd = /** @type {string} */ (flags.umdPattern);
195
+ if (provided.formatPackageJson) draft.packageJson.format = /** @type {boolean} */ (flags.formatPackageJson);
196
+ if (provided.pack) draft.packageJson.pack = /** @type {boolean} */ (flags.pack);
197
+ if (provided.exports) draft.packageJson.exports = /** @type {boolean} */ (flags.exports);
198
+ if (provided.clean) draft.execution.clean = /** @type {boolean} */ (flags.clean);
199
+ if (provided.bundle) draft.execution.bundle = /** @type {boolean} */ (flags.bundle);
200
+ if (provided.removeLegalComments) {
201
+ draft.transforms.removeLegalComments = /** @type {boolean} */ (flags.removeLegalComments);
202
+ }
203
+ }
204
+
205
+ /** @param {BuildConfigurationDraft} draft */
206
+ function normalize(draft) {
207
+ if (Array.isArray(draft.outputs?.formats)) draft.outputs.formats = unique(draft.outputs.formats);
208
+ if (Array.isArray(draft.outputs?.umdEntries)) draft.outputs.umdEntries = unique(draft.outputs.umdEntries);
209
+ if (Array.isArray(draft.outputs?.sourcemaps)) draft.outputs.sourcemaps = unique(draft.outputs.sourcemaps);
210
+ if (Array.isArray(draft.transforms?.compress)) draft.transforms.compress = unique(draft.transforms.compress);
211
+ if (Array.isArray(draft.transforms?.preprocess)) draft.transforms.preprocess = unique(draft.transforms.preprocess);
212
+ if (Array.isArray(draft.transforms.includeExternals)) {
213
+ draft.transforms.includeExternals = unique(draft.transforms.includeExternals);
214
+ }
215
+ if (Array.isArray(draft.packageJson?.executables?.values)) {
216
+ draft.packageJson.executables.values = unique(draft.packageJson.executables.values.filter(Boolean));
217
+ if (draft.packageJson.executables.mode === 'explicit' && draft.packageJson.executables.values.length === 0) {
218
+ draft.packageJson.executables.mode = 'disabled';
219
+ }
220
+ }
221
+ if (
222
+ Array.isArray(draft.outputs?.umdEntries) &&
223
+ Array.isArray(draft.outputs?.formats) &&
224
+ draft.outputs.umdEntries.length > 0 &&
225
+ !draft.outputs.formats.includes('umd')
226
+ ) {
227
+ draft.outputs.formats.push('umd');
228
+ }
229
+ }
230
+
231
+ /**
232
+ * @param {BuildConfigurationDraft} draft
233
+ * @param {PackageJson} packageJson
234
+ */
235
+ function validate(draft, packageJson) {
236
+ /** @type {{ code: string; path: string; message: string; value?: unknown }[]} */
237
+ const issues = [];
238
+ validateFormats(draft.outputs.formats, 'outputs.formats', issues);
239
+ validateFormats(draft.outputs.sourcemaps, 'outputs.sourcemaps', issues);
240
+ validateFormats(draft.transforms.compress, 'transforms.compress', issues);
241
+ validateStrings(draft.outputs.umdEntries, 'outputs.umdEntries', issues);
242
+ validateStrings(draft.transforms.preprocess, 'transforms.preprocess', issues);
243
+ if (draft.outputs.umdEntries.length > 0 && typeof packageJson.name !== 'string') {
244
+ issues.push({ code: 'PACKAGE_NAME_REQUIRED', path: 'package.name', message: 'a package name is required for UMD entries' });
245
+ }
246
+ for (const [format, pattern] of Object.entries(draft.outputs.patterns)) {
247
+ if (typeof pattern !== 'string' || !pattern.includes('[name]')) {
248
+ issues.push({
249
+ code: 'INVALID_FILE_NAME_PATTERN',
250
+ path: `outputs.patterns.${format}`,
251
+ message: 'must contain [name]',
252
+ value: pattern,
253
+ });
254
+ }
255
+ }
256
+ for (const [path, value] of [
257
+ ['paths.sourceDir', draft.paths.sourceDir],
258
+ ['paths.outputDir', draft.paths.outputDir],
259
+ ]) {
260
+ if (typeof value !== 'string' || value.length === 0) {
261
+ issues.push({ code: 'INVALID_PATH', path, message: 'must be a non-empty string', value });
262
+ }
263
+ }
264
+ if (
265
+ typeof draft.transforms.includeExternals !== 'boolean' &&
266
+ (!Array.isArray(draft.transforms.includeExternals) ||
267
+ draft.transforms.includeExternals.some(value => typeof value !== 'string' || value.length === 0))
268
+ ) {
269
+ issues.push({
270
+ code: 'INVALID_EXTERNALS',
271
+ path: 'transforms.includeExternals',
272
+ message: 'must be a boolean or an array of non-empty strings',
273
+ value: draft.transforms.includeExternals,
274
+ });
275
+ }
276
+ if (!['infer', 'disabled', 'explicit'].includes(draft.packageJson.executables.mode)) {
277
+ issues.push({
278
+ code: 'INVALID_EXECUTABLE_MODE',
279
+ path: 'packageJson.executables.mode',
280
+ message: 'must be infer, disabled, or explicit',
281
+ value: draft.packageJson.executables.mode,
282
+ });
283
+ }
284
+ validateStrings(draft.packageJson.executables.values, 'packageJson.executables.values', issues);
285
+ return issues;
286
+ }
287
+
288
+ /**
289
+ * @param {object} expected
290
+ * @param {unknown} actual
291
+ * @returns {{ code: string; path: string; message: string; value?: unknown }[]}
292
+ */
293
+ function validateStructure(expected, actual) {
294
+ /** @type {{ code: string; path: string; message: string; value?: unknown }[]} */
295
+ const issues = [];
296
+ visit(expected, actual, '');
297
+ return issues;
298
+
299
+ /**
300
+ * @param {unknown} expected
301
+ * @param {unknown} actual
302
+ * @param {string} path
303
+ */
304
+ function visit(expected, actual, path) {
305
+ if (Array.isArray(expected)) {
306
+ if (!Array.isArray(actual)) {
307
+ issues.push({ code: 'INVALID_CONFIGURATION_SHAPE', path, message: 'must be an array', value: actual });
308
+ }
309
+ return;
310
+ }
311
+ if (typeof expected !== 'object' || expected == null) {
312
+ const isExternalList = path === 'transforms.includeExternals' && Array.isArray(actual);
313
+ if (!isExternalList && typeof actual !== typeof expected) {
314
+ issues.push({
315
+ code: 'INVALID_CONFIGURATION_SHAPE',
316
+ path,
317
+ message: `must be ${typeof expected}`,
318
+ value: actual,
319
+ });
320
+ }
321
+ return;
322
+ }
323
+ if (typeof actual !== 'object' || actual == null || Array.isArray(actual)) {
324
+ issues.push({ code: 'INVALID_CONFIGURATION_SHAPE', path, message: 'must be an object', value: actual });
325
+ return;
326
+ }
327
+ for (const key of Object.keys(actual)) {
328
+ const keyPath = path ? `${path}.${key}` : key;
329
+ if (!(key in expected)) {
330
+ issues.push({ code: 'UNKNOWN_CONFIGURATION_KEY', path: keyPath, message: 'is not a recognized configuration key' });
331
+ }
332
+ }
333
+ for (const key of Object.keys(expected)) {
334
+ const keyPath = path ? `${path}.${key}` : key;
335
+ if (!(key in actual)) {
336
+ issues.push({ code: 'MISSING_CONFIGURATION_KEY', path: keyPath, message: 'is required' });
337
+ continue;
338
+ }
339
+ visit(/** @type {Record<string, unknown>} */ (expected)[key], /** @type {Record<string, unknown>} */ (actual)[key], keyPath);
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * @param {unknown} values
346
+ * @param {string} path
347
+ * @param {{ code: string; path: string; message: string; value?: unknown }[]} issues
348
+ */
349
+ function validateFormats(values, path, issues) {
350
+ if (!Array.isArray(values)) {
351
+ issues.push({ code: 'INVALID_FORMATS', path, message: 'must be an array', value: values });
352
+ return;
353
+ }
354
+ for (const value of values) {
355
+ if (!formats.has(value)) {
356
+ issues.push({ code: 'UNSUPPORTED_FORMAT', path, message: `unsupported format ${JSON.stringify(value)}`, value });
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * @param {unknown} values
363
+ * @param {string} path
364
+ * @param {{ code: string; path: string; message: string; value?: unknown }[]} issues
365
+ */
366
+ function validateStrings(values, path, issues) {
367
+ if (!Array.isArray(values)) {
368
+ issues.push({ code: 'INVALID_LIST', path, message: 'must be an array', value: values });
369
+ return;
370
+ }
371
+ for (const value of values) {
372
+ if (typeof value !== 'string' || value.length === 0) {
373
+ issues.push({ code: 'INVALID_LIST_VALUE', path, message: 'must contain non-empty strings', value });
374
+ }
375
+ }
376
+ }
377
+
378
+ /** @template T @param {T[]} values @returns {T[]} */
379
+ function unique(values) {
380
+ return [...new Set(values)];
381
+ }
382
+
383
+ /** @param {string} value */
384
+ function toCamelCase(value) {
385
+ return value.replace(/-([a-z])/g, (_, character) => character.toUpperCase());
386
+ }
387
+
388
+ /** @template T @param {T} value @returns {T} */
389
+ function deepFreeze(value) {
390
+ if (typeof value !== 'object' || value == null || Object.isFrozen(value)) return value;
391
+ for (const nested of Object.values(value)) {
392
+ deepFreeze(nested);
393
+ }
394
+ return Object.freeze(value);
395
+ }