@astryxdesign/cli 0.6.3-canary.7210f0d → 0.6.3-canary.7ada42e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/api/upgrade/_adapter.d.mts +22 -4
- package/api/upgrade/_adapter.mjs +19 -6
- package/api/upgrade/run/run.mjs +355 -59
- package/api/upgrade/upgrade.doc.mjs +5 -1
- package/api/upgrade/upgrade.type.d.mts +34 -0
- package/api/upgrade/upgrade.type.mjs +15 -0
- package/assets/codemods/__tests__/runner.test.mjs +330 -8
- package/assets/codemods/integration-runner.mjs +56 -4
- package/assets/codemods/integration-runner.protection.test.mjs +153 -0
- package/assets/codemods/run-codemod.mjs +177 -34
- package/assets/codemods/runner.mjs +248 -110
- package/assets/codemods/transforms/next/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
- package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +4 -0
- package/assets/codemods/transforms/next/index.mjs +8 -0
- package/assets/codemods/transforms/next/migrate-native-picker-to-presentation.mjs +148 -0
- package/assets/docs/cli-integrations.doc.mjs +13 -0
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
- package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
- package/authoring/codemod/type.ts +12 -0
- package/clients/cli/commands/build-theme.mjs +10 -44
- package/clients/cli/commands/docs.mjs +1 -1
- package/clients/cli/commands/text-json-parity.test.mjs +719 -0
- package/clients/cli/commands/theme-targets.behavior.test.mjs +4 -3
- package/clients/cli/commands/upgrade.doc.mjs +2 -1
- package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
- package/clients/cli/commands/upgrade.mjs +29 -7
- package/clients/cli/formatters/index.mjs +2 -0
- package/clients/cli/formatters/index.test.mjs +6 -0
- package/foundation/fs/file-protection.d.mts +33 -0
- package/foundation/fs/file-protection.mjs +825 -0
- package/foundation/fs/file-protection.test.mjs +250 -0
- package/foundation/response/error-codes.d.mts +3 -1
- package/foundation/response/error-codes.d.ts +2 -0
- package/foundation/response/error-codes.doc.mjs +10 -0
- package/foundation/response/error-codes.mjs +6 -0
- package/foundation/response/error-codes.test.mjs +55 -11
- package/package.json +10 -9
|
@@ -121,7 +121,7 @@ export const doc = {
|
|
|
121
121
|
{
|
|
122
122
|
type: 'upgrade.run',
|
|
123
123
|
description:
|
|
124
|
-
'The terminal run receipt: from/to versions,
|
|
124
|
+
'The terminal run receipt: from/to versions, codemod count, integrations processed, agent-docs and registry summaries, modifiedFiles, protectedFiles, declinedCandidates, and completion state. A protected required change returns complete: false with ERR_CODEMOD_PROTECTED; the CLI exits nonzero while preserving the structured receipt.',
|
|
125
125
|
},
|
|
126
126
|
],
|
|
127
127
|
throws: [
|
|
@@ -147,6 +147,10 @@ export const doc = {
|
|
|
147
147
|
code: 'ERR_CODEMOD_FAILED',
|
|
148
148
|
when: 'one or more codemods failed, or a post-codemod hook failed',
|
|
149
149
|
},
|
|
150
|
+
{
|
|
151
|
+
code: 'ERR_CODEMOD_PROTECTION_SOURCE',
|
|
152
|
+
when: 'a working-tree protection declaration cannot be read or parsed before writes begin',
|
|
153
|
+
},
|
|
150
154
|
],
|
|
151
155
|
examples: [
|
|
152
156
|
{
|
|
@@ -131,6 +131,31 @@ export type UpgradeRegistryResponse = {
|
|
|
131
131
|
type: "upgrade.registry";
|
|
132
132
|
data: RegistryCompositionSummary;
|
|
133
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* A required codemod change blocked by effective working-tree protection.
|
|
136
|
+
*/
|
|
137
|
+
export type ProtectedCodemodFile = {
|
|
138
|
+
/**
|
|
139
|
+
* Project-relative path.
|
|
140
|
+
*/
|
|
141
|
+
file: string;
|
|
142
|
+
/**
|
|
143
|
+
* Codemods that still require a change.
|
|
144
|
+
*/
|
|
145
|
+
codemods: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Protection categories (generated, vendored, ignored, and hard boundaries).
|
|
148
|
+
*/
|
|
149
|
+
reasons: string[];
|
|
150
|
+
/**
|
|
151
|
+
* Effective declarations that protect the file.
|
|
152
|
+
*/
|
|
153
|
+
declarations: string[];
|
|
154
|
+
/**
|
|
155
|
+
* Exact regeneration commands found in generated headers, when present.
|
|
156
|
+
*/
|
|
157
|
+
commands: string[];
|
|
158
|
+
};
|
|
134
159
|
/**
|
|
135
160
|
* astryx --json upgrade [--apply]
|
|
136
161
|
*/
|
|
@@ -144,7 +169,16 @@ export type UpgradeRunResponse = {
|
|
|
144
169
|
agentDocsRefreshed: boolean;
|
|
145
170
|
agentDocs: AgentDocsSummary;
|
|
146
171
|
registryCompositions?: RegistryCompositionSummary | undefined;
|
|
172
|
+
complete?: boolean | undefined;
|
|
173
|
+
errorCode?: "ERR_CODEMOD_PROTECTED" | undefined;
|
|
147
174
|
filesChanged?: number | undefined;
|
|
175
|
+
modifiedFiles?: string[] | undefined;
|
|
176
|
+
protectedFiles?: ProtectedCodemodFile[] | undefined;
|
|
177
|
+
declinedCandidates?: {
|
|
178
|
+
file: string;
|
|
179
|
+
location?: string;
|
|
180
|
+
reason: string;
|
|
181
|
+
}[] | undefined;
|
|
148
182
|
transformsApplied?: number | undefined;
|
|
149
183
|
errors?: {
|
|
150
184
|
file: string;
|
|
@@ -95,6 +95,16 @@
|
|
|
95
95
|
* @property {RegistryCompositionSummary} data
|
|
96
96
|
*/
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A required codemod change blocked by effective working-tree protection.
|
|
100
|
+
* @typedef {object} ProtectedCodemodFile
|
|
101
|
+
* @property {string} file Project-relative path.
|
|
102
|
+
* @property {string[]} codemods Codemods that still require a change.
|
|
103
|
+
* @property {string[]} reasons Protection categories (generated, vendored, ignored, and hard boundaries).
|
|
104
|
+
* @property {string[]} declarations Effective declarations that protect the file.
|
|
105
|
+
* @property {string[]} commands Exact regeneration commands found in generated headers, when present.
|
|
106
|
+
*/
|
|
107
|
+
|
|
98
108
|
/**
|
|
99
109
|
* astryx --json upgrade [--apply]
|
|
100
110
|
* @typedef {object} UpgradeRunResponse
|
|
@@ -107,7 +117,12 @@
|
|
|
107
117
|
* @property {boolean} data.agentDocsRefreshed
|
|
108
118
|
* @property {AgentDocsSummary} data.agentDocs
|
|
109
119
|
* @property {RegistryCompositionSummary} [data.registryCompositions]
|
|
120
|
+
* @property {boolean} [data.complete] False when protected required changes remain.
|
|
121
|
+
* @property {'ERR_CODEMOD_PROTECTED'} [data.errorCode] Stable incomplete-result code when complete is false.
|
|
110
122
|
* @property {number} [data.filesChanged] Total files changed across core + integration codemods (apply mode).
|
|
123
|
+
* @property {string[]} [data.modifiedFiles] Project-relative files changed or previewed.
|
|
124
|
+
* @property {ProtectedCodemodFile[]} [data.protectedFiles] Protected files that still require a codemod change after regeneration.
|
|
125
|
+
* @property {Array<{file: string, location?: string, reason: string}>} [data.declinedCandidates] Candidates left unchanged because proof was insufficient.
|
|
111
126
|
* @property {number} [data.transformsApplied] Total transforms that reported a change.
|
|
112
127
|
* @property {Array<{file: string, codemod: string, error: string}>} [data.errors] Per-codemod errors, when any codemod failed.
|
|
113
128
|
*/
|
|
@@ -21,6 +21,40 @@ afterEach(() => {
|
|
|
21
21
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
describe('runCodemods — ordered dry-run state', () => {
|
|
25
|
+
it('feeds each previewed code transform into the next transform', async () => {
|
|
26
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
27
|
+
fs.mkdirSync(srcDir);
|
|
28
|
+
fs.writeFileSync(path.join(srcDir, 'a.ts'), 'const value = "a";\n');
|
|
29
|
+
const transforms = [
|
|
30
|
+
{
|
|
31
|
+
name: 'a-to-b',
|
|
32
|
+
meta: {title: 'a to b'},
|
|
33
|
+
transform: file => file.source.replace('"a"', '"b"'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'b-to-c',
|
|
37
|
+
meta: {title: 'b to c'},
|
|
38
|
+
transform: file => file.source.replace('"b"', '"c"'),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const preview = await runCodemods([{version: '1.0.0', transforms}], {
|
|
43
|
+
apply: false,
|
|
44
|
+
path: srcDir,
|
|
45
|
+
root: tmpDir,
|
|
46
|
+
silent: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(preview.totalFilesChanged).toBe(2);
|
|
50
|
+
expect(preview.changedFiles).toEqual([
|
|
51
|
+
path.join(srcDir, 'a.ts'),
|
|
52
|
+
path.join(srcDir, 'a.ts'),
|
|
53
|
+
]);
|
|
54
|
+
expect(fs.readFileSync(path.join(srcDir, 'a.ts'), 'utf8')).toContain('"a"');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
24
58
|
describe('runCodemods — unified config codemod path', () => {
|
|
25
59
|
it('routes a core config codemod through the (file, api) runner to edit astryx.config.*', async () => {
|
|
26
60
|
fs.writeFileSync(
|
|
@@ -195,17 +229,219 @@ describe('runCodemods — unified config codemod path', () => {
|
|
|
195
229
|
});
|
|
196
230
|
});
|
|
197
231
|
|
|
198
|
-
describe('
|
|
232
|
+
describe('runCodemods — project plans honor existing-file protection', () => {
|
|
233
|
+
it('applies new and owned writes while blocking protected replacements and deletes', async () => {
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
path.join(tmpDir, '.gitattributes'),
|
|
236
|
+
'generated/** linguist-generated=true\n',
|
|
237
|
+
);
|
|
238
|
+
fs.mkdirSync(path.join(tmpDir, 'generated'), {recursive: true});
|
|
239
|
+
fs.writeFileSync(path.join(tmpDir, 'generated', 'replace.ts'), 'old\n');
|
|
240
|
+
fs.writeFileSync(path.join(tmpDir, 'generated', 'remove.ts'), 'old\n');
|
|
241
|
+
fs.writeFileSync(path.join(tmpDir, 'owned.ts'), 'old\n');
|
|
242
|
+
|
|
243
|
+
const manifests = [
|
|
244
|
+
{
|
|
245
|
+
version: '1.0.0',
|
|
246
|
+
transforms: [
|
|
247
|
+
{
|
|
248
|
+
name: 'project-plan',
|
|
249
|
+
meta: {title: 'project plan', codemodType: 'project'},
|
|
250
|
+
transform: async root => ({
|
|
251
|
+
writes: [
|
|
252
|
+
{
|
|
253
|
+
path: path.join(root, 'generated', 'replace.ts'),
|
|
254
|
+
contents: 'new\n',
|
|
255
|
+
},
|
|
256
|
+
{path: path.join(root, 'owned.ts'), contents: 'new\n'},
|
|
257
|
+
{path: path.join(root, 'created.ts'), contents: 'new\n'},
|
|
258
|
+
],
|
|
259
|
+
deletes: [path.join(root, 'generated', 'remove.ts')],
|
|
260
|
+
problems: [],
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const result = await runCodemods(manifests, {
|
|
268
|
+
apply: true,
|
|
269
|
+
path: './missing',
|
|
270
|
+
root: tmpDir,
|
|
271
|
+
silent: true,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(
|
|
275
|
+
fs.readFileSync(path.join(tmpDir, 'generated', 'replace.ts'), 'utf8'),
|
|
276
|
+
).toBe('old\n');
|
|
277
|
+
expect(fs.existsSync(path.join(tmpDir, 'generated', 'remove.ts'))).toBe(
|
|
278
|
+
true,
|
|
279
|
+
);
|
|
280
|
+
expect(fs.readFileSync(path.join(tmpDir, 'owned.ts'), 'utf8')).toBe(
|
|
281
|
+
'new\n',
|
|
282
|
+
);
|
|
283
|
+
expect(fs.readFileSync(path.join(tmpDir, 'created.ts'), 'utf8')).toBe(
|
|
284
|
+
'new\n',
|
|
285
|
+
);
|
|
286
|
+
expect(result.totalFilesChanged).toBe(2);
|
|
287
|
+
expect(result.protectedFiles.map(item => item.file).sort()).toEqual([
|
|
288
|
+
'generated/remove.ts',
|
|
289
|
+
'generated/replace.ts',
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('blocks an existing file protected by a declaration in the same plan', async () => {
|
|
294
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
295
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'out.ts'), 'old\n');
|
|
296
|
+
const manifests = [
|
|
297
|
+
{
|
|
298
|
+
version: '1.0.0',
|
|
299
|
+
transforms: [
|
|
300
|
+
{
|
|
301
|
+
name: 'declare-and-replace',
|
|
302
|
+
meta: {title: 'declare and replace', codemodType: 'project'},
|
|
303
|
+
transform: async root => ({
|
|
304
|
+
writes: [
|
|
305
|
+
{
|
|
306
|
+
path: path.join(root, '.gitattributes'),
|
|
307
|
+
contents: 'src/out.ts linguist-generated\n',
|
|
308
|
+
},
|
|
309
|
+
{path: path.join(root, 'src', 'out.ts'), contents: 'new\n'},
|
|
310
|
+
],
|
|
311
|
+
deletes: [],
|
|
312
|
+
problems: [],
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
const result = await runCodemods(manifests, {
|
|
320
|
+
apply: true,
|
|
321
|
+
path: './missing',
|
|
322
|
+
root: tmpDir,
|
|
323
|
+
silent: true,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(fs.readFileSync(path.join(tmpDir, 'src', 'out.ts'), 'utf8')).toBe(
|
|
327
|
+
'old\n',
|
|
328
|
+
);
|
|
329
|
+
expect(fs.readFileSync(path.join(tmpDir, '.gitattributes'), 'utf8')).toBe(
|
|
330
|
+
'src/out.ts linguist-generated\n',
|
|
331
|
+
);
|
|
332
|
+
expect(result.protectedFiles).toEqual([
|
|
333
|
+
expect.objectContaining({
|
|
334
|
+
file: 'src/out.ts',
|
|
335
|
+
declaration: 'linguist-generated in .gitattributes:1',
|
|
336
|
+
}),
|
|
337
|
+
]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('does not apply rules from a blocked staged declaration', async () => {
|
|
341
|
+
fs.writeFileSync(
|
|
342
|
+
path.join(tmpDir, '.gitattributes'),
|
|
343
|
+
'.gitattributes linguist-generated\n',
|
|
344
|
+
);
|
|
345
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
346
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'out.ts'), 'old\n');
|
|
347
|
+
const result = await runCodemods(
|
|
348
|
+
[
|
|
349
|
+
{
|
|
350
|
+
version: '1.0.0',
|
|
351
|
+
transforms: [
|
|
352
|
+
{
|
|
353
|
+
name: 'blocked-declaration',
|
|
354
|
+
meta: {title: 'blocked declaration', codemodType: 'project'},
|
|
355
|
+
transform: async root => ({
|
|
356
|
+
writes: [
|
|
357
|
+
{
|
|
358
|
+
path: path.join(root, '.gitattributes'),
|
|
359
|
+
contents: 'src/out.ts linguist-generated\n',
|
|
360
|
+
},
|
|
361
|
+
{path: path.join(root, 'src', 'out.ts'), contents: 'new\n'},
|
|
362
|
+
],
|
|
363
|
+
deletes: [],
|
|
364
|
+
problems: [],
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
{apply: true, path: './missing', root: tmpDir, silent: true},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(fs.readFileSync(path.join(tmpDir, 'src', 'out.ts'), 'utf8')).toBe(
|
|
374
|
+
'new\n',
|
|
375
|
+
);
|
|
376
|
+
expect(result.protectedFiles).toEqual([
|
|
377
|
+
expect.objectContaining({file: '.gitattributes'}),
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('refreshes declarations written by an earlier project codemod before later writes', async () => {
|
|
382
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
383
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'out.ts'), 'const foo = 1;\n');
|
|
384
|
+
const manifests = [
|
|
385
|
+
{
|
|
386
|
+
version: '1.0.0',
|
|
387
|
+
transforms: [
|
|
388
|
+
{
|
|
389
|
+
name: 'declare-generated',
|
|
390
|
+
meta: {title: 'declare generated', codemodType: 'project'},
|
|
391
|
+
transform: async root => ({
|
|
392
|
+
writes: [
|
|
393
|
+
{
|
|
394
|
+
path: path.join(root, '.gitattributes'),
|
|
395
|
+
contents: 'src/out.ts linguist-generated=true\n',
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
deletes: [],
|
|
399
|
+
problems: [],
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'rename-after-declaration',
|
|
404
|
+
meta: {title: 'rename after declaration'},
|
|
405
|
+
transform: file => file.source.replace('foo', 'bar'),
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
const result = await runCodemods(manifests, {
|
|
412
|
+
apply: true,
|
|
413
|
+
path: path.join(tmpDir, 'src'),
|
|
414
|
+
root: tmpDir,
|
|
415
|
+
silent: true,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(
|
|
419
|
+
fs.readFileSync(path.join(tmpDir, 'src', 'out.ts'), 'utf8'),
|
|
420
|
+
).toContain('foo');
|
|
421
|
+
expect(result.protectedFiles).toEqual([
|
|
422
|
+
expect.objectContaining({
|
|
423
|
+
file: 'src/out.ts',
|
|
424
|
+
declaration: 'linguist-generated in .gitattributes:1',
|
|
425
|
+
}),
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('findSourceFiles — scan boundaries (symlink + declared protection)', () => {
|
|
199
431
|
const manifests = [
|
|
200
432
|
{
|
|
201
433
|
version: '0.1.3',
|
|
202
434
|
transforms: [
|
|
203
|
-
{
|
|
435
|
+
{
|
|
436
|
+
name: 'p',
|
|
437
|
+
meta: {title: 'p'},
|
|
438
|
+
transform: f => f.source.replace(/foo/g, 'bar'),
|
|
439
|
+
},
|
|
204
440
|
],
|
|
205
441
|
},
|
|
206
442
|
];
|
|
207
443
|
|
|
208
|
-
it('
|
|
444
|
+
it('reports a symlinked file without writing through it', async () => {
|
|
209
445
|
const outside = path.join(tmpDir, 'outside');
|
|
210
446
|
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
211
447
|
fs.mkdirSync(outside, {recursive: true});
|
|
@@ -214,13 +450,45 @@ describe('findSourceFiles — scan boundaries (symlink + build dirs)', () => {
|
|
|
214
450
|
fs.writeFileSync(path.join(tmpDir, 'src', 'real.ts'), 'const foo = 2;\n');
|
|
215
451
|
fs.symlinkSync(secret, path.join(tmpDir, 'src', 'link.ts'));
|
|
216
452
|
|
|
217
|
-
const r = await runCodemods(manifests, {
|
|
453
|
+
const r = await runCodemods(manifests, {
|
|
454
|
+
apply: true,
|
|
455
|
+
path: './src',
|
|
456
|
+
silent: true,
|
|
457
|
+
});
|
|
218
458
|
// the symlink target (outside the tree) must be untouched
|
|
219
459
|
expect(fs.readFileSync(secret, 'utf-8')).toBe('const foo = 1;\n');
|
|
220
460
|
expect(r.writtenFiles.map(f => path.basename(f))).toEqual(['real.ts']);
|
|
461
|
+
expect(r.protectedFiles).toEqual([
|
|
462
|
+
expect.objectContaining({file: 'src/link.ts', reason: 'symlink'}),
|
|
463
|
+
]);
|
|
221
464
|
});
|
|
222
465
|
|
|
223
|
-
it('
|
|
466
|
+
it('reports matching descendants of a symlinked directory without writing through it', async () => {
|
|
467
|
+
const targetDir = path.join(tmpDir, 'target');
|
|
468
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
469
|
+
fs.mkdirSync(targetDir, {recursive: true});
|
|
470
|
+
fs.mkdirSync(srcDir, {recursive: true});
|
|
471
|
+
const target = path.join(targetDir, 'nested.ts');
|
|
472
|
+
fs.writeFileSync(target, 'const foo = 1;\n');
|
|
473
|
+
fs.symlinkSync(targetDir, path.join(srcDir, 'linked'));
|
|
474
|
+
|
|
475
|
+
const result = await runCodemods(manifests, {
|
|
476
|
+
apply: true,
|
|
477
|
+
path: srcDir,
|
|
478
|
+
root: tmpDir,
|
|
479
|
+
silent: true,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(fs.readFileSync(target, 'utf8')).toContain('foo');
|
|
483
|
+
expect(result.protectedFiles).toEqual([
|
|
484
|
+
expect.objectContaining({
|
|
485
|
+
file: 'src/linked/nested.ts',
|
|
486
|
+
reason: 'symlink',
|
|
487
|
+
}),
|
|
488
|
+
]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('does not infer protection from generated-looking directory names', async () => {
|
|
224
492
|
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
225
493
|
fs.mkdirSync(path.join(tmpDir, 'dist'), {recursive: true});
|
|
226
494
|
fs.mkdirSync(path.join(tmpDir, 'build'), {recursive: true});
|
|
@@ -228,8 +496,62 @@ describe('findSourceFiles — scan boundaries (symlink + build dirs)', () => {
|
|
|
228
496
|
fs.writeFileSync(path.join(tmpDir, 'dist', 'b.js'), 'const foo = 2;\n');
|
|
229
497
|
fs.writeFileSync(path.join(tmpDir, 'build', 'c.js'), 'const foo = 3;\n');
|
|
230
498
|
|
|
231
|
-
const r = await runCodemods(manifests, {
|
|
232
|
-
|
|
233
|
-
|
|
499
|
+
const r = await runCodemods(manifests, {
|
|
500
|
+
apply: true,
|
|
501
|
+
path: '.',
|
|
502
|
+
silent: true,
|
|
503
|
+
});
|
|
504
|
+
expect(r.writtenFiles.map(f => path.basename(f)).sort()).toEqual([
|
|
505
|
+
'a.ts',
|
|
506
|
+
'b.js',
|
|
507
|
+
'c.js',
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('changes owned files but reports generated files without writing them', async () => {
|
|
512
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
513
|
+
fs.mkdirSync(path.join(tmpDir, 'dist'), {recursive: true});
|
|
514
|
+
fs.writeFileSync(
|
|
515
|
+
path.join(tmpDir, '.gitattributes'),
|
|
516
|
+
'dist/** linguist-generated=true\n',
|
|
517
|
+
);
|
|
518
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'owned.ts'), 'const foo = 1;\n');
|
|
519
|
+
fs.writeFileSync(
|
|
520
|
+
path.join(tmpDir, 'dist', 'output.js'),
|
|
521
|
+
'const foo = 2;\n',
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const r = await runCodemods(manifests, {
|
|
525
|
+
apply: true,
|
|
526
|
+
path: '.',
|
|
527
|
+
silent: true,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(
|
|
531
|
+
fs.readFileSync(path.join(tmpDir, 'src', 'owned.ts'), 'utf-8'),
|
|
532
|
+
).toContain('bar');
|
|
533
|
+
expect(
|
|
534
|
+
fs.readFileSync(path.join(tmpDir, 'dist', 'output.js'), 'utf-8'),
|
|
535
|
+
).toContain('foo');
|
|
536
|
+
expect(r.protectedFiles).toEqual([
|
|
537
|
+
expect.objectContaining({
|
|
538
|
+
file: 'dist/output.js',
|
|
539
|
+
reason: 'generated',
|
|
540
|
+
declaration: 'linguist-generated in .gitattributes:1',
|
|
541
|
+
}),
|
|
542
|
+
]);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('fails closed before an owned write when a declaration is malformed', async () => {
|
|
546
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
547
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'owned.ts'), 'const foo = 1;\n');
|
|
548
|
+
fs.writeFileSync(path.join(tmpDir, '.hgignore'), 'syntax: regexp\n[bad\n');
|
|
549
|
+
|
|
550
|
+
await expect(
|
|
551
|
+
runCodemods(manifests, {apply: true, path: '.', silent: true}),
|
|
552
|
+
).rejects.toThrow(/file-protection source \.hgignore/);
|
|
553
|
+
expect(
|
|
554
|
+
fs.readFileSync(path.join(tmpDir, 'src', 'owned.ts'), 'utf-8'),
|
|
555
|
+
).toContain('foo');
|
|
234
556
|
});
|
|
235
557
|
});
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
runCodeCodemod,
|
|
25
25
|
runConfigCodemod,
|
|
26
26
|
} from './run-codemod.mjs';
|
|
27
|
+
import {createFileProtectionResolver} from '../../foundation/fs/file-protection.mjs';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Run file-based integration codemods, version-ordered. Config codemods run
|
|
@@ -40,18 +41,40 @@ import {
|
|
|
40
41
|
* @param {Set<string>} [options.skipCodemods] codemod ids to exclude
|
|
41
42
|
* @param {import('../../authoring/codemod/type').JscodeshiftFactory} options.jscodeshift
|
|
42
43
|
* @param {boolean} [options.silent]
|
|
43
|
-
* @
|
|
44
|
+
* @param {string} [options.root]
|
|
45
|
+
* @param {Map<string, string>} [options.contents] staged bytes from earlier dry-run phases
|
|
46
|
+
* @param {{root: string, classify: (file: string) => import('../../foundation/fs/file-protection.mjs').FileProtection[]}} [options.protection]
|
|
47
|
+
* @returns {{totalFilesChanged: number, totalTransformsApplied: number, changedFiles: string[], writtenFiles: string[], stagedContents: Map<string, string>, protectedFiles: import('../../authoring/codemod/type').CodemodRunResult['protectedFiles'], errors: Array<{file: string, codemod: string, error: string}>, skippedOptional: Array<import('../../authoring/codemod/type').CodemodEntry>}}
|
|
44
48
|
*/
|
|
45
49
|
export function runIntegrationCodemods(
|
|
46
50
|
versionGroups,
|
|
47
|
-
{
|
|
51
|
+
{
|
|
52
|
+
apply,
|
|
53
|
+
path: srcPath,
|
|
54
|
+
codemod,
|
|
55
|
+
skipCodemods,
|
|
56
|
+
jscodeshift,
|
|
57
|
+
silent = false,
|
|
58
|
+
root = process.cwd(),
|
|
59
|
+
protection: providedProtection,
|
|
60
|
+
contents: providedContents,
|
|
61
|
+
},
|
|
48
62
|
) {
|
|
49
63
|
const log = makeLog(silent);
|
|
64
|
+
let protection =
|
|
65
|
+
providedProtection ?? createFileProtectionResolver(path.resolve(root));
|
|
66
|
+
let protectionWriteCount = 0;
|
|
67
|
+
/** In-memory pipeline state keeps ordered dry-runs equivalent to apply. */
|
|
68
|
+
const virtualContents = new Map(providedContents ?? []);
|
|
50
69
|
|
|
51
70
|
let totalFilesChanged = 0;
|
|
52
71
|
let totalTransformsApplied = 0;
|
|
53
72
|
/** @type {string[]} */
|
|
73
|
+
const changedFiles = [];
|
|
74
|
+
/** @type {string[]} */
|
|
54
75
|
const writtenFiles = [];
|
|
76
|
+
/** @type {import('../../authoring/codemod/type').CodemodRunResult['protectedFiles']} */
|
|
77
|
+
const protectedFiles = [];
|
|
55
78
|
/** @type {Array<{file: string, codemod: string, error: string}>} */
|
|
56
79
|
const errors = [];
|
|
57
80
|
/** @type {Array<import('../../authoring/codemod/type').CodemodEntry>} */
|
|
@@ -79,11 +102,24 @@ export function runIntegrationCodemods(
|
|
|
79
102
|
|
|
80
103
|
// Config codemods first.
|
|
81
104
|
for (const entry of configEntries) {
|
|
105
|
+
if (apply && writtenFiles.length !== protectionWriteCount) {
|
|
106
|
+
protection = createFileProtectionResolver(path.resolve(root));
|
|
107
|
+
protectionWriteCount = writtenFiles.length;
|
|
108
|
+
}
|
|
82
109
|
log.info(` ${entry.codemod.title} (v${entry.version}, ${entry.package})`);
|
|
83
|
-
const r = runConfigCodemod(entry, {
|
|
110
|
+
const r = runConfigCodemod(entry, {
|
|
111
|
+
apply,
|
|
112
|
+
log,
|
|
113
|
+
jscodeshift,
|
|
114
|
+
root,
|
|
115
|
+
protection,
|
|
116
|
+
contents: virtualContents,
|
|
117
|
+
});
|
|
84
118
|
totalFilesChanged += r.filesChanged;
|
|
85
119
|
totalTransformsApplied += r.filesChanged;
|
|
120
|
+
changedFiles.push(...r.changedFiles);
|
|
86
121
|
writtenFiles.push(...r.writtenFiles);
|
|
122
|
+
protectedFiles.push(...r.protectedFiles);
|
|
87
123
|
errors.push(...r.errors);
|
|
88
124
|
}
|
|
89
125
|
|
|
@@ -94,13 +130,26 @@ export function runIntegrationCodemods(
|
|
|
94
130
|
? findSourceFiles(resolvedPath)
|
|
95
131
|
: [];
|
|
96
132
|
for (const entry of codeEntries) {
|
|
133
|
+
if (apply && writtenFiles.length !== protectionWriteCount) {
|
|
134
|
+
protection = createFileProtectionResolver(path.resolve(root));
|
|
135
|
+
protectionWriteCount = writtenFiles.length;
|
|
136
|
+
}
|
|
97
137
|
log.info(
|
|
98
138
|
` ${entry.codemod.title} (v${entry.version}, ${entry.package})`,
|
|
99
139
|
);
|
|
100
|
-
const r = runCodeCodemod(entry, files, {
|
|
140
|
+
const r = runCodeCodemod(entry, files, {
|
|
141
|
+
apply,
|
|
142
|
+
log,
|
|
143
|
+
jscodeshift,
|
|
144
|
+
root,
|
|
145
|
+
protection,
|
|
146
|
+
contents: virtualContents,
|
|
147
|
+
});
|
|
101
148
|
totalFilesChanged += r.filesChanged;
|
|
102
149
|
totalTransformsApplied += r.filesChanged;
|
|
150
|
+
changedFiles.push(...r.changedFiles);
|
|
103
151
|
writtenFiles.push(...r.writtenFiles);
|
|
152
|
+
protectedFiles.push(...r.protectedFiles);
|
|
104
153
|
errors.push(...r.errors);
|
|
105
154
|
}
|
|
106
155
|
}
|
|
@@ -108,7 +157,10 @@ export function runIntegrationCodemods(
|
|
|
108
157
|
return {
|
|
109
158
|
totalFilesChanged,
|
|
110
159
|
totalTransformsApplied,
|
|
160
|
+
changedFiles,
|
|
111
161
|
writtenFiles,
|
|
162
|
+
stagedContents: virtualContents,
|
|
163
|
+
protectedFiles,
|
|
112
164
|
errors,
|
|
113
165
|
skippedOptional,
|
|
114
166
|
};
|