@astryxdesign/cli 0.7.11 → 0.7.12-canary.ccb4a6d

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.
Files changed (68) hide show
  1. package/api/build/kit/rank.mjs +21 -4
  2. package/api/build/kit/words.d.mts +1 -0
  3. package/api/build/kit/words.mjs +1 -1
  4. package/api/icon/_adapter.d.mts +3 -2
  5. package/api/icon/_adapter.mjs +8 -2
  6. package/api/icon/_authoring.d.mts +1 -0
  7. package/api/icon/_authoring.mjs +586 -66
  8. package/api/icon/_default-look.d.mts +155 -0
  9. package/api/icon/_default-look.mjs +474 -0
  10. package/api/icon/_records.d.mts +1 -1
  11. package/api/icon/_records.mjs +1 -0
  12. package/api/icon/add/add.mjs +224 -38
  13. package/api/icon/icon.type.d.mts +22 -2
  14. package/api/icon/icon.type.mjs +6 -2
  15. package/api/icon/iconAdd.doc.mjs +2 -2
  16. package/api/icon/iconNewDefinition.doc.mjs +3 -2
  17. package/api/icon/iconNewSet.doc.mjs +4 -4
  18. package/api/icon/search/search.mjs +10 -9
  19. package/api/integration/add-helpers.d.mts +11 -1
  20. package/api/integration/add-helpers.mjs +5 -2
  21. package/api/integration/authoring-checks.d.mts +0 -2
  22. package/api/integration/authoring-checks.mjs +1 -6
  23. package/api/logger.mjs +2 -1
  24. package/api/swizzle/_adapter.d.mts +1 -0
  25. package/api/swizzle/_adapter.mjs +4 -1
  26. package/api/swizzle/_moduleGraph.d.mts +171 -0
  27. package/api/swizzle/_moduleGraph.mjs +795 -0
  28. package/api/swizzle/copy/copy.d.mts +2 -43
  29. package/api/swizzle/copy/copy.mjs +195 -314
  30. package/api/swizzle/swizzle.d.mts +0 -2
  31. package/api/swizzle/swizzle.doc.mjs +9 -4
  32. package/api/swizzle/swizzle.mjs +3 -7
  33. package/api/swizzle/swizzle.type.d.mts +2 -1
  34. package/api/swizzle/swizzle.type.mjs +3 -2
  35. package/assets/discover/official-packages.json +20 -0
  36. package/assets/docs/tree/icon-sets.doc.mjs +11 -5
  37. package/assets/templates/blocks/components/Table/TableRowIndexTable.doc.mjs +1 -1
  38. package/assets/templates/blocks/components/Table/TableRowIndexTable.tsx +2 -2
  39. package/assets/templates/pages/course-detail/page.tsx +699 -0
  40. package/assets/templates/pages/course-detail/template.doc.mjs +37 -0
  41. package/assets/templates/pages/file-explorer/page.tsx +103 -79
  42. package/assets/templates/pages/table-grouped/page.tsx +160 -232
  43. package/assets/templates/pages/table-grouped/template.doc.mjs +0 -2
  44. package/assets/templates/pages/theme-showcase/page.tsx +724 -792
  45. package/assets/templates/pages/theme-showcase/template.doc.mjs +4 -5
  46. package/authoring/doctypes/template/type.ts +1 -0
  47. package/clients/cli/commands/experimental/icon.mjs +31 -10
  48. package/clients/cli/commands/experimental-icon-add.doc.mjs +1 -1
  49. package/clients/cli/commands/experimental-icon-new-definition.doc.mjs +2 -1
  50. package/clients/cli/commands/experimental-icon-new-set.doc.mjs +2 -2
  51. package/clients/cli/commands/swizzle.doc.mjs +8 -3
  52. package/clients/cli/commands/swizzle.mjs +21 -9
  53. package/foundation/discovery/component-loader.d.mts +1 -1
  54. package/foundation/discovery/doc-types.mjs +9 -2
  55. package/foundation/discovery/icon-ranking.d.mts +27 -3
  56. package/foundation/discovery/icon-ranking.mjs +125 -8
  57. package/foundation/discovery/icon-set-checks.mjs +44 -35
  58. package/foundation/discovery/icon-set-discovery.d.mts +13 -0
  59. package/foundation/discovery/icon-set-discovery.mjs +25 -0
  60. package/foundation/discovery/icon-set-writer.d.mts +238 -16
  61. package/foundation/discovery/icon-set-writer.mjs +676 -79
  62. package/foundation/discovery/module-exports.d.mts +3 -2
  63. package/foundation/discovery/module-exports.mjs +167 -37
  64. package/foundation/fs/paths.d.mts +2 -1
  65. package/foundation/fs/paths.mjs +3 -4
  66. package/foundation/text/shell-arg.d.mts +14 -0
  67. package/foundation/text/shell-arg.mjs +17 -0
  68. package/package.json +6 -6
@@ -0,0 +1,795 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file swizzle.copy module graph — decides, for each module a component
5
+ * reaches, whether the copy imports it from the owner package's public
6
+ * exports or carries it in the output.
7
+ *
8
+ * @input the owner package's source root and `exports` map, the component folder
9
+ * @output the files the copy holds, each with its rewritten source
10
+ * @position api/swizzle adapter helper; the copy leaf reaches it through _adapter.mjs
11
+ */
12
+
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import {isBuiltin} from 'node:module';
16
+ import {packageOf} from '../../foundation/render/served-packages.mjs';
17
+ import {parse} from '@babel/parser';
18
+ import {
19
+ inspectPackageExport,
20
+ sourceExportTarget,
21
+ } from '../../foundation/discovery/package-exports.mjs';
22
+ import {
23
+ moduleExportOrigins,
24
+ resolveLocalModule,
25
+ } from '../../foundation/discovery/module-exports.mjs';
26
+ import {
27
+ allTargets,
28
+ isRecord,
29
+ subpathDeclaration,
30
+ } from '../../foundation/integrations/exports-subpaths.mjs';
31
+
32
+ /** The extensions an extensionless import may name, in resolution order. */
33
+ const CODE_FILE = /\.[cm]?[jt]sx?$/;
34
+ const DECLARATION_FILE = /\.d\.[cm]?ts$/;
35
+ const STYLEX_FILE = /\.stylex\.[cm]?[jt]sx?$/;
36
+
37
+ /** @param {string} file */
38
+ const toPosix = file => file.split(path.sep).join('/');
39
+
40
+ /**
41
+ * The file an import names, or null when no file is there.
42
+ * @param {string} fromFile absolute path of the importing file
43
+ * @param {string} specifier a relative specifier
44
+ * @param {Map<string, string|null>} cache resolutions already made in this copy
45
+ * @returns {string|null}
46
+ */
47
+ function resolveRelative(fromFile, specifier, cache) {
48
+ const base = path.resolve(path.dirname(fromFile), specifier);
49
+ const known = cache.get(base);
50
+ if (known !== undefined) return known;
51
+ const found = resolveLocalModule(specifier, fromFile);
52
+ cache.set(base, found);
53
+ return found;
54
+ }
55
+
56
+ /**
57
+ * @typedef {object} ImportSpecifier
58
+ * @property {string} imported the exported name (`default` for a default import)
59
+ * @property {string} local
60
+ * @property {boolean} typeOnly
61
+ *
62
+ * @typedef {object} ModuleReference
63
+ * @property {'named'|'namespace'|'sideEffect'|'dynamic'|'reexportAll'|'reexportNamed'} kind
64
+ * @property {string} source the specifier as written
65
+ * @property {number} start statement (or, for `import()`, string literal) start
66
+ * @property {number} end
67
+ * @property {boolean} typeOnly the whole statement is `import type` / `export type`
68
+ * @property {ImportSpecifier[]} specifiers
69
+ * @property {string} [namespace] the local name of `* as X`
70
+ *
71
+ * @typedef {object} ParsedModule
72
+ * @property {string} code
73
+ * @property {ModuleReference[]} references
74
+ * @property {Set<string>} declares names the module declares and exports
75
+
76
+ */
77
+
78
+ /**
79
+ * A module's source and every module reference in it: static imports and
80
+ * re-exports, `import()` calls, and `import('…')` types.
81
+ * @param {string} file
82
+ * @returns {ParsedModule}
83
+ */
84
+ function parseModule(file) {
85
+ const code = fs.readFileSync(file, 'utf-8');
86
+ /** @type {ParsedModule} */
87
+ const parsed = {code, references: [], declares: new Set()};
88
+ if (!CODE_FILE.test(file)) return parsed;
89
+ const ast = parse(code, {
90
+ sourceType: 'module',
91
+ errorRecovery: true,
92
+ plugins: /\.[cm]?tsx?$/.test(file) ? ['typescript', 'jsx'] : ['jsx'],
93
+ });
94
+ const {references} = parsed;
95
+ // Babel's node types mark positions optional; a parsed file always has them.
96
+ for (const node of /** @type {any[]} */ (ast.program.body)) {
97
+ for (const d of node.declaration?.declarations ?? [node.declaration]) {
98
+ if (node.type === 'ExportNamedDeclaration' && d?.id?.name)
99
+ parsed.declares.add(d.id.name);
100
+ }
101
+ if (node.type === 'ImportDeclaration') {
102
+ const typeOnly = node.importKind === 'type';
103
+ const base = {
104
+ source: node.source.value,
105
+ start: node.start,
106
+ end: node.end,
107
+ typeOnly,
108
+ };
109
+ const namespace = node.specifiers.find(
110
+ (/** @type {any} */ s) => s.type === 'ImportNamespaceSpecifier',
111
+ );
112
+ const named = node.specifiers.filter(
113
+ (/** @type {any} */ s) => s.type !== 'ImportNamespaceSpecifier',
114
+ );
115
+ if (node.specifiers.length === 0) {
116
+ references.push({...base, kind: 'sideEffect', specifiers: []});
117
+ } else if (namespace && named.length === 0) {
118
+ references.push({
119
+ ...base,
120
+ kind: 'namespace',
121
+ specifiers: [],
122
+ namespace: namespace.local.name,
123
+ });
124
+ } else if (!namespace) {
125
+ references.push({
126
+ ...base,
127
+ kind: 'named',
128
+ specifiers: named.map((/** @type {any} */ s) => ({
129
+ imported:
130
+ s.type === 'ImportDefaultSpecifier'
131
+ ? 'default'
132
+ : s.imported.type === 'StringLiteral'
133
+ ? s.imported.value
134
+ : s.imported.name,
135
+ local: s.local.name,
136
+ typeOnly: typeOnly || s.importKind === 'type',
137
+ })),
138
+ });
139
+ } else {
140
+ // `import X, * as Y from` names the whole module.
141
+ references.push({...base, kind: 'namespace', specifiers: []});
142
+ }
143
+ } else if (node.type === 'ExportAllDeclaration') {
144
+ references.push({
145
+ kind: 'reexportAll',
146
+ source: node.source.value,
147
+ start: node.start,
148
+ end: node.end,
149
+ typeOnly: node.exportKind === 'type',
150
+ specifiers: [],
151
+ });
152
+ } else if (node.type === 'ExportNamedDeclaration' && node.source) {
153
+ const typeOnly = node.exportKind === 'type';
154
+ references.push({
155
+ kind: 'reexportNamed',
156
+ source: node.source.value,
157
+ start: node.start,
158
+ end: node.end,
159
+ typeOnly,
160
+ specifiers: node.specifiers.map((/** @type {any} */ s) => ({
161
+ imported: s.type === 'ExportNamespaceSpecifier' ? '*' : s.local.name,
162
+ local:
163
+ s.exported.type === 'StringLiteral'
164
+ ? s.exported.value
165
+ : s.exported.name,
166
+ typeOnly: typeOnly || s.exportKind === 'type',
167
+ })),
168
+ });
169
+ }
170
+ }
171
+ visitDynamicReferences(ast.program, literal => {
172
+ references.push({
173
+ kind: 'dynamic',
174
+ source: literal.value,
175
+ start: literal.start,
176
+ end: literal.end,
177
+ typeOnly: false,
178
+ specifiers: [],
179
+ });
180
+ });
181
+ return parsed;
182
+ }
183
+
184
+ /**
185
+ * Call `onLiteral` with the string literal of every `import('…')` call and
186
+ * `import('…')` type under a node.
187
+ * @param {any} node
188
+ * @param {(literal: {value: string, start: number, end: number}) => void} onLiteral
189
+ */
190
+ function visitDynamicReferences(node, onLiteral) {
191
+ if (node == null || typeof node !== 'object') return;
192
+ if (Array.isArray(node)) {
193
+ for (const child of node) visitDynamicReferences(child, onLiteral);
194
+ return;
195
+ }
196
+ if (
197
+ node.type === 'CallExpression' &&
198
+ node.callee?.type === 'Import' &&
199
+ node.arguments[0]?.type === 'StringLiteral'
200
+ ) {
201
+ onLiteral(node.arguments[0]);
202
+ } else if (
203
+ node.type === 'ImportExpression' &&
204
+ node.source?.type === 'StringLiteral'
205
+ ) {
206
+ onLiteral(node.source);
207
+ } else if (node.type === 'TSImportType') {
208
+ const literal =
209
+ node.argument?.type === 'TSLiteralType'
210
+ ? node.argument.literal
211
+ : node.argument;
212
+ if (literal?.type === 'StringLiteral') onLiteral(literal);
213
+ }
214
+ for (const key of Object.keys(node)) {
215
+ if (
216
+ key === 'loc' ||
217
+ key === 'leadingComments' ||
218
+ key === 'trailingComments' ||
219
+ key === 'innerComments'
220
+ )
221
+ continue;
222
+ const value = node[key];
223
+ if (value && typeof value === 'object')
224
+ visitDynamicReferences(value, onLiteral);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * @typedef {'value' | 'type'} ExportKind `value` reads runtime exports only;
230
+ * `type` reads type-only exports too
231
+ *
232
+ * @typedef {(file: string, kind: ExportKind) => Map<string, {file: string, name: string}>} OriginsOf
233
+ * every name a module exports, with where it is declared
234
+ *
235
+ * @typedef {Map<string, Array<{subpath: string, name: string}>>} ExportersByOrigin
236
+ * `file#name` of a declaration -> the subpaths that export it, and as what
237
+ *
238
+ * @typedef {object} PublicSurface
239
+ * @property {Record<ExportKind, ExportersByOrigin>} byOrigin the subpaths
240
+ * that export each declaration as a value, and as a value or a type
241
+ * @property {Map<string, string>} byFile source file -> the subpath that
242
+ * exports exactly that file
243
+ * @property {Record<string, unknown>|undefined} exportsField
244
+ */
245
+
246
+ /**
247
+ * Every public subpath of a package with the source file under `root` that
248
+ * the shared export readers give it. `./internal/*` is never public.
249
+ * @param {{exports?: unknown}} pkg
250
+ * @param {string} packageDir
251
+ * @param {string} root
252
+ * @returns {Array<[string, string]>}
253
+ */
254
+ function exportedSourceFiles(pkg, packageDir, root) {
255
+ const exportsField = pkg.exports;
256
+ const subpathMap =
257
+ isRecord(exportsField) &&
258
+ Object.keys(exportsField).some(key => key.startsWith('.'));
259
+ const candidates = new Set(['.']);
260
+ if (subpathMap) {
261
+ for (const [key, value] of Object.entries(exportsField)) {
262
+ if (!key.includes('*')) {
263
+ candidates.add(key);
264
+ continue;
265
+ }
266
+ for (const subpath of wildcardSubpaths(packageDir, root, key, value)) {
267
+ candidates.add(subpath);
268
+ }
269
+ }
270
+ }
271
+ /** @type {Array<[string, string]>} */
272
+ const found = [];
273
+ for (const subpath of candidates) {
274
+ if (/^\.\/internal(\/|$)/.test(subpath)) continue;
275
+ const inspected = inspectPackageExport(pkg, packageDir, subpath);
276
+ if (!inspected.exported) continue;
277
+ const file =
278
+ sourceExportTarget(pkg, packageDir, subpath) ?? inspected.target;
279
+ if (
280
+ file &&
281
+ file.startsWith(root + path.sep) &&
282
+ !DECLARATION_FILE.test(file)
283
+ ) {
284
+ found.push([subpath, file]);
285
+ }
286
+ }
287
+ return found;
288
+ }
289
+
290
+ /**
291
+ * The subpaths a `*` key names: one per file its target patterns match.
292
+ * @param {string} packageDir
293
+ * @param {string} root
294
+ * @param {string} key
295
+ * @param {unknown} value
296
+ * @returns {string[]}
297
+ */
298
+ function wildcardSubpaths(packageDir, root, key, value) {
299
+ /** @type {string[]} */
300
+ const subpaths = [];
301
+ for (const pattern of allTargets(value)) {
302
+ if (!pattern.startsWith('./') || !pattern.includes('*')) continue;
303
+ const fixed = pattern.slice(2, pattern.indexOf('*'));
304
+ const base = path.resolve(
305
+ packageDir,
306
+ fixed.slice(0, fixed.lastIndexOf('/') + 1),
307
+ );
308
+ if (!base.startsWith(root) || !fs.existsSync(base)) continue;
309
+ for (const entry of fs.readdirSync(base, {
310
+ recursive: true,
311
+ withFileTypes: true,
312
+ })) {
313
+ if (!entry.isFile()) continue;
314
+ const file = path.join(entry.parentPath, entry.name);
315
+ if (path.relative(base, file).split(path.sep).includes('node_modules'))
316
+ continue;
317
+ const rel = toPosix(path.relative(packageDir, file));
318
+ if (!rel.startsWith(fixed)) continue;
319
+ const rest = rel.slice(fixed.length);
320
+ for (let end = 1; end <= rest.length; end++) {
321
+ const star = rest.slice(0, end);
322
+ if (pattern.slice(2).replaceAll('*', star) === rel) {
323
+ subpaths.push(key.replace('*', star));
324
+ break;
325
+ }
326
+ }
327
+ }
328
+ }
329
+ return subpaths;
330
+ }
331
+
332
+ /**
333
+ * The owner package's public surface, read from its `exports` map: each
334
+ * subpath with a `source` condition under the source root, and the
335
+ * declarations it exports. `./internal/*` subpaths are not public.
336
+ * @param {string} packageDir
337
+ * @param {string} srcRoot
338
+ * @param {OriginsOf} originsOf
339
+ * @returns {PublicSurface}
340
+ */
341
+ function publicSurfaceOf(packageDir, srcRoot, originsOf) {
342
+ /** @type {{exports?: unknown}} */
343
+ let pkg;
344
+ try {
345
+ pkg = JSON.parse(
346
+ fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
347
+ );
348
+ } catch {
349
+ pkg = {};
350
+ }
351
+ const exportsField =
352
+ pkg.exports && typeof pkg.exports === 'object'
353
+ ? /** @type {Record<string, unknown>} */ (pkg.exports)
354
+ : undefined;
355
+ /** @type {PublicSurface['byOrigin']} */
356
+ const byOrigin = {value: new Map(), type: new Map()};
357
+ /** @type {PublicSurface['byFile']} */
358
+ const byFile = new Map();
359
+ for (const [subpath, sourceFile] of exportedSourceFiles(
360
+ pkg,
361
+ packageDir,
362
+ srcRoot,
363
+ )) {
364
+ byFile.set(sourceFile, subpath);
365
+ if (!CODE_FILE.test(sourceFile)) continue;
366
+ for (const kind of /** @type {const} */ (['value', 'type'])) {
367
+ for (const [name, origin] of originsOf(sourceFile, kind)) {
368
+ const key = `${origin.file}#${origin.name}`;
369
+ const list = byOrigin[kind].get(key) ?? [];
370
+ list.push({subpath, name});
371
+ byOrigin[kind].set(key, list);
372
+ }
373
+ }
374
+ }
375
+ return {byOrigin, byFile, exportsField};
376
+ }
377
+
378
+ /**
379
+ * A specifier that is either fixed (a public subpath) or computed from where
380
+ * two copied files land.
381
+ * @typedef {string | {from: string, to: string}} SpecifierChoice
382
+ *
383
+ * @typedef {object} PlannedEdit
384
+ * @property {number} start
385
+ * @property {number} end
386
+ * @property {(specifierOf: (choice: SpecifierChoice) => string) => string} render
387
+ *
388
+ * @typedef {object} CopyPlan
389
+ * @property {Map<string, {code: string, edits: PlannedEdit[]}>} files every
390
+ * file the copy holds (absolute source path), with the edits its imports need
391
+ * @property {Map<string, Set<string>>} dependsOn file -> carried files its
392
+ * computed specifiers point at
393
+ * @property {Record<string, string>} dependencies each npm package the copied
394
+ * files import, other than the owner package and Node's built-in modules,
395
+ * with the range the owner package declares
396
+ */
397
+
398
+ /**
399
+ * Follow a component's module graph from its entry files and decide what the
400
+ * copy holds. A module inside the component folder is copied. A module
401
+ * outside it is reached through the owner package's public exports when every
402
+ * name taken from it is public there; otherwise the module that defines the
403
+ * name is copied, and imported by relative path. Tests, stories and docs are
404
+ * never reached, so they are never copied.
405
+ *
406
+ * @param {object} options
407
+ * @param {string} options.packageDir the owner package root
408
+ * @param {string} options.srcRoot its source root
409
+ * @param {string} options.componentDir
410
+ * @param {string} options.ownerPackage
411
+ * @param {string[]} options.entries absolute entry files of the component
412
+ * @returns {CopyPlan}
413
+ */
414
+ export function planCopy({
415
+ packageDir,
416
+ srcRoot,
417
+ componentDir,
418
+ ownerPackage,
419
+ entries,
420
+ }) {
421
+ // Every read is cached for this copy only, so a later copy sees the
422
+ // package as it is then.
423
+ const exportMemos = {value: new Map(), type: new Map()};
424
+ /** @type {OriginsOf} */
425
+ const originsOf = (file, kind) =>
426
+ moduleExportOrigins(file, {
427
+ types: kind === 'type',
428
+ memo: exportMemos[kind],
429
+ });
430
+ /** @param {string} file @param {string} name */
431
+ const originOf = (file, name) => originsOf(file, 'type').get(name) ?? null;
432
+ /**
433
+ * The subpaths that export a module's export: as a value when the import
434
+ * takes a value, since a type-only export is erased at runtime.
435
+ * @param {string} file
436
+ * @param {string} name
437
+ * @param {boolean} typeOnly
438
+ */
439
+ const exportersOf = (file, name, typeOnly) => {
440
+ const origin = originOf(file, name);
441
+ if (!origin) return [];
442
+ const kind = typeOnly ? 'type' : 'value';
443
+ return surface.byOrigin[kind].get(`${origin.file}#${origin.name}`) ?? [];
444
+ };
445
+ /**
446
+ * The name a module exports one of its own bindings under, or null.
447
+ * @param {{file: string, name: string}} origin
448
+ */
449
+ const exportNameOf = origin => {
450
+ for (const [name, found] of originsOf(origin.file, 'type')) {
451
+ if (found.file === origin.file && found.name === origin.name) return name;
452
+ }
453
+ return null;
454
+ };
455
+ /** @type {Map<string, string|null>} */
456
+ const resolutions = new Map();
457
+ const surface = publicSurfaceOf(packageDir, srcRoot, originsOf);
458
+ /** @type {CopyPlan['files']} */
459
+ const files = new Map();
460
+ /** @type {CopyPlan['dependsOn']} */
461
+ const dependsOn = new Map();
462
+ /** @type {Set<string>} */
463
+ const packages = new Set();
464
+ const queue = [...entries];
465
+ const queued = new Set(queue);
466
+ /** @param {string} file */
467
+ const enqueue = file => {
468
+ if (!queued.has(file)) {
469
+ queued.add(file);
470
+ queue.push(file);
471
+ }
472
+ };
473
+ /** @param {string} file */
474
+ const inComponent = file => file.startsWith(componentDir + path.sep);
475
+ /** @param {string} file */
476
+ const inSource = file => file.startsWith(srcRoot + path.sep);
477
+
478
+ /**
479
+ * The subpath that exports every name a module exports, with the same
480
+ * definition. An exactly exported file is its own subpath.
481
+ * @param {string} file
482
+ * @returns {string|null}
483
+ */
484
+ const subpathCoveringModule = file => {
485
+ const exact = surface.byFile.get(file);
486
+ if (exact) return exact;
487
+ const names = [...originsOf(file, 'type').keys()];
488
+ if (names.length === 0) return null;
489
+ const values = originsOf(file, 'value');
490
+ const options = names.map(name =>
491
+ exportersOf(file, name, !values.has(name))
492
+ .filter(o => o.name === name)
493
+ .map(o => o.subpath),
494
+ );
495
+ const common = options.reduce((acc, list) =>
496
+ acc.filter(s => list.includes(s)),
497
+ );
498
+ return STYLEX_FILE.test(file) ? null : pickSubpath(common, file);
499
+ };
500
+
501
+ /**
502
+ * The public subpath to take a name from. A StyleX module is imported by
503
+ * its own exported path, because the StyleX compiler resolves styles and
504
+ * variables from the real module; when that path is not exported, there is
505
+ * no public import.
506
+ * @param {string[]} subpaths subpaths that export the name
507
+ * @param {string} target the module the import names
508
+ * @param {string} [originFile] the module that defines the name
509
+ * @returns {string|null}
510
+ */
511
+ const choosePublic = (subpaths, target, originFile) => {
512
+ const exact = originFile ? surface.byFile.get(originFile) : undefined;
513
+ if (exact && subpaths.includes(exact)) return exact;
514
+ if (
515
+ STYLEX_FILE.test(target) ||
516
+ (originFile && STYLEX_FILE.test(originFile))
517
+ )
518
+ return null;
519
+ return pickSubpath(subpaths, target);
520
+ };
521
+
522
+ /**
523
+ * Prefer the subpath of the module's top folder, then the package root.
524
+ * @param {string[]} subpaths
525
+ * @param {string} file
526
+ * @returns {string|null}
527
+ */
528
+ const pickSubpath = (subpaths, file) => {
529
+ if (subpaths.length === 0) return null;
530
+ const top = `./${toPosix(path.relative(srcRoot, file)).split('/')[0]}`;
531
+ return (
532
+ subpaths.find(s => s === top) ??
533
+ subpaths.find(s => s === '.') ??
534
+ [...subpaths].sort((a, b) => a.length - b.length)[0]
535
+ );
536
+ };
537
+
538
+ /** @param {string} subpath */
539
+ const publicSpecifier = subpath =>
540
+ subpath === '.' ? ownerPackage : `${ownerPackage}/${subpath.slice(2)}`;
541
+
542
+ /**
543
+ * Carry a module in the copy.
544
+ * @param {string} from
545
+ * @param {string} to
546
+ * @returns {SpecifierChoice}
547
+ */
548
+ const carry = (from, to) => {
549
+ enqueue(to);
550
+ const deps = dependsOn.get(from) ?? new Set();
551
+ deps.add(to);
552
+ dependsOn.set(from, deps);
553
+ return {from, to};
554
+ };
555
+
556
+ while (queue.length > 0) {
557
+ const file = /** @type {string} */ (queue.shift());
558
+ const parsed = parseModule(file);
559
+ /** @type {PlannedEdit[]} */
560
+ const edits = [];
561
+ files.set(file, {code: parsed.code, edits});
562
+ if (!CODE_FILE.test(file)) continue;
563
+ for (const ref of parsed.references) {
564
+ if (!ref.source.startsWith('.')) {
565
+ const builtin = ref.source.startsWith('node:') || isBuiltin(ref.source);
566
+ const name = packageOf(ref.source);
567
+ if (!builtin && name !== ownerPackage) packages.add(name);
568
+ continue;
569
+ }
570
+ const target = resolveRelative(file, ref.source, resolutions);
571
+ // A dangling import is left for the build to report.
572
+ if (!target) continue;
573
+ if (inComponent(target)) {
574
+ // Files of the component keep their relative imports of each other.
575
+ if (inComponent(file)) enqueue(target);
576
+ else edits.push(sourceEdit(parsed.code, ref, carry(file, target)));
577
+ continue;
578
+ }
579
+ if (!CODE_FILE.test(target) || !inSource(target)) {
580
+ // An asset, such as `../../locales/en.json` or a stylesheet, that the
581
+ // package exports by its own path is imported from the package.
582
+ const subpath = `./${toPosix(path.relative(packageDir, target))}`;
583
+ if (
584
+ subpathDeclaration(surface.exportsField, subpath).state === 'declared'
585
+ ) {
586
+ edits.push(
587
+ sourceEdit(parsed.code, ref, `${ownerPackage}/${subpath.slice(2)}`),
588
+ );
589
+ continue;
590
+ }
591
+ // An unexported asset in the source is carried; one outside it is
592
+ // left for the build to report.
593
+ if (!inSource(target)) continue;
594
+ edits.push(sourceEdit(parsed.code, ref, carry(file, target)));
595
+ continue;
596
+ }
597
+ edits.push(planReference(file, parsed.code, ref, target));
598
+ }
599
+ }
600
+ return {files, dependsOn, dependencies: declaredRanges(packageDir, packages)};
601
+
602
+ /**
603
+ * Decide one reference that leaves the component folder.
604
+ * @param {string} file
605
+ * @param {string} code
606
+ * @param {ModuleReference} ref
607
+ * @param {string} target
608
+ * @returns {PlannedEdit}
609
+ */
610
+ function planReference(file, code, ref, target) {
611
+ if (ref.kind !== 'named' && ref.kind !== 'reexportNamed') {
612
+ const covering = subpathCoveringModule(target);
613
+ return sourceEdit(
614
+ code,
615
+ ref,
616
+ covering ? publicSpecifier(covering) : carry(file, target),
617
+ );
618
+ }
619
+ /** @type {Array<{choice: SpecifierChoice, specs: ImportSpecifier[]}>} */
620
+ const groups = [];
621
+ /** @param {SpecifierChoice} choice @param {ImportSpecifier} spec */
622
+ const add = (choice, spec) => {
623
+ const same = groups.find(g =>
624
+ typeof choice === 'string'
625
+ ? g.choice === choice
626
+ : typeof g.choice !== 'string' && g.choice.to === choice.to,
627
+ );
628
+ if (same) same.specs.push(spec);
629
+ else groups.push({choice, specs: [spec]});
630
+ };
631
+ for (const spec of ref.specifiers) {
632
+ if (spec.imported === '*') {
633
+ const covering = subpathCoveringModule(target);
634
+ add(covering ? publicSpecifier(covering) : carry(file, target), spec);
635
+ continue;
636
+ }
637
+ const origin = originOf(target, spec.imported);
638
+ const publicOptions = exportersOf(target, spec.imported, spec.typeOnly);
639
+ const chosen = choosePublic(
640
+ publicOptions.map(o => o.subpath),
641
+ target,
642
+ origin?.file,
643
+ );
644
+ if (chosen) {
645
+ const exported = /** @type {{name: string}} */ (
646
+ publicOptions.find(o => o.subpath === chosen)
647
+ ).name;
648
+ add(publicSpecifier(chosen), {...spec, imported: exported});
649
+ } else {
650
+ // Take the name from the module that defines it, so the copy carries
651
+ // that module and not a whole barrel. An origin names a binding, so
652
+ // the import uses the name that module exports it under.
653
+ // A module that declares the name itself, even as an alias, is carried.
654
+ const definingExport =
655
+ origin &&
656
+ origin.file !== target &&
657
+ inSource(origin.file) &&
658
+ !parseModule(target).declares.has(spec.imported)
659
+ ? exportNameOf(origin)
660
+ : null;
661
+ if (origin && definingExport != null) {
662
+ add(carry(file, origin.file), {...spec, imported: definingExport});
663
+ } else {
664
+ add(carry(file, target), spec);
665
+ }
666
+ }
667
+ }
668
+ // One source and the same names: keep the statement as written and swap
669
+ // only its specifier.
670
+ const unchangedNames =
671
+ groups.length === 1 &&
672
+ groups[0].specs.every(
673
+ (spec, i) => spec.imported === ref.specifiers[i].imported,
674
+ );
675
+ if (unchangedNames) return sourceEdit(code, ref, groups[0].choice);
676
+ return {
677
+ start: ref.start,
678
+ end: ref.end,
679
+ render: specifierOf => renderStatements(ref, groups, specifierOf),
680
+ };
681
+ }
682
+ }
683
+
684
+ /**
685
+ * The version range the owner package declares for each npm package the copy
686
+ * imports, by name; `*` for one it does not declare.
687
+ * @param {string} packageDir
688
+ * @param {Iterable<string>} names
689
+ * @returns {Record<string, string>}
690
+ */
691
+ function declaredRanges(packageDir, names) {
692
+ /** @type {Record<string, Record<string, string>>} */
693
+ let manifest = {};
694
+ try {
695
+ manifest = JSON.parse(
696
+ fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
697
+ );
698
+ } catch {
699
+ // An unreadable manifest declares nothing.
700
+ }
701
+ /** @type {Record<string, string>} */
702
+ const ranges = {};
703
+ for (const name of [...names].sort()) {
704
+ ranges[name] =
705
+ manifest.dependencies?.[name] ??
706
+ manifest.optionalDependencies?.[name] ??
707
+ manifest.peerDependencies?.[name] ??
708
+ '*';
709
+ }
710
+ return ranges;
711
+ }
712
+
713
+ /**
714
+ * An edit that replaces only the string literal of a reference.
715
+ * @param {string} code
716
+ * @param {ModuleReference} ref
717
+ * @param {SpecifierChoice} choice
718
+ * @returns {PlannedEdit}
719
+ */
720
+ function sourceEdit(code, ref, choice) {
721
+ const slice = code.slice(ref.start, ref.end);
722
+ const quoted = new RegExp(
723
+ `(['"])${ref.source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\1`,
724
+ );
725
+ const m = slice.match(quoted);
726
+ const at = m && m.index != null ? ref.start + m.index : ref.start;
727
+ const length = m ? m[0].length : 0;
728
+ const quote = m ? m[1] : "'";
729
+ return {
730
+ start: at,
731
+ end: at + length,
732
+ render: specifierOf =>
733
+ m ? `${quote}${specifierOf(choice)}${quote}` : slice,
734
+ };
735
+ }
736
+
737
+ /**
738
+ * The statements that replace one named import or re-export, one per source.
739
+ * @param {ModuleReference} ref
740
+ * @param {Array<{choice: SpecifierChoice, specs: ImportSpecifier[]}>} groups
741
+ * @param {(choice: SpecifierChoice) => string} specifierOf
742
+ */
743
+ function renderStatements(ref, groups, specifierOf) {
744
+ const keyword = ref.kind === 'reexportNamed' ? 'export' : 'import';
745
+ const statements = [];
746
+ for (const {choice, specs} of groups) {
747
+ const specifier = specifierOf(choice);
748
+ const star = specs.find(s => s.imported === '*');
749
+ if (star) statements.push(`export * as ${star.local} from '${specifier}';`);
750
+ const named = specs.filter(s => s.imported !== '*');
751
+ if (named.length === 0) continue;
752
+ const allType = ref.typeOnly || named.every(s => s.typeOnly);
753
+ const parts = named.map(s => {
754
+ const prefix = !allType && s.typeOnly ? 'type ' : '';
755
+ return s.imported === s.local
756
+ ? `${prefix}${s.local}`
757
+ : `${prefix}${s.imported} as ${s.local}`;
758
+ });
759
+ const head = `${keyword} ${allType ? 'type ' : ''}`;
760
+ const oneLine = `${head}{${parts.join(', ')}} from '${specifier}';`;
761
+ statements.push(
762
+ oneLine.length <= 80
763
+ ? oneLine
764
+ : `${head}{\n${parts.map(p => ` ${p},`).join('\n')}\n} from '${specifier}';`,
765
+ );
766
+ }
767
+ return statements.join('\n');
768
+ }
769
+
770
+ /**
771
+ * The source of a planned file with its edits applied.
772
+ * @param {{code: string, edits: PlannedEdit[]}} planned
773
+ * @param {(choice: SpecifierChoice) => string} specifierOf
774
+ */
775
+ export function renderFile(planned, specifierOf) {
776
+ let out = planned.code;
777
+ for (const edit of [...planned.edits].sort((a, b) => b.start - a.start)) {
778
+ out =
779
+ out.slice(0, edit.start) + edit.render(specifierOf) + out.slice(edit.end);
780
+ }
781
+ return out;
782
+ }
783
+
784
+ /**
785
+ * The relative specifier from one written file to another.
786
+ * @param {string} fromDest
787
+ * @param {string} toDest
788
+ */
789
+ export function relativeSpecifierBetween(fromDest, toDest) {
790
+ let rel = toPosix(path.relative(path.dirname(fromDest), toDest));
791
+ rel = DECLARATION_FILE.test(toDest)
792
+ ? rel.replace(/\.d\.[cm]?ts$/, '')
793
+ : rel.replace(/\.(?:[cm]?[jt]sx?)$/, '');
794
+ return rel.startsWith('.') ? rel : `./${rel}`;
795
+ }