@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.
- package/dist/runtime/cli-add-block.js +1 -1
- package/dist/runtime/cli-add-workspace-ability.js +147 -46
- package/dist/runtime/cli-add-workspace-rest.js +0 -1
- package/dist/runtime/cli-diagnostics.d.ts +1 -0
- package/dist/runtime/cli-diagnostics.js +5 -1
- package/dist/runtime/cli-doctor-workspace.js +7 -4
- package/dist/runtime/scaffold-answer-resolution.js +29 -5
- package/dist/runtime/template-source-seeds.js +15 -1
- package/dist/runtime/typia-llm.d.ts +2 -0
- package/dist/runtime/typia-llm.js +38 -10
- package/package.json +1 -1
|
@@ -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
|
|
16
|
-
const
|
|
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
|
|
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
|
|
133
|
-
\
|
|
134
|
-
\t\
|
|
135
|
-
\t
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
\
|
|
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
|
-
|
|
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
|
-
\
|
|
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
|
|
190
|
-
*
|
|
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( '${
|
|
563
|
-
\t\
|
|
564
|
-
\t\
|
|
565
|
-
\t\t\
|
|
566
|
-
\t\t\t
|
|
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
|
-
\
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
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
|
|
82
|
-
return (templateId.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new Error(`Endpoint "${endpoint.operationId}" references missing input contract "${contractName}".`);
|
|
107
|
+
return {
|
|
108
|
+
importTypeNames: [],
|
|
109
|
+
signatureTypeName: null,
|
|
110
|
+
};
|
|
100
111
|
}
|
|
101
|
-
|
|
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.
|
|
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
|
|
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),
|