@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.
- package/api/build/kit/rank.mjs +21 -4
- package/api/build/kit/words.d.mts +1 -0
- package/api/build/kit/words.mjs +1 -1
- package/api/icon/_adapter.d.mts +3 -2
- package/api/icon/_adapter.mjs +8 -2
- package/api/icon/_authoring.d.mts +1 -0
- package/api/icon/_authoring.mjs +586 -66
- package/api/icon/_default-look.d.mts +155 -0
- package/api/icon/_default-look.mjs +474 -0
- package/api/icon/_records.d.mts +1 -1
- package/api/icon/_records.mjs +1 -0
- package/api/icon/add/add.mjs +224 -38
- package/api/icon/icon.type.d.mts +22 -2
- package/api/icon/icon.type.mjs +6 -2
- package/api/icon/iconAdd.doc.mjs +2 -2
- package/api/icon/iconNewDefinition.doc.mjs +3 -2
- package/api/icon/iconNewSet.doc.mjs +4 -4
- package/api/icon/search/search.mjs +10 -9
- package/api/integration/add-helpers.d.mts +11 -1
- package/api/integration/add-helpers.mjs +5 -2
- package/api/integration/authoring-checks.d.mts +0 -2
- package/api/integration/authoring-checks.mjs +1 -6
- package/api/logger.mjs +2 -1
- package/api/swizzle/_adapter.d.mts +1 -0
- package/api/swizzle/_adapter.mjs +4 -1
- package/api/swizzle/_moduleGraph.d.mts +171 -0
- package/api/swizzle/_moduleGraph.mjs +795 -0
- package/api/swizzle/copy/copy.d.mts +2 -43
- package/api/swizzle/copy/copy.mjs +195 -314
- package/api/swizzle/swizzle.d.mts +0 -2
- package/api/swizzle/swizzle.doc.mjs +9 -4
- package/api/swizzle/swizzle.mjs +3 -7
- package/api/swizzle/swizzle.type.d.mts +2 -1
- package/api/swizzle/swizzle.type.mjs +3 -2
- package/assets/discover/official-packages.json +20 -0
- package/assets/docs/tree/icon-sets.doc.mjs +11 -5
- package/assets/templates/blocks/components/Table/TableRowIndexTable.doc.mjs +1 -1
- package/assets/templates/blocks/components/Table/TableRowIndexTable.tsx +2 -2
- package/assets/templates/pages/course-detail/page.tsx +699 -0
- package/assets/templates/pages/course-detail/template.doc.mjs +37 -0
- package/assets/templates/pages/file-explorer/page.tsx +103 -79
- package/assets/templates/pages/table-grouped/page.tsx +160 -232
- package/assets/templates/pages/table-grouped/template.doc.mjs +0 -2
- package/assets/templates/pages/theme-showcase/page.tsx +724 -792
- package/assets/templates/pages/theme-showcase/template.doc.mjs +4 -5
- package/authoring/doctypes/template/type.ts +1 -0
- package/clients/cli/commands/experimental/icon.mjs +31 -10
- package/clients/cli/commands/experimental-icon-add.doc.mjs +1 -1
- package/clients/cli/commands/experimental-icon-new-definition.doc.mjs +2 -1
- package/clients/cli/commands/experimental-icon-new-set.doc.mjs +2 -2
- package/clients/cli/commands/swizzle.doc.mjs +8 -3
- package/clients/cli/commands/swizzle.mjs +21 -9
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/doc-types.mjs +9 -2
- package/foundation/discovery/icon-ranking.d.mts +27 -3
- package/foundation/discovery/icon-ranking.mjs +125 -8
- package/foundation/discovery/icon-set-checks.mjs +44 -35
- package/foundation/discovery/icon-set-discovery.d.mts +13 -0
- package/foundation/discovery/icon-set-discovery.mjs +25 -0
- package/foundation/discovery/icon-set-writer.d.mts +238 -16
- package/foundation/discovery/icon-set-writer.mjs +676 -79
- package/foundation/discovery/module-exports.d.mts +3 -2
- package/foundation/discovery/module-exports.mjs +167 -37
- package/foundation/fs/paths.d.mts +2 -1
- package/foundation/fs/paths.mjs +3 -4
- package/foundation/text/shell-arg.d.mts +14 -0
- package/foundation/text/shell-arg.mjs +17 -0
- 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
|
+
}
|