@wp-typia/project-tools 0.16.12 → 0.16.13
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/dist/runtime/block-generator-service-core.d.ts +8 -0
- package/dist/runtime/block-generator-service-core.js +274 -0
- package/dist/runtime/block-generator-service-spec.d.ts +104 -0
- package/dist/runtime/block-generator-service-spec.js +139 -0
- package/dist/runtime/block-generator-service.d.ts +2 -110
- package/dist/runtime/block-generator-service.js +2 -389
- package/dist/runtime/cli-diagnostics.js +76 -4
- package/dist/runtime/cli-doctor-workspace.d.ts +9 -5
- package/dist/runtime/cli-doctor-workspace.js +18 -6
- package/dist/runtime/cli-help.js +1 -1
- package/dist/runtime/cli-scaffold.d.ts +8 -1
- package/dist/runtime/cli-scaffold.js +47 -4
- package/dist/runtime/migration-maintenance-fixtures.d.ts +23 -0
- package/dist/runtime/migration-maintenance-fixtures.js +126 -0
- package/dist/runtime/migration-maintenance-verify.d.ts +26 -0
- package/dist/runtime/migration-maintenance-verify.js +262 -0
- package/dist/runtime/migration-maintenance.d.ts +2 -51
- package/dist/runtime/migration-maintenance.js +2 -380
- package/dist/runtime/migrations.d.ts +0 -3
- package/dist/runtime/scaffold-apply-utils.d.ts +9 -0
- package/dist/runtime/scaffold-apply-utils.js +27 -4
- package/dist/runtime/scaffold-onboarding.d.ts +12 -0
- package/dist/runtime/scaffold-onboarding.js +42 -5
- package/dist/runtime/scaffold.js +1 -1
- package/package.json +1 -1
|
@@ -1,380 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { ROOT_PHP_MIGRATION_REGISTRY, } from './migration-constants.js';
|
|
5
|
-
import { createMigrationDiff } from './migration-diff.js';
|
|
6
|
-
import { createEdgeFixtureDocument, ensureEdgeFixtureFile, } from './migration-fixtures.js';
|
|
7
|
-
import { collectGeneratedMigrationEntries, } from './migration-generated-artifacts.js';
|
|
8
|
-
import { collectFixtureTargets, formatScaffoldCommand, getSelectedEntriesByBlock, hasSnapshotForVersion, isLegacySingleBlockProject, isSnapshotOptionalForBlockVersion, resolveLegacyVersions, } from './migration-planning.js';
|
|
9
|
-
import { assertRuleHasNoTodos, getFixtureFilePath, getGeneratedDirForBlock, getRuleFilePath, getSnapshotBlockJsonPath, getSnapshotManifestPath, getSnapshotRoot, getSnapshotSavePath, loadMigrationProject, readRuleMetadata, } from './migration-project.js';
|
|
10
|
-
import { renderFuzzFile, renderGeneratedDeprecatedFile, renderGeneratedMigrationIndexFile, renderMigrationRegistryFile, renderPhpMigrationRegistryFile, renderVerifyFile, } from './migration-render.js';
|
|
11
|
-
import { createMigrationRiskSummary, formatMigrationRiskSummary, } from './migration-risk.js';
|
|
12
|
-
import { getLocalTsxBinary, isInteractiveTerminal, readJson, resolveTargetMigrationVersion, } from './migration-utils.js';
|
|
13
|
-
import { readWorkspaceInventory } from './workspace-inventory.js';
|
|
14
|
-
import { getInvalidWorkspaceProjectReason, tryResolveWorkspaceProject, } from './workspace-project.js';
|
|
15
|
-
/**
|
|
16
|
-
* Run deterministic migration verification against generated fixtures.
|
|
17
|
-
*
|
|
18
|
-
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
19
|
-
* @param options Verification scope and console rendering options.
|
|
20
|
-
* @returns Verified legacy versions.
|
|
21
|
-
*/
|
|
22
|
-
export function verifyProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log, } = {}) {
|
|
23
|
-
const state = loadMigrationProject(projectDir);
|
|
24
|
-
const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
|
|
25
|
-
const blockEntries = getSelectedEntriesByBlock(state, targetVersions, 'verify');
|
|
26
|
-
const legacySingleBlock = isLegacySingleBlockProject(state);
|
|
27
|
-
if (targetVersions.length === 0) {
|
|
28
|
-
renderLine('No legacy migration versions configured for verification.');
|
|
29
|
-
return { verifiedVersions: [] };
|
|
30
|
-
}
|
|
31
|
-
const tsxBinary = getLocalTsxBinary(projectDir);
|
|
32
|
-
for (const [blockKey, entries] of Object.entries(blockEntries)) {
|
|
33
|
-
const block = state.blocks.find((entry) => entry.key === blockKey);
|
|
34
|
-
if (!block || entries.length === 0) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
|
|
39
|
-
}
|
|
40
|
-
const verifyScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), 'verify.ts');
|
|
41
|
-
if (!fs.existsSync(verifyScriptPath)) {
|
|
42
|
-
const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
|
|
43
|
-
throw new Error(`Generated verify script is missing for ${block.blockName} (${selectedVersionsForBlock.join(', ')}). ` +
|
|
44
|
-
`Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
|
|
45
|
-
}
|
|
46
|
-
const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
|
|
47
|
-
const filteredArgs = all
|
|
48
|
-
? ['--all']
|
|
49
|
-
: ['--from-migration-version', selectedVersionsForBlock[0]];
|
|
50
|
-
execFileSync(tsxBinary, [verifyScriptPath, ...filteredArgs], {
|
|
51
|
-
cwd: projectDir,
|
|
52
|
-
shell: process.platform === 'win32',
|
|
53
|
-
stdio: 'inherit',
|
|
54
|
-
});
|
|
55
|
-
renderLine(legacySingleBlock
|
|
56
|
-
? `Verified migrations for ${selectedVersionsForBlock.join(', ')}`
|
|
57
|
-
: `Verified ${block.blockName} migrations for ${selectedVersionsForBlock.join(', ')}`);
|
|
58
|
-
}
|
|
59
|
-
return { verifiedVersions: targetVersions };
|
|
60
|
-
}
|
|
61
|
-
function recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck) {
|
|
62
|
-
let invalidWorkspaceReason = null;
|
|
63
|
-
let workspace;
|
|
64
|
-
try {
|
|
65
|
-
invalidWorkspaceReason = getInvalidWorkspaceProjectReason(projectDir);
|
|
66
|
-
workspace = tryResolveWorkspaceProject(projectDir);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
recordCheck('fail', 'Workspace migration targets', error instanceof Error ? error.message : String(error));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (!workspace) {
|
|
73
|
-
if (invalidWorkspaceReason) {
|
|
74
|
-
recordCheck('fail', 'Workspace migration targets', invalidWorkspaceReason);
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
const inventory = readWorkspaceInventory(workspace.projectDir);
|
|
80
|
-
const expectedTargets = inventory.blocks.map((block) => `${workspace.workspace.namespace}/${block.slug}`);
|
|
81
|
-
const configuredTargets = state.blocks.map((block) => block.blockName);
|
|
82
|
-
const expectedTargetSet = new Set(expectedTargets);
|
|
83
|
-
const configuredTargetSet = new Set(configuredTargets);
|
|
84
|
-
const missingTargets = expectedTargets.filter((target) => !configuredTargetSet.has(target));
|
|
85
|
-
const staleTargets = configuredTargets.filter((target) => !expectedTargetSet.has(target));
|
|
86
|
-
recordCheck(missingTargets.length === 0 && staleTargets.length === 0 ? 'pass' : 'fail', 'Workspace migration targets', missingTargets.length === 0 && staleTargets.length === 0
|
|
87
|
-
? `${expectedTargets.length} workspace block target(s) align with migration config`
|
|
88
|
-
: [
|
|
89
|
-
missingTargets.length > 0
|
|
90
|
-
? `Missing from migration config: ${missingTargets.join(', ')}`
|
|
91
|
-
: null,
|
|
92
|
-
staleTargets.length > 0
|
|
93
|
-
? `Not present in scripts/block-config.ts: ${staleTargets.join(', ')}`
|
|
94
|
-
: null,
|
|
95
|
-
]
|
|
96
|
-
.filter((detail) => typeof detail === 'string')
|
|
97
|
-
.join('; '));
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
recordCheck('fail', 'Workspace migration targets', error instanceof Error ? error.message : String(error));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Validate the migration workspace without mutating files.
|
|
105
|
-
*
|
|
106
|
-
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
107
|
-
* @param options Doctor scope and console rendering options.
|
|
108
|
-
* @returns Structured doctor check results for the selected legacy versions.
|
|
109
|
-
*/
|
|
110
|
-
export function doctorProjectMigrations(projectDir, { all = false, fromMigrationVersion, renderLine = console.log, } = {}) {
|
|
111
|
-
const checks = [];
|
|
112
|
-
const recordCheck = (status, label, detail) => {
|
|
113
|
-
checks.push({ detail, label, status });
|
|
114
|
-
renderLine(`${status === 'pass' ? 'PASS' : 'FAIL'} ${label}: ${detail}`);
|
|
115
|
-
};
|
|
116
|
-
let state;
|
|
117
|
-
try {
|
|
118
|
-
state = loadMigrationProject(projectDir);
|
|
119
|
-
const legacySingleBlock = isLegacySingleBlockProject(state);
|
|
120
|
-
recordCheck('pass', 'Migration config', legacySingleBlock
|
|
121
|
-
? `Loaded ${state.blocks[0]?.blockName} @ ${state.config.currentMigrationVersion}`
|
|
122
|
-
: `Loaded ${state.blocks.length} block target(s) @ ${state.config.currentMigrationVersion}`);
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
recordCheck('fail', 'Migration config', error instanceof Error ? error.message : String(error));
|
|
126
|
-
throw new Error('Migration doctor failed.');
|
|
127
|
-
}
|
|
128
|
-
const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
|
|
129
|
-
const legacySingleBlock = isLegacySingleBlockProject(state);
|
|
130
|
-
const snapshotVersions = new Set(targetVersions.length > 0
|
|
131
|
-
? [state.config.currentMigrationVersion, ...targetVersions]
|
|
132
|
-
: state.config.supportedMigrationVersions);
|
|
133
|
-
recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck);
|
|
134
|
-
for (const version of snapshotVersions) {
|
|
135
|
-
for (const block of state.blocks) {
|
|
136
|
-
const snapshotRoot = getSnapshotRoot(projectDir, block, version);
|
|
137
|
-
const blockJsonPath = getSnapshotBlockJsonPath(projectDir, block, version);
|
|
138
|
-
const manifestPath = getSnapshotManifestPath(projectDir, block, version);
|
|
139
|
-
const savePath = getSnapshotSavePath(projectDir, block, version);
|
|
140
|
-
const hasSnapshot = fs.existsSync(snapshotRoot);
|
|
141
|
-
const snapshotIsOptional = !hasSnapshot && isSnapshotOptionalForBlockVersion(state, block, version);
|
|
142
|
-
recordCheck(hasSnapshot || snapshotIsOptional ? 'pass' : 'fail', legacySingleBlock ? `Snapshot ${version}` : `Snapshot ${block.blockName} @ ${version}`, hasSnapshot
|
|
143
|
-
? path.relative(projectDir, snapshotRoot)
|
|
144
|
-
: 'Not present for this version');
|
|
145
|
-
if (!hasSnapshot) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
for (const targetPath of [blockJsonPath, manifestPath, savePath]) {
|
|
149
|
-
recordCheck(fs.existsSync(targetPath) ? 'pass' : 'fail', legacySingleBlock
|
|
150
|
-
? `Snapshot file ${version}`
|
|
151
|
-
: `Snapshot file ${block.blockName} @ ${version}`, fs.existsSync(targetPath)
|
|
152
|
-
? path.relative(projectDir, targetPath)
|
|
153
|
-
: `Missing ${path.relative(projectDir, targetPath)}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
const generatedEntries = collectGeneratedMigrationEntries(state);
|
|
159
|
-
const expectedGeneratedFiles = new Map();
|
|
160
|
-
for (const block of state.blocks) {
|
|
161
|
-
const blockGeneratedEntries = generatedEntries.filter(({ entry }) => entry.block.key === block.key);
|
|
162
|
-
const entries = blockGeneratedEntries.map(({ entry }) => entry);
|
|
163
|
-
const generatedDir = getGeneratedDirForBlock(state.paths, block);
|
|
164
|
-
expectedGeneratedFiles.set(path.join(generatedDir, 'registry.ts'), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries));
|
|
165
|
-
expectedGeneratedFiles.set(path.join(generatedDir, 'deprecated.ts'), renderGeneratedDeprecatedFile(state, block.key, entries));
|
|
166
|
-
expectedGeneratedFiles.set(path.join(generatedDir, 'verify.ts'), renderVerifyFile(state, block.key, entries));
|
|
167
|
-
expectedGeneratedFiles.set(path.join(generatedDir, 'fuzz.ts'), renderFuzzFile(state, block.key, blockGeneratedEntries));
|
|
168
|
-
}
|
|
169
|
-
expectedGeneratedFiles.set(path.join(state.paths.generatedDir, 'index.ts'), renderGeneratedMigrationIndexFile(state, generatedEntries.map(({ entry }) => entry)));
|
|
170
|
-
expectedGeneratedFiles.set(path.join(projectDir, ROOT_PHP_MIGRATION_REGISTRY), renderPhpMigrationRegistryFile(state, generatedEntries.map(({ entry }) => entry)));
|
|
171
|
-
for (const [filePath, expectedSource] of expectedGeneratedFiles) {
|
|
172
|
-
const inSync = fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === expectedSource;
|
|
173
|
-
recordCheck(inSync ? 'pass' : 'fail', `Generated ${path.relative(projectDir, filePath)}`, inSync
|
|
174
|
-
? 'In sync'
|
|
175
|
-
: 'Run `wp-typia migrate scaffold --from-migration-version <label>` or regenerate artifacts');
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
recordCheck('fail', 'Generated artifacts', error instanceof Error ? error.message : String(error));
|
|
180
|
-
}
|
|
181
|
-
for (const version of targetVersions) {
|
|
182
|
-
for (const block of state.blocks) {
|
|
183
|
-
if (!hasSnapshotForVersion(state, block, version)) {
|
|
184
|
-
recordCheck('pass', `Snapshot coverage ${block.blockName} @ ${version}`, 'Target not present for this version');
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const rulePath = getRuleFilePath(state.paths, block, version, state.config.currentMigrationVersion);
|
|
188
|
-
const fixturePath = getFixtureFilePath(state.paths, block, version, state.config.currentMigrationVersion);
|
|
189
|
-
recordCheck(fs.existsSync(rulePath) ? 'pass' : 'fail', legacySingleBlock ? `Rule ${version}` : `Rule ${block.blockName} @ ${version}`, fs.existsSync(rulePath)
|
|
190
|
-
? path.relative(projectDir, rulePath)
|
|
191
|
-
: `Missing ${path.relative(projectDir, rulePath)}`);
|
|
192
|
-
recordCheck(fs.existsSync(fixturePath) ? 'pass' : 'fail', legacySingleBlock ? `Fixture ${version}` : `Fixture ${block.blockName} @ ${version}`, fs.existsSync(fixturePath)
|
|
193
|
-
? path.relative(projectDir, fixturePath)
|
|
194
|
-
: `Missing ${path.relative(projectDir, fixturePath)}`);
|
|
195
|
-
if (!fs.existsSync(rulePath) || !fs.existsSync(fixturePath)) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
assertRuleHasNoTodos(projectDir, block, version, state.config.currentMigrationVersion);
|
|
200
|
-
recordCheck('pass', legacySingleBlock
|
|
201
|
-
? `Rule TODOs ${version}`
|
|
202
|
-
: `Rule TODOs ${block.blockName} @ ${version}`, 'No TODO MIGRATION markers remain');
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
recordCheck('fail', legacySingleBlock
|
|
206
|
-
? `Rule TODOs ${version}`
|
|
207
|
-
: `Rule TODOs ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const ruleMetadata = readRuleMetadata(rulePath);
|
|
211
|
-
recordCheck(ruleMetadata.unresolved.length === 0 ? 'pass' : 'fail', legacySingleBlock
|
|
212
|
-
? `Rule unresolved ${version}`
|
|
213
|
-
: `Rule unresolved ${block.blockName} @ ${version}`, ruleMetadata.unresolved.length === 0
|
|
214
|
-
? 'No unresolved entries remain'
|
|
215
|
-
: ruleMetadata.unresolved.join(', '));
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
recordCheck('fail', legacySingleBlock
|
|
219
|
-
? `Rule unresolved ${version}`
|
|
220
|
-
: `Rule unresolved ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
const fixtureDocument = readJson(fixturePath);
|
|
224
|
-
recordCheck(Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0
|
|
225
|
-
? 'pass'
|
|
226
|
-
: 'fail', legacySingleBlock
|
|
227
|
-
? `Fixture parse ${version}`
|
|
228
|
-
: `Fixture parse ${block.blockName} @ ${version}`, Array.isArray(fixtureDocument.cases) && fixtureDocument.cases.length > 0
|
|
229
|
-
? `${fixtureDocument.cases.length} case(s)`
|
|
230
|
-
: 'Fixture document has no cases');
|
|
231
|
-
const diff = createMigrationDiff(state, block, version, state.config.currentMigrationVersion);
|
|
232
|
-
const expectedFixture = createEdgeFixtureDocument(projectDir, block, version, state.config.currentMigrationVersion, diff);
|
|
233
|
-
const actualCaseNames = new Set((fixtureDocument.cases ?? []).map((fixtureCase) => fixtureCase.name));
|
|
234
|
-
const missingCases = expectedFixture.cases
|
|
235
|
-
.map((fixtureCase) => fixtureCase.name)
|
|
236
|
-
.filter((name) => !actualCaseNames.has(name));
|
|
237
|
-
recordCheck(missingCases.length === 0 ? 'pass' : 'fail', legacySingleBlock
|
|
238
|
-
? `Fixture coverage ${version}`
|
|
239
|
-
: `Fixture coverage ${block.blockName} @ ${version}`, missingCases.length === 0
|
|
240
|
-
? 'All expected fixture cases are present'
|
|
241
|
-
: `Missing ${missingCases.join(', ')}`);
|
|
242
|
-
recordCheck('pass', legacySingleBlock
|
|
243
|
-
? `Risk summary ${version}`
|
|
244
|
-
: `Risk summary ${block.blockName} @ ${version}`, formatMigrationRiskSummary(createMigrationRiskSummary(diff)));
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
recordCheck('fail', legacySingleBlock
|
|
248
|
-
? `Fixture parse ${version}`
|
|
249
|
-
: `Fixture parse ${block.blockName} @ ${version}`, error instanceof Error ? error.message : String(error));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const failedChecks = checks.filter((check) => check.status === 'fail');
|
|
254
|
-
renderLine(`${failedChecks.length === 0 ? 'PASS' : 'FAIL'} Migration doctor summary: ${checks.length - failedChecks.length}/${checks.length} checks passed`);
|
|
255
|
-
if (failedChecks.length > 0) {
|
|
256
|
-
throw new Error('Migration doctor failed.');
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
checkedVersions: targetVersions,
|
|
260
|
-
checks,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Generate or refresh migration fixtures for one or more legacy edges.
|
|
265
|
-
*
|
|
266
|
-
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
267
|
-
* @param options Fixture generation scope and refresh options.
|
|
268
|
-
* @returns Generated and skipped legacy versions.
|
|
269
|
-
*/
|
|
270
|
-
export function fixturesProjectMigrations(projectDir, { all = false, confirmOverwrite, force = false, fromMigrationVersion, isInteractive = isInteractiveTerminal(), renderLine = console.log, toMigrationVersion = 'current', } = {}) {
|
|
271
|
-
const state = loadMigrationProject(projectDir);
|
|
272
|
-
const targetMigrationVersion = resolveTargetMigrationVersion(state.config.currentMigrationVersion, toMigrationVersion);
|
|
273
|
-
const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
|
|
274
|
-
if (targetVersions.length === 0) {
|
|
275
|
-
renderLine('No legacy migration versions configured for fixture generation.');
|
|
276
|
-
return { generatedVersions: [], skippedVersions: [] };
|
|
277
|
-
}
|
|
278
|
-
const generatedVersions = [];
|
|
279
|
-
const skippedVersions = [];
|
|
280
|
-
const fixtureTargets = collectFixtureTargets(state, targetVersions, targetMigrationVersion);
|
|
281
|
-
if (force) {
|
|
282
|
-
const overwriteTargets = fixtureTargets.filter(({ fixturePath }) => fs.existsSync(fixturePath));
|
|
283
|
-
if (isInteractive && overwriteTargets.length > 0) {
|
|
284
|
-
const confirmed = confirmOverwrite?.(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`) ??
|
|
285
|
-
promptForConfirmation(`About to overwrite ${overwriteTargets.length} existing migration fixture file(s). Continue?`);
|
|
286
|
-
if (!confirmed) {
|
|
287
|
-
renderLine(`Cancelled fixture refresh. Kept ${overwriteTargets.length} existing fixture file(s).`);
|
|
288
|
-
return {
|
|
289
|
-
generatedVersions,
|
|
290
|
-
skippedVersions: overwriteTargets.map(({ scopedLabel }) => scopedLabel),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
for (const { block, fixturePath, scopedLabel, version } of fixtureTargets) {
|
|
296
|
-
const existed = fs.existsSync(fixturePath);
|
|
297
|
-
const diff = createMigrationDiff(state, block, version, targetMigrationVersion);
|
|
298
|
-
const result = ensureEdgeFixtureFile(projectDir, block, version, targetMigrationVersion, diff, { force });
|
|
299
|
-
if (result.written) {
|
|
300
|
-
generatedVersions.push(scopedLabel);
|
|
301
|
-
renderLine(`${existed ? 'Refreshed' : 'Generated'} fixture ${path.relative(projectDir, fixturePath)}`);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
skippedVersions.push(scopedLabel);
|
|
305
|
-
renderLine(`Preserved existing fixture ${path.relative(projectDir, fixturePath)} (use --force to refresh)`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return {
|
|
309
|
-
generatedVersions,
|
|
310
|
-
skippedVersions,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Run seeded migration fuzz verification against generated fuzz artifacts.
|
|
315
|
-
*
|
|
316
|
-
* @param projectDir Absolute or relative project directory containing the migration workspace.
|
|
317
|
-
* @param options Fuzz scope, iteration count, seed, and console rendering options.
|
|
318
|
-
* @returns Fuzzed legacy versions and the effective seed.
|
|
319
|
-
*/
|
|
320
|
-
export function fuzzProjectMigrations(projectDir, { all = false, fromMigrationVersion, iterations = 25, renderLine = console.log, seed, } = {}) {
|
|
321
|
-
const state = loadMigrationProject(projectDir);
|
|
322
|
-
const targetVersions = resolveLegacyVersions(state, { all, fromMigrationVersion });
|
|
323
|
-
const blockEntries = getSelectedEntriesByBlock(state, targetVersions, 'fuzz');
|
|
324
|
-
const legacySingleBlock = isLegacySingleBlockProject(state);
|
|
325
|
-
if (targetVersions.length === 0) {
|
|
326
|
-
renderLine('No legacy migration versions configured for fuzzing.');
|
|
327
|
-
return { fuzzedVersions: [] };
|
|
328
|
-
}
|
|
329
|
-
const tsxBinary = getLocalTsxBinary(projectDir);
|
|
330
|
-
for (const [blockKey, entries] of Object.entries(blockEntries)) {
|
|
331
|
-
const block = state.blocks.find((entry) => entry.key === blockKey);
|
|
332
|
-
if (!block || entries.length === 0) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
for (const entry of entries) {
|
|
336
|
-
assertRuleHasNoTodos(projectDir, block, entry.fromVersion, state.config.currentMigrationVersion);
|
|
337
|
-
}
|
|
338
|
-
const fuzzScriptPath = path.join(getGeneratedDirForBlock(state.paths, block), 'fuzz.ts');
|
|
339
|
-
if (!fs.existsSync(fuzzScriptPath)) {
|
|
340
|
-
const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
|
|
341
|
-
throw new Error(`Generated fuzz script is missing for ${block.blockName} (${selectedVersionsForBlock.join(', ')}). ` +
|
|
342
|
-
`Run \`${formatScaffoldCommand(selectedVersionsForBlock)}\` first, then \`wp-typia migrate doctor --all\` if the workspace should already be scaffolded.`);
|
|
343
|
-
}
|
|
344
|
-
const selectedVersionsForBlock = entries.map((entry) => entry.fromVersion);
|
|
345
|
-
const args = [
|
|
346
|
-
fuzzScriptPath,
|
|
347
|
-
...(all ? ['--all'] : ['--from-migration-version', selectedVersionsForBlock[0]]),
|
|
348
|
-
'--iterations',
|
|
349
|
-
String(iterations),
|
|
350
|
-
...(seed === undefined ? [] : ['--seed', String(seed)]),
|
|
351
|
-
];
|
|
352
|
-
execFileSync(tsxBinary, args, {
|
|
353
|
-
cwd: projectDir,
|
|
354
|
-
shell: process.platform === 'win32',
|
|
355
|
-
stdio: 'inherit',
|
|
356
|
-
});
|
|
357
|
-
renderLine(legacySingleBlock
|
|
358
|
-
? `Fuzzed migrations for ${selectedVersionsForBlock.join(', ')}`
|
|
359
|
-
: `Fuzzed ${block.blockName} migrations for ${selectedVersionsForBlock.join(', ')}`);
|
|
360
|
-
}
|
|
361
|
-
return { fuzzedVersions: targetVersions, seed };
|
|
362
|
-
}
|
|
363
|
-
function promptForConfirmation(message) {
|
|
364
|
-
process.stdout.write(`${message} [y/N]: `);
|
|
365
|
-
const buffer = Buffer.alloc(1);
|
|
366
|
-
let answer = '';
|
|
367
|
-
while (true) {
|
|
368
|
-
const bytesRead = fs.readSync(process.stdin.fd, buffer, 0, 1, null);
|
|
369
|
-
if (bytesRead === 0) {
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
const char = buffer.toString('utf8', 0, bytesRead);
|
|
373
|
-
if (char === '\n' || char === '\r') {
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
answer += char;
|
|
377
|
-
}
|
|
378
|
-
const normalized = answer.trim().toLowerCase();
|
|
379
|
-
return normalized === 'y' || normalized === 'yes';
|
|
380
|
-
}
|
|
1
|
+
export * from './migration-maintenance-verify.js';
|
|
2
|
+
export * from './migration-maintenance-fixtures.js';
|
|
@@ -45,9 +45,6 @@ export declare function runMigrationCommand(command: ParsedMigrationArgs, cwd: s
|
|
|
45
45
|
} | {
|
|
46
46
|
generatedVersions: string[];
|
|
47
47
|
skippedVersions: string[];
|
|
48
|
-
} | {
|
|
49
|
-
fuzzedVersions: never[];
|
|
50
|
-
seed?: undefined;
|
|
51
48
|
} | {
|
|
52
49
|
fuzzedVersions: string[];
|
|
53
50
|
seed: number | undefined;
|
|
@@ -8,6 +8,15 @@ export interface InstallDependenciesOptions {
|
|
|
8
8
|
projectDir: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function ensureDirectory(targetDir: string, allowExisting?: boolean): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Builds the generated README markdown for one scaffolded project.
|
|
13
|
+
*
|
|
14
|
+
* @param templateId Scaffold template family or template identifier.
|
|
15
|
+
* @param variables Interpolated scaffold variables used in generated content.
|
|
16
|
+
* @param packageManager Package manager used to format install and script commands.
|
|
17
|
+
* @param options Optional README sections enabled by scaffold presets.
|
|
18
|
+
* @returns Markdown README content for the generated project root.
|
|
19
|
+
*/
|
|
11
20
|
export declare function buildReadme(templateId: string, variables: ScaffoldTemplateVariables, packageManager: PackageManagerId, { withMigrationUi, withTestPreset, withWpEnv, }?: {
|
|
12
21
|
withMigrationUi?: boolean;
|
|
13
22
|
withTestPreset?: boolean;
|
|
@@ -8,7 +8,7 @@ import { applyMigrationUiCapability } from "./migration-ui-capability.js";
|
|
|
8
8
|
import { getPackageVersions } from "./package-versions.js";
|
|
9
9
|
import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
|
|
10
10
|
import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
|
|
11
|
-
import { getCompoundExtensionWorkflowSection, getOptionalOnboardingNote, getOptionalOnboardingSteps, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
|
|
11
|
+
import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getInitialCommitNote, getOptionalOnboardingNote, getOptionalOnboardingSteps, getQuickStartWorkflowNote, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
|
|
12
12
|
import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
|
|
13
13
|
import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
|
|
14
14
|
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
|
|
@@ -36,10 +36,20 @@ export async function ensureDirectory(targetDir, allowExisting = false) {
|
|
|
36
36
|
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the generated README markdown for one scaffolded project.
|
|
41
|
+
*
|
|
42
|
+
* @param templateId Scaffold template family or template identifier.
|
|
43
|
+
* @param variables Interpolated scaffold variables used in generated content.
|
|
44
|
+
* @param packageManager Package manager used to format install and script commands.
|
|
45
|
+
* @param options Optional README sections enabled by scaffold presets.
|
|
46
|
+
* @returns Markdown README content for the generated project root.
|
|
47
|
+
*/
|
|
39
48
|
export function buildReadme(templateId, variables, packageManager, { withMigrationUi = false, withTestPreset = false, withWpEnv = false, } = {}) {
|
|
40
49
|
const optionalOnboardingSteps = getOptionalOnboardingSteps(packageManager, templateId, {
|
|
41
50
|
compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
|
|
42
51
|
});
|
|
52
|
+
const initialCommitCommands = getInitialCommitCommands();
|
|
43
53
|
const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
|
|
44
54
|
compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
|
|
45
55
|
});
|
|
@@ -70,20 +80,25 @@ ${variables.description}
|
|
|
70
80
|
|
|
71
81
|
${templateId}
|
|
72
82
|
|
|
73
|
-
##
|
|
83
|
+
## Quick Start
|
|
74
84
|
|
|
75
85
|
\`\`\`bash
|
|
76
86
|
${formatInstallCommand(packageManager)}
|
|
77
87
|
${formatRunScript(packageManager, developmentScript)}
|
|
78
88
|
\`\`\`
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
${getQuickStartWorkflowNote(packageManager, templateId, {
|
|
91
|
+
compoundPersistenceEnabled,
|
|
92
|
+
})}
|
|
93
|
+
|
|
94
|
+
## Build and Verify
|
|
81
95
|
|
|
82
96
|
\`\`\`bash
|
|
83
97
|
${formatRunScript(packageManager, "build")}
|
|
98
|
+
${formatRunScript(packageManager, "typecheck")}
|
|
84
99
|
\`\`\`
|
|
85
100
|
|
|
86
|
-
##
|
|
101
|
+
## Advanced Sync
|
|
87
102
|
|
|
88
103
|
\`\`\`bash
|
|
89
104
|
${optionalOnboardingSteps.join("\n")}
|
|
@@ -93,6 +108,14 @@ ${getOptionalOnboardingNote(packageManager, templateId, {
|
|
|
93
108
|
compoundPersistenceEnabled,
|
|
94
109
|
})}
|
|
95
110
|
|
|
111
|
+
## Before First Commit
|
|
112
|
+
|
|
113
|
+
\`\`\`bash
|
|
114
|
+
${initialCommitCommands.join("\n")}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
${getInitialCommitNote()}
|
|
118
|
+
|
|
96
119
|
${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ""}${migrationSection ? `\n\n${migrationSection}` : ""}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ""}${wpEnvSection ? `\n\n${wpEnvSection}` : ""}${testPresetSection ? `\n\n${testPresetSection}` : ""}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ""}
|
|
97
120
|
`;
|
|
98
121
|
}
|
|
@@ -14,10 +14,22 @@ export declare function getOptionalSyncScriptNames(templateId: string, options?:
|
|
|
14
14
|
* Formats optional onboarding sync commands for the selected package manager.
|
|
15
15
|
*/
|
|
16
16
|
export declare function getOptionalOnboardingSteps(packageManager: PackageManagerId, templateId: string, options?: SyncOnboardingOptions): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Returns the quick-start note explaining the scaffold's primary local loop.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getQuickStartWorkflowNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
17
21
|
/**
|
|
18
22
|
* Returns the onboarding note explaining when manual sync is optional.
|
|
19
23
|
*/
|
|
20
24
|
export declare function getOptionalOnboardingNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the recommended version-control commands for a fresh scaffold.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getInitialCommitCommands(): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Returns the version-control note shown after the initial scaffold.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getInitialCommitNote(): string;
|
|
21
33
|
/**
|
|
22
34
|
* Returns source-of-truth guidance for generated artifacts by template mode.
|
|
23
35
|
*/
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { formatRunScript } from "./package-managers.js";
|
|
2
2
|
import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
|
|
3
3
|
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
4
|
+
const INITIAL_COMMIT_COMMANDS = [
|
|
5
|
+
"git init",
|
|
6
|
+
"git add .",
|
|
7
|
+
'git commit -m "Initial scaffold"',
|
|
8
|
+
];
|
|
4
9
|
function templateHasPersistenceSync(templateId, { compoundPersistenceEnabled = false } = {}) {
|
|
5
10
|
return templateId === "persistence" || (templateId === "compound" && compoundPersistenceEnabled);
|
|
6
11
|
}
|
|
@@ -28,6 +33,24 @@ export function getOptionalSyncScriptNames(templateId, options = {}) {
|
|
|
28
33
|
export function getOptionalOnboardingSteps(packageManager, templateId, options = {}) {
|
|
29
34
|
return getOptionalSyncScriptNames(templateId, options).map((scriptName) => formatRunScript(packageManager, scriptName));
|
|
30
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the quick-start note explaining the scaffold's primary local loop.
|
|
38
|
+
*/
|
|
39
|
+
export function getQuickStartWorkflowNote(packageManager, templateId = "basic", options = {}) {
|
|
40
|
+
const developmentScript = getPrimaryDevelopmentScript(templateId);
|
|
41
|
+
const devCommand = formatRunScript(packageManager, developmentScript);
|
|
42
|
+
const startCommand = formatRunScript(packageManager, "start");
|
|
43
|
+
if (developmentScript === "start") {
|
|
44
|
+
return `${startCommand} is the primary local entry point for this template. If the template also exposes a dedicated watch mode or alternate editor workflow, follow that template-specific documentation alongside the generated project scripts.`;
|
|
45
|
+
}
|
|
46
|
+
if (developmentScript !== "dev") {
|
|
47
|
+
return `${devCommand} is the primary local entry point for this template. Use ${startCommand} when you want the scaffold's one-shot startup flow instead of the watch-oriented workflow.`;
|
|
48
|
+
}
|
|
49
|
+
if (templateHasPersistenceSync(templateId, options)) {
|
|
50
|
+
return `${devCommand} keeps the editor, type-derived artifacts, and REST-derived artifacts moving together during local development. Use ${startCommand} when you want a one-shot sync plus editor startup without the long-running watch loop.`;
|
|
51
|
+
}
|
|
52
|
+
return `${devCommand} keeps the editor and type-derived artifacts moving together during local development. Use ${startCommand} when you want a one-shot sync plus editor startup without the long-running watch loop.`;
|
|
53
|
+
}
|
|
31
54
|
/**
|
|
32
55
|
* Returns the onboarding note explaining when manual sync is optional.
|
|
33
56
|
*/
|
|
@@ -46,18 +69,32 @@ export function getOptionalOnboardingNote(packageManager, templateId = "basic",
|
|
|
46
69
|
const advancedPersistenceNote = templateHasPersistenceSync(templateId, options)
|
|
47
70
|
? ` ${syncRestCommand} remains available for advanced REST-only refreshes, but it now fails fast when type-derived artifacts are stale; run \`${syncCommand}\` or \`${syncTypesCommand}\` first.`
|
|
48
71
|
: "";
|
|
72
|
+
const isCustomTemplate = !isBuiltInTemplateId(templateId) &&
|
|
73
|
+
templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE;
|
|
49
74
|
const fallbackCustomTemplateNote = !hasUnifiedSync &&
|
|
50
75
|
syncSteps.length > 0 &&
|
|
51
|
-
|
|
52
|
-
templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
76
|
+
isCustomTemplate
|
|
53
77
|
? `Run ${syncSteps.join(" then ")} manually before build, typecheck, or commit. ${syncCheckCommand} verifies the current type-derived artifacts without rewriting them.${optionalSyncScripts.includes("sync-rest") ? ` ${syncRestCommand} remains available for REST-only refreshes after ${syncTypesCommand}.` : ""}`
|
|
54
78
|
: null;
|
|
55
79
|
if (fallbackCustomTemplateNote) {
|
|
56
80
|
return fallbackCustomTemplateNote;
|
|
57
81
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
if (isCustomTemplate && syncSteps.length === 0) {
|
|
83
|
+
return "No optional sync command was detected for this custom template. Follow the template's own artifact-refresh guidance before build, typecheck, or your first commit.";
|
|
84
|
+
}
|
|
85
|
+
return `You usually do not need to run ${syncCommand} during a normal ${formatRunScript(packageManager, developmentScript)} session. Run ${syncCommand} manually when you want a reviewable artifact refresh before ${formatRunScript(packageManager, "build")}, ${typecheckCommand}, or your first commit. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` to fail only on lossy WordPress projections, or \`${strictSyncCommand}\` for the stricter CI-oriented report.${advancedPersistenceNote} They do not create migration history. If this directory is new, create your first Git commit after that refresh.`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the recommended version-control commands for a fresh scaffold.
|
|
89
|
+
*/
|
|
90
|
+
export function getInitialCommitCommands() {
|
|
91
|
+
return [...INITIAL_COMMIT_COMMANDS];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the version-control note shown after the initial scaffold.
|
|
95
|
+
*/
|
|
96
|
+
export function getInitialCommitNote() {
|
|
97
|
+
return "Skip `git init` if this directory already lives inside an existing repository. If you want generated artifacts refreshed before the first checkpoint, run your manual sync step first and then create the commit.";
|
|
61
98
|
}
|
|
62
99
|
/**
|
|
63
100
|
* Returns source-of-truth guidance for generated artifacts by template mode.
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function resolvePackageManagerId({ packageManager, yes = false, isI
|
|
|
81
81
|
return getPackageManager(packageManager).id;
|
|
82
82
|
}
|
|
83
83
|
if (yes) {
|
|
84
|
-
|
|
84
|
+
return "npm";
|
|
85
85
|
}
|
|
86
86
|
if (!isInteractive || !selectPackageManager) {
|
|
87
87
|
throw new Error(`Package manager is required in non-interactive mode. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
|