@wp-typia/project-tools 0.20.1 → 0.21.0
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/cli-add-block.js +1 -1
- package/dist/runtime/cli-add-shared.d.ts +73 -5
- package/dist/runtime/cli-add-shared.js +58 -11
- package/dist/runtime/cli-add-workspace-ability.js +11 -57
- package/dist/runtime/cli-add-workspace-admin-view.d.ts +23 -0
- package/dist/runtime/cli-add-workspace-admin-view.js +872 -0
- package/dist/runtime/cli-add-workspace-ai-anchors.js +2 -5
- package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +0 -4
- package/dist/runtime/cli-add-workspace-ai-source-emitters.js +7 -17
- package/dist/runtime/cli-add-workspace-ai.js +4 -6
- package/dist/runtime/cli-add-workspace-assets.d.ts +13 -5
- package/dist/runtime/cli-add-workspace-assets.js +290 -106
- package/dist/runtime/cli-add-workspace-rest-anchors.js +2 -5
- package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +0 -1
- package/dist/runtime/cli-add-workspace-rest-source-emitters.js +7 -14
- package/dist/runtime/cli-add-workspace-rest.js +4 -6
- package/dist/runtime/cli-add-workspace.d.ts +58 -1
- package/dist/runtime/cli-add-workspace.js +588 -18
- package/dist/runtime/cli-add.d.ts +1 -1
- package/dist/runtime/cli-add.js +1 -1
- package/dist/runtime/cli-core.d.ts +8 -5
- package/dist/runtime/cli-core.js +7 -4
- package/dist/runtime/cli-diagnostics.d.ts +84 -1
- package/dist/runtime/cli-diagnostics.js +90 -3
- package/dist/runtime/cli-doctor-workspace.js +552 -13
- package/dist/runtime/cli-doctor.d.ts +4 -2
- package/dist/runtime/cli-doctor.js +2 -1
- package/dist/runtime/cli-help.js +19 -9
- package/dist/runtime/cli-init.d.ts +67 -3
- package/dist/runtime/cli-init.js +603 -64
- package/dist/runtime/cli-validation.js +4 -3
- package/dist/runtime/index.d.ts +9 -4
- package/dist/runtime/index.js +7 -3
- package/dist/runtime/package-json-types.d.ts +12 -0
- package/dist/runtime/package-json-types.js +1 -0
- package/dist/runtime/package-versions.d.ts +17 -2
- package/dist/runtime/package-versions.js +46 -1
- package/dist/runtime/php-utils.d.ts +16 -0
- package/dist/runtime/php-utils.js +59 -0
- package/dist/runtime/scaffold-answer-resolution.js +35 -10
- package/dist/runtime/scaffold-apply-utils.d.ts +2 -3
- package/dist/runtime/scaffold-apply-utils.js +3 -43
- package/dist/runtime/template-source-cache.d.ts +112 -0
- package/dist/runtime/template-source-cache.js +434 -0
- package/dist/runtime/template-source-seeds.js +333 -53
- package/dist/runtime/workspace-inventory.d.ts +43 -2
- package/dist/runtime/workspace-inventory.js +138 -5
- package/package.json +2 -2
package/dist/runtime/cli-init.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { promises as fsp } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { analyzeSourceTypes } from "@wp-typia/block-runtime/metadata-parser";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from "./cli-diagnostics.js";
|
|
7
|
+
import { quoteTsString, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
|
|
3
8
|
import { discoverMigrationInitLayout } from "./migration-project.js";
|
|
4
9
|
import { formatAddDevDependenciesCommand, formatPackageExecCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
|
|
5
10
|
import { getPackageVersions } from "./package-versions.js";
|
|
11
|
+
import { toPascalCase } from "./string-case.js";
|
|
12
|
+
import { updateWorkspaceInventorySource } from "./workspace-inventory.js";
|
|
6
13
|
import { parseWorkspacePackageManagerId, tryResolveWorkspaceProject, } from "./workspace-project.js";
|
|
7
14
|
const SUPPORTED_RETROFIT_LAYOUT_NOTE = "Supported retrofit layouts currently mirror the migration bootstrap detector: `src/block.json` + `src/types.ts` + `src/save.tsx`, legacy root `block.json` + `src/types.ts` + `src/save.tsx`, or multi-block `src/blocks/*/block.json` workspaces.";
|
|
15
|
+
const RETROFIT_APPLY_PREVIEW_NOTE = "If you rerun with `wp-typia init --apply`, package.json and generated helper files are snapshotted and rolled back automatically if a write fails.";
|
|
16
|
+
const RETROFIT_ROLLBACK_NOTE = "Apply mode writes package.json and generated helper files with rollback-on-failure protection.";
|
|
8
17
|
const BASE_RETROFIT_SCRIPTS = {
|
|
9
18
|
sync: "tsx scripts/sync-project.ts",
|
|
10
19
|
"sync-types": "tsx scripts/sync-types-to-block-json.ts",
|
|
@@ -26,7 +35,14 @@ function readProjectPackageJson(projectDir) {
|
|
|
26
35
|
if (!fs.existsSync(packageJsonPath)) {
|
|
27
36
|
return null;
|
|
28
37
|
}
|
|
29
|
-
|
|
38
|
+
const source = fs.readFileSync(packageJsonPath, "utf8");
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(source);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, `Unable to parse ${packageJsonPath}: ${message}`, error instanceof Error ? { cause: error } : undefined);
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
function inferInitPackageManager(projectDir, packageJson) {
|
|
32
48
|
if (packageJson?.packageManager) {
|
|
@@ -45,6 +61,18 @@ function inferInitPackageManager(projectDir, packageJson) {
|
|
|
45
61
|
}
|
|
46
62
|
return "npm";
|
|
47
63
|
}
|
|
64
|
+
function resolveInitPackageManager(projectDir, packageJson, override) {
|
|
65
|
+
if (!override) {
|
|
66
|
+
return inferInitPackageManager(projectDir, packageJson);
|
|
67
|
+
}
|
|
68
|
+
if (override !== "bun" &&
|
|
69
|
+
override !== "npm" &&
|
|
70
|
+
override !== "pnpm" &&
|
|
71
|
+
override !== "yarn") {
|
|
72
|
+
throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, `Unknown package manager "${override}". Expected one of: bun, npm, pnpm, yarn.`);
|
|
73
|
+
}
|
|
74
|
+
return override;
|
|
75
|
+
}
|
|
48
76
|
function getWpTypiaCliSpecifier() {
|
|
49
77
|
const versions = getPackageVersions();
|
|
50
78
|
return versions.wpTypiaPackageExactVersion === "0.0.0"
|
|
@@ -54,12 +82,12 @@ function getWpTypiaCliSpecifier() {
|
|
|
54
82
|
function buildRequiredDevDependencyMap() {
|
|
55
83
|
const versions = getPackageVersions();
|
|
56
84
|
return {
|
|
57
|
-
"@typia/unplugin":
|
|
85
|
+
"@typia/unplugin": versions.typiaUnpluginPackageVersion,
|
|
58
86
|
"@wp-typia/block-runtime": versions.blockRuntimePackageVersion,
|
|
59
87
|
"@wp-typia/block-types": versions.blockTypesPackageVersion,
|
|
60
|
-
tsx:
|
|
61
|
-
typescript:
|
|
62
|
-
typia:
|
|
88
|
+
tsx: versions.tsxPackageVersion,
|
|
89
|
+
typescript: versions.typescriptPackageVersion,
|
|
90
|
+
typia: versions.typiaPackageVersion,
|
|
63
91
|
};
|
|
64
92
|
}
|
|
65
93
|
function getExistingDependencyVersion(packageJson, name) {
|
|
@@ -101,8 +129,8 @@ function buildScriptChanges(packageJson, packageManager) {
|
|
|
101
129
|
];
|
|
102
130
|
});
|
|
103
131
|
}
|
|
104
|
-
function buildPackageManagerFieldChange(packageJson, packageManager) {
|
|
105
|
-
if (packageManager === "npm") {
|
|
132
|
+
function buildPackageManagerFieldChange(packageJson, packageManager, options = {}) {
|
|
133
|
+
if (!options.persistExplicitOverride && packageManager === "npm") {
|
|
106
134
|
return undefined;
|
|
107
135
|
}
|
|
108
136
|
const requiredValue = getPackageManager(packageManager).packageManagerField;
|
|
@@ -127,14 +155,298 @@ function buildGeneratedArtifactPaths(blockJsonFile, manifestFile) {
|
|
|
127
155
|
];
|
|
128
156
|
return Array.from(new Set(artifactPaths.map((filePath) => normalizeRelativePath(filePath))));
|
|
129
157
|
}
|
|
158
|
+
function collectNamedSourceTypeCandidates(typesSource) {
|
|
159
|
+
const sourceFile = ts.createSourceFile("types.ts", typesSource, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
160
|
+
return sourceFile.statements.flatMap((statement) => {
|
|
161
|
+
if (ts.isInterfaceDeclaration(statement) ||
|
|
162
|
+
ts.isTypeAliasDeclaration(statement)) {
|
|
163
|
+
return [statement.name.text];
|
|
164
|
+
}
|
|
165
|
+
return [];
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function isObjectLikeSourceType(projectDir, typesFile, sourceTypeName) {
|
|
169
|
+
const analyzedTypes = analyzeSourceTypes({
|
|
170
|
+
projectRoot: projectDir,
|
|
171
|
+
typesFile,
|
|
172
|
+
}, [sourceTypeName]);
|
|
173
|
+
return analyzedTypes[sourceTypeName]?.kind === "object";
|
|
174
|
+
}
|
|
175
|
+
function inferRetrofitAttributeTypeName(projectDir, block) {
|
|
176
|
+
const typesPath = path.join(projectDir, block.typesFile);
|
|
177
|
+
const typesSource = fs.readFileSync(typesPath, "utf8");
|
|
178
|
+
const blockNameSegments = block.blockName.split("/");
|
|
179
|
+
const slug = blockNameSegments[blockNameSegments.length - 1] ?? block.key;
|
|
180
|
+
const candidateNames = collectNamedSourceTypeCandidates(typesSource);
|
|
181
|
+
const validCandidates = candidateNames.filter((candidateName) => isObjectLikeSourceType(projectDir, block.typesFile, candidateName));
|
|
182
|
+
const preferredName = `${toPascalCase(slug)}Attributes`;
|
|
183
|
+
if (validCandidates.includes(preferredName)) {
|
|
184
|
+
return preferredName;
|
|
185
|
+
}
|
|
186
|
+
const attributeCandidates = validCandidates.filter((candidateName) => candidateName.endsWith("Attributes"));
|
|
187
|
+
if (attributeCandidates.length === 1) {
|
|
188
|
+
return attributeCandidates[0];
|
|
189
|
+
}
|
|
190
|
+
if (validCandidates.length === 1) {
|
|
191
|
+
return validCandidates[0];
|
|
192
|
+
}
|
|
193
|
+
if (validCandidates.length === 0) {
|
|
194
|
+
throw new Error(`Unable to infer an object-like source type from ${block.typesFile}. Add one interface or type alias such as ${preferredName} before rerunning \`wp-typia init\`.`);
|
|
195
|
+
}
|
|
196
|
+
throw new Error(`Unable to infer a unique source type from ${block.typesFile}. Candidate object-like exports: ${validCandidates.join(", ")}. Rename one to ${preferredName} or leave a single object-like attributes type before rerunning \`wp-typia init\`.`);
|
|
197
|
+
}
|
|
198
|
+
function buildRetrofitBlockTarget(projectDir, block) {
|
|
199
|
+
const blockNameSegments = block.blockName.split("/");
|
|
200
|
+
const slug = blockNameSegments[blockNameSegments.length - 1] ?? block.key;
|
|
201
|
+
return {
|
|
202
|
+
attributeTypeName: inferRetrofitAttributeTypeName(projectDir, block),
|
|
203
|
+
blockJsonFile: block.blockJsonFile,
|
|
204
|
+
blockName: block.blockName,
|
|
205
|
+
manifestFile: block.manifestFile,
|
|
206
|
+
saveFile: block.saveFile,
|
|
207
|
+
slug,
|
|
208
|
+
typesFile: block.typesFile,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function buildRetrofitBlockConfigEntry(target) {
|
|
212
|
+
return [
|
|
213
|
+
"\t{",
|
|
214
|
+
`\t\tslug: ${quoteTsString(target.slug)},`,
|
|
215
|
+
`\t\tattributeTypeName: ${quoteTsString(target.attributeTypeName)},`,
|
|
216
|
+
`\t\tblockJsonFile: ${quoteTsString(target.blockJsonFile)},`,
|
|
217
|
+
`\t\tmanifestFile: ${quoteTsString(target.manifestFile)},`,
|
|
218
|
+
`\t\ttypesFile: ${quoteTsString(target.typesFile)},`,
|
|
219
|
+
"\t},",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function buildRetrofitBlockConfigSource(targets) {
|
|
223
|
+
const blockEntries = targets.map(buildRetrofitBlockConfigEntry).join("\n");
|
|
224
|
+
const baseSource = `export interface WorkspaceBlockConfig {
|
|
225
|
+
\tattributeTypeName: string;
|
|
226
|
+
\tapiTypesFile?: string;
|
|
227
|
+
\tblockJsonFile?: string;
|
|
228
|
+
\tmanifestFile?: string;
|
|
229
|
+
\topenApiFile?: string;
|
|
230
|
+
\trestManifest?: ReturnType<
|
|
231
|
+
\t\ttypeof import( '@wp-typia/block-runtime/metadata-core' ).defineEndpointManifest
|
|
232
|
+
\t>;
|
|
233
|
+
\tslug: string;
|
|
234
|
+
\ttypesFile: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const BLOCKS: WorkspaceBlockConfig[] = [
|
|
238
|
+
${blockEntries}
|
|
239
|
+
];
|
|
240
|
+
`;
|
|
241
|
+
return `${updateWorkspaceInventorySource(baseSource)}\n`;
|
|
242
|
+
}
|
|
243
|
+
function buildRetrofitSyncTypesScriptSource() {
|
|
244
|
+
return `/* eslint-disable no-console */
|
|
245
|
+
import path from 'node:path';
|
|
246
|
+
|
|
247
|
+
import { syncBlockMetadata } from '@wp-typia/block-runtime/metadata-core';
|
|
248
|
+
|
|
249
|
+
import { BLOCKS } from './block-config';
|
|
250
|
+
|
|
251
|
+
function parseCliOptions( argv: string[] ) {
|
|
252
|
+
\tconst options = {
|
|
253
|
+
\t\tcheck: false,
|
|
254
|
+
\t};
|
|
255
|
+
|
|
256
|
+
\tfor ( const argument of argv ) {
|
|
257
|
+
\t\tif ( argument === '--check' ) {
|
|
258
|
+
\t\t\toptions.check = true;
|
|
259
|
+
\t\t\tcontinue;
|
|
260
|
+
\t\t}
|
|
261
|
+
|
|
262
|
+
\t\tthrow new Error( \`Unknown sync-types flag: \${ argument }\` );
|
|
263
|
+
\t}
|
|
264
|
+
|
|
265
|
+
\treturn options;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function main() {
|
|
269
|
+
\tconst options = parseCliOptions( process.argv.slice( 2 ) );
|
|
270
|
+
|
|
271
|
+
\tif ( BLOCKS.length === 0 ) {
|
|
272
|
+
\t\tconsole.log(
|
|
273
|
+
\t\t\toptions.check
|
|
274
|
+
\t\t\t\t? 'ℹ️ No retrofit blocks are registered yet. \`sync-types --check\` is already clean.'
|
|
275
|
+
\t\t\t\t: 'ℹ️ No retrofit blocks are registered yet. Add one block target to scripts/block-config.ts before rerunning sync-types.'
|
|
276
|
+
\t\t);
|
|
277
|
+
\t\treturn;
|
|
278
|
+
\t}
|
|
279
|
+
|
|
280
|
+
\tfor ( const block of BLOCKS ) {
|
|
281
|
+
\t\tconst blockDir = path.dirname( block.typesFile );
|
|
282
|
+
\t\tconst blockJsonFile =
|
|
283
|
+
\t\t\tblock.blockJsonFile ?? path.join( blockDir, 'block.json' );
|
|
284
|
+
\t\tconst manifestFile =
|
|
285
|
+
\t\t\tblock.manifestFile ?? path.join( blockDir, 'typia.manifest.json' );
|
|
286
|
+
\t\tconst manifestDir = path.dirname( manifestFile );
|
|
287
|
+
\t\tconst result = await syncBlockMetadata(
|
|
288
|
+
\t\t\t{
|
|
289
|
+
\t\t\t\tblockJsonFile,
|
|
290
|
+
\t\t\t\tjsonSchemaFile: path.join( manifestDir, 'typia.schema.json' ),
|
|
291
|
+
\t\t\t\tmanifestFile,
|
|
292
|
+
\t\t\t\topenApiFile: path.join( manifestDir, 'typia.openapi.json' ),
|
|
293
|
+
\t\t\t\tsourceTypeName: block.attributeTypeName,
|
|
294
|
+
\t\t\t\ttypesFile: block.typesFile,
|
|
295
|
+
\t\t\t},
|
|
296
|
+
\t\t\t{
|
|
297
|
+
\t\t\t\tcheck: options.check,
|
|
298
|
+
\t\t\t}
|
|
299
|
+
\t\t);
|
|
300
|
+
\t\tfor ( const warning of result.lossyProjectionWarnings ) {
|
|
301
|
+
\t\t\tconsole.warn( \`⚠️ \${ block.slug }: \${ warning }\` );
|
|
302
|
+
\t\t}
|
|
303
|
+
\t\tfor ( const warning of result.phpGenerationWarnings ) {
|
|
304
|
+
\t\t\tconsole.warn( \`⚠️ \${ block.slug }: \${ warning }\` );
|
|
305
|
+
\t\t}
|
|
306
|
+
|
|
307
|
+
\t\tconsole.log(
|
|
308
|
+
\t\t\toptions.check
|
|
309
|
+
\t\t\t\t? \`✅ \${ block.slug }: block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json are already up to date with the TypeScript types!\`
|
|
310
|
+
\t\t\t\t: \`✅ \${ block.slug }: block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json were generated from TypeScript types!\`
|
|
311
|
+
\t\t);
|
|
312
|
+
\t\tconsole.log( '📝 Generated attributes:', result.attributeNames );
|
|
313
|
+
\t}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main().catch( ( error ) => {
|
|
317
|
+
\tconsole.error( '❌ Type sync failed:', error );
|
|
318
|
+
\tprocess.exit( 1 );
|
|
319
|
+
} );
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
function buildRetrofitSyncProjectScriptSource() {
|
|
323
|
+
return `/* eslint-disable no-console */
|
|
324
|
+
import { spawnSync } from 'node:child_process';
|
|
325
|
+
import fs from 'node:fs';
|
|
326
|
+
import path from 'node:path';
|
|
327
|
+
|
|
328
|
+
interface SyncCliOptions {
|
|
329
|
+
\tcheck: boolean;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseCliOptions( argv: string[] ): SyncCliOptions {
|
|
333
|
+
\tconst options: SyncCliOptions = {
|
|
334
|
+
\t\tcheck: false,
|
|
335
|
+
\t};
|
|
336
|
+
|
|
337
|
+
\tfor ( const argument of argv ) {
|
|
338
|
+
\t\tif ( argument === '--check' ) {
|
|
339
|
+
\t\t\toptions.check = true;
|
|
340
|
+
\t\t\tcontinue;
|
|
341
|
+
\t\t}
|
|
342
|
+
|
|
343
|
+
\t\tthrow new Error( \`Unknown sync flag: \${ argument }\` );
|
|
344
|
+
\t}
|
|
345
|
+
|
|
346
|
+
\treturn options;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getSyncScriptEnv() {
|
|
350
|
+
\tconst binaryDirectory = path.join( process.cwd(), 'node_modules', '.bin' );
|
|
351
|
+
\tconst inheritedPath =
|
|
352
|
+
\t\tprocess.env.PATH ??
|
|
353
|
+
\t\tprocess.env.Path ??
|
|
354
|
+
\t\tObject.entries( process.env ).find(
|
|
355
|
+
\t\t\t( [ key ] ) => key.toLowerCase() === 'path'
|
|
356
|
+
\t\t)?.[ 1 ] ??
|
|
357
|
+
\t\t'';
|
|
358
|
+
\tconst nextPath = fs.existsSync( binaryDirectory )
|
|
359
|
+
\t\t? \`\${ binaryDirectory }\${ path.delimiter }\${ inheritedPath }\`
|
|
360
|
+
\t\t: inheritedPath;
|
|
361
|
+
\tconst env: NodeJS.ProcessEnv = {
|
|
362
|
+
\t\t...process.env,
|
|
363
|
+
\t};
|
|
364
|
+
|
|
365
|
+
\tfor ( const key of Object.keys( env ) ) {
|
|
366
|
+
\t\tif ( key.toLowerCase() === 'path' ) {
|
|
367
|
+
\t\t\tdelete env[ key ];
|
|
368
|
+
\t\t}
|
|
369
|
+
\t}
|
|
370
|
+
|
|
371
|
+
\tenv.PATH = nextPath;
|
|
372
|
+
|
|
373
|
+
\treturn env;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function runSyncScript( scriptPath: string, options: SyncCliOptions ) {
|
|
377
|
+
\tconst args = [ scriptPath ];
|
|
378
|
+
\tif ( options.check ) {
|
|
379
|
+
\t\targs.push( '--check' );
|
|
380
|
+
\t}
|
|
381
|
+
|
|
382
|
+
\tconst result = spawnSync( 'tsx', args, {
|
|
383
|
+
\t\tcwd: process.cwd(),
|
|
384
|
+
\t\tenv: getSyncScriptEnv(),
|
|
385
|
+
\t\tshell: process.platform === 'win32',
|
|
386
|
+
\t\tstdio: 'inherit',
|
|
387
|
+
\t} );
|
|
388
|
+
|
|
389
|
+
\tif ( result.error ) {
|
|
390
|
+
\t\tif ( ( result.error as NodeJS.ErrnoException ).code === 'ENOENT' ) {
|
|
391
|
+
\t\t\tthrow new Error(
|
|
392
|
+
\t\t\t\t'Unable to resolve \`tsx\` for project sync. Install project dependencies or rerun the command through your package manager.'
|
|
393
|
+
\t\t\t);
|
|
394
|
+
\t\t}
|
|
395
|
+
|
|
396
|
+
\t\tthrow result.error;
|
|
397
|
+
\t}
|
|
398
|
+
|
|
399
|
+
\tif ( result.status !== 0 ) {
|
|
400
|
+
\t\tthrow new Error( \`Sync script failed: \${ scriptPath }\` );
|
|
401
|
+
\t}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function main() {
|
|
405
|
+
\tconst options = parseCliOptions( process.argv.slice( 2 ) );
|
|
406
|
+
\tconst syncTypesScriptPath = path.join( 'scripts', 'sync-types-to-block-json.ts' );
|
|
407
|
+
|
|
408
|
+
\trunSyncScript( syncTypesScriptPath, options );
|
|
409
|
+
|
|
410
|
+
\tconsole.log(
|
|
411
|
+
\t\toptions.check
|
|
412
|
+
\t\t\t? '✅ Generated project metadata is already synchronized.'
|
|
413
|
+
\t\t\t: '✅ Generated project metadata was synchronized.'
|
|
414
|
+
\t);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch( ( error ) => {
|
|
418
|
+
\tconsole.error( '❌ Project sync failed:', error );
|
|
419
|
+
\tprocess.exit( 1 );
|
|
420
|
+
} );
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
130
423
|
function buildLayoutDetails(projectDir) {
|
|
131
424
|
try {
|
|
132
425
|
const discoveredLayout = discoverMigrationInitLayout(projectDir);
|
|
426
|
+
const discoveredBlocks = discoveredLayout.mode === "multi"
|
|
427
|
+
? discoveredLayout.blocks
|
|
428
|
+
: [discoveredLayout.block];
|
|
429
|
+
let blockTargets;
|
|
430
|
+
try {
|
|
431
|
+
blockTargets = discoveredBlocks.map((block) => buildRetrofitBlockTarget(projectDir, block));
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
435
|
+
return {
|
|
436
|
+
blockNames: discoveredBlocks.map((block) => block.blockName),
|
|
437
|
+
blockTargets: [],
|
|
438
|
+
description: "Detected supported block files, but could not infer retrofit block-config metadata automatically yet.",
|
|
439
|
+
generatedArtifacts: [],
|
|
440
|
+
kind: "unsupported",
|
|
441
|
+
notes: [message, SUPPORTED_RETROFIT_LAYOUT_NOTE],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
133
444
|
if (discoveredLayout.mode === "multi") {
|
|
134
445
|
return {
|
|
135
|
-
blockNames:
|
|
136
|
-
|
|
137
|
-
|
|
446
|
+
blockNames: discoveredBlocks.map((block) => block.blockName),
|
|
447
|
+
blockTargets,
|
|
448
|
+
description: `Detected a supported multi-block retrofit candidate (${discoveredBlocks.length} targets).`,
|
|
449
|
+
generatedArtifacts: discoveredBlocks.flatMap((block) => buildGeneratedArtifactPaths(block.blockJsonFile, block.manifestFile)),
|
|
138
450
|
kind: "multi-block",
|
|
139
451
|
notes: [
|
|
140
452
|
"Migration bootstrap can stay optional. Add it later with `wp-typia migrate init --current-migration-version v1` once the typed sync surface is in place.",
|
|
@@ -143,6 +455,7 @@ function buildLayoutDetails(projectDir) {
|
|
|
143
455
|
}
|
|
144
456
|
return {
|
|
145
457
|
blockNames: [discoveredLayout.block.blockName],
|
|
458
|
+
blockTargets,
|
|
146
459
|
description: "Detected a supported single-block retrofit candidate.",
|
|
147
460
|
generatedArtifacts: buildGeneratedArtifactPaths(discoveredLayout.block.blockJsonFile, discoveredLayout.block.manifestFile),
|
|
148
461
|
kind: "single-block",
|
|
@@ -157,6 +470,7 @@ function buildLayoutDetails(projectDir) {
|
|
|
157
470
|
const message = error instanceof Error ? error.message : String(error);
|
|
158
471
|
return {
|
|
159
472
|
blockNames: [],
|
|
473
|
+
blockTargets: [],
|
|
160
474
|
description: "No supported retrofit layout was auto-detected yet.",
|
|
161
475
|
generatedArtifacts: [],
|
|
162
476
|
kind: "unsupported",
|
|
@@ -164,35 +478,49 @@ function buildLayoutDetails(projectDir) {
|
|
|
164
478
|
};
|
|
165
479
|
}
|
|
166
480
|
}
|
|
167
|
-
function hasExistingWpTypiaProjectSurface(packageJson) {
|
|
481
|
+
function hasExistingWpTypiaProjectSurface(projectDir, packageJson) {
|
|
168
482
|
const scripts = packageJson?.scripts ?? {};
|
|
169
483
|
const hasSyncSurface = typeof scripts.sync === "string" || typeof scripts["sync-types"] === "string";
|
|
484
|
+
const hasHelperFiles = [
|
|
485
|
+
path.join("scripts", "block-config.ts"),
|
|
486
|
+
path.join("scripts", "sync-project.ts"),
|
|
487
|
+
path.join("scripts", "sync-types-to-block-json.ts"),
|
|
488
|
+
].every((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
|
|
170
489
|
const hasRuntimeDeps = typeof getExistingDependencyVersion(packageJson, "@wp-typia/block-runtime") ===
|
|
171
490
|
"string" &&
|
|
172
491
|
typeof getExistingDependencyVersion(packageJson, "@wp-typia/block-types") ===
|
|
173
492
|
"string";
|
|
174
|
-
return hasSyncSurface && hasRuntimeDeps;
|
|
493
|
+
return hasSyncSurface && hasHelperFiles && hasRuntimeDeps;
|
|
175
494
|
}
|
|
176
|
-
function buildPlannedFiles(layoutKind) {
|
|
177
|
-
|
|
495
|
+
function buildPlannedFiles(projectDir, layoutKind) {
|
|
496
|
+
if (layoutKind === "unsupported") {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
return [
|
|
500
|
+
{
|
|
501
|
+
action: fs.existsSync(path.join(projectDir, "scripts", "block-config.ts"))
|
|
502
|
+
? "update"
|
|
503
|
+
: "add",
|
|
504
|
+
path: "scripts/block-config.ts",
|
|
505
|
+
purpose: "Declare the current retrofit block targets so sync-types can regenerate metadata from the existing TypeScript source of truth.",
|
|
506
|
+
},
|
|
178
507
|
{
|
|
508
|
+
action: fs.existsSync(path.join(projectDir, "scripts", "sync-types-to-block-json.ts"))
|
|
509
|
+
? "update"
|
|
510
|
+
: "add",
|
|
179
511
|
path: "scripts/sync-types-to-block-json.ts",
|
|
180
512
|
purpose: "Generate block.json and Typia metadata artifacts from the current TypeScript source of truth.",
|
|
181
513
|
},
|
|
182
514
|
{
|
|
515
|
+
action: fs.existsSync(path.join(projectDir, "scripts", "sync-project.ts"))
|
|
516
|
+
? "update"
|
|
517
|
+
: "add",
|
|
183
518
|
path: "scripts/sync-project.ts",
|
|
184
519
|
purpose: "Provide one shared sync entrypoint that can grow into sync-rest or workspace-aware refresh steps later.",
|
|
185
520
|
},
|
|
186
521
|
];
|
|
187
|
-
if (layoutKind === "unsupported") {
|
|
188
|
-
plannedFiles.unshift({
|
|
189
|
-
path: "package.json",
|
|
190
|
-
purpose: "Add the minimum wp-typia devDependencies and scripts once the project matches a supported retrofit layout.",
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
return plannedFiles;
|
|
194
522
|
}
|
|
195
|
-
function buildChangeSummary(changes) {
|
|
523
|
+
function buildChangeSummary(changes, options) {
|
|
196
524
|
const lines = [];
|
|
197
525
|
for (const dependencyChange of changes.packageChanges.addDevDependencies) {
|
|
198
526
|
lines.push(`devDependency ${dependencyChange.action} ${dependencyChange.name} -> ${dependencyChange.requiredValue}`);
|
|
@@ -204,10 +532,12 @@ function buildChangeSummary(changes) {
|
|
|
204
532
|
lines.push(`script ${scriptChange.action} ${scriptChange.name} -> ${scriptChange.requiredValue}`);
|
|
205
533
|
}
|
|
206
534
|
for (const filePlan of changes.plannedFiles) {
|
|
207
|
-
lines.push(`file
|
|
535
|
+
lines.push(`file ${filePlan.action} ${filePlan.path} (${filePlan.purpose})`);
|
|
208
536
|
}
|
|
209
|
-
|
|
210
|
-
|
|
537
|
+
if (options.includeGeneratedArtifacts) {
|
|
538
|
+
for (const artifactPath of changes.generatedArtifacts) {
|
|
539
|
+
lines.push(`generated artifact ${artifactPath}`);
|
|
540
|
+
}
|
|
211
541
|
}
|
|
212
542
|
return lines;
|
|
213
543
|
}
|
|
@@ -226,11 +556,24 @@ function buildNextSteps(options) {
|
|
|
226
556
|
doctorRun,
|
|
227
557
|
];
|
|
228
558
|
}
|
|
559
|
+
if (options.commandMode === "apply") {
|
|
560
|
+
return [
|
|
561
|
+
...(options.dependencyChangeCount > 0
|
|
562
|
+
? [
|
|
563
|
+
"Install or reinstall project dependencies so the retrofit sync scripts and metadata generators are available locally.",
|
|
564
|
+
dependencyInstallCommand,
|
|
565
|
+
]
|
|
566
|
+
: []),
|
|
567
|
+
syncRun,
|
|
568
|
+
doctorRun,
|
|
569
|
+
`Optional migration bootstrap: ${migrationInitRun}`,
|
|
570
|
+
];
|
|
571
|
+
}
|
|
229
572
|
const steps = [
|
|
230
|
-
...(options.
|
|
573
|
+
...(options.hasPlannedChanges
|
|
231
574
|
? [
|
|
232
|
-
"
|
|
233
|
-
dependencyInstallCommand,
|
|
575
|
+
"Re-run `wp-typia init --apply` to write the planned package.json changes and helper files automatically.",
|
|
576
|
+
...(options.dependencyChangeCount > 0 ? [dependencyInstallCommand] : []),
|
|
234
577
|
]
|
|
235
578
|
: []),
|
|
236
579
|
syncRun,
|
|
@@ -242,13 +585,148 @@ function buildNextSteps(options) {
|
|
|
242
585
|
function buildRequiredDevDependencyMapEntries() {
|
|
243
586
|
return Object.entries(buildRequiredDevDependencyMap()).map(([name, version]) => `${name}@${version.replace(/^workspace:/u, "")}`);
|
|
244
587
|
}
|
|
245
|
-
|
|
588
|
+
function buildRetrofitPlanSummary(options) {
|
|
589
|
+
if (options.status === "already-initialized") {
|
|
590
|
+
return options.commandMode === "apply"
|
|
591
|
+
? "This project already exposes the minimum wp-typia retrofit surface. No files were changed."
|
|
592
|
+
: "This project already exposes the minimum wp-typia retrofit surface.";
|
|
593
|
+
}
|
|
594
|
+
if (options.commandMode === "apply") {
|
|
595
|
+
return "Applied the minimum wp-typia retrofit surface so package.json and helper scripts are ready for the next install and sync run.";
|
|
596
|
+
}
|
|
597
|
+
return "This command previews the minimum wp-typia adoption layer for the current project without rewriting it into a full scaffold.";
|
|
598
|
+
}
|
|
599
|
+
function createRetrofitPlan(options) {
|
|
600
|
+
const includeGeneratedArtifacts = options.commandMode === "preview-only";
|
|
601
|
+
const plannedChanges = buildChangeSummary({
|
|
602
|
+
generatedArtifacts: options.generatedArtifacts,
|
|
603
|
+
packageChanges: options.packageChanges,
|
|
604
|
+
plannedFiles: options.plannedFiles,
|
|
605
|
+
}, {
|
|
606
|
+
includeGeneratedArtifacts,
|
|
607
|
+
});
|
|
608
|
+
return {
|
|
609
|
+
blockTargets: options.blockTargets,
|
|
610
|
+
commandMode: options.commandMode,
|
|
611
|
+
detectedLayout: options.detectedLayout,
|
|
612
|
+
generatedArtifacts: options.generatedArtifacts,
|
|
613
|
+
nextSteps: options.nextSteps ??
|
|
614
|
+
buildNextSteps({
|
|
615
|
+
commandMode: options.commandMode,
|
|
616
|
+
dependencyChangeCount: options.packageChanges.addDevDependencies.length,
|
|
617
|
+
hasPlannedChanges: plannedChanges.length > 0,
|
|
618
|
+
layoutKind: options.detectedLayout.kind,
|
|
619
|
+
packageManager: options.packageManager,
|
|
620
|
+
}),
|
|
621
|
+
notes: options.notes,
|
|
622
|
+
packageChanges: options.packageChanges,
|
|
623
|
+
plannedFiles: options.plannedFiles,
|
|
624
|
+
packageManager: options.packageManager,
|
|
625
|
+
projectDir: options.projectDir,
|
|
626
|
+
projectName: options.projectName,
|
|
627
|
+
status: options.status,
|
|
628
|
+
summary: buildRetrofitPlanSummary({
|
|
629
|
+
commandMode: options.commandMode,
|
|
630
|
+
status: options.status,
|
|
631
|
+
}),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function setDependencyVersion(packageJson, name, requiredValue) {
|
|
635
|
+
if (packageJson.devDependencies?.[name] !== undefined) {
|
|
636
|
+
packageJson.devDependencies[name] = requiredValue;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (packageJson.dependencies?.[name] !== undefined) {
|
|
640
|
+
packageJson.dependencies[name] = requiredValue;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
packageJson.devDependencies ?? (packageJson.devDependencies = {});
|
|
644
|
+
packageJson.devDependencies[name] = requiredValue;
|
|
645
|
+
}
|
|
646
|
+
function buildNextProjectPackageJson(options) {
|
|
647
|
+
const nextPackageJson = options.packageJson
|
|
648
|
+
? JSON.parse(JSON.stringify(options.packageJson))
|
|
649
|
+
: {
|
|
650
|
+
name: options.projectName,
|
|
651
|
+
private: true,
|
|
652
|
+
};
|
|
653
|
+
nextPackageJson.devDependencies ?? (nextPackageJson.devDependencies = {});
|
|
654
|
+
nextPackageJson.scripts ?? (nextPackageJson.scripts = {});
|
|
655
|
+
for (const dependencyChange of options.packageChanges.addDevDependencies) {
|
|
656
|
+
setDependencyVersion(nextPackageJson, dependencyChange.name, dependencyChange.requiredValue);
|
|
657
|
+
}
|
|
658
|
+
if (options.packageChanges.packageManagerField) {
|
|
659
|
+
nextPackageJson.packageManager =
|
|
660
|
+
options.packageChanges.packageManagerField.requiredValue;
|
|
661
|
+
}
|
|
662
|
+
else if (!nextPackageJson.packageManager &&
|
|
663
|
+
options.packageManager !== "npm") {
|
|
664
|
+
nextPackageJson.packageManager =
|
|
665
|
+
getPackageManager(options.packageManager).packageManagerField;
|
|
666
|
+
}
|
|
667
|
+
for (const scriptChange of options.packageChanges.scripts) {
|
|
668
|
+
nextPackageJson.scripts[scriptChange.name] = scriptChange.requiredValue;
|
|
669
|
+
}
|
|
670
|
+
return nextPackageJson;
|
|
671
|
+
}
|
|
672
|
+
function buildProjectPackageJsonSource(packageJson) {
|
|
673
|
+
return `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
674
|
+
}
|
|
675
|
+
function buildRetrofitHelperFiles(blockTargets) {
|
|
676
|
+
return {
|
|
677
|
+
[path.join("scripts", "block-config.ts")]: buildRetrofitBlockConfigSource(blockTargets),
|
|
678
|
+
[path.join("scripts", "sync-project.ts")]: buildRetrofitSyncProjectScriptSource(),
|
|
679
|
+
[path.join("scripts", "sync-types-to-block-json.ts")]: buildRetrofitSyncTypesScriptSource(),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function createRetrofitMutationSnapshot(projectDir, filePaths) {
|
|
683
|
+
const scriptsDir = path.join(projectDir, "scripts");
|
|
684
|
+
const scriptsDirExisted = fs.existsSync(scriptsDir);
|
|
685
|
+
const fileSources = await snapshotWorkspaceFiles(filePaths);
|
|
686
|
+
const targetPaths = fileSources
|
|
687
|
+
.filter((entry) => entry.source === null)
|
|
688
|
+
.map((entry) => entry.filePath);
|
|
689
|
+
if (!scriptsDirExisted) {
|
|
690
|
+
targetPaths.push(scriptsDir);
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
fileSources,
|
|
694
|
+
snapshotDirs: [],
|
|
695
|
+
targetPaths,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async function writeRetrofitFiles(options) {
|
|
699
|
+
const helperFiles = buildRetrofitHelperFiles(options.blockTargets);
|
|
700
|
+
const scriptsDir = path.join(options.projectDir, "scripts");
|
|
701
|
+
await fsp.mkdir(scriptsDir, { recursive: true });
|
|
702
|
+
await fsp.writeFile(path.join(options.projectDir, "package.json"), buildProjectPackageJsonSource(options.packageJson), "utf8");
|
|
703
|
+
for (const [relativePath, source] of Object.entries(helperFiles)) {
|
|
704
|
+
await fsp.writeFile(path.join(options.projectDir, relativePath), source, "utf8");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function buildApplyFailureError(error) {
|
|
708
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
709
|
+
return createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, `Unable to apply the retrofit init plan safely. The command restored the previous package.json/helper-file snapshot. ${message}`, error instanceof Error ? { cause: error } : undefined);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Inspect one project directory and return the current retrofit init plan.
|
|
713
|
+
*
|
|
714
|
+
* @param projectDir Project root or nested path that should be analyzed.
|
|
715
|
+
* @param options Optional package-manager override used for emitted scripts and
|
|
716
|
+
* follow-up guidance.
|
|
717
|
+
* @returns The preview-only retrofit init plan for the resolved project.
|
|
718
|
+
*/
|
|
719
|
+
export function getInitPlan(projectDir, options = {}) {
|
|
246
720
|
const resolvedProjectDir = path.resolve(projectDir);
|
|
247
721
|
const packageJson = readProjectPackageJson(resolvedProjectDir);
|
|
248
|
-
const packageManager =
|
|
722
|
+
const packageManager = resolveInitPackageManager(resolvedProjectDir, packageJson, options.packageManager);
|
|
249
723
|
const workspace = tryResolveWorkspaceProject(resolvedProjectDir);
|
|
250
724
|
if (workspace) {
|
|
251
|
-
|
|
725
|
+
const workspacePackageJson = readProjectPackageJson(workspace.projectDir);
|
|
726
|
+
const workspacePackageManager = resolveInitPackageManager(workspace.projectDir, workspacePackageJson, options.packageManager);
|
|
727
|
+
const cliSpecifier = getWpTypiaCliSpecifier();
|
|
728
|
+
return createRetrofitPlan({
|
|
729
|
+
blockTargets: [],
|
|
252
730
|
commandMode: "preview-only",
|
|
253
731
|
detectedLayout: {
|
|
254
732
|
blockNames: [],
|
|
@@ -257,8 +735,9 @@ export function getInitPlan(projectDir) {
|
|
|
257
735
|
},
|
|
258
736
|
generatedArtifacts: [],
|
|
259
737
|
nextSteps: [
|
|
260
|
-
|
|
261
|
-
|
|
738
|
+
"Use `wp-typia add <kind> <name>` to extend the official workspace instead of rerunning init.",
|
|
739
|
+
formatRunScript(workspacePackageManager, "sync"),
|
|
740
|
+
formatPackageExecCommand(workspacePackageManager, cliSpecifier, "doctor"),
|
|
262
741
|
],
|
|
263
742
|
notes: [
|
|
264
743
|
"The official workspace template already owns inventory, doctor, and add-command workflows.",
|
|
@@ -267,13 +746,12 @@ export function getInitPlan(projectDir) {
|
|
|
267
746
|
addDevDependencies: [],
|
|
268
747
|
scripts: [],
|
|
269
748
|
},
|
|
749
|
+
packageManager: workspacePackageManager,
|
|
270
750
|
plannedFiles: [],
|
|
271
|
-
packageManager,
|
|
272
751
|
projectDir: workspace.projectDir,
|
|
273
752
|
projectName: workspace.packageName,
|
|
274
753
|
status: "already-initialized",
|
|
275
|
-
|
|
276
|
-
};
|
|
754
|
+
});
|
|
277
755
|
}
|
|
278
756
|
const projectName = typeof packageJson?.name === "string" && packageJson.name.length > 0
|
|
279
757
|
? packageJson.name
|
|
@@ -281,11 +759,13 @@ export function getInitPlan(projectDir) {
|
|
|
281
759
|
const layout = buildLayoutDetails(resolvedProjectDir);
|
|
282
760
|
const dependencyChanges = buildDependencyChanges(packageJson);
|
|
283
761
|
const scriptChanges = buildScriptChanges(packageJson, packageManager);
|
|
284
|
-
const packageManagerFieldChange = buildPackageManagerFieldChange(packageJson, packageManager
|
|
762
|
+
const packageManagerFieldChange = buildPackageManagerFieldChange(packageJson, packageManager, {
|
|
763
|
+
persistExplicitOverride: typeof options.packageManager === "string",
|
|
764
|
+
});
|
|
285
765
|
const rawPlannedFiles = layout.kind === "generated-project" || layout.kind === "official-workspace"
|
|
286
766
|
? []
|
|
287
|
-
: buildPlannedFiles(layout.kind);
|
|
288
|
-
const hasExistingSurface = hasExistingWpTypiaProjectSurface(packageJson);
|
|
767
|
+
: buildPlannedFiles(resolvedProjectDir, layout.kind);
|
|
768
|
+
const hasExistingSurface = hasExistingWpTypiaProjectSurface(resolvedProjectDir, packageJson);
|
|
289
769
|
const status = hasExistingSurface &&
|
|
290
770
|
dependencyChanges.length === 0 &&
|
|
291
771
|
scriptChanges.length === 0 &&
|
|
@@ -306,32 +786,16 @@ export function getInitPlan(projectDir) {
|
|
|
306
786
|
description: layout.description,
|
|
307
787
|
kind: layout.kind,
|
|
308
788
|
};
|
|
309
|
-
|
|
789
|
+
return createRetrofitPlan({
|
|
790
|
+
blockTargets: layout.blockTargets,
|
|
310
791
|
commandMode: "preview-only",
|
|
311
792
|
detectedLayout,
|
|
312
793
|
generatedArtifacts: status === "already-initialized" && detectedLayout.kind === "generated-project"
|
|
313
794
|
? []
|
|
314
795
|
: layout.generatedArtifacts,
|
|
315
|
-
nextSteps: buildNextSteps({
|
|
316
|
-
changeSummaryLines: buildChangeSummary({
|
|
317
|
-
generatedArtifacts: status === "already-initialized" &&
|
|
318
|
-
detectedLayout.kind === "generated-project"
|
|
319
|
-
? []
|
|
320
|
-
: layout.generatedArtifacts,
|
|
321
|
-
packageChanges: {
|
|
322
|
-
addDevDependencies: dependencyChanges,
|
|
323
|
-
...(packageManagerFieldChange
|
|
324
|
-
? { packageManagerField: packageManagerFieldChange }
|
|
325
|
-
: {}),
|
|
326
|
-
scripts: scriptChanges,
|
|
327
|
-
},
|
|
328
|
-
plannedFiles,
|
|
329
|
-
}),
|
|
330
|
-
layoutKind: detectedLayout.kind,
|
|
331
|
-
packageManager,
|
|
332
|
-
}),
|
|
333
796
|
notes: Array.from(new Set([
|
|
334
797
|
"Preview only: `wp-typia init` does not write files yet.",
|
|
798
|
+
RETROFIT_APPLY_PREVIEW_NOTE,
|
|
335
799
|
...layout.notes,
|
|
336
800
|
])),
|
|
337
801
|
packageChanges: {
|
|
@@ -341,14 +805,89 @@ export function getInitPlan(projectDir) {
|
|
|
341
805
|
: {}),
|
|
342
806
|
scripts: scriptChanges,
|
|
343
807
|
},
|
|
344
|
-
plannedFiles,
|
|
345
808
|
packageManager,
|
|
809
|
+
plannedFiles,
|
|
346
810
|
projectDir: resolvedProjectDir,
|
|
347
811
|
projectName,
|
|
348
812
|
status,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Apply the previewed retrofit init plan to disk.
|
|
817
|
+
*
|
|
818
|
+
* The command snapshots package.json and generated helper targets before
|
|
819
|
+
* writing, then rolls those files back automatically if any write fails.
|
|
820
|
+
*
|
|
821
|
+
* @param projectDir Project root that should receive the retrofit surface.
|
|
822
|
+
* @param options Optional package-manager override used for emitted scripts and
|
|
823
|
+
* follow-up guidance.
|
|
824
|
+
* @returns The applied retrofit init plan describing the persisted changes.
|
|
825
|
+
*/
|
|
826
|
+
export async function applyInitPlan(projectDir, options = {}) {
|
|
827
|
+
const previewPlan = getInitPlan(projectDir, options);
|
|
828
|
+
if (previewPlan.detectedLayout.kind === "unsupported") {
|
|
829
|
+
throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, "`wp-typia init --apply` requires a supported retrofit layout. Run `wp-typia init` first to inspect the preview plan and any blocking notes.");
|
|
830
|
+
}
|
|
831
|
+
if (previewPlan.status === "already-initialized") {
|
|
832
|
+
return createRetrofitPlan({
|
|
833
|
+
...previewPlan,
|
|
834
|
+
commandMode: "apply",
|
|
835
|
+
notes: Array.from(new Set([
|
|
836
|
+
...previewPlan.notes.filter((note) => note !== "Preview only: `wp-typia init` does not write files yet." &&
|
|
837
|
+
note !== RETROFIT_APPLY_PREVIEW_NOTE),
|
|
838
|
+
RETROFIT_ROLLBACK_NOTE,
|
|
839
|
+
])),
|
|
840
|
+
status: "already-initialized",
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const nextPackageJson = buildNextProjectPackageJson({
|
|
844
|
+
packageChanges: previewPlan.packageChanges,
|
|
845
|
+
packageJson: readProjectPackageJson(previewPlan.projectDir),
|
|
846
|
+
packageManager: previewPlan.packageManager,
|
|
847
|
+
projectName: previewPlan.projectName,
|
|
848
|
+
});
|
|
849
|
+
const helperFiles = buildRetrofitHelperFiles(previewPlan.blockTargets);
|
|
850
|
+
const filePaths = [
|
|
851
|
+
path.join(previewPlan.projectDir, "package.json"),
|
|
852
|
+
...Object.keys(helperFiles).map((relativePath) => path.join(previewPlan.projectDir, relativePath)),
|
|
853
|
+
];
|
|
854
|
+
const mutationSnapshot = await createRetrofitMutationSnapshot(previewPlan.projectDir, filePaths);
|
|
855
|
+
try {
|
|
856
|
+
await writeRetrofitFiles({
|
|
857
|
+
blockTargets: previewPlan.blockTargets,
|
|
858
|
+
packageJson: nextPackageJson,
|
|
859
|
+
projectDir: previewPlan.projectDir,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
await rollbackWorkspaceMutation(mutationSnapshot);
|
|
864
|
+
throw buildApplyFailureError(error);
|
|
865
|
+
}
|
|
866
|
+
return createRetrofitPlan({
|
|
867
|
+
...previewPlan,
|
|
868
|
+
commandMode: "apply",
|
|
869
|
+
notes: Array.from(new Set([
|
|
870
|
+
...previewPlan.notes.filter((note) => note !== "Preview only: `wp-typia init` does not write files yet." &&
|
|
871
|
+
note !== RETROFIT_APPLY_PREVIEW_NOTE),
|
|
872
|
+
RETROFIT_ROLLBACK_NOTE,
|
|
873
|
+
])),
|
|
874
|
+
status: "applied",
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Execute `wp-typia init` in preview or apply mode.
|
|
879
|
+
*
|
|
880
|
+
* @param options Resolved command options including the target project
|
|
881
|
+
* directory, optional package-manager override, and whether writes should be
|
|
882
|
+
* applied.
|
|
883
|
+
* @returns The previewed or applied retrofit init plan.
|
|
884
|
+
*/
|
|
885
|
+
export async function runInitCommand(options) {
|
|
886
|
+
return options.apply
|
|
887
|
+
? applyInitPlan(options.projectDir, {
|
|
888
|
+
packageManager: options.packageManager,
|
|
889
|
+
})
|
|
890
|
+
: getInitPlan(options.projectDir, {
|
|
891
|
+
packageManager: options.packageManager,
|
|
892
|
+
});
|
|
354
893
|
}
|