@wp-typia/project-tools 0.24.10 → 0.24.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/runtime/add/cli-add-block-config.js +7 -15
  2. package/dist/runtime/add/cli-add-block-package-json.d.ts +5 -0
  3. package/dist/runtime/add/cli-add-block-package-json.js +32 -0
  4. package/dist/runtime/add/cli-add-block.js +17 -4
  5. package/dist/runtime/add/cli-add-workspace-admin-view-scaffold.js +1 -5
  6. package/dist/runtime/add/persistence-rest-artifacts.d.ts +32 -7
  7. package/dist/runtime/add/persistence-rest-artifacts.js +41 -15
  8. package/dist/runtime/cli/cli-diagnostics.d.ts +9 -0
  9. package/dist/runtime/cli/cli-diagnostics.js +8 -0
  10. package/dist/runtime/cli/cli-init-package-json.js +5 -1
  11. package/dist/runtime/doctor/cli-doctor-standalone-control-flow.d.ts +38 -0
  12. package/dist/runtime/doctor/cli-doctor-standalone-control-flow.js +365 -0
  13. package/dist/runtime/doctor/cli-doctor-standalone-rest.d.ts +21 -0
  14. package/dist/runtime/doctor/cli-doctor-standalone-rest.js +1373 -0
  15. package/dist/runtime/doctor/cli-doctor-standalone.d.ts +32 -0
  16. package/dist/runtime/doctor/cli-doctor-standalone.js +2613 -0
  17. package/dist/runtime/doctor/cli-doctor-wordpress-version.js +6 -3
  18. package/dist/runtime/doctor/cli-doctor-workspace.d.ts +5 -3
  19. package/dist/runtime/doctor/cli-doctor-workspace.js +27 -3
  20. package/dist/runtime/doctor/cli-doctor.d.ts +4 -2
  21. package/dist/runtime/doctor/cli-doctor.js +4 -2
  22. package/dist/runtime/shared/json-utils.d.ts +7 -0
  23. package/dist/runtime/shared/json-utils.js +10 -0
  24. package/dist/runtime/shared/package-json-types.d.ts +1 -1
  25. package/dist/runtime/shared/package-versions.d.ts +2 -0
  26. package/dist/runtime/shared/package-versions.js +2 -0
  27. package/dist/runtime/shared/php-utils.d.ts +39 -2
  28. package/dist/runtime/shared/php-utils.js +205 -6
  29. package/dist/runtime/templates/block-generator-service-core.d.ts +1 -1
  30. package/dist/runtime/templates/block-generator-service-core.js +9 -3
  31. package/dist/runtime/templates/block-generator-service-spec.d.ts +5 -0
  32. package/dist/runtime/templates/built-in-block-artifact-documents.d.ts +1 -1
  33. package/dist/runtime/templates/built-in-block-artifact-documents.js +1 -1
  34. package/dist/runtime/templates/built-in-block-artifact-types.js +2 -2
  35. package/dist/runtime/templates/built-in-block-artifacts.d.ts +1 -1
  36. package/dist/runtime/templates/built-in-block-artifacts.js +16 -12
  37. package/dist/runtime/templates/built-in-block-attribute-emitters.d.ts +5 -0
  38. package/dist/runtime/templates/built-in-block-attribute-emitters.js +113 -8
  39. package/dist/runtime/templates/built-in-block-attribute-specs.js +4 -9
  40. package/dist/runtime/templates/built-in-block-code-artifacts.js +6 -1
  41. package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.d.ts +2 -2
  42. package/dist/runtime/templates/built-in-block-code-templates/compound-persistence.js +24 -1
  43. package/dist/runtime/templates/built-in-block-code-templates/persistence.d.ts +2 -2
  44. package/dist/runtime/templates/built-in-block-code-templates/persistence.js +22 -1
  45. package/dist/runtime/templates/built-in-block-non-ts-compound-templates.d.ts +2 -2
  46. package/dist/runtime/templates/built-in-block-non-ts-compound-templates.js +6 -2
  47. package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.d.ts +2 -2
  48. package/dist/runtime/templates/built-in-block-non-ts-persistence-templates.js +6 -2
  49. package/dist/runtime/templates/cli-scaffold-emission.d.ts +7 -1
  50. package/dist/runtime/templates/cli-scaffold-emission.js +10 -1
  51. package/dist/runtime/templates/cli-templates.d.ts +47 -0
  52. package/dist/runtime/templates/cli-templates.js +50 -1
  53. package/dist/runtime/templates/local-dev-presets.js +1 -1
  54. package/dist/runtime/templates/scaffold-apply-utils.d.ts +4 -8
  55. package/dist/runtime/templates/scaffold-apply-utils.js +44 -28
  56. package/dist/runtime/templates/scaffold-compiler-artifacts.d.ts +17 -0
  57. package/dist/runtime/templates/scaffold-compiler-artifacts.js +81 -0
  58. package/dist/runtime/templates/scaffold-onboarding.d.ts +5 -0
  59. package/dist/runtime/templates/scaffold-onboarding.js +13 -2
  60. package/dist/runtime/templates/scaffold-template-variable-groups.d.ts +4 -0
  61. package/dist/runtime/templates/scaffold-template-variable-groups.js +7 -0
  62. package/dist/runtime/templates/scaffold.d.ts +3 -1
  63. package/dist/runtime/templates/scaffold.js +2 -1
  64. package/dist/runtime/templates/starter-manifests.js +1 -1
  65. package/package.json +3 -3
  66. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +1 -1
  67. package/templates/_shared/compound/persistence/package.json.mustache +1 -0
  68. package/templates/_shared/persistence/core/package.json.mustache +1 -0
@@ -541,6 +541,9 @@ function formatRequirementSummary(requirements, floor) {
541
541
  .map((requirement) => requirement.label);
542
542
  return labels.length > 0 ? labels.join(", ") : "generated workspace features";
543
543
  }
544
+ function formatFeatureMinimumAction(floor) {
545
+ return `Update the plugin bootstrap \`Requires at least\` header to ${floor} or remove the features that require that floor.`;
546
+ }
544
547
  function createFeatureMinimumCheck(workspace, inventory, headers) {
545
548
  const { issues, requirements } = collectWordPressVersionRequirements(workspace, inventory);
546
549
  const highestFloor = pickHighestRequirementFloor(requirements, issues);
@@ -551,11 +554,11 @@ function createFeatureMinimumCheck(workspace, inventory, headers) {
551
554
  return createDoctorCheck("WordPress feature minimum", "pass", "No generated workspace features declare an additional WordPress hard floor.", WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
552
555
  }
553
556
  if (!headers.requiresAtLeast) {
554
- return createDoctorCheck("WordPress feature minimum", "fail", `Plugin bootstrap is missing a Requires at least header but generated features require WordPress ${highestFloor}.`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
557
+ return createDoctorCheck("WordPress feature minimum", "fail", `Plugin bootstrap is missing a Requires at least header but generated features require WordPress ${highestFloor}. ${formatFeatureMinimumAction(highestFloor)}`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
555
558
  }
556
559
  try {
557
560
  if (compareVersionFloors(headers.requiresAtLeast, highestFloor) < 0) {
558
- return createDoctorCheck("WordPress feature minimum", "fail", `Requires at least ${headers.requiresAtLeast} is below generated feature floor ${highestFloor} (${formatRequirementSummary(requirements, highestFloor)}).`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
561
+ return createDoctorCheck("WordPress feature minimum", "fail", `Requires at least ${headers.requiresAtLeast} is below generated feature floor ${highestFloor} (${formatRequirementSummary(requirements, highestFloor)}). ${formatFeatureMinimumAction(highestFloor)}`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
559
562
  }
560
563
  }
561
564
  catch {
@@ -569,7 +572,7 @@ function createTestedTargetCheck(headers, targetVersion) {
569
572
  }
570
573
  try {
571
574
  if (compareVersionFloors(headers.testedUpTo, targetVersion) < 0) {
572
- return createDoctorCheck("WordPress tested target", "warn", `Tested up to ${headers.testedUpTo} is below the selected WordPress target ${targetVersion}.`, WORDPRESS_VERSION_CHECK_CODES.testedTarget);
575
+ return createDoctorCheck("WordPress tested target", "warn", `Tested up to ${headers.testedUpTo} is below the selected WordPress target ${targetVersion}. Update the plugin bootstrap \`Tested up to\` header when the workspace is verified against WordPress ${targetVersion}.`, WORDPRESS_VERSION_CHECK_CODES.testedTarget);
573
576
  }
574
577
  }
575
578
  catch {
@@ -7,9 +7,11 @@ export interface WorkspaceDoctorChecksOptions {
7
7
  /**
8
8
  * Collect workspace-scoped doctor checks for the given working directory.
9
9
  *
10
- * When the directory is not an official workspace, the function returns a
11
- * "Doctor scope" row explaining that only environment checks ran, plus a
12
- * failing workspace metadata row when a nearby candidate workspace is invalid.
10
+ * When the directory is not an official workspace, the function runs supported
11
+ * standalone single-block diagnostics when that project surface is detected.
12
+ * Otherwise it returns a "Doctor scope" row explaining that only environment
13
+ * checks ran, plus a failing workspace metadata row when a nearby candidate
14
+ * workspace is invalid.
13
15
  * When workspace resolution or metadata parsing throws, the corresponding
14
16
  * failing rows are returned early and the remaining checks are skipped.
15
17
  * When an official workspace is detected, a passing "Doctor scope" row is
@@ -4,6 +4,7 @@ import { getWorkspaceFeatureDoctorChecks, } from "./cli-doctor-workspace-feature
4
4
  import { getWorkspaceWordPressVersionDoctorChecks, } from "./cli-doctor-wordpress-version.js";
5
5
  import { getMigrationWorkspaceHintCheck, getWorkspacePackageMetadataCheck, prepareWorkspacePackageDoctorSnapshot, } from "./cli-doctor-workspace-package.js";
6
6
  import { createDoctorCheck, createDoctorScopeCheck, } from "./cli-doctor-workspace-shared.js";
7
+ import { getStandaloneScaffoldDoctorChecks, tryResolveStandaloneScaffoldProject, } from "./cli-doctor-standalone.js";
7
8
  import { readWorkspaceInventoryAsync, } from "../workspace/workspace-inventory.js";
8
9
  import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, tryResolveWorkspaceProject, } from "../workspace/workspace-project.js";
9
10
  function formatWorkspaceInventorySummary(inventory) {
@@ -25,9 +26,11 @@ function formatWorkspaceInventorySummary(inventory) {
25
26
  /**
26
27
  * Collect workspace-scoped doctor checks for the given working directory.
27
28
  *
28
- * When the directory is not an official workspace, the function returns a
29
- * "Doctor scope" row explaining that only environment checks ran, plus a
30
- * failing workspace metadata row when a nearby candidate workspace is invalid.
29
+ * When the directory is not an official workspace, the function runs supported
30
+ * standalone single-block diagnostics when that project surface is detected.
31
+ * Otherwise it returns a "Doctor scope" row explaining that only environment
32
+ * checks ran, plus a failing workspace metadata row when a nearby candidate
33
+ * workspace is invalid.
31
34
  * When workspace resolution or metadata parsing throws, the corresponding
32
35
  * failing rows are returned early and the remaining checks are skipped.
33
36
  * When an official workspace is detected, a passing "Doctor scope" row is
@@ -42,6 +45,20 @@ function formatWorkspaceInventorySummary(inventory) {
42
45
  */
43
46
  export async function getWorkspaceDoctorChecks(cwd, options = {}) {
44
47
  const checks = [];
48
+ let standaloneProject = null;
49
+ let standaloneDiscoveryError = null;
50
+ try {
51
+ // The nearest standalone package boundary wins over unrelated ancestor
52
+ // workspace metadata. Official workspace manifests are rejected by the
53
+ // standalone detector and continue through workspace discovery below.
54
+ standaloneProject = tryResolveStandaloneScaffoldProject(cwd);
55
+ }
56
+ catch (error) {
57
+ standaloneDiscoveryError = error;
58
+ }
59
+ if (standaloneProject) {
60
+ return getStandaloneScaffoldDoctorChecks(standaloneProject);
61
+ }
45
62
  let workspace = null;
46
63
  let invalidWorkspaceReason = null;
47
64
  try {
@@ -61,6 +78,13 @@ export async function getWorkspaceDoctorChecks(cwd, options = {}) {
61
78
  checks.push(createDoctorCheck("Workspace package metadata", "fail", invalidWorkspaceReason));
62
79
  }
63
80
  else {
81
+ if (standaloneDiscoveryError) {
82
+ checks.push(createDoctorScopeCheck("fail", "Scope: blocked before standalone scaffold checks. Environment checks ran, but standalone discovery could not continue. Fix the nearest package metadata and rerun `wp-typia doctor`."));
83
+ checks.push(createDoctorCheck("Standalone package metadata", "fail", standaloneDiscoveryError instanceof Error
84
+ ? standaloneDiscoveryError.message
85
+ : String(standaloneDiscoveryError)));
86
+ return checks;
87
+ }
64
88
  checks.push(createDoctorScopeCheck("pass", "Scope: environment-only. No official wp-typia workspace root was detected, so this run only covered environment readiness. Re-run `wp-typia doctor` from a workspace root if you expected package metadata, inventory, or generated artifact checks."));
65
89
  }
66
90
  return checks;
@@ -56,8 +56,10 @@ export interface GetDoctorChecksOptions {
56
56
  *
57
57
  * The returned array concatenates environment checks (command availability,
58
58
  * directory writability, and built-in template assets) followed by
59
- * workspace checks (package metadata, inventory, blocks, variations,
60
- * patterns, bindings, and optional migration hints) in display order.
59
+ * project checks in display order. Official workspaces cover package metadata,
60
+ * inventory, blocks, variations, patterns, bindings, and optional migration
61
+ * hints. Supported standalone scaffolds cover package/bootstrap integrity,
62
+ * source layout, dependencies, and canonical generated-artifact freshness.
61
63
  *
62
64
  * @param cwd Working directory to validate for writability.
63
65
  * @param options Optional feature gates for additional doctor rows.
@@ -42,8 +42,10 @@ function toFailureSummary(check, severity) {
42
42
  *
43
43
  * The returned array concatenates environment checks (command availability,
44
44
  * directory writability, and built-in template assets) followed by
45
- * workspace checks (package metadata, inventory, blocks, variations,
46
- * patterns, bindings, and optional migration hints) in display order.
45
+ * project checks in display order. Official workspaces cover package metadata,
46
+ * inventory, blocks, variations, patterns, bindings, and optional migration
47
+ * hints. Supported standalone scaffolds cover package/bootstrap integrity,
48
+ * source layout, dependencies, and canonical generated-artifact freshness.
47
49
  *
48
50
  * @param cwd Working directory to validate for writability.
49
51
  * @param options Optional feature gates for additional doctor rows.
@@ -24,6 +24,13 @@
24
24
  * @returns A deep-cloned copy created with `JSON.parse(JSON.stringify(...))`.
25
25
  */
26
26
  export declare function cloneJsonValue<T>(value: T): T;
27
+ /**
28
+ * Detect the indentation used by a formatted JSON document.
29
+ *
30
+ * @param source Raw JSON source text.
31
+ * @returns The first property indentation, or two spaces for compact/empty JSON.
32
+ */
33
+ export declare function detectJsonIndent(source: string): string | number;
27
34
  /**
28
35
  * Optional metadata used to enrich JSON parse errors.
29
36
  */
@@ -28,6 +28,16 @@ import { promises as fsp } from "node:fs";
28
28
  export function cloneJsonValue(value) {
29
29
  return JSON.parse(JSON.stringify(value));
30
30
  }
31
+ /**
32
+ * Detect the indentation used by a formatted JSON document.
33
+ *
34
+ * @param source Raw JSON source text.
35
+ * @returns The first property indentation, or two spaces for compact/empty JSON.
36
+ */
37
+ export function detectJsonIndent(source) {
38
+ const indentMatch = /\n([ \t]+)"/u.exec(source);
39
+ return indentMatch?.[1] ?? 2;
40
+ }
31
41
  function formatJsonParseTarget({ context, filePath }) {
32
42
  const operation = context?.trim() || "JSON";
33
43
  return filePath ? `${operation} at ${filePath}` : operation;
@@ -1,7 +1,7 @@
1
1
  export interface GeneratedPackageJson {
2
2
  dependencies?: Record<string, string>;
3
3
  devDependencies?: Record<string, string>;
4
- packageManager?: string;
4
+ packageManager?: unknown;
5
5
  scripts?: Record<string, string>;
6
6
  wpTypia?: {
7
7
  projectType?: string;
@@ -19,6 +19,8 @@ export interface PackageVersions {
19
19
  * not drift across runtime modules.
20
20
  */
21
21
  export declare const DEFAULT_WORDPRESS_ABILITIES_VERSION = "^0.10.0";
22
+ export declare const DEFAULT_WORDPRESS_BLOCKS_TYPES_VERSION = "^12.5.18";
23
+ export declare const DEFAULT_WORDPRESS_BLOCKS_VERSION = "~15.19.0";
22
24
  export declare const DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION = "^0.9.0";
23
25
  export declare const DEFAULT_WORDPRESS_CORE_DATA_VERSION = "~7.46.0";
24
26
  export declare const DEFAULT_WORDPRESS_DATA_VERSION = "~10.46.0";
@@ -17,6 +17,8 @@ const DEFAULT_TYPIA_UNPLUGIN_PACKAGE_VERSION = '^12.0.1';
17
17
  * not drift across runtime modules.
18
18
  */
19
19
  export const DEFAULT_WORDPRESS_ABILITIES_VERSION = '^0.10.0';
20
+ export const DEFAULT_WORDPRESS_BLOCKS_TYPES_VERSION = '^12.5.18';
21
+ export const DEFAULT_WORDPRESS_BLOCKS_VERSION = '~15.19.0';
20
22
  export const DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION = '^0.9.0';
21
23
  export const DEFAULT_WORDPRESS_CORE_DATA_VERSION = '~7.46.0';
22
24
  export const DEFAULT_WORDPRESS_DATA_VERSION = '~10.46.0';
@@ -3,7 +3,12 @@ export type PhpFunctionRange = {
3
3
  source: string;
4
4
  start: number;
5
5
  };
6
- export type PhpFunctionRangeOptions = {
6
+ /** Options controlling whether PHP call helpers scan snippets or full PHP files. */
7
+ export type PhpFunctionCallScanOptions = {
8
+ /** Ignore text outside explicit PHP open/close tag regions. */
9
+ requirePhpOpenTag?: boolean;
10
+ };
11
+ export type PhpFunctionRangeOptions = PhpFunctionCallScanOptions & {
7
12
  includeTrailingNewlines?: boolean;
8
13
  };
9
14
  export type ReplacePhpFunctionDefinitionOptions = PhpFunctionRangeOptions & {
@@ -17,9 +22,38 @@ export declare function hasPhpFunctionDefinition(source: string, functionName: s
17
22
  *
18
23
  * @param source PHP source to scan.
19
24
  * @param functionName Literal PHP function identifier to find.
25
+ * @param options Scanner options for full files that require explicit PHP tags.
20
26
  * @returns Whether `source` contains a code-mode call to `functionName`.
21
27
  */
22
- export declare function hasPhpFunctionCall(source: string, functionName: string): boolean;
28
+ export declare function hasPhpFunctionCall(source: string, functionName: string, options?: PhpFunctionCallScanOptions): boolean;
29
+ /**
30
+ * Find the exclusive end offset of one PHP function call at a known offset.
31
+ *
32
+ * Parentheses inside strings, comments, heredoc, and nowdoc blocks are ignored
33
+ * by the same scanner used by {@link hasPhpFunctionCall}.
34
+ *
35
+ * @param source PHP source containing the call.
36
+ * @param functionOffset Offset where the function identifier starts.
37
+ * @param functionName Literal PHP function identifier expected at the offset.
38
+ * @param options Scanner options for full files that require explicit PHP tags.
39
+ * @returns The offset after the matching closing parenthesis, or `null`.
40
+ */
41
+ export declare function findPhpFunctionCallEnd(source: string, functionOffset: number, functionName: string, options?: PhpFunctionCallScanOptions): number | null;
42
+ type PhpStringArgumentMatcher = string | ((value: string) => boolean);
43
+ /**
44
+ * Detect a PHP function call whose leading arguments are literal strings that
45
+ * satisfy the supplied matchers.
46
+ *
47
+ * Each matcher applies to the corresponding argument in the same code-mode
48
+ * function call. Additional arguments after the matched prefix are allowed.
49
+ *
50
+ * @param source PHP source to scan.
51
+ * @param functionName Literal PHP function identifier to find.
52
+ * @param argumentMatchers Matchers for the leading literal string arguments.
53
+ * @param options Scanner options for full files that require explicit PHP tags.
54
+ * @returns Whether one code-mode call satisfies every supplied matcher.
55
+ */
56
+ export declare function hasPhpFunctionCallWithStringArguments(source: string, functionName: string, argumentMatchers: readonly PhpStringArgumentMatcher[], options?: PhpFunctionCallScanOptions): boolean;
23
57
  /**
24
58
  * Detect a PHP function call whose first argument is a variable previously
25
59
  * assigned from a literal string prefix in executable PHP code.
@@ -46,6 +80,8 @@ export declare function hasPhpFunctionCallWithStringArgumentPrefix(source: strin
46
80
  * global function call consumes the variable.
47
81
  */
48
82
  export declare function hasPhpCodeStringLiteralPrefix(source: string, literalPrefix: string): boolean;
83
+ /** Return the PHP code-block brace depth at an executable source offset. */
84
+ export declare function getPhpCodeBraceDepth(source: string, offset: number, options?: PhpFunctionCallScanOptions): number | null;
49
85
  /**
50
86
  * Locate a PHP function body without counting braces in non-code regions.
51
87
  *
@@ -56,3 +92,4 @@ export declare function hasPhpCodeStringLiteralPrefix(source: string, literalPre
56
92
  */
57
93
  export declare function findPhpFunctionRange(source: string, functionName: string, options?: PhpFunctionRangeOptions): PhpFunctionRange | null;
58
94
  export declare function replacePhpFunctionDefinition(source: string, functionName: string, replacement: string, options?: ReplacePhpFunctionDefinitionOptions): string | null;
95
+ export {};
@@ -202,17 +202,43 @@ function getPhpFunctionCallFirstArgumentStart(source, index, functionName) {
202
202
  }
203
203
  return skipPhpCallTrivia(source, callStart + 1);
204
204
  }
205
- function createPhpScannerState() {
205
+ function createPhpScannerState(options = {}) {
206
+ const requirePhpOpenTag = options.requirePhpOpenTag === true;
206
207
  return {
207
208
  heredocDelimiter: "",
209
+ inPhp: !requirePhpOpenTag,
208
210
  interpolationComment: "",
209
211
  interpolationDepth: 0,
210
212
  interpolationQuote: "",
211
213
  mode: "code",
214
+ requirePhpOpenTag,
212
215
  };
213
216
  }
217
+ function getPhpOpenTagLength(source, index) {
218
+ if (source.startsWith("<?=", index)) {
219
+ return 3;
220
+ }
221
+ if (source.slice(index, index + 5).toLowerCase() === "<?php") {
222
+ const nextCharacter = source[index + 5];
223
+ return nextCharacter === undefined || isPhpWhitespace(nextCharacter) ? 5 : 0;
224
+ }
225
+ return 0;
226
+ }
214
227
  function advancePhpScanner(source, index, state) {
215
228
  const character = source[index];
229
+ if (state.requirePhpOpenTag && !state.inPhp) {
230
+ const openTagLength = getPhpOpenTagLength(source, index);
231
+ if (openTagLength > 0) {
232
+ state.inPhp = true;
233
+ state.mode = "code";
234
+ return {
235
+ ambiguous: false,
236
+ inCode: false,
237
+ index: index + openTagLength,
238
+ };
239
+ }
240
+ return { ambiguous: false, inCode: false, index: index + 1 };
241
+ }
216
242
  if (state.mode === "heredoc") {
217
243
  const closingEnd = findPhpHeredocClosingEnd(source, index, state.heredocDelimiter);
218
244
  if (closingEnd !== null) {
@@ -300,6 +326,13 @@ function advancePhpScanner(source, index, state) {
300
326
  return { ambiguous: false, inCode: false, index: index + 1 };
301
327
  }
302
328
  if (state.mode === "line-comment") {
329
+ if (state.requirePhpOpenTag &&
330
+ character === "?" &&
331
+ source[index + 1] === ">") {
332
+ state.inPhp = false;
333
+ state.mode = "code";
334
+ return { ambiguous: false, inCode: false, index: index + 2 };
335
+ }
303
336
  if (character === "\r" || character === "\n") {
304
337
  state.mode = "code";
305
338
  }
@@ -312,6 +345,13 @@ function advancePhpScanner(source, index, state) {
312
345
  }
313
346
  return { ambiguous: false, inCode: false, index: index + 1 };
314
347
  }
348
+ if (state.requirePhpOpenTag &&
349
+ character === "?" &&
350
+ source[index + 1] === ">") {
351
+ state.inPhp = false;
352
+ state.mode = "code";
353
+ return { ambiguous: false, inCode: false, index: index + 2 };
354
+ }
315
355
  if (character === "'") {
316
356
  state.mode = "single-quoted";
317
357
  return { ambiguous: false, inCode: false, index: index + 1 };
@@ -351,10 +391,11 @@ function advancePhpScanner(source, index, state) {
351
391
  *
352
392
  * @param source PHP source to scan.
353
393
  * @param functionName Literal PHP function identifier to find.
394
+ * @param options Scanner options for full files that require explicit PHP tags.
354
395
  * @returns Whether `source` contains a code-mode call to `functionName`.
355
396
  */
356
- export function hasPhpFunctionCall(source, functionName) {
357
- const scanner = createPhpScannerState();
397
+ export function hasPhpFunctionCall(source, functionName, options = {}) {
398
+ const scanner = createPhpScannerState(options);
358
399
  let index = 0;
359
400
  while (index < source.length) {
360
401
  const scan = advancePhpScanner(source, index, scanner);
@@ -372,6 +413,125 @@ export function hasPhpFunctionCall(source, functionName) {
372
413
  }
373
414
  return false;
374
415
  }
416
+ /**
417
+ * Find the exclusive end offset of one PHP function call at a known offset.
418
+ *
419
+ * Parentheses inside strings, comments, heredoc, and nowdoc blocks are ignored
420
+ * by the same scanner used by {@link hasPhpFunctionCall}.
421
+ *
422
+ * @param source PHP source containing the call.
423
+ * @param functionOffset Offset where the function identifier starts.
424
+ * @param functionName Literal PHP function identifier expected at the offset.
425
+ * @param options Scanner options for full files that require explicit PHP tags.
426
+ * @returns The offset after the matching closing parenthesis, or `null`.
427
+ */
428
+ export function findPhpFunctionCallEnd(source, functionOffset, functionName, options = {}) {
429
+ if (!matchesPhpFunctionCallAt(source, functionOffset, functionName)) {
430
+ return null;
431
+ }
432
+ const openParenthesis = skipPhpCallTrivia(source, functionOffset + functionName.length);
433
+ if (openParenthesis === null || source[openParenthesis] !== "(") {
434
+ return null;
435
+ }
436
+ const scanner = createPhpScannerState(options);
437
+ let depth = 0;
438
+ let index = 0;
439
+ while (index < source.length) {
440
+ const scan = advancePhpScanner(source, index, scanner);
441
+ if (scan.ambiguous) {
442
+ return null;
443
+ }
444
+ if (!scan.inCode) {
445
+ index = scan.index;
446
+ continue;
447
+ }
448
+ if (index < openParenthesis) {
449
+ index += 1;
450
+ continue;
451
+ }
452
+ if (source[index] === "(") {
453
+ depth += 1;
454
+ }
455
+ else if (source[index] === ")") {
456
+ depth -= 1;
457
+ if (depth === 0) {
458
+ return index + 1;
459
+ }
460
+ }
461
+ index += 1;
462
+ }
463
+ return null;
464
+ }
465
+ function matchesPhpStringArgument(matcher, value) {
466
+ return typeof matcher === "string" ? value === matcher : matcher(value);
467
+ }
468
+ /**
469
+ * Detect a PHP function call whose leading arguments are literal strings that
470
+ * satisfy the supplied matchers.
471
+ *
472
+ * Each matcher applies to the corresponding argument in the same code-mode
473
+ * function call. Additional arguments after the matched prefix are allowed.
474
+ *
475
+ * @param source PHP source to scan.
476
+ * @param functionName Literal PHP function identifier to find.
477
+ * @param argumentMatchers Matchers for the leading literal string arguments.
478
+ * @param options Scanner options for full files that require explicit PHP tags.
479
+ * @returns Whether one code-mode call satisfies every supplied matcher.
480
+ */
481
+ export function hasPhpFunctionCallWithStringArguments(source, functionName, argumentMatchers, options = {}) {
482
+ if (argumentMatchers.length === 0) {
483
+ return hasPhpFunctionCall(source, functionName, options);
484
+ }
485
+ const scanner = createPhpScannerState(options);
486
+ let index = 0;
487
+ while (index < source.length) {
488
+ const scan = advancePhpScanner(source, index, scanner);
489
+ if (scan.ambiguous) {
490
+ return false;
491
+ }
492
+ if (!scan.inCode) {
493
+ index = scan.index;
494
+ continue;
495
+ }
496
+ if (!matchesPhpFunctionCallAt(source, index, functionName)) {
497
+ index += 1;
498
+ continue;
499
+ }
500
+ let argumentStart = getPhpFunctionCallFirstArgumentStart(source, index, functionName);
501
+ let argumentsMatch = argumentStart !== null;
502
+ for (const [argumentIndex, matcher] of argumentMatchers.entries()) {
503
+ if (!argumentsMatch || argumentStart === null) {
504
+ argumentsMatch = false;
505
+ break;
506
+ }
507
+ const argument = parsePhpQuotedStringLiteralAt(source, argumentStart);
508
+ if (!argument ||
509
+ !matchesPhpStringArgument(matcher, argument.value)) {
510
+ argumentsMatch = false;
511
+ break;
512
+ }
513
+ const argumentEnd = skipPhpCallTrivia(source, argument.end);
514
+ const nextToken = argumentEnd === null ? undefined : source[argumentEnd];
515
+ const isLastMatcher = argumentIndex === argumentMatchers.length - 1;
516
+ if (isLastMatcher) {
517
+ argumentsMatch =
518
+ argumentEnd !== null && (nextToken === "," || nextToken === ")");
519
+ break;
520
+ }
521
+ if (argumentEnd === null || nextToken !== ",") {
522
+ argumentsMatch = false;
523
+ break;
524
+ }
525
+ argumentStart = skipPhpCallTrivia(source, argumentEnd + 1);
526
+ argumentsMatch = argumentStart !== null;
527
+ }
528
+ if (argumentsMatch) {
529
+ return true;
530
+ }
531
+ index += functionName.length;
532
+ }
533
+ return false;
534
+ }
375
535
  function isPhpAssignmentOperatorAt(source, index) {
376
536
  if (index === null || source[index] !== "=") {
377
537
  return false;
@@ -577,6 +737,36 @@ function hasPhpFunctionCallWithStringArgumentMatching(source, functionName, matc
577
737
  }
578
738
  return false;
579
739
  }
740
+ /** Return the PHP code-block brace depth at an executable source offset. */
741
+ export function getPhpCodeBraceDepth(source, offset, options = {}) {
742
+ const scanner = createPhpScannerState(options);
743
+ let depth = 0;
744
+ let index = 0;
745
+ while (index < source.length && index <= offset) {
746
+ const scan = advancePhpScanner(source, index, scanner);
747
+ if (index === offset) {
748
+ return !scan.ambiguous && scan.inCode ? depth : null;
749
+ }
750
+ if (scan.ambiguous) {
751
+ return null;
752
+ }
753
+ if (!scan.inCode) {
754
+ index = scan.index;
755
+ continue;
756
+ }
757
+ if (source[index] === "{") {
758
+ depth += 1;
759
+ }
760
+ else if (source[index] === "}") {
761
+ depth = Math.max(0, depth - 1);
762
+ }
763
+ index += 1;
764
+ }
765
+ return null;
766
+ }
767
+ function isPhpCodeOffset(source, offset, options) {
768
+ return getPhpCodeBraceDepth(source, offset, options) !== null;
769
+ }
580
770
  /**
581
771
  * Locate a PHP function body without counting braces in non-code regions.
582
772
  *
@@ -586,8 +776,14 @@ function hasPhpFunctionCallWithStringArgumentMatching(source, functionName, matc
586
776
  * @returns The matched {@link PhpFunctionRange}, or `null` when no safe range exists.
587
777
  */
588
778
  export function findPhpFunctionRange(source, functionName, options = {}) {
589
- const signaturePattern = new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\([^)]*\\)\\s*(?::\\s*[^{};]+)?\\s*\\{`, "u");
590
- const signatureMatch = signaturePattern.exec(source);
779
+ const signaturePattern = new RegExp(`function\\s+&?\\s*${escapeRegex(functionName)}\\s*\\([^)]*\\)\\s*(?::\\s*[^{};]+)?\\s*\\{`, "gu");
780
+ let signatureMatch = null;
781
+ for (let candidate = signaturePattern.exec(source); candidate; candidate = signaturePattern.exec(source)) {
782
+ if (isPhpCodeOffset(source, candidate.index, options)) {
783
+ signatureMatch = candidate;
784
+ break;
785
+ }
786
+ }
591
787
  if (!signatureMatch) {
592
788
  return null;
593
789
  }
@@ -598,7 +794,10 @@ export function findPhpFunctionRange(source, functionName, options = {}) {
598
794
  }
599
795
  const openBraceIndex = functionStart + openBraceOffset;
600
796
  let depth = 0;
601
- const scanner = createPhpScannerState();
797
+ const scanner = createPhpScannerState(options);
798
+ // The signature was already verified at a PHP code offset, and the body scan
799
+ // starts at its opening brace rather than at the beginning of the source.
800
+ scanner.inPhp = true;
602
801
  let index = openBraceIndex;
603
802
  while (index < source.length) {
604
803
  const scan = advancePhpScanner(source, index, scanner);
@@ -4,5 +4,5 @@ export declare class BlockGeneratorService {
4
4
  plan({ allowExistingDir, alternateRenderTargets, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, noInstall, packageManager, persistencePolicy, projectDir, repositoryReference, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, wpVersion, }: PlanBlockInput): Promise<PlanBlockResult>;
5
5
  validate({ plan }: ValidateBlockInput): Promise<ValidateBlockResult>;
6
6
  render({ validated }: RenderBlockInput): Promise<RenderBlockResult>;
7
- apply({ rendered, installDependencies, onProgress, }: ApplyBlockInput): Promise<ScaffoldProjectResult>;
7
+ apply({ rendered, installDependencies, onProgress, seedCompilerArtifacts, }: ApplyBlockInput): Promise<ScaffoldProjectResult>;
8
8
  }
@@ -7,6 +7,7 @@ import { resolveTemplateSeed, parseTemplateLocator } from "./template-source.js"
7
7
  import { assertExternalTemplateLayersDoNotWriteProtectedOutputs, resolveExternalTemplateLayers, } from "./template-layers.js";
8
8
  import { getBuiltInTemplateOverlayDir, getBuiltInTemplateSharedLayerDirs, resolveBuiltInTemplateSource, resolveBuiltInTemplateSourceFromLayerDirs, } from "./template-builtins.js";
9
9
  import { buildTemplateVariablesFromBlockSpec, createBuiltInBlockSpec, } from "./block-generator-service-spec.js";
10
+ import { collectBuiltInCompilerArtifactPaths } from "./scaffold-compiler-artifacts.js";
10
11
  import { assertBuiltInTemplateVariantAllowed, assertExternalLayerCompositionOptions, } from "../cli/cli-validation.js";
11
12
  const renderedArtifactCache = new WeakMap();
12
13
  function createVariablesFingerprint(variables) {
@@ -32,6 +33,9 @@ function buildProtectedTemplateOutputPaths({ codeArtifacts, spec, variables, art
32
33
  protectedOutputs.add(`${artifact.relativeDir}/block.json`);
33
34
  protectedOutputs.add(`${artifact.relativeDir}/types.ts`);
34
35
  }
36
+ for (const artifactPath of collectBuiltInCompilerArtifactPaths(spec.template.family, variables)) {
37
+ protectedOutputs.add(artifactPath);
38
+ }
35
39
  for (const manifest of getStarterManifestFiles(spec.template.family, variables)) {
36
40
  protectedOutputs.add(manifest.relativePath);
37
41
  }
@@ -227,7 +231,7 @@ export class BlockGeneratorService {
227
231
  });
228
232
  return rendered;
229
233
  }
230
- async apply({ rendered, installDependencies, onProgress, }) {
234
+ async apply({ rendered, installDependencies, onProgress, seedCompilerArtifacts = true, }) {
231
235
  const cachedArtifacts = renderedArtifactCache.get(rendered);
232
236
  const currentVariablesFingerprint = createVariablesFingerprint(rendered.variables);
233
237
  const artifacts = cachedArtifacts &&
@@ -244,8 +248,9 @@ export class BlockGeneratorService {
244
248
  templateId: rendered.spec.template.family,
245
249
  variables: rendered.variables,
246
250
  });
251
+ let applyWarnings = [];
247
252
  try {
248
- await applyBuiltInScaffoldProjectFiles({
253
+ applyWarnings = await applyBuiltInScaffoldProjectFiles({
249
254
  allowExistingDir: rendered.target.allowExistingDir,
250
255
  artifacts,
251
256
  codeArtifacts,
@@ -257,6 +262,7 @@ export class BlockGeneratorService {
257
262
  repositoryReference: rendered.target.repositoryReference,
258
263
  gitignoreContent: rendered.gitignoreContent,
259
264
  readmeContent: rendered.readmeContent,
265
+ seedCompilerArtifacts,
260
266
  templateDir: rendered.templateDir,
261
267
  templateId: rendered.spec.template.family,
262
268
  variables: rendered.variables,
@@ -274,7 +280,7 @@ export class BlockGeneratorService {
274
280
  selectedVariant: rendered.selectedVariant,
275
281
  templateId: rendered.spec.template.family,
276
282
  variables: rendered.variables,
277
- warnings: rendered.warnings,
283
+ warnings: [...rendered.warnings, ...applyWarnings],
278
284
  };
279
285
  }
280
286
  }
@@ -116,6 +116,11 @@ export interface ApplyBlockInput {
116
116
  rendered: RenderBlockResult;
117
117
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
118
118
  onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
119
+ /**
120
+ * Whether compiler-backed metadata and REST artifacts should be seeded.
121
+ * Dry-run previews disable this and report the expected paths separately.
122
+ */
123
+ seedCompilerArtifacts?: boolean;
119
124
  }
120
125
  export declare function createBuiltInBlockSpec({ alternateRenderTargets, answers, dataStorageMode, persistencePolicy, templateId, withMigrationUi, withTestPreset, withWpEnv, wpVersion, }: Omit<PlanBlockInput, "allowExistingDir" | "cwd" | "noInstall" | "packageManager" | "projectDir" | "variant">): BlockSpec;
121
126
  export declare function buildTemplateVariablesFromBlockSpec(spec: BlockSpec): ScaffoldTemplateVariables;
@@ -1,3 +1,3 @@
1
- export { DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, buildBlockJsonAttributes, buildManifestDocument, } from "./built-in-block-attribute-emitters.js";
1
+ export { DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, buildBlockJsonAttributes, buildBlockJsonExampleAttributes, buildManifestDocument, } from "./built-in-block-attribute-emitters.js";
2
2
  export type { AttributeDescription, EmittedAttributeDefinition, } from "./built-in-block-attribute-emitters.js";
3
3
  export { buildBasicAttributes, buildCompoundChildAttributes, buildCompoundParentAttributes, buildInteractivityAttributes, buildPersistenceAttributes, } from "./built-in-block-attribute-specs.js";
@@ -1,2 +1,2 @@
1
- export { DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, buildBlockJsonAttributes, buildManifestDocument, } from "./built-in-block-attribute-emitters.js";
1
+ export { DEFAULT_COMPOUND_CHILD_BODY_PLACEHOLDER, buildBlockJsonAttributes, buildBlockJsonExampleAttributes, buildManifestDocument, } from "./built-in-block-attribute-emitters.js";
2
2
  export { buildBasicAttributes, buildCompoundChildAttributes, buildCompoundParentAttributes, buildInteractivityAttributes, buildPersistenceAttributes, } from "./built-in-block-attribute-specs.js";
@@ -5,13 +5,13 @@ const STANDARD_PREAMBLE_LINES = [
5
5
  "\tTypiaValidationError,",
6
6
  "\tValidationResult,",
7
7
  '} from "@wp-typia/block-runtime/validation";',
8
- 'import { tags } from "typia";',
8
+ 'import type { tags } from "@wp-typia/block-runtime/typia-tags";',
9
9
  "",
10
10
  'export type { TypiaValidationError, ValidationResult } from "@wp-typia/block-runtime/validation";',
11
11
  ];
12
12
  const VALIDATION_ONLY_PREAMBLE_LINES = [
13
13
  'import type { ValidationResult } from "@wp-typia/block-runtime/validation";',
14
- 'import { tags } from "typia";',
14
+ 'import type { tags } from "@wp-typia/block-runtime/typia-tags";',
15
15
  "",
16
16
  'export type { ValidationResult } from "@wp-typia/block-runtime/validation";',
17
17
  ];