@wp-typia/project-tools 0.15.4 → 0.16.1

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 (35) hide show
  1. package/dist/runtime/cli-help.js +1 -1
  2. package/dist/runtime/cli-scaffold.d.ts +2 -1
  3. package/dist/runtime/cli-scaffold.js +23 -2
  4. package/dist/runtime/scaffold-onboarding.d.ts +2 -1
  5. package/dist/runtime/scaffold-onboarding.js +37 -8
  6. package/dist/runtime/scaffold.js +26 -15
  7. package/package.json +2 -2
  8. package/templates/_shared/base/package.json.mustache +4 -3
  9. package/templates/_shared/base/scripts/sync-project.ts.mustache +103 -0
  10. package/templates/_shared/compound/core/package.json.mustache +4 -3
  11. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +42 -28
  12. package/templates/_shared/compound/core/scripts/sync-project.ts.mustache +103 -0
  13. package/templates/_shared/compound/persistence/package.json.mustache +4 -3
  14. package/templates/_shared/compound/persistence/scripts/sync-project.ts.mustache +103 -0
  15. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +28 -0
  16. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +8 -0
  17. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +5 -5
  18. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +5 -5
  19. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +5 -5
  20. package/templates/_shared/persistence/core/package.json.mustache +4 -3
  21. package/templates/_shared/persistence/core/scripts/sync-project.ts.mustache +103 -0
  22. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +28 -0
  23. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +5 -0
  24. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +5 -5
  25. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +5 -5
  26. package/templates/_shared/workspace/persistence-auth/server.php.mustache +7 -5
  27. package/templates/_shared/workspace/persistence-public/server.php.mustache +7 -5
  28. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +5 -0
  29. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +10 -3
  30. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +5 -0
  31. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +10 -3
  32. package/templates/interactivity/package.json.mustache +4 -3
  33. package/templates/interactivity/src/edit.tsx.mustache +1 -1
  34. package/templates/interactivity/src/save.tsx.mustache +1 -1
  35. package/templates/persistence/src/edit.tsx.mustache +2 -2
@@ -34,7 +34,7 @@ Package managers: ${PACKAGE_MANAGER_IDS.join(", ")}
34
34
  Notes:
35
35
  \`wp-typia create\` is the canonical scaffold command.
36
36
  \`wp-typia <project-dir>\` remains a backward-compatible alias to \`create\`.
37
- Use \`--template @wp-typia/create-workspace-template\` for the official empty workspace scaffold behind \`wp-typia add ...\`.
37
+ Use \`--template workspace\` as shorthand for \`@wp-typia/create-workspace-template\`, the official empty workspace scaffold behind \`wp-typia add ...\`.
38
38
  \`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
39
39
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
40
40
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
@@ -10,6 +10,7 @@ interface GetNextStepsOptions {
10
10
  templateId: string;
11
11
  }
12
12
  interface GetOptionalOnboardingOptions {
13
+ availableScripts?: string[];
13
14
  packageManager: PackageManagerId;
14
15
  templateId: string;
15
16
  compoundPersistenceEnabled?: boolean;
@@ -60,7 +61,7 @@ export declare function getNextSteps({ projectInput, projectDir, packageManager,
60
61
  * @param options Package-manager and template context for optional guidance.
61
62
  * @returns Optional onboarding note and step list.
62
63
  */
63
- export declare function getOptionalOnboarding({ packageManager, templateId, compoundPersistenceEnabled, }: GetOptionalOnboardingOptions): OptionalOnboardingGuidance;
64
+ export declare function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled, }: GetOptionalOnboardingOptions): OptionalOnboardingGuidance;
64
65
  /**
65
66
  * Resolve scaffold options, prompts, and follow-up steps for one CLI run.
66
67
  *
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId, scaffoldProject, } from "./scaffold.js";
3
4
  import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
@@ -79,10 +80,14 @@ export function getNextSteps({ projectInput, projectDir, packageManager, noInsta
79
80
  * @param options Package-manager and template context for optional guidance.
80
81
  * @returns Optional onboarding note and step list.
81
82
  */
82
- export function getOptionalOnboarding({ packageManager, templateId, compoundPersistenceEnabled = false, }) {
83
+ export function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled = false, }) {
83
84
  return {
84
- note: getOptionalOnboardingNote(packageManager, templateId),
85
+ note: getOptionalOnboardingNote(packageManager, templateId, {
86
+ availableScripts,
87
+ compoundPersistenceEnabled,
88
+ }),
85
89
  steps: getOptionalOnboardingSteps(packageManager, templateId, {
90
+ availableScripts,
86
91
  compoundPersistenceEnabled,
87
92
  }),
88
93
  };
@@ -185,8 +190,24 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
185
190
  withTestPreset: resolvedWithTestPreset,
186
191
  withWpEnv: resolvedWithWpEnv,
187
192
  });
193
+ let availableScripts;
194
+ try {
195
+ const parsedPackageJson = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
196
+ const scripts = parsedPackageJson.scripts &&
197
+ typeof parsedPackageJson.scripts === "object" &&
198
+ !Array.isArray(parsedPackageJson.scripts)
199
+ ? parsedPackageJson.scripts
200
+ : {};
201
+ availableScripts = Object.entries(scripts)
202
+ .filter(([, value]) => typeof value === "string")
203
+ .map(([scriptName]) => scriptName);
204
+ }
205
+ catch {
206
+ availableScripts = undefined;
207
+ }
188
208
  return {
189
209
  optionalOnboarding: getOptionalOnboarding({
210
+ availableScripts,
190
211
  packageManager: resolvedPackageManager,
191
212
  templateId: resolvedTemplateId,
192
213
  compoundPersistenceEnabled: result.variables.compoundPersistenceEnabled === "true",
@@ -1,5 +1,6 @@
1
1
  import type { PackageManagerId } from "./package-managers.js";
2
2
  interface SyncOnboardingOptions {
3
+ availableScripts?: string[];
3
4
  compoundPersistenceEnabled?: boolean;
4
5
  }
5
6
  interface PhpRestExtensionOptions extends SyncOnboardingOptions {
@@ -16,7 +17,7 @@ export declare function getOptionalOnboardingSteps(packageManager: PackageManage
16
17
  /**
17
18
  * Returns the onboarding note explaining when manual sync is optional.
18
19
  */
19
- export declare function getOptionalOnboardingNote(packageManager: PackageManagerId, templateId?: string): string;
20
+ export declare function getOptionalOnboardingNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
20
21
  /**
21
22
  * Returns source-of-truth guidance for generated artifacts by template mode.
22
23
  */
@@ -1,5 +1,6 @@
1
1
  import { formatRunScript } from "./package-managers.js";
2
2
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
3
+ import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
3
4
  function templateHasPersistenceSync(templateId, { compoundPersistenceEnabled = false } = {}) {
4
5
  return templateId === "persistence" || (templateId === "compound" && compoundPersistenceEnabled);
5
6
  }
@@ -7,9 +8,19 @@ function templateHasPersistenceSync(templateId, { compoundPersistenceEnabled = f
7
8
  * Returns the optional sync script names to suggest for a template.
8
9
  */
9
10
  export function getOptionalSyncScriptNames(templateId, options = {}) {
10
- return templateHasPersistenceSync(templateId, options)
11
- ? ["sync-types", "sync-rest"]
12
- : ["sync-types"];
11
+ const availableScripts = new Set(options.availableScripts ?? []);
12
+ if (availableScripts.has("sync")) {
13
+ return ["sync"];
14
+ }
15
+ const fallbackScripts = ["sync-types", "sync-rest"].filter((scriptName) => availableScripts.has(scriptName));
16
+ if (fallbackScripts.length > 0) {
17
+ return fallbackScripts;
18
+ }
19
+ if (!isBuiltInTemplateId(templateId) &&
20
+ templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
21
+ return [];
22
+ }
23
+ return ["sync"];
13
24
  }
14
25
  /**
15
26
  * Formats optional onboarding sync commands for the selected package manager.
@@ -20,15 +31,33 @@ export function getOptionalOnboardingSteps(packageManager, templateId, options =
20
31
  /**
21
32
  * Returns the onboarding note explaining when manual sync is optional.
22
33
  */
23
- export function getOptionalOnboardingNote(packageManager, templateId = "basic") {
34
+ export function getOptionalOnboardingNote(packageManager, templateId = "basic", options = {}) {
35
+ const optionalSyncScripts = getOptionalSyncScriptNames(templateId, options);
36
+ const hasUnifiedSync = optionalSyncScripts.includes("sync");
37
+ const syncSteps = optionalSyncScripts.map((scriptName) => formatRunScript(packageManager, scriptName));
24
38
  const developmentScript = getPrimaryDevelopmentScript(templateId);
39
+ const syncCommand = formatRunScript(packageManager, hasUnifiedSync ? "sync" : "sync-types");
40
+ const syncCheckCommand = formatRunScript(packageManager, hasUnifiedSync ? "sync" : "sync-types", "--check");
25
41
  const failOnLossySyncCommand = formatRunScript(packageManager, "sync-types", "--fail-on-lossy");
26
42
  const syncTypesCommand = formatRunScript(packageManager, "sync-types");
43
+ const syncRestCommand = formatRunScript(packageManager, "sync-rest");
27
44
  const typecheckCommand = formatRunScript(packageManager, "typecheck");
28
45
  const strictSyncCommand = formatRunScript(packageManager, "sync-types", "--strict --report json");
46
+ const advancedPersistenceNote = templateHasPersistenceSync(templateId, options)
47
+ ? ` ${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
+ : "";
49
+ const fallbackCustomTemplateNote = !hasUnifiedSync &&
50
+ syncSteps.length > 0 &&
51
+ !isBuiltInTemplateId(templateId) &&
52
+ templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
53
+ ? `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
+ : null;
55
+ if (fallbackCustomTemplateNote) {
56
+ return fallbackCustomTemplateNote;
57
+ }
29
58
  return `${formatRunScript(packageManager, developmentScript)} ${developmentScript === "dev"
30
59
  ? "watches the relevant sync scripts during local development."
31
- : "remains the primary local entry point."} ${formatRunScript(packageManager, "start")} still runs one-shot syncs before starting, while ${formatRunScript(packageManager, "build")} and ${typecheckCommand} verify that generated metadata/schema artifacts are already current and fail if they are stale. Run the sync scripts manually when you want to refresh generated artifacts before build, typecheck, or commit. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` to fail only on lossy WordPress projections, or \`${strictSyncCommand}\` for a CI-friendly JSON report that fails on all warnings. They do not create migration history.`;
60
+ : "remains the primary local entry point."} ${formatRunScript(packageManager, "start")} still runs one-shot syncs before starting, while ${formatRunScript(packageManager, "build")} and ${typecheckCommand} verify that generated metadata/schema artifacts are already current through ${syncCheckCommand}. Run ${syncCommand} manually when you want to refresh generated artifacts before build, typecheck, or commit. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` to fail only on lossy WordPress projections, or \`${strictSyncCommand}\` for a CI-friendly JSON report that fails on all warnings.${advancedPersistenceNote} They do not create migration history.`;
32
61
  }
33
62
  /**
34
63
  * Returns source-of-truth guidance for generated artifacts by template mode.
@@ -37,12 +66,12 @@ export function getTemplateSourceOfTruthNote(templateId, { compoundPersistenceEn
37
66
  if (templateId === "compound") {
38
67
  const compoundBase = "`src/blocks/*/types.ts` files remain the source of truth for each block's `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include starter `typia.manifest.json` files so editor imports resolve before the first sync.";
39
68
  if (compoundPersistenceEnabled) {
40
- return `${compoundBase} For persistence-enabled parents, \`src/blocks/*/api-types.ts\` files remain the source of truth for \`src/blocks/*/api-schemas/*\` when you run \`sync-rest\`, while \`src/blocks/*/transport.ts\` is the first-class transport seam for editor and frontend requests.`;
69
+ return `${compoundBase} For persistence-enabled parents, \`src/blocks/*/api-types.ts\` files remain the source of truth for \`src/blocks/*/api-schemas/*\` when you run \`sync\` or \`sync-rest\`, while \`src/blocks/*/transport.ts\` is the first-class transport seam for editor and frontend requests.`;
41
70
  }
42
71
  return compoundBase;
43
72
  }
44
73
  if (templateId === "persistence") {
45
- return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync-rest`, while `src/transport.ts` is the first-class transport seam for editor and frontend requests. This scaffold is intentionally server-rendered: `src/render.php` is the canonical frontend entry, `src/save.tsx` returns `null`, and session-only write data now refreshes through the dedicated `/bootstrap` endpoint after hydration instead of being frozen into markup.";
74
+ return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync` or `sync-rest`, while `src/transport.ts` is the first-class transport seam for editor and frontend requests. This scaffold is intentionally server-rendered: `src/render.php` is the canonical frontend entry, `src/save.tsx` returns `null`, and session-only write data now refreshes through the dedicated `/bootstrap` endpoint after hydration instead of being frozen into markup.";
46
75
  }
47
76
  return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. The basic scaffold stays static by design: `src/render.php` is only an opt-in server placeholder, `src/save.tsx` remains the canonical frontend output, and the generated webpack config keeps the current `@wordpress/scripts` CommonJS baseline unless you intentionally add `render` to `block.json`.";
48
77
  }
@@ -69,7 +98,7 @@ function formatPhpRestExtensionPointsSection({ apiTypesPath, extraNote, mainPhpP
69
98
  `- Edit \`${mainPhpPath}\` when you need to ${mainPhpScope}.`,
70
99
  "- Edit `inc/rest-auth.php` or `inc/rest-public.php` when you need to customize write permissions or token/request-id/nonce checks for the selected policy.",
71
100
  `- Edit \`${transportPath}\` when you need to switch between direct WordPress REST and a contract-compatible proxy or BFF without changing the endpoint contracts.`,
72
- `- Keep \`${apiTypesPath}\` as the source of truth for request and response contracts, then regenerate \`${schemaJsonGlob}\`, per-contract \`${perContractOpenApiGlob}\`, and \`${aggregateOpenApiPath}\` with \`sync-rest\`.`,
101
+ `- Keep \`${apiTypesPath}\` as the source of truth for request and response contracts, then regenerate \`${schemaJsonGlob}\`, per-contract \`${perContractOpenApiGlob}\`, and \`${aggregateOpenApiPath}\` with \`sync\` (or \`sync-rest\` after \`sync-types\` when you only need the REST layer).`,
73
102
  "- Avoid hand-editing generated schema and OpenAPI artifacts unless you are debugging generated output; they are meant to be regenerated from TypeScript contracts.",
74
103
  ];
75
104
  if (typeof extraNote === "string" && extraNote.length > 0) {
@@ -19,6 +19,7 @@ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
19
19
  const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
20
20
  const PHP_PREFIX_MAX_LENGTH = 50;
21
21
  const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
22
+ const WORKSPACE_TEMPLATE_ALIAS = "workspace";
22
23
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
23
24
  const LOCKFILES = {
24
25
  bun: ["bun.lock", "bun.lockb"],
@@ -138,15 +139,21 @@ export function getDefaultAnswers(projectName, templateId) {
138
139
  title: toTitleCase(slugDefault),
139
140
  };
140
141
  }
142
+ function normalizeTemplateSelection(templateId) {
143
+ return templateId === WORKSPACE_TEMPLATE_ALIAS
144
+ ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
145
+ : templateId;
146
+ }
141
147
  export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
142
148
  if (templateId) {
149
+ const normalizedTemplateId = normalizeTemplateSelection(templateId);
143
150
  if (isRemovedBuiltInTemplateId(templateId)) {
144
151
  throw new Error(getRemovedBuiltInTemplateMessage(templateId));
145
152
  }
146
- if (isBuiltInTemplateId(templateId)) {
147
- return getTemplateById(templateId).id;
153
+ if (isBuiltInTemplateId(normalizedTemplateId)) {
154
+ return getTemplateById(normalizedTemplateId).id;
148
155
  }
149
- return templateId;
156
+ return normalizedTemplateId;
150
157
  }
151
158
  if (yes) {
152
159
  return "basic";
@@ -154,7 +161,7 @@ export async function resolveTemplateId({ templateId, yes = false, isInteractive
154
161
  if (!isInteractive || !selectTemplate) {
155
162
  throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join("|")}|./path|github:owner/repo/path[#ref]|npm-package>.`);
156
163
  }
157
- return selectTemplate();
164
+ return normalizeTemplateSelection(await selectTemplate());
158
165
  }
159
166
  export async function resolvePackageManagerId({ packageManager, yes = false, isInteractive = false, selectPackageManager, }) {
160
167
  if (packageManager) {
@@ -317,12 +324,13 @@ function buildReadme(templateId, variables, packageManager, { withMigrationUi =
317
324
  const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
318
325
  compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
319
326
  });
327
+ const compoundPersistenceEnabled = variables.compoundPersistenceEnabled === "true";
320
328
  const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === "true"
321
329
  ? "Public persistence writes use signed short-lived tokens, per-request ids, and coarse rate limiting by default. Add application-specific abuse controls before using the same pattern for high-value metrics or experiments."
322
330
  : null;
323
331
  const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
324
332
  const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
325
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
333
+ compoundPersistenceEnabled,
326
334
  slug: variables.slug,
327
335
  });
328
336
  const developmentScript = getPrimaryDevelopmentScript(templateId);
@@ -362,7 +370,9 @@ ${formatRunScript(packageManager, "build")}
362
370
  ${optionalOnboardingSteps.join("\n")}
363
371
  \`\`\`
364
372
 
365
- ${getOptionalOnboardingNote(packageManager, templateId)}
373
+ ${getOptionalOnboardingNote(packageManager, templateId, {
374
+ compoundPersistenceEnabled,
375
+ })}
366
376
 
367
377
  ${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}` : ""}
368
378
  `;
@@ -625,14 +635,15 @@ async function applyWorkspaceMigrationCapability(projectDir, packageManager) {
625
635
  writeInitialMigrationScaffold(projectDir, "v1", []);
626
636
  }
627
637
  export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
638
+ const resolvedTemplateId = normalizeTemplateSelection(templateId);
628
639
  const resolvedPackageManager = getPackageManager(packageManager).id;
629
- const isBuiltInTemplate = isBuiltInTemplateId(templateId);
630
- const variables = getTemplateVariables(templateId, {
640
+ const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
641
+ const variables = getTemplateVariables(resolvedTemplateId, {
631
642
  ...answers,
632
643
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
633
644
  persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
634
645
  });
635
- const templateSource = await resolveTemplateSource(templateId, cwd, variables, variant);
646
+ const templateSource = await resolveTemplateSource(resolvedTemplateId, cwd, variables, variant);
636
647
  const supportsMigrationUi = isBuiltInTemplate || templateSource.isOfficialWorkspaceTemplate === true;
637
648
  if (withMigrationUi && !supportsMigrationUi) {
638
649
  await templateSource.cleanup?.();
@@ -649,8 +660,8 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
649
660
  }
650
661
  const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
651
662
  if (isBuiltInTemplate) {
652
- await writeStarterManifestFiles(projectDir, templateId, variables);
653
- await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
663
+ await writeStarterManifestFiles(projectDir, resolvedTemplateId, variables);
664
+ await seedBuiltInPersistenceArtifacts(projectDir, resolvedTemplateId, variables);
654
665
  await applyLocalDevPresetFiles({
655
666
  projectDir,
656
667
  variables,
@@ -661,7 +672,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
661
672
  await applyMigrationUiCapability({
662
673
  packageManager: resolvedPackageManager,
663
674
  projectDir,
664
- templateId,
675
+ templateId: resolvedTemplateId,
665
676
  variables,
666
677
  });
667
678
  }
@@ -671,7 +682,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
671
682
  }
672
683
  const readmePath = path.join(projectDir, "README.md");
673
684
  if (!fs.existsSync(readmePath)) {
674
- await fsp.writeFile(readmePath, buildReadme(templateId, variables, resolvedPackageManager, {
685
+ await fsp.writeFile(readmePath, buildReadme(resolvedTemplateId, variables, resolvedPackageManager, {
675
686
  withMigrationUi: isBuiltInTemplate || isOfficialWorkspace ? withMigrationUi : false,
676
687
  withTestPreset: isBuiltInTemplate ? withTestPreset : false,
677
688
  withWpEnv: isBuiltInTemplate ? withWpEnv : false,
@@ -688,7 +699,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
688
699
  compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
689
700
  packageManager: resolvedPackageManager,
690
701
  projectDir,
691
- templateId,
702
+ templateId: resolvedTemplateId,
692
703
  withTestPreset,
693
704
  withWpEnv,
694
705
  });
@@ -706,7 +717,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
706
717
  return {
707
718
  projectDir,
708
719
  selectedVariant: templateSource.selectedVariant ?? null,
709
- templateId,
720
+ templateId: resolvedTemplateId,
710
721
  packageManager: resolvedPackageManager,
711
722
  variables,
712
723
  warnings: templateSource.warnings ?? [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.15.4",
3
+ "version": "0.16.1",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@wp-typia/api-client": "^0.4.2",
65
- "@wp-typia/block-runtime": "^0.4.3",
65
+ "@wp-typia/block-runtime": "^0.4.4",
66
66
  "@wp-typia/rest": "^0.3.5",
67
67
  "@wp-typia/block-types": "^0.2.1",
68
68
  "mustache": "^4.2.0",
@@ -7,15 +7,16 @@
7
7
  "license": "GPL-2.0-or-later",
8
8
  "main": "build/index.js",
9
9
  "scripts": {
10
+ "sync": "tsx scripts/sync-project.ts",
10
11
  "sync-types": "tsx scripts/sync-types-to-block-json.ts",
11
- "build": "bun run sync-types --check && wp-scripts build --experimental-modules",
12
- "start": "bun run sync-types && wp-scripts start --experimental-modules",
12
+ "build": "bun run sync --check && wp-scripts build --experimental-modules",
13
+ "start": "bun run sync && wp-scripts start --experimental-modules",
13
14
  "dev": "bun run start",
14
15
  "lint:js": "wp-scripts lint-js",
15
16
  "lint:css": "wp-scripts lint-style",
16
17
  "lint": "bun run lint:js && bun run lint:css",
17
18
  "format": "wp-scripts format",
18
- "typecheck": "bun run sync-types --check && tsc --noEmit"
19
+ "typecheck": "bun run sync --check && tsc --noEmit"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@wp-typia/block-runtime": "{{blockRuntimePackageVersion}}",
@@ -0,0 +1,103 @@
1
+ /* eslint-disable no-console */
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ interface SyncCliOptions {
7
+ check: boolean;
8
+ }
9
+
10
+ function parseCliOptions( argv: string[] ): SyncCliOptions {
11
+ const options: SyncCliOptions = {
12
+ check: false,
13
+ };
14
+
15
+ for ( const argument of argv ) {
16
+ if ( argument === '--check' ) {
17
+ options.check = true;
18
+ continue;
19
+ }
20
+
21
+ throw new Error( `Unknown sync flag: ${ argument }` );
22
+ }
23
+
24
+ return options;
25
+ }
26
+
27
+ function getSyncScriptEnv() {
28
+ const binaryDirectory = path.join( process.cwd(), 'node_modules', '.bin' );
29
+ const inheritedPath =
30
+ process.env.PATH ??
31
+ process.env.Path ??
32
+ Object.entries( process.env ).find(
33
+ ( [ key ] ) => key.toLowerCase() === 'path'
34
+ )?.[ 1 ] ??
35
+ '';
36
+ const nextPath = fs.existsSync( binaryDirectory )
37
+ ? `${ binaryDirectory }${ path.delimiter }${ inheritedPath }`
38
+ : inheritedPath;
39
+ const env: NodeJS.ProcessEnv = {
40
+ ...process.env,
41
+ };
42
+
43
+ for ( const key of Object.keys( env ) ) {
44
+ if ( key.toLowerCase() === 'path' ) {
45
+ delete env[ key ];
46
+ }
47
+ }
48
+
49
+ env.PATH = nextPath;
50
+
51
+ return env;
52
+ }
53
+
54
+ function runSyncScript( scriptPath: string, options: SyncCliOptions ) {
55
+ const args = [ scriptPath ];
56
+ if ( options.check ) {
57
+ args.push( '--check' );
58
+ }
59
+
60
+ const result = spawnSync( 'tsx', args, {
61
+ cwd: process.cwd(),
62
+ env: getSyncScriptEnv(),
63
+ shell: process.platform === 'win32',
64
+ stdio: 'inherit',
65
+ } );
66
+
67
+ if ( result.error ) {
68
+ if ( ( result.error as NodeJS.ErrnoException ).code === 'ENOENT' ) {
69
+ throw new Error(
70
+ 'Unable to resolve `tsx` for project sync. Install project dependencies or rerun the command through your package manager.'
71
+ );
72
+ }
73
+
74
+ throw result.error;
75
+ }
76
+
77
+ if ( result.status !== 0 ) {
78
+ throw new Error( `Sync script failed: ${ scriptPath }` );
79
+ }
80
+ }
81
+
82
+ async function main() {
83
+ const options = parseCliOptions( process.argv.slice( 2 ) );
84
+ const syncTypesScriptPath = path.join( 'scripts', 'sync-types-to-block-json.ts' );
85
+ const syncRestScriptPath = path.join( 'scripts', 'sync-rest-contracts.ts' );
86
+
87
+ runSyncScript( syncTypesScriptPath, options );
88
+
89
+ if ( fs.existsSync( path.resolve( process.cwd(), syncRestScriptPath ) ) ) {
90
+ runSyncScript( syncRestScriptPath, options );
91
+ }
92
+
93
+ console.log(
94
+ options.check
95
+ ? '✅ Generated project metadata and REST artifacts are already synchronized.'
96
+ : '✅ Generated project metadata and REST artifacts were synchronized.'
97
+ );
98
+ }
99
+
100
+ main().catch( ( error ) => {
101
+ console.error( '❌ Project sync failed:', error );
102
+ process.exit( 1 );
103
+ } );
@@ -8,11 +8,12 @@
8
8
  "main": "build/index.js",
9
9
  "scripts": {
10
10
  "add-child": "tsx scripts/add-compound-child.ts",
11
+ "sync": "tsx scripts/sync-project.ts",
11
12
  "sync-types": "tsx scripts/sync-types-to-block-json.ts",
12
- "build": "bun run sync-types --check && wp-scripts build --experimental-modules",
13
- "start": "bun run sync-types && wp-scripts start --experimental-modules",
13
+ "build": "bun run sync --check && wp-scripts build --experimental-modules",
14
+ "start": "bun run sync && wp-scripts start --experimental-modules",
14
15
  "dev": "bun run start",
15
- "typecheck": "bun run sync-types --check && tsc --noEmit",
16
+ "typecheck": "bun run sync --check && tsc --noEmit",
16
17
  "lint:js": "wp-scripts lint-js",
17
18
  "lint:css": "wp-scripts lint-style",
18
19
  "lint": "bun run lint:js && bun run lint:css",
@@ -346,8 +346,15 @@ function renderBlockJson(
346
346
  ) }\n`;
347
347
  }
348
348
 
349
- function renderTypesFile( childTypeName: string, childTitle: string ): string {
350
- return `import { tags } from 'typia';
349
+ function renderTypesFile(
350
+ childTypeName: string,
351
+ childInterfaceName: string,
352
+ childTitle: string
353
+ ): string {
354
+ return `import type { ValidationResult } from '@wp-typia/block-runtime/validation';
355
+ import { tags } from 'typia';
356
+
357
+ export type { ValidationResult } from '@wp-typia/block-runtime/validation';
351
358
 
352
359
  export interface ${ childTypeName } {
353
360
  \ttitle: string &
@@ -359,6 +366,8 @@ export interface ${ childTypeName } {
359
366
  \t\ttags.MaxLength< 280 > &
360
367
  \t\ttags.Default< ${ JSON.stringify( CHILD_PLACEHOLDER ) } >;
361
368
  }
369
+
370
+ export type ${ childInterfaceName }ValidationResult = ValidationResult< ${ childTypeName } >;
362
371
  `;
363
372
  }
364
373
 
@@ -407,36 +416,37 @@ function renderValidatorsFile(
407
416
  ): string {
408
417
  return `import typia from 'typia';
409
418
  import currentManifest from './typia.manifest.json';
410
- import {
411
- \ttype ManifestDefaultsDocument,
412
- } from '@wp-typia/block-runtime/defaults';
413
- import {
414
- \tcreateScaffoldValidatorToolkit,
415
- } from '@wp-typia/block-runtime/validation';
416
-
417
- import type { ${ childTypeName } } from './types';
418
-
419
- const validate = typia.createValidate< ${ childTypeName } >();
420
- const assert = typia.createAssert< ${ childTypeName } >();
421
- const is = typia.createIs< ${ childTypeName } >();
422
- const random = typia.createRandom< ${ childTypeName } >();
423
- const clone = typia.misc.createClone< ${ childTypeName } >();
424
- const prune = typia.misc.createPrune< ${ childTypeName } >();
425
- const scaffoldValidators = createScaffoldValidatorToolkit< ${ childTypeName } >( {
426
- \tmanifest: currentManifest as ManifestDefaultsDocument,
427
- \tvalidate,
428
- \tassert,
429
- \tis,
430
- \trandom,
431
- \tclone,
432
- \tprune,
419
+ import type {
420
+ \t${ childTypeName },
421
+ \t${ childInterfaceName }ValidationResult,
422
+ } from './types';
423
+ import { createTemplateValidatorToolkit } from '../../validator-toolkit';
424
+
425
+ const scaffoldValidators = createTemplateValidatorToolkit< ${ childTypeName } >( {
426
+ \tassert: typia.createAssert< ${ childTypeName } >(),
427
+ \tclone: typia.misc.createClone< ${ childTypeName } >() as (
428
+ \t\tvalue: ${ childTypeName },
429
+ \t) => ${ childTypeName },
430
+ \tis: typia.createIs< ${ childTypeName } >(),
431
+ \tmanifest: currentManifest,
432
+ \tprune: typia.misc.createPrune< ${ childTypeName } >(),
433
+ \trandom: typia.createRandom< ${ childTypeName } >() as (
434
+ \t\t...args: unknown[]
435
+ \t) => ${ childTypeName },
436
+ \tvalidate: typia.createValidate< ${ childTypeName } >(),
433
437
  } );
434
438
 
435
- export const validate${ childInterfaceName } = scaffoldValidators.validateAttributes;
439
+ export const validate${ childInterfaceName } =
440
+ \tscaffoldValidators.validateAttributes as (
441
+ \t\tattributes: unknown
442
+ \t) => ${ childInterfaceName }ValidationResult;
436
443
 
437
444
  export const validators = scaffoldValidators.validators;
438
445
 
439
- export const sanitize${ childInterfaceName } = scaffoldValidators.sanitizeAttributes;
446
+ export const sanitize${ childInterfaceName } =
447
+ \tscaffoldValidators.sanitizeAttributes as (
448
+ \t\tattributes: Partial< ${ childTypeName } >
449
+ \t) => ${ childTypeName };
440
450
 
441
451
  export const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;
442
452
  `;
@@ -605,7 +615,11 @@ function main() {
605
615
  renderBlockJson( childBlockName, childFolderSlug, childTitle ),
606
616
  'utf8'
607
617
  );
608
- fs.writeFileSync( path.join( childDir, 'types.ts' ), renderTypesFile( childTypeName, childTitle ), 'utf8' );
618
+ fs.writeFileSync(
619
+ path.join( childDir, 'types.ts' ),
620
+ renderTypesFile( childTypeName, childInterfaceName, childTitle ),
621
+ 'utf8'
622
+ );
609
623
  fs.writeFileSync(
610
624
  path.join( childDir, 'typia.manifest.json' ),
611
625
  renderStarterManifestFile( childTypeName, childTitle ),