@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
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file swizzle.copy leaf — copy a component's source into the consumer project
|
|
5
|
-
* for customization
|
|
6
|
-
*
|
|
5
|
+
* for customization: what the OWNER package exports is imported from it, and
|
|
6
|
+
* every internal module the component needs is copied beside it.
|
|
7
7
|
*
|
|
8
8
|
* Side-effecting: writes files and returns a `swizzle.copy` receipt describing
|
|
9
|
-
* what it did. Shared core discovery
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* what it did. Shared core discovery, component listing and the module graph
|
|
10
|
+
* (which modules the copy holds and how each import reads) come from
|
|
11
|
+
* api/swizzle/_adapter.mjs; owner resolution, where each file lands, reuse of
|
|
12
|
+
* existing helpers, StyleX detection and maintainer feedback are here. Errors
|
|
12
13
|
* throw AstryxError (stable code + suggestions). All human prose /
|
|
13
14
|
* package-manager prefixing lives in the CLI renderer.
|
|
14
15
|
*/
|
|
@@ -17,11 +18,15 @@ import * as fs from 'node:fs';
|
|
|
17
18
|
import * as path from 'node:path';
|
|
18
19
|
import {
|
|
19
20
|
checkGhCli,
|
|
21
|
+
planCopy,
|
|
22
|
+
relativeSpecifierBetween,
|
|
23
|
+
renderFile,
|
|
20
24
|
resolveComponentReplacement,
|
|
21
25
|
resolveCore,
|
|
22
26
|
} from '../_adapter.mjs';
|
|
23
27
|
import {assertWithin, sanitizeName, PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
|
|
24
28
|
import {Project} from '../../../foundation/config/project.mjs';
|
|
29
|
+
import {findPackageRoot} from '../../../foundation/fs/paths.mjs';
|
|
25
30
|
import {
|
|
26
31
|
CORE_PACKAGE,
|
|
27
32
|
docStem,
|
|
@@ -35,212 +40,71 @@ import {AstryxError, writeFailed} from '../../error.mjs';
|
|
|
35
40
|
/** Default issue tracker for maintainer feedback after swizzling. */
|
|
36
41
|
const DEFAULT_ISSUES_URL = 'https://github.com/meta/astryx/issues/new';
|
|
37
42
|
|
|
38
|
-
/**
|
|
39
|
-
* Rewrite relative imports that point outside the component directory to use
|
|
40
|
-
* the OWNER package's subpaths. Imports within the copied directory (./x) are
|
|
41
|
-
* left untouched.
|
|
42
|
-
*
|
|
43
|
-
* e.g. with ownerPackage '@astryxdesign/core':
|
|
44
|
-
* '../theme/tokens.stylex' -> '@astryxdesign/core/theme/tokens.stylex'
|
|
45
|
-
* '../utils/mergeProps' -> '@astryxdesign/core/utils'
|
|
46
|
-
* '../../locales/en.json' -> '@astryxdesign/core/locales/en.json'
|
|
47
|
-
* import('../Tooltip/Tooltip') -> import('@astryxdesign/core/Tooltip')
|
|
48
|
-
*
|
|
49
|
-
* Most sibling dirs are barrels, so the top-dir collapse (`../utils/x` ->
|
|
50
|
-
* `<pkg>/utils`) resolves. Two shapes must NOT be collapsed to the top dir:
|
|
51
|
-
* - Asset files (`.json`, `.css`): exported by full subpath (e.g.
|
|
52
|
-
* `./locales/*.json`, `./reset.css`), and collapsing a two-levels-up
|
|
53
|
-
* `../../locales/x.json` would emit the invalid `<pkg>/..`.
|
|
54
|
-
* - StyleX modules (`*.stylex`): the StyleX compiler needs the real module
|
|
55
|
-
* path so it can resolve the styles at compile time. The barrel re-export
|
|
56
|
-
* loses the module identity. Core exports every `.stylex` file by its
|
|
57
|
-
* deep subpath (e.g. `./utils/focusOutline.stylex`,
|
|
58
|
-
* `./theme/tokens.stylex`).
|
|
59
|
-
*
|
|
60
|
-
* @param {string} content
|
|
61
|
-
* @param {string} [ownerPackage]
|
|
62
|
-
* @param {(importPath: string) => boolean} [keep] Imports to leave relative:
|
|
63
|
-
* the modules the copy carries in the same layout (see `sharedModulesOf`).
|
|
64
|
-
*/
|
|
65
|
-
export function rewriteImports(content, ownerPackage = CORE_PACKAGE, keep = () => false) {
|
|
66
|
-
/** @param {string} importPath */
|
|
67
|
-
const mapTarget = importPath => {
|
|
68
|
-
// A module the copy carries beside the component keeps its relative path.
|
|
69
|
-
if (keep(importPath)) {
|
|
70
|
-
return importPath;
|
|
71
|
-
}
|
|
72
|
-
// Strip ALL leading `../` so a two-levels-up path never yields `<pkg>/..`.
|
|
73
|
-
const rest = importPath.replace(/^(?:\.\.\/)+/, '');
|
|
74
|
-
const parts = rest.split('/');
|
|
75
|
-
const last = parts[parts.length - 1];
|
|
76
|
-
// Asset files are resolved by exact export / wildcard, never a barrel.
|
|
77
|
-
if (/\.(?:json|css)$/.test(last)) {
|
|
78
|
-
return `${ownerPackage}/${rest}`;
|
|
79
|
-
}
|
|
80
|
-
// StyleX modules need the deep path so the compiler can resolve them.
|
|
81
|
-
if (/\.stylex(?:\.[cm]?[jt]sx?)?$/.test(last)) {
|
|
82
|
-
return `${ownerPackage}/${rest}`;
|
|
83
|
-
}
|
|
84
|
-
return `${ownerPackage}/${parts[0]}`;
|
|
85
|
-
};
|
|
86
|
-
return content
|
|
87
|
-
.replace(
|
|
88
|
-
/(from\s+['"])(\.\.\/[^'"]+)(['"])/g,
|
|
89
|
-
(_m, prefix, importPath, suffix) =>
|
|
90
|
-
`${prefix}${mapTarget(importPath)}${suffix}`,
|
|
91
|
-
)
|
|
92
|
-
.replace(
|
|
93
|
-
/(import\(\s*['"])(\.\.\/[^'"]+)(['"])/g,
|
|
94
|
-
(_m, prefix, importPath, suffix) =>
|
|
95
|
-
`${prefix}${mapTarget(importPath)}${suffix}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
43
|
/** A private data attribute's owner module (`architecture:public-component-api` INV23). */
|
|
100
44
|
const OWNER_MODULE = /^privateAttributes\/[^/]+\.stylex$/;
|
|
101
45
|
/** A StyleX module, with or without its file extension. */
|
|
102
46
|
const STYLEX_MODULE = /\.stylex(?:\.[cm]?[jt]sx?)?$/;
|
|
103
|
-
/**
|
|
104
|
-
|
|
47
|
+
/** @param {string} packageDir */
|
|
48
|
+
function packageVersionOf(packageDir) {
|
|
49
|
+
try {
|
|
50
|
+
return String(JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8')).version ?? '0');
|
|
51
|
+
} catch {
|
|
52
|
+
return '0';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
105
55
|
|
|
106
56
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
57
|
+
* Where a component's module graph starts: its folder's index, or else every
|
|
58
|
+
* source file at the top of the folder.
|
|
59
|
+
* @param {string} componentDir
|
|
109
60
|
* @returns {string[]}
|
|
110
61
|
*/
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
/import\(\s*['"](\.\.?\/[^'"]+)['"]/g,
|
|
116
|
-
]) {
|
|
117
|
-
for (const m of content.matchAll(re)) found.push(m[1]);
|
|
62
|
+
function entryFilesOf(componentDir) {
|
|
63
|
+
for (const name of ['index.ts', 'index.tsx', 'index.mjs', 'index.js']) {
|
|
64
|
+
const file = path.join(componentDir, name);
|
|
65
|
+
if (fs.existsSync(file)) return [file];
|
|
118
66
|
}
|
|
119
|
-
return
|
|
67
|
+
return fs
|
|
68
|
+
.readdirSync(componentDir)
|
|
69
|
+
.filter(f => /\.[cm]?[jt]sx?$/.test(f) && !/\.(?:test|spec|stories|doc)\./.test(f) && !/\.d\.[cm]?ts$/.test(f))
|
|
70
|
+
.map(f => path.join(componentDir, f))
|
|
71
|
+
.filter(f => fs.statSync(f).isFile());
|
|
120
72
|
}
|
|
121
73
|
|
|
122
74
|
/**
|
|
123
|
-
*
|
|
124
|
-
* `srcRoot` without its extension (`privateAttributes/press.stylex`), and the
|
|
125
|
-
* file's own relative path; null when it resolves outside `srcRoot` or to no
|
|
126
|
-
* file.
|
|
75
|
+
* A module path relative to the source root, without its extension.
|
|
127
76
|
* @param {string} srcRoot
|
|
128
|
-
* @param {string}
|
|
129
|
-
* @param {string} importPath
|
|
130
|
-
* @returns {{module: string, file: string} | null}
|
|
77
|
+
* @param {string} file
|
|
131
78
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
if (module.startsWith('..') || path.posix.isAbsolute(module)) return null;
|
|
135
|
-
for (const ext of ['', ...SOURCE_EXTENSIONS]) {
|
|
136
|
-
const file = module + ext;
|
|
137
|
-
const abs = path.join(srcRoot, file);
|
|
138
|
-
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
139
|
-
return {
|
|
140
|
-
module: ext ? module : module.replace(/\.[cm]?[jt]sx?$/, ''),
|
|
141
|
-
file,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
79
|
+
function toModulePath(srcRoot, file) {
|
|
80
|
+
return toPosix(path.relative(srcRoot, file)).replace(/\.[cm]?[jt]sx?$/, '');
|
|
146
81
|
}
|
|
147
82
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* @param {string} srcRoot
|
|
152
|
-
* @returns {Set<string>}
|
|
153
|
-
*/
|
|
154
|
-
function exportedSubpathsOf(srcRoot) {
|
|
155
|
-
let dir = srcRoot;
|
|
156
|
-
for (;;) {
|
|
157
|
-
const manifest = path.join(dir, 'package.json');
|
|
158
|
-
if (fs.existsSync(manifest)) {
|
|
159
|
-
try {
|
|
160
|
-
const {exports} = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
161
|
-
return new Set(
|
|
162
|
-
exports && typeof exports === 'object' ? Object.keys(exports) : [],
|
|
163
|
-
);
|
|
164
|
-
} catch {
|
|
165
|
-
return new Set();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const parent = path.dirname(dir);
|
|
169
|
-
if (parent === dir) return new Set();
|
|
170
|
-
dir = parent;
|
|
171
|
-
}
|
|
83
|
+
/** @param {string} file */
|
|
84
|
+
function toPosix(file) {
|
|
85
|
+
return file.split(path.sep).join('/');
|
|
172
86
|
}
|
|
173
87
|
|
|
174
88
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* attribute owner module the component imports, and every unexported StyleX
|
|
178
|
-
* module such a carried module imports, transitively. The component's own
|
|
179
|
-
* imports of them, and theirs of each other, stay relative.
|
|
180
|
-
*
|
|
181
|
-
* @param {string} srcRoot the owner package's source root
|
|
182
|
-
* @param {string} componentDir
|
|
183
|
-
* @param {string[]} componentFiles files of the component that are copied
|
|
184
|
-
* @returns {Map<string, string>} module path (relative to srcRoot, no
|
|
185
|
-
* extension) -> its file path relative to srcRoot
|
|
89
|
+
* @param {string} target
|
|
90
|
+
* @param {string} root
|
|
186
91
|
*/
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
.relative(srcRoot, componentDir)
|
|
191
|
-
.split(path.sep)
|
|
192
|
-
.join('/');
|
|
193
|
-
/** @type {Map<string, string>} */
|
|
194
|
-
const carried = new Map();
|
|
195
|
-
/** @type {Array<{fromDir: string, content: string, owner: boolean}>} */
|
|
196
|
-
const queue = componentFiles
|
|
197
|
-
.filter(f => /\.[cm]?[jt]sx?$/.test(f))
|
|
198
|
-
.map(f => ({
|
|
199
|
-
fromDir: componentRel,
|
|
200
|
-
content: fs.readFileSync(path.join(componentDir, f), 'utf-8'),
|
|
201
|
-
owner: false,
|
|
202
|
-
}));
|
|
203
|
-
while (queue.length > 0) {
|
|
204
|
-
const {fromDir, content, owner} = /** @type {typeof queue[number]} */ (
|
|
205
|
-
queue.shift()
|
|
206
|
-
);
|
|
207
|
-
for (const importPath of relativeImportsOf(content)) {
|
|
208
|
-
const target = resolveSourceModule(srcRoot, fromDir, importPath);
|
|
209
|
-
if (target == null || carried.has(target.module)) continue;
|
|
210
|
-
if (target.module.startsWith(componentRel + '/')) continue;
|
|
211
|
-
const isOwner = OWNER_MODULE.test(target.module);
|
|
212
|
-
// A carried module's unexported StyleX dependency travels with it; a
|
|
213
|
-
// component's own imports of other modules are rewritten as before.
|
|
214
|
-
const isPrivateStyle =
|
|
215
|
-
owner &&
|
|
216
|
-
STYLEX_MODULE.test(target.module) &&
|
|
217
|
-
!exported.has(`./${target.module}`);
|
|
218
|
-
if (!isOwner && !isPrivateStyle) continue;
|
|
219
|
-
carried.set(target.module, target.file);
|
|
220
|
-
queue.push({
|
|
221
|
-
fromDir: path.posix.dirname(target.module),
|
|
222
|
-
content: fs.readFileSync(path.join(srcRoot, target.file), 'utf-8'),
|
|
223
|
-
owner: true,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return carried;
|
|
92
|
+
function isWithin(target, root) {
|
|
93
|
+
const rel = path.relative(root, target);
|
|
94
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
228
95
|
}
|
|
229
96
|
|
|
230
97
|
/**
|
|
231
|
-
* A
|
|
232
|
-
*
|
|
233
|
-
* @param {
|
|
234
|
-
* @param {string}
|
|
235
|
-
* @
|
|
98
|
+
* A helper's file name with the owner version before its extension:
|
|
99
|
+
* `layerStack.ts` -> `layerStack.v0.7.9.ts`, or `layerStack.v0.7.9-2.ts`.
|
|
100
|
+
* @param {string} dest
|
|
101
|
+
* @param {string} version
|
|
102
|
+
* @param {number} n
|
|
236
103
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.replace(/\.[cm]?[jt]sx?$/, '');
|
|
242
|
-
return carried.has(module);
|
|
243
|
-
};
|
|
104
|
+
function versionedPath(dest, version, n) {
|
|
105
|
+
const ext = /(\.stylex)?\.[cm]?[jt]sx?$/.exec(dest)?.[0] ?? path.extname(dest);
|
|
106
|
+
const stem = dest.slice(0, dest.length - ext.length);
|
|
107
|
+
return `${stem}.v${version}${n > 1 ? `-${n}` : ''}${ext}`;
|
|
244
108
|
}
|
|
245
109
|
|
|
246
110
|
/**
|
|
@@ -339,20 +203,10 @@ function confineWrite(target, cwd, options) {
|
|
|
339
203
|
}
|
|
340
204
|
}
|
|
341
205
|
|
|
342
|
-
/** @param {string} file */
|
|
343
|
-
function isExcludedFromCopy(file) {
|
|
344
|
-
return (
|
|
345
|
-
file.includes('.test.') ||
|
|
346
|
-
file.includes('.doc.') ||
|
|
347
|
-
file === 'README.md' ||
|
|
348
|
-
// Browser test harnesses import dev-only packages a consumer cannot resolve.
|
|
349
|
-
/\.spec\.[cm]?[jt]sx?$/.test(file)
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
206
|
/**
|
|
354
207
|
* Copy one component's source into the consumer project for customization,
|
|
355
|
-
*
|
|
208
|
+
* with the internal modules it needs and its imports of the owner package's
|
|
209
|
+
* public paths.
|
|
356
210
|
*
|
|
357
211
|
* @param {string} component bare or XDS-prefixed component name
|
|
358
212
|
* @param {{cwd?: string, output?: string, package?: string, overwrite?: boolean}} [options]
|
|
@@ -463,58 +317,146 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
463
317
|
// writes anything.
|
|
464
318
|
const feedback = buildFeedback(dirName, owner.issuesUrl);
|
|
465
319
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
320
|
+
// The copy follows the component's module graph through the whole source
|
|
321
|
+
// root: `src/` when the package has one around the component, else the
|
|
322
|
+
// package itself.
|
|
323
|
+
const packageDir = findPackageRoot(componentDir) ?? componentDir;
|
|
324
|
+
const packageSrc = path.join(packageDir, 'src');
|
|
325
|
+
const inPackageSrc = componentDir.startsWith(packageSrc + path.sep);
|
|
326
|
+
const graphRoot = inPackageSrc ? packageSrc : packageDir;
|
|
327
|
+
// Carried modules keep their layout relative to this folder.
|
|
328
|
+
const srcRoot = inPackageSrc ? packageSrc : path.dirname(componentDir);
|
|
329
|
+
const plan = planCopy({
|
|
330
|
+
packageDir,
|
|
331
|
+
srcRoot: graphRoot,
|
|
332
|
+
componentDir,
|
|
333
|
+
ownerPackage: owner.ownerPackage,
|
|
334
|
+
entries: entryFilesOf(componentDir),
|
|
470
335
|
});
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
336
|
+
const ownerVersion = packageVersionOf(packageDir);
|
|
337
|
+
|
|
338
|
+
// Carried modules keep the package's layout around the component folder.
|
|
339
|
+
const depth = path.relative(srcRoot, componentDir).split(path.sep).length;
|
|
340
|
+
const shiftedRoot = path.resolve(outputDir, ...Array(depth).fill('..'));
|
|
341
|
+
const layoutRoot = isWithin(shiftedRoot, outputBase) ? shiftedRoot : outputBase;
|
|
342
|
+
/** @param {string} file */
|
|
343
|
+
const isComponentFile = file => file.startsWith(componentDir + path.sep);
|
|
344
|
+
// A module above that folder, such as `lib/` beside an integration's
|
|
345
|
+
// `components/`, goes under the output folder by its path in the package.
|
|
346
|
+
/** @param {string} file */
|
|
347
|
+
const layoutDestOf = file => {
|
|
348
|
+
if (isComponentFile(file)) {
|
|
349
|
+
return path.join(outputDir, path.relative(componentDir, file));
|
|
350
|
+
}
|
|
351
|
+
if (file.startsWith(srcRoot + path.sep)) {
|
|
352
|
+
return path.join(layoutRoot, path.relative(srcRoot, file));
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
};
|
|
356
|
+
const allFiles = [...plan.files.keys()];
|
|
357
|
+
// Such a module takes a folder named after the package when its own path
|
|
358
|
+
// is already a destination, so two modules never share one file.
|
|
359
|
+
const takenDests = new Set(
|
|
360
|
+
allFiles.map(layoutDestOf).filter(dest => dest != null),
|
|
361
|
+
);
|
|
362
|
+
/** @type {Map<string, string>} */
|
|
363
|
+
const rootDests = new Map();
|
|
364
|
+
for (const file of [...allFiles].sort()) {
|
|
365
|
+
if (layoutDestOf(file) != null) continue;
|
|
366
|
+
const packagePath = path.relative(graphRoot, file);
|
|
367
|
+
let dest = path.join(outputBase, packagePath);
|
|
368
|
+
for (let n = 1; takenDests.has(dest); n++) {
|
|
369
|
+
const folder = n === 1 ? path.basename(packageDir) : `${path.basename(packageDir)}-${n}`;
|
|
370
|
+
dest = path.join(outputBase, folder, packagePath);
|
|
371
|
+
}
|
|
372
|
+
takenDests.add(dest);
|
|
373
|
+
rootDests.set(file, dest);
|
|
374
|
+
}
|
|
375
|
+
/** @param {string} file */
|
|
376
|
+
const baseDestOf = file =>
|
|
377
|
+
layoutDestOf(file) ?? /** @type {string} */ (rootDests.get(file));
|
|
378
|
+
|
|
379
|
+
const componentFiles = allFiles.filter(isComponentFile);
|
|
380
|
+
const helpers = allFiles.filter(f => !isComponentFile(f));
|
|
381
|
+
// A private attribute owner module and the unexported style modules it
|
|
382
|
+
// imports travel whole and are replaced only with --overwrite.
|
|
383
|
+
const ownerGroup = new Set();
|
|
384
|
+
const ownerQueue = helpers.filter(f =>
|
|
385
|
+
OWNER_MODULE.test(toModulePath(srcRoot, f)),
|
|
386
|
+
);
|
|
387
|
+
while (ownerQueue.length > 0) {
|
|
388
|
+
const file = /** @type {string} */ (ownerQueue.shift());
|
|
389
|
+
if (ownerGroup.has(file)) continue;
|
|
390
|
+
ownerGroup.add(file);
|
|
391
|
+
for (const dep of plan.dependsOn.get(file) ?? []) {
|
|
392
|
+
if (!isComponentFile(dep) && STYLEX_MODULE.test(toModulePath(srcRoot, dep))) {
|
|
393
|
+
ownerQueue.push(dep);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
476
396
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
owner.ownerPackage,
|
|
489
|
-
keepCarried(carried, path.posix.dirname(file)),
|
|
397
|
+
|
|
398
|
+
/** @type {Map<string, string>} helper -> the file it is written to instead */
|
|
399
|
+
const redirected = new Map();
|
|
400
|
+
/** @param {string} file */
|
|
401
|
+
const destOf = file => redirected.get(file) ?? baseDestOf(file);
|
|
402
|
+
/** @param {string} file */
|
|
403
|
+
const render = file =>
|
|
404
|
+
renderFile(/** @type {any} */ (plan.files.get(file)), choice =>
|
|
405
|
+
typeof choice === 'string'
|
|
406
|
+
? choice
|
|
407
|
+
: relativeSpecifierBetween(destOf(choice.from), destOf(choice.to)),
|
|
490
408
|
);
|
|
491
|
-
|
|
409
|
+
/** @param {string} dest */
|
|
410
|
+
const readExisting = dest => {
|
|
411
|
+
if (!fs.existsSync(dest)) return null;
|
|
412
|
+
try {
|
|
413
|
+
return fs.readFileSync(dest, 'utf-8');
|
|
414
|
+
} catch {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
// A helper an earlier swizzle wrote is reused when it is the same. A
|
|
419
|
+
// different file is never overwritten: this copy moves its own helper to
|
|
420
|
+
// the next free version-suffixed name. A move changes the import in every
|
|
421
|
+
// helper that imports it, so the check repeats until nothing moves.
|
|
422
|
+
/** @type {Map<string, number>} helper -> how many names it tried */
|
|
423
|
+
const attempts = new Map();
|
|
424
|
+
for (let moved = true; moved; ) {
|
|
425
|
+
moved = false;
|
|
426
|
+
for (const file of helpers) {
|
|
427
|
+
if (ownerGroup.has(file)) continue;
|
|
428
|
+
const existing = readExisting(destOf(file));
|
|
429
|
+
if (existing == null || existing === render(file)) continue;
|
|
430
|
+
const attempt = (attempts.get(file) ?? 0) + 1;
|
|
431
|
+
attempts.set(file, attempt);
|
|
432
|
+
redirected.set(
|
|
433
|
+
file,
|
|
434
|
+
versionedPath(baseDestOf(file), ownerVersion, attempt),
|
|
435
|
+
);
|
|
436
|
+
moved = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// The component's own files come first, then the helpers it carries.
|
|
441
|
+
const outputs = [...componentFiles, ...helpers].map(file => {
|
|
442
|
+
const dest = confineWrite(destOf(file), cwd, {
|
|
492
443
|
allowAbsolute: true,
|
|
493
444
|
label: 'output file',
|
|
494
445
|
});
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (fs.existsSync(dest)) {
|
|
498
|
-
try {
|
|
499
|
-
existing = fs.readFileSync(dest, 'utf-8');
|
|
500
|
-
} catch {
|
|
501
|
-
existing = '';
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// A copy another component's swizzle already wrote is reused when it is
|
|
505
|
-
// the same; a changed one is a file this run would overwrite.
|
|
446
|
+
const content = render(file);
|
|
447
|
+
const existing = readExisting(dest);
|
|
506
448
|
return {
|
|
507
449
|
file,
|
|
508
450
|
dest,
|
|
509
451
|
content,
|
|
510
|
-
|
|
452
|
+
component: isComponentFile(file),
|
|
453
|
+
owner: ownerGroup.has(file),
|
|
511
454
|
exists: existing != null,
|
|
455
|
+
unchanged: existing === content,
|
|
512
456
|
};
|
|
513
457
|
});
|
|
514
|
-
const existingFiles =
|
|
515
|
-
|
|
516
|
-
);
|
|
517
|
-
const changedShared = shared.filter(s => s.exists && !s.unchanged);
|
|
458
|
+
const existingFiles = outputs.filter(o => o.component && o.exists);
|
|
459
|
+
const changedShared = outputs.filter(o => o.owner && o.exists && !o.unchanged);
|
|
518
460
|
if ((existingFiles.length > 0 || changedShared.length > 0) && !overwrite) {
|
|
519
461
|
const relOutputForMsg = path.relative(cwd, outputDir) || '.';
|
|
520
462
|
const sharedNote =
|
|
@@ -531,109 +473,48 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
531
473
|
);
|
|
532
474
|
}
|
|
533
475
|
|
|
534
|
-
|
|
535
476
|
const outputDirExisted = fs.existsSync(outputDir);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
} catch (err) {
|
|
539
|
-
throw writeFailed(outputDir, cwd, err);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const files = fs.readdirSync(componentDir);
|
|
543
|
-
let copied = 0;
|
|
544
|
-
let usesStyleX = false;
|
|
545
|
-
// A copy that fails part-way undoes what it already wrote, so the report is
|
|
546
|
-
// true and a retry does not trip over half a component. Each entry keeps
|
|
547
|
-
// the bytes the file had before this run (null when the copy created it).
|
|
477
|
+
/** @type {string[]} folders this run created, outermost first */
|
|
478
|
+
const createdDirs = [];
|
|
548
479
|
/** @type {Array<{dest: string, original: Buffer|null}>} */
|
|
549
480
|
const written = [];
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
555
|
-
if (file.endsWith('.ts') || file.endsWith('.tsx')) {
|
|
556
|
-
content = rewriteImports(content, owner.ownerPackage, keepCarried(carried, componentRel));
|
|
557
|
-
}
|
|
558
|
-
if (
|
|
559
|
-
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
|
|
560
|
-
content.includes('@stylexjs/stylex')
|
|
561
|
-
) {
|
|
562
|
-
usesStyleX = true;
|
|
563
|
-
}
|
|
564
|
-
const dest = path.join(outputDir, file);
|
|
565
|
-
try {
|
|
566
|
-
// The snapshot read is guarded too: a destination that cannot be read
|
|
567
|
-
// back (no permission, or a directory with this name) must still undo
|
|
568
|
-
// the earlier writes and report ERR_WRITE_FAILED.
|
|
569
|
-
const original = fs.existsSync(dest) ? fs.readFileSync(dest) : null;
|
|
570
|
-
written.push({dest, original});
|
|
571
|
-
fs.writeFileSync(dest, content);
|
|
572
|
-
} catch (err) {
|
|
573
|
-
const unrestored = undoCopy(written);
|
|
574
|
-
if (!outputDirExisted) {
|
|
575
|
-
try {
|
|
576
|
-
fs.rmdirSync(outputDir);
|
|
577
|
-
} catch {
|
|
578
|
-
// Not empty (something could not be undone), or already gone.
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
throw writeFailed(dest, cwd, err, unrestored);
|
|
582
|
-
}
|
|
583
|
-
copied++;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/** @type {string[]} folders this run created for carried modules, outermost first */
|
|
587
|
-
const createdDirs = [];
|
|
588
|
-
for (const module of shared) {
|
|
589
|
-
// An identical copy is already in place: it is part of this copy, unwritten.
|
|
590
|
-
if (module.unchanged) {
|
|
481
|
+
let copied = 0;
|
|
482
|
+
for (const output of outputs) {
|
|
483
|
+
// An identical file is already in place: it is part of this copy, unwritten.
|
|
484
|
+
if (output.unchanged) {
|
|
591
485
|
copied++;
|
|
592
486
|
continue;
|
|
593
487
|
}
|
|
594
488
|
try {
|
|
595
|
-
const original = fs.existsSync(
|
|
596
|
-
|
|
597
|
-
: null;
|
|
598
|
-
const firstCreated = fs.mkdirSync(path.dirname(module.dest), {recursive: true});
|
|
489
|
+
const original = fs.existsSync(output.dest) ? fs.readFileSync(output.dest) : null;
|
|
490
|
+
const firstCreated = fs.mkdirSync(path.dirname(output.dest), {recursive: true});
|
|
599
491
|
if (firstCreated != null) createdDirs.push(firstCreated);
|
|
600
|
-
written.push({dest:
|
|
601
|
-
fs.writeFileSync(
|
|
492
|
+
written.push({dest: output.dest, original});
|
|
493
|
+
fs.writeFileSync(output.dest, output.content);
|
|
602
494
|
copied++;
|
|
603
495
|
} catch (err) {
|
|
604
496
|
const unrestored = undoCopy(written);
|
|
605
|
-
// Remove the folders this run created, as a failed component-file write
|
|
606
|
-
// does, so a retry finds the output as it was.
|
|
607
497
|
for (const dir of [...createdDirs].reverse()) {
|
|
608
498
|
fs.rmSync(dir, {recursive: true, force: true});
|
|
609
499
|
}
|
|
610
500
|
if (!outputDirExisted) {
|
|
611
501
|
fs.rmSync(outputDir, {recursive: true, force: true});
|
|
612
502
|
}
|
|
613
|
-
throw writeFailed(
|
|
503
|
+
throw writeFailed(output.dest, cwd, err, unrestored);
|
|
614
504
|
}
|
|
615
505
|
}
|
|
616
|
-
if (
|
|
506
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, {recursive: true});
|
|
617
507
|
|
|
618
508
|
const relOutput = path.relative(cwd, outputDir);
|
|
619
|
-
const copiedFiles = files.filter(
|
|
620
|
-
f =>
|
|
621
|
-
!isExcludedFromCopy(f) &&
|
|
622
|
-
fs.statSync(path.join(componentDir, f)).isFile(),
|
|
623
|
-
);
|
|
624
509
|
/** @type {import('../swizzle.type.mjs').SwizzleCopyResponse['data']} */
|
|
625
510
|
const data = {
|
|
626
511
|
component: dirName,
|
|
627
512
|
package: owner.package,
|
|
628
513
|
outputDir: relOutput,
|
|
629
514
|
filesCopied: copied,
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
...copiedFiles.map(f => f),
|
|
634
|
-
...shared.map(m => path.relative(outputDir, m.dest).split(path.sep).join('/')),
|
|
635
|
-
],
|
|
636
|
-
usesStyleX,
|
|
515
|
+
files: outputs.map(o => toPosix(path.relative(outputDir, o.dest))),
|
|
516
|
+
usesStyleX: outputs.some(o => o.content.includes('@stylexjs/stylex')),
|
|
517
|
+
dependencies: plan.dependencies,
|
|
637
518
|
};
|
|
638
519
|
if (feedback) data.feedback = feedback;
|
|
639
520
|
return {type: 'swizzle.copy', package: data.package, data};
|
|
@@ -15,5 +15,3 @@ export function swizzle(component?: string, options?: {
|
|
|
15
15
|
list?: boolean;
|
|
16
16
|
overwrite?: boolean;
|
|
17
17
|
}): Promise<import("./swizzle.type.mjs").SwizzleListResponse | import("./swizzle.type.mjs").SwizzleCopyResponse>;
|
|
18
|
-
export { rewriteImports };
|
|
19
|
-
import { rewriteImports } from './copy/copy.mjs';
|
|
@@ -17,8 +17,12 @@ export const doc = {
|
|
|
17
17
|
description:
|
|
18
18
|
"Ejects a component's source into the consumer project for deep customization. " +
|
|
19
19
|
'It copies from the locally resolved @astryxdesign/core (or the owning integration) ' +
|
|
20
|
-
'package source
|
|
21
|
-
|
|
20
|
+
'package source. Every import in the copy names a file the copy holds or a path ' +
|
|
21
|
+
'the owner package exports: names the package exports are imported from its ' +
|
|
22
|
+
'public paths, and every internal module the component needs is ' +
|
|
23
|
+
"copied beside it in the package's folder layout. Tests, stories and docs are not " +
|
|
24
|
+
'copied. The receipt flags whether any copied file uses StyleX and lists each npm ' +
|
|
25
|
+
'package the copy imports with the range the owner package declares. With no name ' +
|
|
22
26
|
'(or list) it returns the swizzlable component names instead. An integration ' +
|
|
23
27
|
'component that replaces a Core component is what that Core name copies; ' +
|
|
24
28
|
"options.package '@astryxdesign/core' copies the original. The list names Core " +
|
|
@@ -63,7 +67,8 @@ export const doc = {
|
|
|
63
67
|
{
|
|
64
68
|
name: 'options.overwrite',
|
|
65
69
|
type: 'boolean',
|
|
66
|
-
description:
|
|
70
|
+
description:
|
|
71
|
+
"Replace the component's existing files instead of erroring. A helper an earlier swizzle wrote is never replaced by a copy that carries it: a different one stays, and the new copy writes its own under a version-suffixed name. Swizzling the component whose folder holds that helper counts it as one of that component's own files. A changed private attribute owner module, or a style module it imports, throws ERR_FILE_EXISTS without overwrite and is replaced with it.",
|
|
67
72
|
default: 'false',
|
|
68
73
|
},
|
|
69
74
|
],
|
|
@@ -76,7 +81,7 @@ export const doc = {
|
|
|
76
81
|
{
|
|
77
82
|
type: 'swizzle.copy',
|
|
78
83
|
description:
|
|
79
|
-
'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the copied file names (relative to the component folder
|
|
84
|
+
'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the copied file names (relative to the component folder; the internal modules copied beside it start with `../`, e.g. `../InputGroup/groupStyles.ts`), whether any file uses StyleX, and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling.',
|
|
80
85
|
},
|
|
81
86
|
],
|
|
82
87
|
throws: [
|