@astryxdesign/cli 0.6.3-canary.866c9ea → 0.6.3-canary.8d3d5b6
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/assets/codemods/integration-discovery.mjs +40 -2
- package/assets/codemods/integration-discovery.test.mjs +58 -0
- package/assets/docs/cli-integrations.doc.mjs +9 -0
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/package.json +9 -9
|
@@ -32,6 +32,40 @@ import {semverCompare} from '../../foundation/env/semver.mjs';
|
|
|
32
32
|
/** File extensions recognized as codemod modules. */
|
|
33
33
|
const CODEMOD_EXTENSIONS = ['.ts', '.mjs', '.js'];
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Directories never walked for codemods: a test directory beside a transform is
|
|
37
|
+
* the natural place to put its test, and every file found here is loaded and
|
|
38
|
+
* validated as a codemod.
|
|
39
|
+
*/
|
|
40
|
+
const SKIP_DIRS = new Set([
|
|
41
|
+
'node_modules',
|
|
42
|
+
'.git',
|
|
43
|
+
'__tests__',
|
|
44
|
+
'__fixtures__',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a file name is a test or fixture rather than a codemod.
|
|
49
|
+
*
|
|
50
|
+
* The trap this closes: EVERY `.ts`/`.mjs`/`.js` under a version folder used to
|
|
51
|
+
* be loaded as a codemod, so a test file colocated with its transform failed
|
|
52
|
+
* validation and — because a definition error is a hard error — took every
|
|
53
|
+
* codemod in that package's version with it. `astryx upgrade` then applied no
|
|
54
|
+
* transforms and reported success, which is the worst shape a failure can take.
|
|
55
|
+
*
|
|
56
|
+
* Core's own codemods never hit this: they are enumerated in a registry, and
|
|
57
|
+
* their colocated tests are simply not in it. Only integrations are discovered
|
|
58
|
+
* by walking a directory, so only integrations carry the landmine — which is why
|
|
59
|
+
* this is a loader fix and not a documentation one. It protects the integrations
|
|
60
|
+
* that already exist, which no authoring tool can reach.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name a file's base name
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isTestFile(name) {
|
|
66
|
+
return /\.(test|spec)\.[^.]+$/.test(name) || /\.fixture\.[^.]+$/.test(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
/**
|
|
36
70
|
* Recursively collect codemod module files under a version folder. Returns
|
|
37
71
|
* entries of {id, file} where id is the extension-less relative path
|
|
@@ -49,14 +83,18 @@ function collectCodemodFiles(versionDir) {
|
|
|
49
83
|
for (const entry of entries) {
|
|
50
84
|
const full = path.join(dir, entry.name);
|
|
51
85
|
if (entry.isDirectory()) {
|
|
52
|
-
if (
|
|
86
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
53
87
|
walk(full);
|
|
54
88
|
continue;
|
|
55
89
|
}
|
|
56
90
|
const ext = path.extname(entry.name);
|
|
57
91
|
if (!CODEMOD_EXTENSIONS.includes(ext)) continue;
|
|
92
|
+
if (isTestFile(entry.name)) continue;
|
|
58
93
|
const rel = path.relative(versionDir, full);
|
|
59
|
-
const id = rel
|
|
94
|
+
const id = rel
|
|
95
|
+
.slice(0, rel.length - ext.length)
|
|
96
|
+
.split(path.sep)
|
|
97
|
+
.join('/');
|
|
60
98
|
out.push({id, file: full});
|
|
61
99
|
}
|
|
62
100
|
}
|
|
@@ -221,3 +221,61 @@ describe('integration codemod discovery', () => {
|
|
|
221
221
|
).rejects.toThrow(/across versions/i);
|
|
222
222
|
});
|
|
223
223
|
});
|
|
224
|
+
|
|
225
|
+
describe('test files beside a codemod', () => {
|
|
226
|
+
// The incident this closes: every .ts/.mjs/.js under a version folder was
|
|
227
|
+
// loaded AND VALIDATED as a codemod, so a test file colocated with its
|
|
228
|
+
// transform failed validation — and because a definition error is a hard
|
|
229
|
+
// error, it took every codemod in that version with it. `astryx upgrade`
|
|
230
|
+
// then applied nothing and reported success. Core is immune because its own
|
|
231
|
+
// codemods are enumerated in a registry rather than discovered by walking a
|
|
232
|
+
// directory, so its colocated tests are simply never visited. Only
|
|
233
|
+
// integrations carry the landmine.
|
|
234
|
+
const TRANSFORM = `
|
|
235
|
+
export default {
|
|
236
|
+
type: 'code',
|
|
237
|
+
title: 'Drop foo',
|
|
238
|
+
transform: (file) => file.source.replace(/foo/g, 'bar'),
|
|
239
|
+
};
|
|
240
|
+
`;
|
|
241
|
+
// Not a codemod: no default export of the right shape. Loading it throws.
|
|
242
|
+
const A_TEST = `
|
|
243
|
+
import {describe, it, expect} from 'vitest';
|
|
244
|
+
describe('drop-foo', () => {
|
|
245
|
+
it('drops foo', () => expect(1).toBe(1));
|
|
246
|
+
});
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
it.each([
|
|
250
|
+
['a .test. sibling', '0.2.0/drop-foo.test.mjs'],
|
|
251
|
+
['a .spec. sibling', '0.2.0/drop-foo.spec.mjs'],
|
|
252
|
+
['a fixture sibling', '0.2.0/drop-foo.fixture.mjs'],
|
|
253
|
+
['a __tests__ directory', '0.2.0/__tests__/drop-foo.mjs'],
|
|
254
|
+
['a __fixtures__ directory', '0.2.0/__fixtures__/input.mjs'],
|
|
255
|
+
])('ignores %s and still discovers the codemod', async (_label, testPath) => {
|
|
256
|
+
scaffold({'0.2.0/drop-foo.mjs': TRANSFORM, [testPath]: A_TEST});
|
|
257
|
+
|
|
258
|
+
const project = await Project.load(tmpDir);
|
|
259
|
+
const byVersion = await discoverIntegrationCodemods(
|
|
260
|
+
project.loadedIntegrations,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect([...byVersion.keys()]).toEqual(['0.2.0']);
|
|
264
|
+
expect(byVersion.get('0.2.0').map(entry => entry.id)).toEqual(['drop-foo']);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('a nested helper directory is still walked', async () => {
|
|
268
|
+
// Only test and fixture names are skipped. A package that organises its
|
|
269
|
+
// transforms into subdirectories keeps working.
|
|
270
|
+
scaffold({'0.2.0/imports/drop-foo.mjs': TRANSFORM});
|
|
271
|
+
|
|
272
|
+
const project = await Project.load(tmpDir);
|
|
273
|
+
const byVersion = await discoverIntegrationCodemods(
|
|
274
|
+
project.loadedIntegrations,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(byVersion.get('0.2.0').map(entry => entry.id)).toEqual([
|
|
278
|
+
'imports/drop-foo',
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -320,6 +320,15 @@ export const docs = {
|
|
|
320
320
|
type: 'prose',
|
|
321
321
|
text: 'Codemod ids (the extension-less relative path under the version folder, e.g. `rename-widget-prop`, `config/rename-integration`) must be unique within a package across all versions. A duplicate id across versions is a hard error.',
|
|
322
322
|
},
|
|
323
|
+
{
|
|
324
|
+
type: 'prose',
|
|
325
|
+
text: 'The loader automatically skips test and fixture files so you can colocate tests with transforms. Reserved names: files matching `*.test.*`, `*.spec.*`, or `*.fixture.*`, and any file under a `__tests__/` or `__fixtures__/` directory. These are never loaded as codemods regardless of their extension.',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
type: 'code',
|
|
329
|
+
lang: 'text',
|
|
330
|
+
code: 'codemods/\n 0.2.0/\n rename-widget-prop.ts # loaded as a codemod\n rename-widget-prop.test.ts # skipped (reserved name)\n __tests__/\n rename-widget-prop.test.ts # skipped (reserved directory)',
|
|
331
|
+
},
|
|
323
332
|
{
|
|
324
333
|
type: 'code',
|
|
325
334
|
lang: 'typescript',
|