@wp-typia/project-tools 0.15.3 → 0.16.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.
@@ -648,6 +648,7 @@ const LEGACY_ASSERT_PATTERN = /assert:\s*typia\.createAssert</u;
648
648
  const LEGACY_MANIFEST_PATTERN = /\r?\n[ \t]*manifest:\s*currentManifest,/u;
649
649
  const LEGACY_TOOLKIT_CALL_PATTERN = /createTemplateValidatorToolkit<\s*(?<typeName>[A-Za-z0-9_]+)\s*>\s*\(\s*\{/u;
650
650
  const LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN = /from\s*["']\.\.\/\.\.\/validator-toolkit["']/u;
651
+ const TYPIA_IMPORT_PATTERN = /^[\uFEFF \t]*import\s+typia\s+from\s*["']typia["'];?/mu;
651
652
  const COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS = [
652
653
  /interface\s+TemplateValidatorFunctions\s*<\s*T\s+extends\s+object\s*>\s*\{/u,
653
654
  /\bassert\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']assert["']\s*\]/u,
@@ -667,6 +668,9 @@ function isLegacyCompoundValidatorSource(source) {
667
668
  LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN.test(source) &&
668
669
  !LEGACY_ASSERT_PATTERN.test(source));
669
670
  }
671
+ function hasTypiaImport(source) {
672
+ return TYPIA_IMPORT_PATTERN.test(source.replace(/\/\*[\s\S]*?\*\//gu, ""));
673
+ }
670
674
  function upgradeLegacyCompoundValidatorSource(source) {
671
675
  const typeNameMatch = source.match(LEGACY_TOOLKIT_CALL_PATTERN);
672
676
  const typeName = typeNameMatch?.groups?.typeName;
@@ -674,7 +678,7 @@ function upgradeLegacyCompoundValidatorSource(source) {
674
678
  throw new Error("Unable to upgrade a legacy compound validator without a generated type import.");
675
679
  }
676
680
  let nextSource = source;
677
- if (!nextSource.includes("import typia from 'typia';")) {
681
+ if (!hasTypiaImport(nextSource)) {
678
682
  nextSource = `import typia from 'typia';\n${nextSource}`;
679
683
  }
680
684
  nextSource = nextSource.replace(LEGACY_TOOLKIT_CALL_PATTERN, [
@@ -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) {
@@ -317,12 +317,13 @@ function buildReadme(templateId, variables, packageManager, { withMigrationUi =
317
317
  const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
318
318
  compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
319
319
  });
320
+ const compoundPersistenceEnabled = variables.compoundPersistenceEnabled === "true";
320
321
  const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === "true"
321
322
  ? "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
323
  : null;
323
324
  const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
324
325
  const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
325
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
326
+ compoundPersistenceEnabled,
326
327
  slug: variables.slug,
327
328
  });
328
329
  const developmentScript = getPrimaryDevelopmentScript(templateId);
@@ -362,7 +363,9 @@ ${formatRunScript(packageManager, "build")}
362
363
  ${optionalOnboardingSteps.join("\n")}
363
364
  \`\`\`
364
365
 
365
- ${getOptionalOnboardingNote(packageManager, templateId)}
366
+ ${getOptionalOnboardingNote(packageManager, templateId, {
367
+ compoundPersistenceEnabled,
368
+ })}
366
369
 
367
370
  ${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
371
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -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",
@@ -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,12 +8,13 @@
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
13
  "sync-rest": "tsx scripts/sync-rest-contracts.ts",
13
- "build": "bun run sync-types --check && bun run sync-rest --check && wp-scripts build --experimental-modules",
14
- "start": "bun run sync-types && bun run sync-rest && wp-scripts start --experimental-modules",
14
+ "build": "bun run sync --check && wp-scripts build --experimental-modules",
15
+ "start": "bun run sync && wp-scripts start --experimental-modules",
15
16
  "dev": "bun run start",
16
- "typecheck": "bun run sync-types --check && bun run sync-rest --check && tsc --noEmit",
17
+ "typecheck": "bun run sync --check && tsc --noEmit",
17
18
  "lint:js": "wp-scripts lint-js",
18
19
  "lint:css": "wp-scripts lint-style",
19
20
  "lint": "bun run lint:js && bun run lint:css",
@@ -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
+ } );
@@ -2,6 +2,7 @@
2
2
  import path from 'node:path';
3
3
 
4
4
  import {
5
+ runSyncBlockMetadata,
5
6
  syncEndpointClient,
6
7
  syncRestOpenApi,
7
8
  syncTypeSchemas,
@@ -26,6 +27,31 @@ function parseCliOptions( argv: string[] ) {
26
27
  return options;
27
28
  }
28
29
 
30
+ async function assertTypeArtifactsCurrent( block: ( typeof BLOCKS )[ number ] ) {
31
+ const report = await runSyncBlockMetadata( {
32
+ blockJsonFile: path.join( 'src', 'blocks', block.slug, 'block.json' ),
33
+ jsonSchemaFile: path.join( 'src', 'blocks', block.slug, 'typia.schema.json' ),
34
+ manifestFile: path.join( 'src', 'blocks', block.slug, 'typia.manifest.json' ),
35
+ openApiFile: path.join( 'src', 'blocks', block.slug, 'typia.openapi.json' ),
36
+ sourceTypeName: block.attributeTypeName,
37
+ typesFile: block.typesFile,
38
+ }, {
39
+ check: true,
40
+ } );
41
+
42
+ if ( report.failure?.code === 'stale-generated-artifact' ) {
43
+ throw new Error(
44
+ `${ block.slug }: ${ report.failure.message }\nRun \`sync\` or \`sync-types\` first to refresh type-derived artifacts before rerunning \`sync-rest\`.`
45
+ );
46
+ }
47
+
48
+ if ( report.failure ) {
49
+ throw new Error(
50
+ `${ block.slug }: type-derived artifact preflight failed before sync-rest.\n${ report.failure.message }`
51
+ );
52
+ }
53
+ }
54
+
29
55
  async function main() {
30
56
  const options = parseCliOptions( process.argv.slice( 2 ) );
31
57
 
@@ -39,6 +65,8 @@ async function main() {
39
65
  continue;
40
66
  }
41
67
 
68
+ await assertTypeArtifactsCurrent( block );
69
+
42
70
  for ( const [ baseName, contract ] of Object.entries( block.restManifest.contracts ) ) {
43
71
  await syncTypeSchemas( {
44
72
  jsonSchemaFile: path.join(
@@ -7,16 +7,17 @@
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
12
  "sync-rest": "tsx scripts/sync-rest-contracts.ts",
12
- "build": "bun run sync-types --check && bun run sync-rest --check && wp-scripts build --experimental-modules",
13
- "start": "bun run sync-types && bun run sync-rest && 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
16
  "lint:js": "wp-scripts lint-js",
16
17
  "lint:css": "wp-scripts lint-style",
17
18
  "lint": "bun run lint:js && bun run lint:css",
18
19
  "format": "wp-scripts format",
19
- "typecheck": "bun run sync-types --check && bun run sync-rest --check && tsc --noEmit"
20
+ "typecheck": "bun run sync --check && tsc --noEmit"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@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
+ } );
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import {
3
3
  defineEndpointManifest,
4
+ runSyncBlockMetadata,
4
5
  syncEndpointClient,
5
6
  syncRestOpenApi,
6
7
  syncTypeSchemas,
@@ -23,6 +24,31 @@ function parseCliOptions( argv: string[] ) {
23
24
  return options;
24
25
  }
25
26
 
27
+ async function assertTypeArtifactsCurrent() {
28
+ const report = await runSyncBlockMetadata( {
29
+ blockJsonFile: 'src/block.json',
30
+ jsonSchemaFile: 'src/typia.schema.json',
31
+ manifestFile: 'src/typia.manifest.json',
32
+ openApiFile: 'src/typia.openapi.json',
33
+ sourceTypeName: '{{pascalCase}}Attributes',
34
+ typesFile: 'src/types.ts',
35
+ }, {
36
+ check: true,
37
+ } );
38
+
39
+ if ( report.failure?.code === 'stale-generated-artifact' ) {
40
+ throw new Error(
41
+ `${ report.failure.message }\nRun \`sync\` or \`sync-types\` first to refresh type-derived artifacts before rerunning \`sync-rest\`.`
42
+ );
43
+ }
44
+
45
+ if ( report.failure ) {
46
+ throw new Error(
47
+ `Type-derived artifact preflight failed before sync-rest.\n${ report.failure.message }`
48
+ );
49
+ }
50
+ }
51
+
26
52
  const REST_ENDPOINT_MANIFEST = defineEndpointManifest( {
27
53
  contracts: {
28
54
  'state-query': {
@@ -85,6 +111,8 @@ const REST_ENDPOINT_MANIFEST = defineEndpointManifest( {
85
111
  async function main() {
86
112
  const options = parseCliOptions( process.argv.slice( 2 ) );
87
113
 
114
+ await assertTypeArtifactsCurrent();
115
+
88
116
  for ( const [ baseName, contract ] of Object.entries( REST_ENDPOINT_MANIFEST.contracts ) ) {
89
117
  await syncTypeSchemas( {
90
118
  jsonSchemaFile: `src/api-schemas/${ baseName }.schema.json`,
@@ -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}}",