@wp-typia/project-tools 0.20.0 → 0.20.2

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.
@@ -281,7 +281,7 @@ export async function runAddBlockCommand({ alternateRenderTargets, blockName, cw
281
281
  throw new Error("`wp-typia add block --template query-loop` is not supported. Query Loop is a create-time `core/query` variation scaffold, so use `wp-typia create <project-dir> --template query-loop` instead.");
282
282
  }
283
283
  if (!isAddBlockTemplateId(templateId)) {
284
- throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
284
+ throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}. Run \`wp-typia templates list\` to inspect available templates.`);
285
285
  }
286
286
  const resolvedTemplateId = templateId;
287
287
  assertPersistenceFlagsAllowed(resolvedTemplateId, {
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { syncTypeSchemas } from "@wp-typia/block-runtime/metadata-core";
5
+ import semver from "semver";
5
6
  import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from "./workspace-inventory.js";
6
7
  import { resolveWorkspaceProject } from "./workspace-project.js";
7
8
  import { toTitleCase } from "./string-case.js";
@@ -12,14 +13,63 @@ const ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
12
13
  const ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
13
14
  const ABILITY_REGISTRY_END_MARKER = "// wp-typia add ability entries end";
14
15
  const ABILITY_REGISTRY_START_MARKER = "// wp-typia add ability entries start";
15
- const WP_ABILITIES_SCRIPT_HANDLE = "wp-abilities";
16
- const WP_CORE_ABILITIES_SCRIPT_HANDLE = "wp-core-abilities";
16
+ const WP_ABILITIES_PACKAGE_VERSION = "^0.10.0";
17
+ const WP_CORE_ABILITIES_PACKAGE_VERSION = "^0.9.0";
18
+ const WP_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/abilities";
19
+ const WP_CORE_ABILITIES_SCRIPT_MODULE_ID = "@wordpress/core-abilities";
17
20
  function escapeRegex(value) {
18
21
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
19
22
  }
20
23
  function quotePhpString(value) {
21
24
  return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
22
25
  }
26
+ function findPhpFunctionRange(source, functionName) {
27
+ const functionPattern = new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u");
28
+ const match = functionPattern.exec(source);
29
+ if (!match) {
30
+ return null;
31
+ }
32
+ const openingBraceIndex = match.index + match[0].length - 1;
33
+ let depth = 0;
34
+ for (let index = openingBraceIndex; index < source.length; index += 1) {
35
+ const character = source[index];
36
+ if (character === "{") {
37
+ depth += 1;
38
+ }
39
+ else if (character === "}") {
40
+ depth -= 1;
41
+ if (depth === 0) {
42
+ const end = index + 1;
43
+ return {
44
+ end,
45
+ source: source.slice(match.index, end),
46
+ start: match.index,
47
+ };
48
+ }
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ function replacePhpFunctionDefinition(source, functionName, replacement) {
54
+ const functionRange = findPhpFunctionRange(source, functionName);
55
+ if (!functionRange) {
56
+ return source;
57
+ }
58
+ return `${source.slice(0, functionRange.start)}${replacement.trimStart()}${source.slice(functionRange.end)}`;
59
+ }
60
+ function resolveManagedDependencyVersion(existingVersion, requiredVersion) {
61
+ if (!existingVersion) {
62
+ return requiredVersion;
63
+ }
64
+ const existingMinimum = semver.minVersion(existingVersion);
65
+ const requiredMinimum = semver.minVersion(requiredVersion);
66
+ if (!existingMinimum || !requiredMinimum) {
67
+ return requiredVersion;
68
+ }
69
+ return semver.gte(existingMinimum, requiredMinimum)
70
+ ? existingVersion
71
+ : requiredVersion;
72
+ }
23
73
  function toPascalCaseFromAbilitySlug(abilitySlug) {
24
74
  return normalizeBlockSlug(abilitySlug)
25
75
  .split("-")
@@ -98,7 +148,14 @@ function buildAbilityDataSource(abilitySlug) {
98
148
  .replace(/[^A-Z0-9]+/gu, "_")
99
149
  .replace(/_{2,}/gu, "_")
100
150
  .replace(/^_|_$/gu, "");
101
- return `import abilityConfig from './ability.config.json';
151
+ return `import {
152
+ \texecuteAbility,
153
+ \tgetAbilities,
154
+ \tgetAbility as getRegisteredAbility,
155
+ } from '@wordpress/abilities';
156
+ import '@wordpress/core-abilities';
157
+
158
+ import abilityConfig from './ability.config.json';
102
159
 
103
160
  import type { ${pascalCase}AbilityInput, ${pascalCase}AbilityOutput } from './types';
104
161
 
@@ -110,55 +167,56 @@ interface WordPressAbilityDefinition {
110
167
  \tname?: string;
111
168
  }
112
169
 
113
- interface WordPressAbilitiesClient {
114
- \texecuteAbility( name: string, input?: unknown ): Promise< unknown >;
115
- \tgetAbilities( args?: { category?: string } ): WordPressAbilityDefinition[];
116
- \tgetAbility( name: string ): WordPressAbilityDefinition | undefined;
117
- }
118
-
119
- const ABILITY_CLIENT_UNAVAILABLE_MESSAGE =
120
- \t'The WordPress abilities client is unavailable on this screen. Ensure the Abilities API and @wordpress/core-abilities integration are loaded before using this scaffold.';
121
-
122
170
  export const ${abilityConstBase}_ABILITY = abilityConfig;
123
171
  export const ${abilityConstBase}_ABILITY_CATEGORY = abilityConfig.category;
124
172
  export const ${abilityConstBase}_ABILITY_ID = abilityConfig.abilityId;
125
173
  export const ${abilityConstBase}_ABILITY_META = abilityConfig.meta;
174
+ const ABILITY_DISCOVERY_POLL_INTERVAL_MS = 50;
175
+ const ABILITY_DISCOVERY_TIMEOUT_MS = 5000;
126
176
 
127
177
  export type {
128
178
  \t${pascalCase}AbilityInput,
129
179
  \t${pascalCase}AbilityOutput,
130
180
  };
131
181
 
132
- function resolveAbilitiesClient(): WordPressAbilitiesClient {
133
- \tconst runtime = globalThis as typeof globalThis & {
134
- \t\twindow?: {
135
- \t\t\twp?: {
136
- \t\t\t\tabilities?: WordPressAbilitiesClient;
137
- \t\t\t};
138
- \t\t};
139
- \t};
140
- \tconst client = runtime.window?.wp?.abilities;
141
- \tif ( ! client ) {
142
- \t\tthrow new Error( ABILITY_CLIENT_UNAVAILABLE_MESSAGE );
143
- \t}
182
+ function sleep( milliseconds: number ): Promise< void > {
183
+ \treturn new Promise( ( resolve ) => {
184
+ \t\tsetTimeout( resolve, milliseconds );
185
+ \t} );
186
+ }
144
187
 
145
- \treturn client;
188
+ async function waitFor${pascalCase}AbilityRegistration(): Promise< void > {
189
+ \tconst deadline = Date.now() + ABILITY_DISCOVERY_TIMEOUT_MS;
190
+ \twhile ( ! getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) ) {
191
+ \t\tif ( Date.now() >= deadline ) {
192
+ \t\t\treturn;
193
+ \t\t}
194
+
195
+ \t\tawait sleep( ABILITY_DISCOVERY_POLL_INTERVAL_MS );
196
+ \t}
146
197
  }
147
198
 
148
- export function list${pascalCase}CategoryAbilities(): WordPressAbilityDefinition[] {
149
- \treturn resolveAbilitiesClient().getAbilities( {
199
+ export async function list${pascalCase}CategoryAbilities(): Promise< WordPressAbilityDefinition[] > {
200
+ \tawait waitFor${pascalCase}AbilityRegistration();
201
+
202
+ \treturn getAbilities( {
150
203
  \t\tcategory: ${abilityConstBase}_ABILITY_CATEGORY.slug,
151
- \t} );
204
+ \t} ) as WordPressAbilityDefinition[];
152
205
  }
153
206
 
154
- export function get${pascalCase}Ability():
207
+ export async function get${pascalCase}Ability(): Promise<
155
208
  \t| WordPressAbilityDefinition
156
- \t| undefined {
157
- \treturn resolveAbilitiesClient().getAbility( ${abilityConstBase}_ABILITY_ID );
209
+ \t| undefined
210
+ > {
211
+ \tawait waitFor${pascalCase}AbilityRegistration();
212
+
213
+ \treturn getRegisteredAbility( ${abilityConstBase}_ABILITY_ID ) as
214
+ \t\t| WordPressAbilityDefinition
215
+ \t\t| undefined;
158
216
  }
159
217
 
160
- export function require${pascalCase}Ability(): WordPressAbilityDefinition {
161
- \tconst ability = get${pascalCase}Ability();
218
+ export async function require${pascalCase}Ability(): Promise< WordPressAbilityDefinition > {
219
+ \tconst ability = await get${pascalCase}Ability();
162
220
  \tif ( ability ) {
163
221
  \t\treturn ability;
164
222
  \t}
@@ -174,7 +232,9 @@ export function require${pascalCase}Ability(): WordPressAbilityDefinition {
174
232
  export async function run${pascalCase}Ability(
175
233
  \tinput: ${pascalCase}AbilityInput
176
234
  ): Promise< ${pascalCase}AbilityOutput > {
177
- \treturn ( await resolveAbilitiesClient().executeAbility(
235
+ \tawait waitFor${pascalCase}AbilityRegistration();
236
+
237
+ \treturn ( await executeAbility(
178
238
  \t\t${abilityConstBase}_ABILITY_ID,
179
239
  \t\tinput
180
240
  \t) ) as ${pascalCase}AbilityOutput;
@@ -186,8 +246,8 @@ function buildAbilityClientSource(abilitySlug) {
186
246
  return `/**
187
247
  * Re-export the typed ${pascalCase} ability client helpers.
188
248
  *
189
- * The underlying WordPress abilities client is expected to have been hydrated
190
- * by the site's admin/editor bootstrap before these helpers execute.
249
+ * The helper methods load the WordPress core abilities integration and wait for
250
+ * this server-registered ability before reading or executing it.
191
251
  */
192
252
  export * from './data';
193
253
  `;
@@ -559,22 +619,31 @@ function ${enqueueFunctionName}() {
559
619
  \t\t? $asset['dependencies']
560
620
  \t\t: array();
561
621
 
562
- \tforeach ( array( '${WP_CORE_ABILITIES_SCRIPT_HANDLE}', '${WP_ABILITIES_SCRIPT_HANDLE}' ) as $ability_dependency ) {
563
- \t\tif (
564
- \t\t\tfunction_exists( 'wp_script_is' ) &&
565
- \t\t\twp_script_is( $ability_dependency, 'registered' ) &&
566
- \t\t\t! in_array( $ability_dependency, $dependencies, true )
567
- \t\t) {
622
+ \tforeach ( array( '${WP_CORE_ABILITIES_SCRIPT_MODULE_ID}', '${WP_ABILITIES_SCRIPT_MODULE_ID}' ) as $ability_dependency ) {
623
+ \t\t$has_dependency = false;
624
+ \t\tforeach ( $dependencies as $dependency ) {
625
+ \t\t\t$dependency_id = is_array( $dependency ) && isset( $dependency['id'] )
626
+ \t\t\t\t? $dependency['id']
627
+ \t\t\t\t: $dependency;
628
+ \t\t\tif ( $dependency_id === $ability_dependency ) {
629
+ \t\t\t\t$has_dependency = true;
630
+ \t\t\t\tbreak;
631
+ \t\t\t}
632
+ \t\t}
633
+ \t\tif ( ! $has_dependency ) {
568
634
  \t\t\t$dependencies[] = $ability_dependency;
569
635
  \t\t}
570
636
  \t}
571
637
 
572
- \twp_enqueue_script(
638
+ \tif ( ! function_exists( 'wp_enqueue_script_module' ) ) {
639
+ \t\treturn;
640
+ \t}
641
+
642
+ \twp_enqueue_script_module(
573
643
  \t\t'${workspaceBaseName}-abilities',
574
644
  \t\tplugins_url( '${ABILITY_EDITOR_SCRIPT}', __FILE__ ),
575
645
  \t\t$dependencies,
576
- \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path ),
577
- \t\ttrue
646
+ \t\tisset( $asset['version'] ) ? $asset['version'] : filemtime( $script_path )
578
647
  \t);
579
648
  }
580
649
  `;
@@ -607,6 +676,9 @@ function ${enqueueFunctionName}() {
607
676
  if (!hasPhpFunctionDefinition(enqueueFunctionName)) {
608
677
  insertPhpSnippet(enqueueFunction);
609
678
  }
679
+ else if (!findPhpFunctionRange(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
680
+ nextSource = replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction);
681
+ }
610
682
  if (!nextSource.includes(loadHook)) {
611
683
  appendPhpSnippet(loadHook);
612
684
  }
@@ -626,10 +698,17 @@ async function ensureAbilityPackageScripts(workspace) {
626
698
  ...(packageJson.scripts ?? {}),
627
699
  "sync-abilities": packageJson.scripts?.["sync-abilities"] ?? "tsx scripts/sync-abilities.ts",
628
700
  };
629
- if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {})) {
701
+ const nextDependencies = {
702
+ ...(packageJson.dependencies ?? {}),
703
+ [WP_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_ABILITIES_SCRIPT_MODULE_ID], WP_ABILITIES_PACKAGE_VERSION),
704
+ [WP_CORE_ABILITIES_SCRIPT_MODULE_ID]: resolveManagedDependencyVersion(packageJson.dependencies?.[WP_CORE_ABILITIES_SCRIPT_MODULE_ID], WP_CORE_ABILITIES_PACKAGE_VERSION),
705
+ };
706
+ if (JSON.stringify(nextScripts) === JSON.stringify(packageJson.scripts ?? {}) &&
707
+ JSON.stringify(nextDependencies) === JSON.stringify(packageJson.dependencies ?? {})) {
630
708
  return;
631
709
  }
632
710
  packageJson.scripts = nextScripts;
711
+ packageJson.dependencies = nextDependencies;
633
712
  await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
634
713
  }
635
714
  async function ensureAbilitySyncProjectAnchors(workspace) {
@@ -702,6 +781,28 @@ async function ensureAbilityWebpackAnchors(workspace) {
702
781
  if (/['"]abilities\/index['"]/u.test(source)) {
703
782
  return source;
704
783
  }
784
+ const optionalModuleReturnPattern = /(function\s+getOptionalModuleEntries\s*\(\)\s*\{[\s\S]*?)(\n\treturn Object\.fromEntries\(\s*entries\s*\);\n\})/u;
785
+ if (optionalModuleReturnPattern.test(source)) {
786
+ return source.replace(optionalModuleReturnPattern, `$1
787
+
788
+ \tfor ( const [ entryName, candidates ] of [
789
+ \t\t[
790
+ \t\t\t'abilities/index',
791
+ \t\t\t[ 'src/abilities/index.ts', 'src/abilities/index.js' ],
792
+ \t\t],
793
+ \t] ) {
794
+ \t\tfor ( const relativePath of candidates ) {
795
+ \t\t\tconst entryPath = path.resolve( process.cwd(), relativePath );
796
+ \t\t\tif ( ! fs.existsSync( entryPath ) ) {
797
+ \t\t\t\tcontinue;
798
+ \t\t\t}
799
+
800
+ \t\t\tentries.push( [ entryName, entryPath ] );
801
+ \t\t\tbreak;
802
+ \t\t}
803
+ \t}
804
+ $2`);
805
+ }
705
806
  const sharedEntriesPattern = /for\s*\(\s*const\s+\[\s*entryName\s*,\s*candidates\s*\]\s+of\s+\[([\s\S]*?)\]\s*\)\s*\{/u;
706
807
  const match = source.match(sharedEntriesPattern);
707
808
  if (!match ||
@@ -446,7 +446,6 @@ export async function runAddRestResourceCommand({ cwd = process.cwd(), methods,
446
446
  const validatorsFilePath = path.join(restResourceDir, "api-validators.ts");
447
447
  const apiFilePath = path.join(restResourceDir, "api.ts");
448
448
  const dataFilePath = path.join(restResourceDir, "data.ts");
449
- const clientFilePath = path.join(restResourceDir, "api-client.ts");
450
449
  const phpFilePath = path.join(workspace.projectDir, "inc", "rest", `${restResourceSlug}.php`);
451
450
  const mutationSnapshot = {
452
451
  fileSources: await snapshotWorkspaceFiles([
@@ -19,6 +19,7 @@ export declare const CLI_DIAGNOSTIC_CODES: {
19
19
  readonly OUTSIDE_PROJECT_ROOT: "outside-project-root";
20
20
  readonly TEMPLATE_SOURCE_TIMEOUT: "template-source-timeout";
21
21
  readonly TEMPLATE_SOURCE_TOO_LARGE: "template-source-too-large";
22
+ readonly UNKNOWN_TEMPLATE: "unknown-template";
22
23
  readonly UNSUPPORTED_COMMAND: "unsupported-command";
23
24
  };
24
25
  export type CliDiagnosticCode = (typeof CLI_DIAGNOSTIC_CODES)[keyof typeof CLI_DIAGNOSTIC_CODES];
@@ -10,6 +10,7 @@ export const CLI_DIAGNOSTIC_CODES = {
10
10
  OUTSIDE_PROJECT_ROOT: "outside-project-root",
11
11
  TEMPLATE_SOURCE_TIMEOUT: "template-source-timeout",
12
12
  TEMPLATE_SOURCE_TOO_LARGE: "template-source-too-large",
13
+ UNKNOWN_TEMPLATE: "unknown-template",
13
14
  UNSUPPORTED_COMMAND: "unsupported-command",
14
15
  };
15
16
  const DEFAULT_CLI_FAILURE_SUMMARIES = {
@@ -183,7 +184,10 @@ function inferCliDiagnosticCode(options) {
183
184
  if (/requires <|requires --|requires a value/u.test(haystack)) {
184
185
  return CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT;
185
186
  }
186
- if (/Unknown .*subcommand|Unknown add kind|Unknown template|removed in favor|does not support|The Bun-free fallback runtime does not support|The positional alias only accepts/u.test(haystack)) {
187
+ if (/Unknown (?:add-block )?template\s+(?:"|\\")/u.test(haystack)) {
188
+ return CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE;
189
+ }
190
+ if (/Unknown .*subcommand|Unknown add kind|removed in favor|does not support|The Bun-free fallback runtime does not support|The positional alias only accepts/u.test(haystack)) {
187
191
  return haystack.includes("does not support") ||
188
192
  haystack.includes("The Bun-free fallback runtime does not support")
189
193
  ? CLI_DIAGNOSTIC_CODES.UNSUPPORTED_COMMAND
@@ -303,21 +303,24 @@ function checkWorkspaceAbilityBootstrap(projectDir, packageName, phpPrefix) {
303
303
  const hasServerGlob = source.includes(WORKSPACE_ABILITY_GLOB);
304
304
  const hasEditorScript = source.includes(WORKSPACE_ABILITY_EDITOR_SCRIPT);
305
305
  const hasEditorAsset = source.includes(WORKSPACE_ABILITY_EDITOR_ASSET);
306
+ const hasScriptModuleEnqueue = source.includes("wp_enqueue_script_module");
306
307
  return createDoctorCheck("Ability bootstrap", hasLoaderHook &&
307
308
  hasAdminEnqueueHook &&
308
309
  hasEditorEnqueueHook &&
309
310
  hasServerGlob &&
310
311
  hasEditorScript &&
311
- hasEditorAsset
312
+ hasEditorAsset &&
313
+ hasScriptModuleEnqueue
312
314
  ? "pass"
313
315
  : "fail", hasLoaderHook &&
314
316
  hasAdminEnqueueHook &&
315
317
  hasEditorEnqueueHook &&
316
318
  hasServerGlob &&
317
319
  hasEditorScript &&
318
- hasEditorAsset
319
- ? "Ability loader and admin/editor client bootstrap hooks are present"
320
- : "Missing ability loader hook or build/abilities script asset references");
320
+ hasEditorAsset &&
321
+ hasScriptModuleEnqueue
322
+ ? "Ability loader and admin/editor script-module bootstrap hooks are present"
323
+ : "Missing ability loader hook, script-module enqueue, or build/abilities asset references");
321
324
  }
322
325
  function checkWorkspaceAbilityIndex(projectDir, abilities) {
323
326
  const indexRelativePath = [
@@ -1,8 +1,10 @@
1
1
  import { execSync } from 'node:child_process';
2
+ import path from 'node:path';
2
3
  import { PACKAGE_MANAGER_IDS, getPackageManager, } from './package-managers.js';
3
4
  import { normalizeBlockSlug, resolveScaffoldIdentifiers, validateBlockSlug, validateNamespace, } from './scaffold-identifiers.js';
4
5
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
5
6
  import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
7
+ import { parseNpmTemplateLocator } from './template-source-locators.js';
6
8
  import { toSnakeCase, toTitleCase, } from './string-case.js';
7
9
  const WORKSPACE_TEMPLATE_ALIAS = 'workspace';
8
10
  const TEMPLATE_SELECTION_HINT = `--template <${[
@@ -11,6 +13,10 @@ const TEMPLATE_SELECTION_HINT = `--template <${[
11
13
  ].join('|')}|./path|github:owner/repo/path[#ref]|npm-package>`;
12
14
  const TEMPLATE_SUGGESTION_IDS = [...TEMPLATE_IDS, WORKSPACE_TEMPLATE_ALIAS];
13
15
  const QUERY_POST_TYPE_RULE = 'Use lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
16
+ const USER_FACING_TEMPLATE_IDS = [
17
+ ...TEMPLATE_IDS,
18
+ WORKSPACE_TEMPLATE_ALIAS,
19
+ ];
14
20
  /**
15
21
  * Detect the current author name from local Git config.
16
22
  *
@@ -78,14 +84,22 @@ function normalizeTemplateSelection(templateId) {
78
84
  ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
79
85
  : templateId;
80
86
  }
81
- function looksLikeExplicitExternalTemplateLocator(templateId) {
82
- return (templateId.startsWith('./') ||
87
+ function looksLikeWindowsAbsoluteTemplatePath(templateId) {
88
+ return /^[a-z]:[\\/]/iu.test(templateId) || /^\\\\[^\\]+\\[^\\]+/u.test(templateId);
89
+ }
90
+ function looksLikeExplicitNonNpmExternalTemplateLocator(templateId) {
91
+ return (path.isAbsolute(templateId) ||
92
+ looksLikeWindowsAbsoluteTemplatePath(templateId) ||
93
+ templateId.startsWith('./') ||
83
94
  templateId.startsWith('../') ||
84
- templateId.startsWith('/') ||
85
95
  templateId.startsWith('@') ||
86
96
  templateId.startsWith('github:') ||
87
97
  templateId.includes('/'));
88
98
  }
99
+ function looksLikeExplicitExternalTemplateLocator(templateId) {
100
+ return (looksLikeExplicitNonNpmExternalTemplateLocator(templateId) ||
101
+ parseNpmTemplateLocator(templateId) !== null);
102
+ }
89
103
  function getEditDistance(left, right) {
90
104
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
91
105
  const current = new Array(right.length + 1);
@@ -104,7 +118,7 @@ function getEditDistance(left, right) {
104
118
  function findMistypedBuiltInTemplateSuggestion(templateId) {
105
119
  const normalizedTemplateId = templateId.trim().toLowerCase();
106
120
  if (normalizedTemplateId.length === 0 ||
107
- looksLikeExplicitExternalTemplateLocator(normalizedTemplateId)) {
121
+ looksLikeExplicitNonNpmExternalTemplateLocator(normalizedTemplateId)) {
108
122
  return null;
109
123
  }
110
124
  let bestCandidate = null;
@@ -132,6 +146,13 @@ function getMistypedBuiltInTemplateMessage(templateId) {
132
146
  : 'built-in scaffold';
133
147
  return `Unknown template "${templateId}". Did you mean "${suggestion}"? Use \`--template ${suggestion}\` for the ${suggestionDescription}, or pass a local path, \`github:owner/repo/path[#ref]\`, or an npm package spec for an external template.`;
134
148
  }
149
+ function getUnknownTemplateMessage(templateId) {
150
+ return [
151
+ `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(', ')}.`,
152
+ 'Run `wp-typia templates list` to inspect available templates.',
153
+ 'Pass an explicit external template locator such as `./path`, `github:owner/repo/path[#ref]`, or `@scope/template` for custom templates.',
154
+ ].join(' ');
155
+ }
135
156
  /**
136
157
  * Resolve the scaffold template id from flags, defaults, and interactive selection.
137
158
  *
@@ -154,6 +175,9 @@ export async function resolveTemplateId({ templateId, yes = false, isInteractive
154
175
  if (mistypedBuiltInTemplateMessage) {
155
176
  throw new Error(mistypedBuiltInTemplateMessage);
156
177
  }
178
+ if (!looksLikeExplicitExternalTemplateLocator(normalizedTemplateId)) {
179
+ throw new Error(getUnknownTemplateMessage(templateId));
180
+ }
157
181
  return normalizedTemplateId;
158
182
  }
159
183
  if (yes) {
@@ -190,7 +214,7 @@ export async function resolvePackageManagerId({ packageManager, yes = false, isI
190
214
  */
191
215
  export async function collectScaffoldAnswers({ projectName, templateId, yes = false, dataStorageMode, namespace, persistencePolicy, phpPrefix, promptText, queryPostType, textDomain, }) {
192
216
  const defaults = getDefaultAnswers(projectName, templateId);
193
- if (yes) {
217
+ if (yes || (!isBuiltInTemplateId(templateId) && !promptText)) {
194
218
  const identifiers = resolveScaffoldIdentifiers({
195
219
  namespace: namespace ?? defaults.namespace,
196
220
  phpPrefix,
@@ -6,9 +6,20 @@ import { spawnSync } from 'node:child_process';
6
6
  import semver from 'semver';
7
7
  import { x as extractTarball } from 'tar';
8
8
  import { createExternalTemplateTimeoutError, fetchWithExternalTemplateTimeout, getExternalTemplateMetadataMaxBytes, getExternalTemplateTarballMaxBytes, getExternalTemplateTimeoutMs, readBufferResponseWithLimit, readJsonResponseWithLimit, } from './external-template-guards.js';
9
- import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, } from './template-registry.js';
9
+ import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, OFFICIAL_WORKSPACE_TEMPLATE_ALIAS, PROJECT_TOOLS_PACKAGE_ROOT, TEMPLATE_IDS, } from './template-registry.js';
10
10
  import { isPlainObject } from './object-utils.js';
11
11
  import { createManagedTempRoot } from './temp-roots.js';
12
+ const USER_FACING_TEMPLATE_IDS = [
13
+ ...TEMPLATE_IDS,
14
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
15
+ ];
16
+ function getUnknownNpmTemplateMessage(templateId) {
17
+ return [
18
+ `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(', ')}.`,
19
+ 'Run `wp-typia templates list` to inspect available templates.',
20
+ 'If you meant an npm template package, verify the package name and configured npm registry.',
21
+ ].join(' ');
22
+ }
12
23
  function selectRegistryVersion(metadata, locator) {
13
24
  const distTags = isPlainObject(metadata['dist-tags'])
14
25
  ? metadata['dist-tags']
@@ -49,6 +60,9 @@ async function fetchNpmTemplateSource(locator) {
49
60
  label: metadataLabel,
50
61
  });
51
62
  if (!metadataResponse.ok) {
63
+ if (metadataResponse.status === 404) {
64
+ throw new Error(getUnknownNpmTemplateMessage(locator.raw));
65
+ }
52
66
  throw new Error(`Failed to fetch npm template metadata for ${locator.raw}: ${metadataResponse.status}`);
53
67
  }
54
68
  const metadata = await readJsonResponseWithLimit(metadataResponse, {
@@ -14,6 +14,8 @@ export interface TypiaLlmEndpointMethodDescriptor {
14
14
  description?: string;
15
15
  /** Input type used by the generated controller method, or null for no input. */
16
16
  inputTypeName: string | null;
17
+ /** Type names that must be imported when inputTypeName is an inline composite. */
18
+ inputTypeImportNames?: readonly string[];
17
19
  /** HTTP method from the source endpoint manifest. */
18
20
  method: EndpointManifestEndpointDefinition['method'];
19
21
  /** Stable operation identifier used as the generated method name. */
@@ -84,21 +84,36 @@ const TYPESCRIPT_RESERVED_WORDS = new Set([
84
84
  function cloneJsonValueIfDefined(value) {
85
85
  return value === undefined ? undefined : cloneJsonValue(value);
86
86
  }
87
- function getEndpointInputTypeName(manifest, endpoint) {
87
+ function getContractSourceTypeName(manifest, endpoint, contractName) {
88
+ const contract = manifest.contracts[contractName];
89
+ if (!contract) {
90
+ throw new Error(`Endpoint "${endpoint.operationId}" references missing input contract "${contractName}".`);
91
+ }
92
+ return contract.sourceTypeName;
93
+ }
94
+ function getEndpointInputTypeDescriptor(manifest, endpoint) {
88
95
  if (endpoint.bodyContract && endpoint.queryContract) {
89
- throw new Error(`Endpoint "${endpoint.operationId}" defines both bodyContract and queryContract; typia.llm input mapping is ambiguous.`);
96
+ const bodyTypeName = getContractSourceTypeName(manifest, endpoint, endpoint.bodyContract);
97
+ const queryTypeName = getContractSourceTypeName(manifest, endpoint, endpoint.queryContract);
98
+ return {
99
+ importTypeNames: [bodyTypeName, queryTypeName],
100
+ signatureTypeName: `{ body: ${bodyTypeName}; query: ${queryTypeName} }`,
101
+ };
90
102
  }
91
103
  const contractName = endpoint.method === 'GET'
92
104
  ? endpoint.queryContract ?? null
93
105
  : endpoint.bodyContract ?? endpoint.queryContract ?? null;
94
106
  if (!contractName) {
95
- return null;
96
- }
97
- const contract = manifest.contracts[contractName];
98
- if (!contract) {
99
- throw new Error(`Endpoint "${endpoint.operationId}" references missing input contract "${contractName}".`);
107
+ return {
108
+ importTypeNames: [],
109
+ signatureTypeName: null,
110
+ };
100
111
  }
101
- return contract.sourceTypeName;
112
+ const sourceTypeName = getContractSourceTypeName(manifest, endpoint, contractName);
113
+ return {
114
+ importTypeNames: [sourceTypeName],
115
+ signatureTypeName: sourceTypeName,
116
+ };
102
117
  }
103
118
  function getEndpointOutputTypeName(manifest, endpoint) {
104
119
  const contract = manifest.contracts[endpoint.responseContract];
@@ -157,7 +172,12 @@ function renderTypiaLlmModuleFromMethodDescriptors({ applicationExportName, inte
157
172
  const importedTypeNames = new Set([structuredOutputTypeName]);
158
173
  for (const method of methods) {
159
174
  importedTypeNames.add(method.outputTypeName);
160
- if (method.inputTypeName) {
175
+ if (method.inputTypeImportNames) {
176
+ for (const inputTypeImportName of method.inputTypeImportNames) {
177
+ importedTypeNames.add(inputTypeImportName);
178
+ }
179
+ }
180
+ else if (method.inputTypeName) {
161
181
  importedTypeNames.add(method.inputTypeName);
162
182
  }
163
183
  }
@@ -233,11 +253,19 @@ async function reconcileGeneratedTypiaLlmArtifacts(artifacts, check) {
233
253
  export function buildTypiaLlmEndpointMethodDescriptors(manifest) {
234
254
  return manifest.endpoints.map((endpoint) => {
235
255
  const normalizedAuth = normalizeEndpointAuthDefinition(endpoint);
256
+ const inputTypeDescriptor = getEndpointInputTypeDescriptor(manifest, endpoint);
257
+ const inputTypeName = inputTypeDescriptor.signatureTypeName;
258
+ const shouldUseInlineInputImports = inputTypeName !== null &&
259
+ inputTypeDescriptor.importTypeNames.length > 0 &&
260
+ inputTypeDescriptor.importTypeNames[0] !== inputTypeName;
236
261
  return {
237
262
  authIntent: normalizedAuth.auth,
238
263
  ...(normalizedAuth.authMode ? { authMode: normalizedAuth.authMode } : {}),
239
264
  description: endpoint.summary,
240
- inputTypeName: getEndpointInputTypeName(manifest, endpoint),
265
+ inputTypeName,
266
+ ...(shouldUseInlineInputImports
267
+ ? { inputTypeImportNames: inputTypeDescriptor.importTypeNames }
268
+ : {}),
241
269
  method: endpoint.method,
242
270
  operationId: endpoint.operationId,
243
271
  outputTypeName: getEndpointOutputTypeName(manifest, endpoint),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",