@wp-typia/project-tools 0.16.7 → 0.16.9

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.
Files changed (54) hide show
  1. package/README.md +26 -1
  2. package/dist/runtime/block-generator-service.d.ts +9 -1
  3. package/dist/runtime/block-generator-service.js +137 -16
  4. package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
  5. package/dist/runtime/block-generator-tool-contract.js +157 -0
  6. package/dist/runtime/built-in-block-artifacts.js +171 -423
  7. package/dist/runtime/built-in-block-code-artifacts.js +96 -46
  8. package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
  9. package/dist/runtime/built-in-block-code-templates.js +2233 -0
  10. package/dist/runtime/cli-add-block.d.ts +2 -1
  11. package/dist/runtime/cli-add-block.js +163 -25
  12. package/dist/runtime/cli-add-shared.d.ts +7 -0
  13. package/dist/runtime/cli-add-shared.js +4 -6
  14. package/dist/runtime/cli-add-workspace.js +56 -17
  15. package/dist/runtime/cli-core.d.ts +4 -0
  16. package/dist/runtime/cli-core.js +3 -0
  17. package/dist/runtime/cli-diagnostics.d.ts +58 -0
  18. package/dist/runtime/cli-diagnostics.js +101 -0
  19. package/dist/runtime/cli-doctor.d.ts +2 -1
  20. package/dist/runtime/cli-doctor.js +16 -5
  21. package/dist/runtime/cli-help.js +4 -4
  22. package/dist/runtime/cli-scaffold.d.ts +5 -1
  23. package/dist/runtime/cli-scaffold.js +138 -111
  24. package/dist/runtime/external-layer-selection.d.ts +14 -0
  25. package/dist/runtime/external-layer-selection.js +35 -0
  26. package/dist/runtime/index.d.ts +4 -2
  27. package/dist/runtime/index.js +2 -1
  28. package/dist/runtime/migration-render.d.ts +23 -1
  29. package/dist/runtime/migration-render.js +58 -10
  30. package/dist/runtime/migration-ui-capability.js +17 -8
  31. package/dist/runtime/migration-utils.d.ts +7 -6
  32. package/dist/runtime/migration-utils.js +76 -73
  33. package/dist/runtime/migrations.js +2 -2
  34. package/dist/runtime/object-utils.d.ts +8 -1
  35. package/dist/runtime/object-utils.js +21 -1
  36. package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
  37. package/dist/runtime/scaffold-apply-utils.js +19 -6
  38. package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
  39. package/dist/runtime/scaffold-repository-reference.js +119 -0
  40. package/dist/runtime/scaffold.d.ts +5 -1
  41. package/dist/runtime/scaffold.js +15 -37
  42. package/dist/runtime/template-builtins.d.ts +11 -1
  43. package/dist/runtime/template-builtins.js +118 -25
  44. package/dist/runtime/template-layers.d.ts +37 -0
  45. package/dist/runtime/template-layers.js +184 -0
  46. package/dist/runtime/template-render.d.ts +28 -2
  47. package/dist/runtime/template-render.js +122 -55
  48. package/dist/runtime/template-source.d.ts +23 -5
  49. package/dist/runtime/template-source.js +296 -217
  50. package/package.json +8 -3
  51. package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
  52. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +58 -12
  53. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
  54. package/templates/_shared/migration-ui/common/src/migrations/index.ts +40 -11
@@ -120,14 +120,34 @@ export function renderMigrationRuleFile({ block, currentAttributes, currentTypeN
120
120
  lines.push("");
121
121
  return `${lines.join("\n")}\n`;
122
122
  }
123
+ /**
124
+ * Renders the generated migration registry module for a block target.
125
+ *
126
+ * Prefers manifest wrapper modules when they are available in the project,
127
+ * while still validating the imported manifest before the registry consumes it.
128
+ *
129
+ * @param state The resolved migration project state.
130
+ * @param blockKey The stable key for the block whose registry is being generated.
131
+ * @param entries The generated migration entries to include in the registry.
132
+ * @returns The generated TypeScript source code for the migration registry file.
133
+ */
123
134
  export function renderMigrationRegistryFile(state, blockKey, entries) {
124
135
  const block = state.blocks.find((entry) => entry.key === blockKey);
125
136
  if (!block) {
126
137
  throw new Error(`Unknown migration block target: ${blockKey}`);
127
138
  }
139
+ const generatedDir = getGeneratedDir(block, state);
140
+ const currentManifestWrapperCandidates = [
141
+ block.manifestFile.replace(/typia\.manifest\.json$/u, "manifest-document.ts"),
142
+ path.join(path.dirname(block.typesFile), "manifest-document.ts"),
143
+ ].filter((candidate, index, allCandidates) => candidate !== block.manifestFile && allCandidates.indexOf(candidate) === index);
144
+ const currentManifestWrapperFile = currentManifestWrapperCandidates.find((candidate) => fs.existsSync(path.join(state.projectDir, candidate))) ?? null;
145
+ const currentManifestSourceFile = currentManifestWrapperFile ?? block.manifestFile;
146
+ const currentManifestImport = normalizeImportPath(path.relative(generatedDir, path.join(state.projectDir, currentManifestSourceFile)), currentManifestWrapperFile !== null);
128
147
  const imports = [
129
- `import currentManifest from "${normalizeImportPath(path.relative(getGeneratedDir(block, state), path.join(state.projectDir, block.manifestFile)))}";`,
148
+ `import rawCurrentManifest from "${currentManifestImport}";`,
130
149
  `import type { ManifestDocument, MigrationRiskSummary } from "${normalizeImportPath(path.relative(getGeneratedDir(block, state), path.join(state.projectDir, "src", "migrations", "helpers.ts")), true)}";`,
150
+ `import { parseManifestDocument } from "@wp-typia/block-runtime/editor";`,
131
151
  ];
132
152
  const body = [];
133
153
  entries.forEach(({ entry, riskSummary }, index) => {
@@ -135,7 +155,7 @@ export function renderMigrationRegistryFile(state, blockKey, entries) {
135
155
  imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
136
156
  body.push(`\t{`);
137
157
  body.push(`\t\tfromMigrationVersion: "${entry.fromVersion}",`);
138
- body.push(`\t\tmanifest: manifest_${index},`);
158
+ body.push(`\t\tmanifest: parseManifestDocument<ManifestDocument>(manifest_${index}),`);
139
159
  body.push(`\t\triskSummary: ${JSON.stringify(riskSummary, null, "\t").replace(/\n/g, "\n\t\t")},`);
140
160
  body.push(`\t\trule: rule_${index},`);
141
161
  body.push(`\t},`);
@@ -159,7 +179,7 @@ export const migrationRegistry: {
159
179
  entries: MigrationRegistryEntry[];
160
180
  } = {
161
181
  currentMigrationVersion: "${state.config.currentMigrationVersion}",
162
- currentManifest: currentManifest as ManifestDocument,
182
+ currentManifest: parseManifestDocument<ManifestDocument>(rawCurrentManifest),
163
183
  entries: [
164
184
  ${body.join("\n")}
165
185
  ],
@@ -168,23 +188,51 @@ ${body.join("\n")}
168
188
  export default migrationRegistry;
169
189
  `;
170
190
  }
171
- export function renderGeneratedDeprecatedFile(entries) {
191
+ /**
192
+ * Renders the generated deprecated module for a block target.
193
+ *
194
+ * The emitted module exposes the ordered deprecation array consumed by block
195
+ * registration and migration helpers.
196
+ *
197
+ * @param state The resolved migration project state.
198
+ * @param blockKey The stable key for the block whose deprecated entries are being generated.
199
+ * @param entries The migration entries that define deprecated manifest versions.
200
+ * @returns The generated TypeScript source code for the deprecated module.
201
+ */
202
+ export function renderGeneratedDeprecatedFile(state, blockKey, entries) {
203
+ const block = state.blocks.find((entry) => entry.key === blockKey);
204
+ if (!block) {
205
+ throw new Error(`Unknown migration block target: ${blockKey}`);
206
+ }
207
+ const currentTypeName = typeof block.currentManifest.sourceType === "string" &&
208
+ block.currentManifest.sourceType.length > 0
209
+ ? block.currentManifest.sourceType
210
+ : "Record<string, unknown>";
211
+ const hasNamedCurrentType = currentTypeName !== "Record<string, unknown>";
212
+ const generatedDir = getGeneratedDir(block, state);
213
+ const typesImport = normalizeImportPath(path.relative(generatedDir, path.join(state.projectDir, block.typesFile)), true);
172
214
  if (entries.length === 0) {
173
215
  return `/* eslint-disable prettier/prettier */
174
- import type { BlockConfiguration } from "@wordpress/blocks";
216
+ import type { BlockDeprecationList } from "@wp-typia/block-types/blocks/registration";
217
+ ${hasNamedCurrentType ? `import type { ${currentTypeName} } from "${typesImport}";\n` : ""}
175
218
 
176
- export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [];
219
+ export const deprecated: BlockDeprecationList<${currentTypeName}> = [];
177
220
  `;
178
221
  }
179
- const imports = [`import type { BlockConfiguration } from "@wordpress/blocks";`];
222
+ const imports = [
223
+ `import type { BlockConfiguration, BlockDeprecationList } from "@wp-typia/block-types/blocks/registration";`,
224
+ ...(hasNamedCurrentType
225
+ ? [`import type { ${currentTypeName} } from "${typesImport}";`]
226
+ : []),
227
+ ];
180
228
  const definitions = [];
181
229
  const arrayEntries = [];
182
230
  entries.forEach((entry, index) => {
183
231
  imports.push(`import block_${index} from "${entry.blockJsonImport}";`);
184
232
  imports.push(`import save_${index} from "${entry.saveImport}";`);
185
233
  imports.push(`import * as rule_${index} from "${entry.ruleImport}";`);
186
- definitions.push(`const deprecated_${index}: NonNullable<BlockConfiguration["deprecated"]>[number] = {`);
187
- definitions.push(`\tattributes: (block_${index}.attributes ?? {}) as Record<string, unknown>,`);
234
+ definitions.push(`const deprecated_${index}: BlockDeprecationList<${currentTypeName}>[number] = {`);
235
+ definitions.push(`\tattributes: (block_${index}.attributes ?? {}) as BlockConfiguration["attributes"],`);
188
236
  definitions.push(`\tsave: save_${index} as BlockConfiguration["save"],`);
189
237
  definitions.push(`\tmigrate(attributes: Record<string, unknown>) {`);
190
238
  definitions.push(`\t\treturn rule_${index}.migrate(attributes);`);
@@ -197,7 +245,7 @@ ${imports.join("\n")}
197
245
 
198
246
  ${definitions.join("\n\n")}
199
247
 
200
- export const deprecated: NonNullable<BlockConfiguration["deprecated"]> = [${arrayEntries.join(", ")}];
248
+ export const deprecated: BlockDeprecationList<${currentTypeName}> = [${arrayEntries.join(", ")}];
201
249
  `;
202
250
  }
203
251
  export function renderGeneratedMigrationIndexFile(state, entries) {
@@ -6,6 +6,8 @@ import { seedProjectMigrations } from "./migrations.js";
6
6
  import { copyInterpolatedDirectory } from "./template-render.js";
7
7
  import { SHARED_MIGRATION_UI_TEMPLATE_ROOT, } from "./template-registry.js";
8
8
  const INITIAL_MIGRATION_VERSION = "v1";
9
+ const BLOCK_METADATA_IMPORT_LINE = "import metadata from './block-metadata';";
10
+ const LEGACY_BLOCK_JSON_IMPORT_LINE = "import metadata from './block.json';";
9
11
  async function mutatePackageJson(projectDir, mutate) {
10
12
  const packageJsonPath = path.join(projectDir, "package.json");
11
13
  const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
@@ -38,6 +40,13 @@ function injectBefore(source, needle, insertion) {
38
40
  }
39
41
  return source.replace(needle, `${insertion}\n${needle}`);
40
42
  }
43
+ function injectAfterBlockMetadataImport(source, insertion) {
44
+ let nextSource = injectAfter(source, BLOCK_METADATA_IMPORT_LINE, insertion);
45
+ if (nextSource !== source) {
46
+ return nextSource;
47
+ }
48
+ return injectAfter(source, LEGACY_BLOCK_JSON_IMPORT_LINE, insertion);
49
+ }
41
50
  function buildMigrationBlocks(templateId, variables) {
42
51
  if (templateId === "compound") {
43
52
  return [
@@ -74,11 +83,11 @@ async function applySingleBlockPatches(projectDir, variables) {
74
83
  const editPath = path.join(projectDir, "src", "edit.tsx");
75
84
  const indexPath = path.join(projectDir, "src", "index.tsx");
76
85
  const deprecatedImport = `import { deprecated } from './migrations/generated/${variables.slugKebabCase}/deprecated';`;
77
- const deprecatedLine = ` deprecated: deprecated as NonNullable<BlockConfiguration<${variables.pascalCase}Attributes>['deprecated']>,`;
86
+ const deprecatedLine = " deprecated,";
78
87
  const dashboardImport = `import { MigrationDashboard } from './admin/migration-dashboard';`;
79
88
  const migrationPanel = `\n <PanelBody title={__('Migration Manager', '${variables.textDomain}')}>\n <MigrationDashboard />\n </PanelBody>\n </InspectorControls>`;
80
89
  await patchFile(indexPath, (source) => {
81
- let nextSource = injectAfter(source, "import metadata from './block.json';", deprecatedImport);
90
+ let nextSource = injectAfterBlockMetadataImport(source, deprecatedImport);
82
91
  nextSource = injectBefore(nextSource, " edit: Edit,", deprecatedLine);
83
92
  return nextSource;
84
93
  });
@@ -94,13 +103,13 @@ async function applyCompoundPatches(projectDir, variables) {
94
103
  const childIndexPath = path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`, "index.tsx");
95
104
  const addChildScriptPath = path.join(projectDir, "scripts", "add-compound-child.ts");
96
105
  await patchFile(parentIndexPath, (source) => {
97
- let nextSource = injectAfter(source, "import metadata from './block.json';", `import { deprecated } from '../../migrations/generated/${variables.slugKebabCase}/deprecated';`);
98
- nextSource = injectBefore(nextSource, "\tedit: Edit,", `\tdeprecated: deprecated as NonNullable<BlockConfiguration< ${variables.pascalCase}Attributes >['deprecated']>,`);
106
+ let nextSource = injectAfterBlockMetadataImport(source, `import { deprecated } from '../../migrations/generated/${variables.slugKebabCase}/deprecated';`);
107
+ nextSource = injectBefore(nextSource, "\tedit: Edit,", "\tdeprecated,");
99
108
  return nextSource;
100
109
  });
101
110
  await patchFile(childIndexPath, (source) => {
102
- let nextSource = injectAfter(source, "import metadata from './block.json';", `import { deprecated } from '../../migrations/generated/${variables.slugKebabCase}-item/deprecated';`);
103
- nextSource = injectBefore(nextSource, "\tedit: Edit,", `\tdeprecated: deprecated as NonNullable<BlockConfiguration< ${variables.pascalCase}ItemAttributes >['deprecated']>,`);
111
+ let nextSource = injectAfterBlockMetadataImport(source, `import { deprecated } from '../../migrations/generated/${variables.slugKebabCase}-item/deprecated';`);
112
+ nextSource = injectBefore(nextSource, "\tedit: Edit,", "\tdeprecated,");
104
113
  return nextSource;
105
114
  });
106
115
  await patchFile(parentEditPath, (source) => {
@@ -110,8 +119,8 @@ async function applyCompoundPatches(projectDir, variables) {
110
119
  });
111
120
  await patchFile(addChildScriptPath, (source) => {
112
121
  let nextSource = injectAfter(source, "const PROJECT_ROOT = process.cwd();", "const MIGRATION_CONFIG_FILE = path.join( PROJECT_ROOT, 'src', 'migrations', 'config.ts' );");
113
- nextSource = nextSource.replace("import metadata from './block.json';\nimport '${ PARENT_STYLE_IMPORT }';", "import metadata from './block.json';\nimport { deprecated } from '../../migrations/generated/${ childFolderSlug }/deprecated';\nimport '${ PARENT_STYLE_IMPORT }';");
114
- nextSource = nextSource.replace("\tedit: Edit,\n\tsave: Save,", "\tdeprecated: deprecated as NonNullable<BlockConfiguration< ${ childTypeName } >['deprecated']>,\n\tedit: Edit,\n\tsave: Save,");
122
+ nextSource = injectAfterBlockMetadataImport(nextSource, "import { deprecated } from '../../migrations/generated/${ childFolderSlug }/deprecated';");
123
+ nextSource = nextSource.replace("\\t\\tedit: Edit,\n\\t\\tsave: Save,", "\\t\\tdeprecated,\n\\t\\tedit: Edit,\n\\t\\tsave: Save,");
115
124
  nextSource = injectAfter(nextSource, "function insertBeforeMarker( filePath: string, marker: string, insertionLines: string[] ) {", "");
116
125
  if (!nextSource.includes("function appendMigrationBlockConfig")) {
117
126
  const appendMigrationHelper = [
@@ -1,8 +1,9 @@
1
- import type { JsonValue, ManifestAttribute, JsonObject } from "./migration-types.js";
2
- export { cloneJsonValue } from "./json-utils.js";
3
- export declare function getValueAtPath(input: Record<string, unknown>, pathLabel: string): unknown;
4
- export declare function setValueAtPath(input: Record<string, unknown>, pathLabel: string, value: unknown): void;
5
- export declare function deleteValueAtPath(input: Record<string, unknown>, pathLabel: string): void;
1
+ import type { JsonValue, ManifestAttribute, JsonObject } from './migration-types.js';
2
+ import { type UnknownRecord } from './object-utils.js';
3
+ export { cloneJsonValue } from './json-utils.js';
4
+ export declare function getValueAtPath<TRecord extends UnknownRecord>(input: TRecord, pathLabel: string): unknown;
5
+ export declare function setValueAtPath<TRecord extends UnknownRecord>(input: TRecord, pathLabel: string, value: unknown): TRecord;
6
+ export declare function deleteValueAtPath<TRecord extends UnknownRecord>(input: TRecord, pathLabel: string): TRecord;
6
7
  export declare function createFixtureScalarValue(pathLabel: string): JsonValue;
7
8
  export declare function createTransformFixtureValue(attribute: ManifestAttribute | null | undefined, pathLabel: string): JsonValue;
8
9
  export declare function readJson<T = unknown>(filePath: string): T;
@@ -11,7 +12,7 @@ export declare function copyFile(sourcePath: string, targetPath: string): void;
11
12
  export declare function sanitizeSaveSnapshotSource(source: string): string;
12
13
  export declare function sanitizeSnapshotBlockJson(blockJson: JsonObject): JsonObject;
13
14
  export declare function runProjectScriptIfPresent(projectDir: string, scriptName: string): void;
14
- export declare function detectPackageManagerId(projectDir: string): "bun" | "npm" | "pnpm" | "yarn";
15
+ export declare function detectPackageManagerId(projectDir: string): 'bun' | 'npm' | 'pnpm' | 'yarn';
15
16
  export declare function getLocalTsxBinary(projectDir: string): string;
16
17
  /**
17
18
  * Returns whether isInteractiveTerminal() is running with both stdin and stdout
@@ -1,96 +1,99 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { execSync } from "node:child_process";
4
- import { formatRunScript } from "./package-managers.js";
5
- export { cloneJsonValue } from "./json-utils.js";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { formatRunScript } from './package-managers.js';
5
+ import { isPlainObject } from './object-utils.js';
6
+ export { cloneJsonValue } from './json-utils.js';
6
7
  const MIGRATION_VERSION_LABEL_PATTERN = /^v([1-9]\d*)$/;
7
8
  const LEGACY_SEMVER_MIGRATION_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
8
9
  export function getValueAtPath(input, pathLabel) {
9
10
  return String(pathLabel)
10
- .split(".")
11
- .reduce((value, segment) => value && typeof value === "object" ? value[segment] : undefined, input);
11
+ .split('.')
12
+ .reduce((value, segment) => (isPlainObject(value) ? value[segment] : undefined), input);
12
13
  }
13
14
  export function setValueAtPath(input, pathLabel, value) {
14
- const segments = String(pathLabel).split(".");
15
+ const segments = String(pathLabel).split('.');
15
16
  let target = input;
16
17
  while (segments.length > 1) {
17
18
  const segment = segments.shift();
18
19
  if (!segment) {
19
20
  continue;
20
21
  }
21
- if (!target[segment] || typeof target[segment] !== "object" || Array.isArray(target[segment])) {
22
+ if (!isPlainObject(target[segment])) {
22
23
  target[segment] = {};
23
24
  }
24
25
  target = target[segment];
25
26
  }
26
27
  target[segments[0]] = value;
28
+ return input;
27
29
  }
28
30
  export function deleteValueAtPath(input, pathLabel) {
29
- const segments = String(pathLabel).split(".");
31
+ const segments = String(pathLabel).split('.');
30
32
  let target = input;
31
33
  while (segments.length > 1) {
32
34
  const segment = segments.shift();
33
- if (!segment || !target[segment] || typeof target[segment] !== "object") {
34
- return;
35
+ if (!segment || !isPlainObject(target[segment])) {
36
+ return input;
35
37
  }
36
38
  target = target[segment];
37
39
  }
38
40
  delete target[segments[0]];
41
+ return input;
39
42
  }
40
43
  export function createFixtureScalarValue(pathLabel) {
41
44
  const normalized = String(pathLabel).toLowerCase();
42
- if (normalized.includes("id")) {
43
- return "00000000-0000-4000-8000-000000000000";
45
+ if (normalized.includes('id')) {
46
+ return '00000000-0000-4000-8000-000000000000';
44
47
  }
45
- if (normalized.includes("count") || normalized.includes("number")) {
48
+ if (normalized.includes('count') || normalized.includes('number')) {
46
49
  return 1;
47
50
  }
48
- if (normalized.includes("visible") || normalized.startsWith("is")) {
51
+ if (normalized.includes('visible') || normalized.startsWith('is')) {
49
52
  return true;
50
53
  }
51
54
  return `legacy:${pathLabel}`;
52
55
  }
53
56
  export function createTransformFixtureValue(attribute, pathLabel) {
54
57
  switch (attribute?.ts?.kind) {
55
- case "number":
56
- return "42";
57
- case "boolean":
58
- return "1";
59
- case "union":
60
- return { kind: "unknown" };
58
+ case 'number':
59
+ return '42';
60
+ case 'boolean':
61
+ return '1';
62
+ case 'union':
63
+ return { kind: 'unknown' };
61
64
  default:
62
65
  return createFixtureScalarValue(pathLabel);
63
66
  }
64
67
  }
65
68
  export function readJson(filePath) {
66
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
69
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
67
70
  }
68
71
  export function renderPhpValue(value, indentLevel) {
69
- const indent = "\t".repeat(indentLevel);
70
- const nestedIndent = "\t".repeat(indentLevel + 1);
72
+ const indent = '\t'.repeat(indentLevel);
73
+ const nestedIndent = '\t'.repeat(indentLevel + 1);
71
74
  if (value === null) {
72
- return "null";
75
+ return 'null';
73
76
  }
74
- if (typeof value === "string") {
75
- return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
77
+ if (typeof value === 'string') {
78
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
76
79
  }
77
- if (typeof value === "number" || typeof value === "boolean") {
80
+ if (typeof value === 'number' || typeof value === 'boolean') {
78
81
  return String(value);
79
82
  }
80
83
  if (Array.isArray(value)) {
81
84
  if (value.length === 0) {
82
- return "[]";
85
+ return '[]';
83
86
  }
84
87
  const items = value.map((item) => `${nestedIndent}${renderPhpValue(item, indentLevel + 1)}`);
85
- return `[\n${items.join(",\n")}\n${indent}]`;
88
+ return `[\n${items.join(',\n')}\n${indent}]`;
86
89
  }
87
- if (typeof value === "object") {
90
+ if (typeof value === 'object') {
88
91
  const entries = Object.entries(value);
89
92
  if (entries.length === 0) {
90
- return "[]";
93
+ return '[]';
91
94
  }
92
- const items = entries.map(([key, item]) => `${nestedIndent}'${String(key).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}' => ${renderPhpValue(item, indentLevel + 1)}`);
93
- return `[\n${items.join(",\n")}\n${indent}]`;
95
+ const items = entries.map(([key, item]) => `${nestedIndent}'${String(key).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' => ${renderPhpValue(item, indentLevel + 1)}`);
96
+ return `[\n${items.join(',\n')}\n${indent}]`;
94
97
  }
95
98
  throw new Error(`Unable to encode PHP migration registry value for ${String(value)}`);
96
99
  }
@@ -100,60 +103,60 @@ export function copyFile(sourcePath, targetPath) {
100
103
  }
101
104
  export function sanitizeSaveSnapshotSource(source) {
102
105
  return source
103
- .replace(/^import\s+\{[^}]+\}\s+from\s+['"]\.\/types['"];?\n?/gm, "")
104
- .replace(/^interface\s+SaveProps\s*\{[\s\S]*?\}\n?/m, "")
105
- .replace(/: SaveProps/g, ": { attributes: any }")
106
- .replace(/attributes:\s*[A-Za-z0-9_<>{}\[\]|&,\s]+;/g, "attributes: any;")
107
- .replace(/\(\{\s*attributes\s*\}:\s*\{\s*attributes:\s*any\s*\}\)/g, "({ attributes }: { attributes: any })")
108
- .replace(/\n{3,}/g, "\n\n")
106
+ .replace(/^import\s+\{[^}]+\}\s+from\s+['"]\.\/types['"];?\n?/gm, '')
107
+ .replace(/^interface\s+SaveProps\s*\{[\s\S]*?\}\n?/m, '')
108
+ .replace(/: SaveProps/g, ': { attributes: any }')
109
+ .replace(/attributes:\s*[A-Za-z0-9_<>{}\[\]|&,\s]+;/g, 'attributes: any;')
110
+ .replace(/\(\{\s*attributes\s*\}:\s*\{\s*attributes:\s*any\s*\}\)/g, '({ attributes }: { attributes: any })')
111
+ .replace(/\n{3,}/g, '\n\n')
109
112
  .trimEnd()
110
- .concat("\n");
113
+ .concat('\n');
111
114
  }
112
115
  export function sanitizeSnapshotBlockJson(blockJson) {
113
116
  const snapshot = { ...blockJson };
114
117
  for (const key of [
115
- "editorScript",
116
- "script",
117
- "scriptModule",
118
- "viewScript",
119
- "viewScriptModule",
120
- "style",
121
- "editorStyle",
122
- "render",
118
+ 'editorScript',
119
+ 'script',
120
+ 'scriptModule',
121
+ 'viewScript',
122
+ 'viewScriptModule',
123
+ 'style',
124
+ 'editorStyle',
125
+ 'render',
123
126
  ]) {
124
127
  delete snapshot[key];
125
128
  }
126
129
  return snapshot;
127
130
  }
128
131
  export function runProjectScriptIfPresent(projectDir, scriptName) {
129
- const packageJson = readJson(path.join(projectDir, "package.json"));
132
+ const packageJson = readJson(path.join(projectDir, 'package.json'));
130
133
  if (!packageJson.scripts?.[scriptName]) {
131
134
  return;
132
135
  }
133
136
  const packageManagerId = detectPackageManagerId(projectDir);
134
137
  execSync(formatRunScript(packageManagerId, scriptName), {
135
138
  cwd: projectDir,
136
- stdio: "inherit",
139
+ stdio: 'inherit',
137
140
  });
138
141
  }
139
142
  export function detectPackageManagerId(projectDir) {
140
- const packageJson = readJson(path.join(projectDir, "package.json"));
141
- const field = String(packageJson.packageManager ?? "");
142
- if (field.startsWith("bun@"))
143
- return "bun";
144
- if (field.startsWith("npm@"))
145
- return "npm";
146
- if (field.startsWith("pnpm@"))
147
- return "pnpm";
148
- if (field.startsWith("yarn@"))
149
- return "yarn";
150
- return "bun";
143
+ const packageJson = readJson(path.join(projectDir, 'package.json'));
144
+ const field = String(packageJson.packageManager ?? '');
145
+ if (field.startsWith('bun@'))
146
+ return 'bun';
147
+ if (field.startsWith('npm@'))
148
+ return 'npm';
149
+ if (field.startsWith('pnpm@'))
150
+ return 'pnpm';
151
+ if (field.startsWith('yarn@'))
152
+ return 'yarn';
153
+ return 'bun';
151
154
  }
152
155
  export function getLocalTsxBinary(projectDir) {
153
- const filename = process.platform === "win32" ? "tsx.cmd" : "tsx";
154
- const binaryPath = path.join(projectDir, "node_modules", ".bin", filename);
156
+ const filename = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
157
+ const binaryPath = path.join(projectDir, 'node_modules', '.bin', filename);
155
158
  if (!fs.existsSync(binaryPath)) {
156
- throw new Error("Local tsx binary was not found. Install project dependencies before running migration verification.");
159
+ throw new Error('Local tsx binary was not found. Install project dependencies before running migration verification.');
157
160
  }
158
161
  return binaryPath;
159
162
  }
@@ -172,7 +175,7 @@ export function isInteractiveTerminal() {
172
175
  * @returns The concrete migration version label that should be used.
173
176
  */
174
177
  export function resolveTargetMigrationVersion(currentMigrationVersion, value) {
175
- return value === "current" ? currentMigrationVersion : value;
178
+ return value === 'current' ? currentMigrationVersion : value;
176
179
  }
177
180
  /**
178
181
  * Returns whether a value matches the canonical `vN` migration label format.
@@ -230,17 +233,17 @@ export function compareMigrationVersionLabels(left, right) {
230
233
  */
231
234
  export function formatLegacyMigrationWorkspaceResetGuidance(reason) {
232
235
  return [
233
- reason ? `${reason} ` : "",
234
- "Back up `src/migrations/` if needed, remove or reset the existing migration workspace, ",
235
- "then rerun `wp-typia migrate init --current-migration-version v1`.",
236
- ].join("");
236
+ reason ? `${reason} ` : '',
237
+ 'Back up `src/migrations/` if needed, remove or reset the existing migration workspace, ',
238
+ 'then rerun `wp-typia migrate init --current-migration-version v1`.',
239
+ ].join('');
237
240
  }
238
241
  export function escapeForCode(value) {
239
- return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
242
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
240
243
  }
241
244
  export function renderObjectKey(key) {
242
245
  return JSON.stringify(String(key));
243
246
  }
244
247
  export function isNumber(value) {
245
- return typeof value === "number" && Number.isFinite(value);
248
+ return typeof value === 'number' && Number.isFinite(value);
246
249
  }
@@ -667,7 +667,7 @@ export function doctorProjectMigrations(projectDir, { all = false, fromMigration
667
667
  const entries = blockGeneratedEntries.map(({ entry }) => entry);
668
668
  const generatedDir = getGeneratedDirForBlock(state.paths, block);
669
669
  expectedGeneratedFiles.set(path.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries));
670
- expectedGeneratedFiles.set(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(entries));
670
+ expectedGeneratedFiles.set(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(state, block.key, entries));
671
671
  expectedGeneratedFiles.set(path.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries));
672
672
  expectedGeneratedFiles.set(path.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries));
673
673
  }
@@ -942,7 +942,7 @@ function regenerateGeneratedArtifacts(projectDir) {
942
942
  const generatedDir = getGeneratedDirForBlock(state.paths, block);
943
943
  fs.mkdirSync(generatedDir, { recursive: true });
944
944
  fs.writeFileSync(path.join(generatedDir, "registry.ts"), renderMigrationRegistryFile(state, block.key, blockGeneratedEntries), "utf8");
945
- fs.writeFileSync(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(entries), "utf8");
945
+ fs.writeFileSync(path.join(generatedDir, "deprecated.ts"), renderGeneratedDeprecatedFile(state, block.key, entries), "utf8");
946
946
  fs.writeFileSync(path.join(generatedDir, "verify.ts"), renderVerifyFile(state, block.key, entries), "utf8");
947
947
  fs.writeFileSync(path.join(generatedDir, "fuzz.ts"), renderFuzzFile(state, block.key, blockGeneratedEntries), "utf8");
948
948
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Generic record type used for JSON-like plain-object inspection.
3
3
  */
4
- export type UnknownRecord = Record<string, unknown>;
4
+ export type UnknownRecord<TValue = unknown> = Record<string, TValue>;
5
5
  /**
6
6
  * Delegate plain-object detection to the shared runtime primitive owner.
7
7
  *
@@ -10,3 +10,10 @@ export type UnknownRecord = Record<string, unknown>;
10
10
  * `Object.prototype` or `null` prototype.
11
11
  */
12
12
  export declare function isPlainObject(value: unknown): value is UnknownRecord;
13
+ /**
14
+ * Serialize JSON-like values with deterministic plain-object key ordering.
15
+ *
16
+ * Arrays preserve their declared order, while plain objects are normalized by
17
+ * sorting keys recursively before calling `JSON.stringify`.
18
+ */
19
+ export declare function stableJsonStringify(value: unknown): string;
@@ -1,4 +1,4 @@
1
- import { isPlainObject as isSharedPlainObject } from "@wp-typia/api-client/runtime-primitives";
1
+ import { isPlainObject as isSharedPlainObject } from '@wp-typia/api-client/runtime-primitives';
2
2
  /**
3
3
  * Delegate plain-object detection to the shared runtime primitive owner.
4
4
  *
@@ -9,3 +9,23 @@ import { isPlainObject as isSharedPlainObject } from "@wp-typia/api-client/runti
9
9
  export function isPlainObject(value) {
10
10
  return isSharedPlainObject(value);
11
11
  }
12
+ function toStableJsonValue(value) {
13
+ if (Array.isArray(value)) {
14
+ return value.map((entry) => toStableJsonValue(entry));
15
+ }
16
+ if (isPlainObject(value)) {
17
+ return Object.fromEntries(Object.keys(value)
18
+ .sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
19
+ .map((key) => [key, toStableJsonValue(value[key])]));
20
+ }
21
+ return value;
22
+ }
23
+ /**
24
+ * Serialize JSON-like values with deterministic plain-object key ordering.
25
+ *
26
+ * Arrays preserve their declared order, while plain objects are normalized by
27
+ * sorting keys recursively before calling `JSON.stringify`.
28
+ */
29
+ export function stableJsonStringify(value) {
30
+ return JSON.stringify(toStableJsonValue(value));
31
+ }
@@ -24,11 +24,22 @@ export declare function seedBuiltInPersistenceArtifacts(targetDir: string, templ
24
24
  export declare function normalizePackageManagerFiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
25
25
  export declare function normalizePackageJson(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
26
26
  export declare function removeUnexpectedLockfiles(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
27
- export declare function replaceTextRecursively(targetDir: string, packageManagerId: PackageManagerId): Promise<void>;
27
+ /**
28
+ * Recursively normalizes generated text files for the selected package manager
29
+ * and repository reference.
30
+ */
31
+ export declare function replaceTextRecursively(targetDir: string, packageManagerId: PackageManagerId, { repositoryManifestPaths, repositoryReference, }?: {
32
+ repositoryManifestPaths?: readonly string[];
33
+ repositoryReference?: string;
34
+ }): Promise<void>;
28
35
  export declare function defaultInstallDependencies({ projectDir, packageManager, }: InstallDependenciesOptions): Promise<void>;
29
36
  export declare function isOfficialWorkspaceProject(projectDir: string): boolean;
30
37
  export declare function applyWorkspaceMigrationCapability(projectDir: string, packageManager: PackageManagerId): Promise<void>;
31
- export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir, packageManager, withMigrationUi, withTestPreset, withWpEnv, noInstall, installDependencies, }: {
38
+ /**
39
+ * Applies a built-in scaffold into the target directory, including generated
40
+ * code artifacts, starter manifests, preset files, and placeholder rewrites.
41
+ */
42
+ export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir, packageManager, withMigrationUi, withTestPreset, withWpEnv, noInstall, installDependencies, repositoryReference, }: {
32
43
  projectDir: string;
33
44
  templateDir: string;
34
45
  templateId: BuiltInTemplateId;
@@ -44,4 +55,5 @@ export declare function applyBuiltInScaffoldProjectFiles({ projectDir, templateD
44
55
  withWpEnv?: boolean;
45
56
  noInstall?: boolean;
46
57
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
58
+ repositoryReference?: string;
47
59
  }): Promise<void>;