@wp-typia/project-tools 0.22.6 → 0.22.7

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.
@@ -16,10 +16,10 @@ export declare function resolveWorkspaceBlock(inventory: WorkspaceInventory, blo
16
16
  * @returns Parsed block metadata and the absolute `block.json` path.
17
17
  * @throws {Error} When the file is missing or cannot be parsed as scaffold metadata.
18
18
  */
19
- export declare function readWorkspaceBlockJson(projectDir: string, blockSlug: string): {
19
+ export declare function readWorkspaceBlockJson(projectDir: string, blockSlug: string): Promise<{
20
20
  blockJson: Record<string, unknown>;
21
21
  blockJsonPath: string;
22
- };
22
+ }>;
23
23
  /**
24
24
  * Return a mutable `blockHooks` object for a parsed block metadata document.
25
25
  *
@@ -1,6 +1,6 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
3
+ import { readOptionalUtf8File } from "./fs-async.js";
4
4
  /**
5
5
  * Resolve an existing workspace block inventory entry by slug.
6
6
  *
@@ -24,14 +24,15 @@ export function resolveWorkspaceBlock(inventory, blockSlug) {
24
24
  * @returns Parsed block metadata and the absolute `block.json` path.
25
25
  * @throws {Error} When the file is missing or cannot be parsed as scaffold metadata.
26
26
  */
27
- export function readWorkspaceBlockJson(projectDir, blockSlug) {
27
+ export async function readWorkspaceBlockJson(projectDir, blockSlug) {
28
28
  const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
29
- if (!fs.existsSync(blockJsonPath)) {
29
+ const source = await readOptionalUtf8File(blockJsonPath);
30
+ if (source === null) {
30
31
  throw new Error(`Missing ${path.relative(projectDir, blockJsonPath)} for workspace block "${blockSlug}".`);
31
32
  }
32
33
  let blockJson;
33
34
  try {
34
- blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
35
+ blockJson = parseScaffoldBlockMetadata(JSON.parse(source));
35
36
  }
36
37
  catch (error) {
37
38
  throw new Error(error instanceof Error
@@ -1,6 +1,6 @@
1
- import fs from "node:fs";
2
1
  import { promises as fsp } from "node:fs";
3
2
  import path from "node:path";
3
+ import { pathExists } from "./fs-async.js";
4
4
  import { resolveWorkspaceProject } from "./workspace-project.js";
5
5
  import { appendWorkspaceInventoryEntries, readWorkspaceInventory } from "./workspace-inventory.js";
6
6
  import { toKebabCase, toSnakeCase, toTitleCase } from "./string-case.js";
@@ -381,8 +381,7 @@ async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
381
381
  const variationsDir = path.join(projectDir, "src", "blocks", blockSlug, "variations");
382
382
  const variationsIndexPath = path.join(variationsDir, "index.ts");
383
383
  await fsp.mkdir(variationsDir, { recursive: true });
384
- const existingVariationSlugs = fs
385
- .readdirSync(variationsDir)
384
+ const existingVariationSlugs = (await fsp.readdir(variationsDir))
386
385
  .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
387
386
  .map((entry) => entry.replace(/\.ts$/u, ""));
388
387
  const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
@@ -392,8 +391,7 @@ async function writeBlockStyleRegistry(projectDir, blockSlug, styleSlug) {
392
391
  const stylesDir = path.join(projectDir, "src", "blocks", blockSlug, "styles");
393
392
  const stylesIndexPath = path.join(stylesDir, "index.ts");
394
393
  await fsp.mkdir(stylesDir, { recursive: true });
395
- const existingStyleSlugs = fs
396
- .readdirSync(stylesDir)
394
+ const existingStyleSlugs = (await fsp.readdir(stylesDir))
397
395
  .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
398
396
  .map((entry) => entry.replace(/\.ts$/u, ""));
399
397
  const nextStyleSlugs = Array.from(new Set([...existingStyleSlugs, styleSlug])).sort();
@@ -403,8 +401,7 @@ async function writeBlockTransformRegistry(projectDir, blockSlug, transformSlug)
403
401
  const transformsDir = path.join(projectDir, "src", "blocks", blockSlug, "transforms");
404
402
  const transformsIndexPath = path.join(transformsDir, "index.ts");
405
403
  await fsp.mkdir(transformsDir, { recursive: true });
406
- const existingTransformSlugs = fs
407
- .readdirSync(transformsDir)
404
+ const existingTransformSlugs = (await fsp.readdir(transformsDir))
408
405
  .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
409
406
  .map((entry) => entry.replace(/\.ts$/u, ""));
410
407
  const nextTransformSlugs = Array.from(new Set([...existingTransformSlugs, transformSlug])).sort();
@@ -495,7 +492,7 @@ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), v
495
492
  const variationsDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
496
493
  const variationFilePath = path.join(variationsDir, `${variationSlug}.ts`);
497
494
  const variationsIndexPath = path.join(variationsDir, "index.ts");
498
- const shouldRemoveVariationsDirOnRollback = !fs.existsSync(variationsDir);
495
+ const shouldRemoveVariationsDirOnRollback = !(await pathExists(variationsDir));
499
496
  const mutationSnapshot = {
500
497
  fileSources: await snapshotWorkspaceFiles([
501
498
  blockConfigPath,
@@ -555,7 +552,7 @@ export async function runAddBlockStyleCommand({ blockName, cwd = process.cwd(),
555
552
  const stylesDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "styles");
556
553
  const styleFilePath = path.join(stylesDir, `${styleSlug}.ts`);
557
554
  const stylesIndexPath = path.join(stylesDir, "index.ts");
558
- const shouldRemoveStylesDirOnRollback = !fs.existsSync(stylesDir);
555
+ const shouldRemoveStylesDirOnRollback = !(await pathExists(stylesDir));
559
556
  const mutationSnapshot = {
560
557
  fileSources: await snapshotWorkspaceFiles([
561
558
  blockConfigPath,
@@ -624,7 +621,7 @@ export async function runAddBlockTransformCommand({ cwd = process.cwd(), fromBlo
624
621
  const transformsDir = path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "transforms");
625
622
  const transformFilePath = path.join(transformsDir, `${transformSlug}.ts`);
626
623
  const transformsIndexPath = path.join(transformsDir, "index.ts");
627
- const shouldRemoveTransformsDirOnRollback = !fs.existsSync(transformsDir);
624
+ const shouldRemoveTransformsDirOnRollback = !(await pathExists(transformsDir));
628
625
  const mutationSnapshot = {
629
626
  fileSources: await snapshotWorkspaceFiles([
630
627
  blockConfigPath,
@@ -695,7 +692,7 @@ export async function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd
695
692
  if (resolvedAnchorBlockName === selfHookAnchor) {
696
693
  throw new Error("`wp-typia add hooked-block` cannot hook a block relative to its own block name.");
697
694
  }
698
- const { blockJson, blockJsonPath } = readWorkspaceBlockJson(workspace.projectDir, blockSlug);
695
+ const { blockJson, blockJsonPath } = await readWorkspaceBlockJson(workspace.projectDir, blockSlug);
699
696
  const blockJsonRelativePath = path.relative(workspace.projectDir, blockJsonPath);
700
697
  const blockHooks = getMutableBlockHooks(blockJson, blockJsonRelativePath);
701
698
  if (Object.prototype.hasOwnProperty.call(blockHooks, resolvedAnchorBlockName)) {
@@ -0,0 +1,16 @@
1
+ import { type PackageManagerId } from "./package-managers.js";
2
+ import type { InitCommandMode, InitPlanLayoutKind, InitPlanStatus, RetrofitInitPlan } from "./cli-init-types.js";
3
+ export declare function buildInitPlanChangeSummary(changes: Pick<RetrofitInitPlan, "generatedArtifacts" | "packageChanges" | "plannedFiles">, options: {
4
+ includeGeneratedArtifacts: boolean;
5
+ }): string[];
6
+ export declare function buildInitPlanNextSteps(options: {
7
+ commandMode: InitCommandMode;
8
+ dependencyChangeCount: number;
9
+ hasPlannedChanges: boolean;
10
+ layoutKind: InitPlanLayoutKind;
11
+ packageManager: PackageManagerId;
12
+ }): string[];
13
+ export declare function buildRetrofitPlanSummary(options: {
14
+ commandMode: InitCommandMode;
15
+ status: InitPlanStatus;
16
+ }): string;
@@ -0,0 +1,74 @@
1
+ import { formatAddDevDependenciesCommand, formatPackageExecCommand, formatRunScript, } from "./package-managers.js";
2
+ import { buildRequiredDevDependencyMapEntries, getWpTypiaCliSpecifier, } from "./cli-init-package-json.js";
3
+ export function buildInitPlanChangeSummary(changes, options) {
4
+ const lines = [];
5
+ for (const dependencyChange of changes.packageChanges.addDevDependencies) {
6
+ lines.push(`devDependency ${dependencyChange.action} ${dependencyChange.name} -> ${dependencyChange.requiredValue}`);
7
+ }
8
+ if (changes.packageChanges.packageManagerField) {
9
+ lines.push(`packageManager ${changes.packageChanges.packageManagerField.action} -> ${changes.packageChanges.packageManagerField.requiredValue}`);
10
+ }
11
+ for (const scriptChange of changes.packageChanges.scripts) {
12
+ lines.push(`script ${scriptChange.action} ${scriptChange.name} -> ${scriptChange.requiredValue}`);
13
+ }
14
+ for (const filePlan of changes.plannedFiles) {
15
+ lines.push(`file ${filePlan.action} ${filePlan.path} (${filePlan.purpose})`);
16
+ }
17
+ if (options.includeGeneratedArtifacts) {
18
+ for (const artifactPath of changes.generatedArtifacts) {
19
+ lines.push(`generated artifact ${artifactPath}`);
20
+ }
21
+ }
22
+ return lines;
23
+ }
24
+ export function buildInitPlanNextSteps(options) {
25
+ const cliSpecifier = getWpTypiaCliSpecifier();
26
+ const syncTypesRun = formatRunScript(options.packageManager, "sync-types");
27
+ const syncRun = formatRunScript(options.packageManager, "sync");
28
+ const doctorRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "doctor");
29
+ const migrationInitRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "migrate init --current-migration-version v1");
30
+ const dependencyInstallCommand = formatAddDevDependenciesCommand(options.packageManager, buildRequiredDevDependencyMapEntries());
31
+ if (options.layoutKind === "unsupported") {
32
+ return [
33
+ "Align the project to one of the supported retrofit layouts listed below, then rerun `wp-typia init`.",
34
+ dependencyInstallCommand,
35
+ syncTypesRun,
36
+ doctorRun,
37
+ ];
38
+ }
39
+ if (options.commandMode === "apply") {
40
+ return [
41
+ ...(options.dependencyChangeCount > 0
42
+ ? [
43
+ "Install or reinstall project dependencies so the retrofit sync scripts and metadata generators are available locally.",
44
+ dependencyInstallCommand,
45
+ ]
46
+ : []),
47
+ syncRun,
48
+ doctorRun,
49
+ `Optional migration bootstrap: ${migrationInitRun}`,
50
+ ];
51
+ }
52
+ return [
53
+ ...(options.hasPlannedChanges
54
+ ? [
55
+ "Re-run `wp-typia init --apply` to write the planned package.json changes and helper files automatically.",
56
+ ...(options.dependencyChangeCount > 0 ? [dependencyInstallCommand] : []),
57
+ ]
58
+ : []),
59
+ syncRun,
60
+ doctorRun,
61
+ `Optional migration bootstrap: ${migrationInitRun}`,
62
+ ];
63
+ }
64
+ export function buildRetrofitPlanSummary(options) {
65
+ if (options.status === "already-initialized") {
66
+ return options.commandMode === "apply"
67
+ ? "This project already exposes the minimum wp-typia retrofit surface. No files were changed."
68
+ : "This project already exposes the minimum wp-typia retrofit surface.";
69
+ }
70
+ if (options.commandMode === "apply") {
71
+ return "Applied the minimum wp-typia retrofit surface so package.json and helper scripts are ready for the next install and sync run.";
72
+ }
73
+ return "This command previews the minimum wp-typia adoption layer for the current project without rewriting it into a full scaffold.";
74
+ }
@@ -3,9 +3,10 @@ import path from "node:path";
3
3
  import { analyzeSourceTypes } from "@wp-typia/block-runtime/metadata-parser";
4
4
  import ts from "typescript";
5
5
  import { discoverMigrationInitLayout } from "./migration-project.js";
6
- import { formatAddDevDependenciesCommand, formatPackageExecCommand, formatRunScript, } from "./package-managers.js";
6
+ import { formatPackageExecCommand, formatRunScript } from "./package-managers.js";
7
7
  import { toPascalCase } from "./string-case.js";
8
- import { buildDependencyChanges, buildPackageManagerFieldChange, buildRequiredDevDependencyMapEntries, buildScriptChanges, getWpTypiaCliSpecifier, hasExistingWpTypiaProjectSurface, readProjectPackageJson, resolveInitPackageManager, } from "./cli-init-package-json.js";
8
+ import { buildDependencyChanges, buildPackageManagerFieldChange, buildScriptChanges, getWpTypiaCliSpecifier, hasExistingWpTypiaProjectSurface, readProjectPackageJson, resolveInitPackageManager, } from "./cli-init-package-json.js";
9
+ import { buildInitPlanChangeSummary, buildInitPlanNextSteps, buildRetrofitPlanSummary, } from "./cli-init-plan-presentation.js";
9
10
  import { RETROFIT_APPLY_PREVIEW_NOTE, SUPPORTED_RETROFIT_LAYOUT_NOTE, } from "./cli-init-types.js";
10
11
  import { tryResolveWorkspaceProject } from "./workspace-project.js";
11
12
  function normalizeRelativePath(value) {
@@ -161,82 +162,9 @@ function buildPlannedFiles(projectDir, layoutKind) {
161
162
  },
162
163
  ];
163
164
  }
164
- function buildChangeSummary(changes, options) {
165
- const lines = [];
166
- for (const dependencyChange of changes.packageChanges.addDevDependencies) {
167
- lines.push(`devDependency ${dependencyChange.action} ${dependencyChange.name} -> ${dependencyChange.requiredValue}`);
168
- }
169
- if (changes.packageChanges.packageManagerField) {
170
- lines.push(`packageManager ${changes.packageChanges.packageManagerField.action} -> ${changes.packageChanges.packageManagerField.requiredValue}`);
171
- }
172
- for (const scriptChange of changes.packageChanges.scripts) {
173
- lines.push(`script ${scriptChange.action} ${scriptChange.name} -> ${scriptChange.requiredValue}`);
174
- }
175
- for (const filePlan of changes.plannedFiles) {
176
- lines.push(`file ${filePlan.action} ${filePlan.path} (${filePlan.purpose})`);
177
- }
178
- if (options.includeGeneratedArtifacts) {
179
- for (const artifactPath of changes.generatedArtifacts) {
180
- lines.push(`generated artifact ${artifactPath}`);
181
- }
182
- }
183
- return lines;
184
- }
185
- function buildNextSteps(options) {
186
- const cliSpecifier = getWpTypiaCliSpecifier();
187
- const syncTypesRun = formatRunScript(options.packageManager, "sync-types");
188
- const syncRun = formatRunScript(options.packageManager, "sync");
189
- const doctorRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "doctor");
190
- const migrationInitRun = formatPackageExecCommand(options.packageManager, cliSpecifier, "migrate init --current-migration-version v1");
191
- const dependencyInstallCommand = formatAddDevDependenciesCommand(options.packageManager, buildRequiredDevDependencyMapEntries());
192
- if (options.layoutKind === "unsupported") {
193
- return [
194
- "Align the project to one of the supported retrofit layouts listed below, then rerun `wp-typia init`.",
195
- dependencyInstallCommand,
196
- syncTypesRun,
197
- doctorRun,
198
- ];
199
- }
200
- if (options.commandMode === "apply") {
201
- return [
202
- ...(options.dependencyChangeCount > 0
203
- ? [
204
- "Install or reinstall project dependencies so the retrofit sync scripts and metadata generators are available locally.",
205
- dependencyInstallCommand,
206
- ]
207
- : []),
208
- syncRun,
209
- doctorRun,
210
- `Optional migration bootstrap: ${migrationInitRun}`,
211
- ];
212
- }
213
- const steps = [
214
- ...(options.hasPlannedChanges
215
- ? [
216
- "Re-run `wp-typia init --apply` to write the planned package.json changes and helper files automatically.",
217
- ...(options.dependencyChangeCount > 0 ? [dependencyInstallCommand] : []),
218
- ]
219
- : []),
220
- syncRun,
221
- doctorRun,
222
- `Optional migration bootstrap: ${migrationInitRun}`,
223
- ];
224
- return steps;
225
- }
226
- function buildRetrofitPlanSummary(options) {
227
- if (options.status === "already-initialized") {
228
- return options.commandMode === "apply"
229
- ? "This project already exposes the minimum wp-typia retrofit surface. No files were changed."
230
- : "This project already exposes the minimum wp-typia retrofit surface.";
231
- }
232
- if (options.commandMode === "apply") {
233
- return "Applied the minimum wp-typia retrofit surface so package.json and helper scripts are ready for the next install and sync run.";
234
- }
235
- return "This command previews the minimum wp-typia adoption layer for the current project without rewriting it into a full scaffold.";
236
- }
237
165
  export function createRetrofitPlan(options) {
238
166
  const includeGeneratedArtifacts = options.commandMode === "preview-only";
239
- const plannedChanges = buildChangeSummary({
167
+ const plannedChanges = buildInitPlanChangeSummary({
240
168
  generatedArtifacts: options.generatedArtifacts,
241
169
  packageChanges: options.packageChanges,
242
170
  plannedFiles: options.plannedFiles,
@@ -249,7 +177,7 @@ export function createRetrofitPlan(options) {
249
177
  detectedLayout: options.detectedLayout,
250
178
  generatedArtifacts: options.generatedArtifacts,
251
179
  nextSteps: options.nextSteps ??
252
- buildNextSteps({
180
+ buildInitPlanNextSteps({
253
181
  commandMode: options.commandMode,
254
182
  dependencyChangeCount: options.packageChanges.addDevDependencies.length,
255
183
  hasPlannedChanges: plannedChanges.length > 0,
@@ -23,7 +23,7 @@ export declare const DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION = "^0.9.0";
23
23
  export declare const DEFAULT_WORDPRESS_CORE_DATA_VERSION = "^7.44.0";
24
24
  export declare const DEFAULT_WORDPRESS_DATA_VERSION = "^9.28.0";
25
25
  export declare const DEFAULT_WORDPRESS_DATAVIEWS_VERSION = "^14.1.0";
26
- export declare const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = "^0.1.0";
26
+ export declare const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = "^0.1.1";
27
27
  /**
28
28
  * Resolve a managed package version range from linked workspace packages first,
29
29
  * then installed package manifests, while preserving the shared normalization
@@ -19,7 +19,7 @@ export const DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION = '^0.9.0';
19
19
  export const DEFAULT_WORDPRESS_CORE_DATA_VERSION = '^7.44.0';
20
20
  export const DEFAULT_WORDPRESS_DATA_VERSION = '^9.28.0';
21
21
  export const DEFAULT_WORDPRESS_DATAVIEWS_VERSION = '^14.1.0';
22
- export const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = '^0.1.0';
22
+ export const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = '^0.1.1';
23
23
  let cachedPackageVersions = null;
24
24
  function getErrorCode(error) {
25
25
  return typeof error === 'object' && error !== null && 'code' in error
@@ -139,6 +139,84 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
139
139
  const callStart = skipPhpCallTrivia(source, cursor);
140
140
  return callStart !== null && source[callStart] === "(";
141
141
  }
142
+ function createPhpScannerState() {
143
+ return {
144
+ heredocDelimiter: "",
145
+ mode: "code",
146
+ };
147
+ }
148
+ function advancePhpScanner(source, index, state) {
149
+ const character = source[index];
150
+ if (state.mode === "heredoc") {
151
+ const closingEnd = findPhpHeredocClosingEnd(source, index, state.heredocDelimiter);
152
+ if (closingEnd !== null) {
153
+ state.mode = "code";
154
+ state.heredocDelimiter = "";
155
+ return { ambiguous: false, inCode: false, index: closingEnd };
156
+ }
157
+ const nextLineStart = findPhpLineBoundary(source, index).nextStart;
158
+ if (nextLineStart <= index) {
159
+ return { ambiguous: true, inCode: false, index };
160
+ }
161
+ return { ambiguous: false, inCode: false, index: nextLineStart };
162
+ }
163
+ if (state.mode === "single-quoted" || state.mode === "double-quoted") {
164
+ const quote = state.mode === "single-quoted" ? "'" : '"';
165
+ if (character === "\\") {
166
+ return { ambiguous: false, inCode: false, index: index + 2 };
167
+ }
168
+ if (character === quote) {
169
+ state.mode = "code";
170
+ }
171
+ return { ambiguous: false, inCode: false, index: index + 1 };
172
+ }
173
+ if (state.mode === "line-comment") {
174
+ if (character === "\r" || character === "\n") {
175
+ state.mode = "code";
176
+ }
177
+ return { ambiguous: false, inCode: false, index: index + 1 };
178
+ }
179
+ if (state.mode === "block-comment") {
180
+ if (character === "*" && source[index + 1] === "/") {
181
+ state.mode = "code";
182
+ return { ambiguous: false, inCode: false, index: index + 2 };
183
+ }
184
+ return { ambiguous: false, inCode: false, index: index + 1 };
185
+ }
186
+ if (character === "'") {
187
+ state.mode = "single-quoted";
188
+ return { ambiguous: false, inCode: false, index: index + 1 };
189
+ }
190
+ if (character === '"') {
191
+ state.mode = "double-quoted";
192
+ return { ambiguous: false, inCode: false, index: index + 1 };
193
+ }
194
+ if (character === "/" && source[index + 1] === "/") {
195
+ state.mode = "line-comment";
196
+ return { ambiguous: false, inCode: false, index: index + 2 };
197
+ }
198
+ if (character === "#" && source[index + 1] !== "[") {
199
+ state.mode = "line-comment";
200
+ return { ambiguous: false, inCode: false, index: index + 1 };
201
+ }
202
+ if (character === "/" && source[index + 1] === "*") {
203
+ state.mode = "block-comment";
204
+ return { ambiguous: false, inCode: false, index: index + 2 };
205
+ }
206
+ if (character === "<") {
207
+ const heredocStart = parsePhpHeredocStart(source, index);
208
+ if (heredocStart) {
209
+ state.mode = "heredoc";
210
+ state.heredocDelimiter = heredocStart.delimiter;
211
+ return {
212
+ ambiguous: false,
213
+ inCode: false,
214
+ index: heredocStart.contentStart,
215
+ };
216
+ }
217
+ }
218
+ return { ambiguous: false, inCode: true, index };
219
+ }
142
220
  /**
143
221
  * Detect a PHP function call outside strings, comments, heredoc, and nowdoc blocks.
144
222
  *
@@ -147,88 +225,17 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
147
225
  * @returns Whether `source` contains a code-mode call to `functionName`.
148
226
  */
149
227
  export function hasPhpFunctionCall(source, functionName) {
150
- let mode = "code";
151
- let heredocDelimiter = "";
228
+ const scanner = createPhpScannerState();
152
229
  let index = 0;
153
230
  while (index < source.length) {
154
- const character = source[index];
155
- if (mode === "heredoc") {
156
- const closingEnd = findPhpHeredocClosingEnd(source, index, heredocDelimiter);
157
- if (closingEnd !== null) {
158
- mode = "code";
159
- heredocDelimiter = "";
160
- index = closingEnd;
161
- continue;
162
- }
163
- const nextLineStart = findPhpLineBoundary(source, index).nextStart;
164
- if (nextLineStart <= index) {
165
- return false;
166
- }
167
- index = nextLineStart;
168
- continue;
169
- }
170
- if (mode === "single-quoted" || mode === "double-quoted") {
171
- const quote = mode === "single-quoted" ? "'" : '"';
172
- if (character === "\\") {
173
- index += 2;
174
- continue;
175
- }
176
- if (character === quote) {
177
- mode = "code";
178
- }
179
- index += 1;
180
- continue;
181
- }
182
- if (mode === "line-comment") {
183
- if (character === "\r" || character === "\n") {
184
- mode = "code";
185
- }
186
- index += 1;
187
- continue;
188
- }
189
- if (mode === "block-comment") {
190
- if (character === "*" && source[index + 1] === "/") {
191
- mode = "code";
192
- index += 2;
193
- continue;
194
- }
195
- index += 1;
196
- continue;
197
- }
198
- if (character === "'") {
199
- mode = "single-quoted";
200
- index += 1;
201
- continue;
231
+ const scan = advancePhpScanner(source, index, scanner);
232
+ if (scan.ambiguous) {
233
+ return false;
202
234
  }
203
- if (character === '"') {
204
- mode = "double-quoted";
205
- index += 1;
206
- continue;
207
- }
208
- if (character === "/" && source[index + 1] === "/") {
209
- mode = "line-comment";
210
- index += 2;
211
- continue;
212
- }
213
- if (character === "#" && source[index + 1] !== "[") {
214
- mode = "line-comment";
215
- index += 1;
235
+ if (!scan.inCode) {
236
+ index = scan.index;
216
237
  continue;
217
238
  }
218
- if (character === "/" && source[index + 1] === "*") {
219
- mode = "block-comment";
220
- index += 2;
221
- continue;
222
- }
223
- if (character === "<") {
224
- const heredocStart = parsePhpHeredocStart(source, index);
225
- if (heredocStart) {
226
- mode = "heredoc";
227
- heredocDelimiter = heredocStart.delimiter;
228
- index = heredocStart.contentStart;
229
- continue;
230
- }
231
- }
232
239
  if (matchesPhpFunctionCallAt(source, index, functionName)) {
233
240
  return true;
234
241
  }
@@ -257,88 +264,18 @@ export function findPhpFunctionRange(source, functionName, options = {}) {
257
264
  }
258
265
  const openBraceIndex = functionStart + openBraceOffset;
259
266
  let depth = 0;
260
- let mode = "code";
261
- let heredocDelimiter = "";
267
+ const scanner = createPhpScannerState();
262
268
  let index = openBraceIndex;
263
269
  while (index < source.length) {
264
- const character = source[index];
265
- if (mode === "heredoc") {
266
- const closingEnd = findPhpHeredocClosingEnd(source, index, heredocDelimiter);
267
- if (closingEnd !== null) {
268
- mode = "code";
269
- heredocDelimiter = "";
270
- index = closingEnd;
271
- continue;
272
- }
273
- const nextLineStart = findPhpLineBoundary(source, index).nextStart;
274
- if (nextLineStart <= index) {
275
- return null;
276
- }
277
- index = nextLineStart;
278
- continue;
279
- }
280
- if (mode === "single-quoted" || mode === "double-quoted") {
281
- const quote = mode === "single-quoted" ? "'" : '"';
282
- if (character === "\\") {
283
- index += 2;
284
- continue;
285
- }
286
- if (character === quote) {
287
- mode = "code";
288
- }
289
- index += 1;
290
- continue;
291
- }
292
- if (mode === "line-comment") {
293
- if (character === "\r" || character === "\n") {
294
- mode = "code";
295
- }
296
- index += 1;
297
- continue;
298
- }
299
- if (mode === "block-comment") {
300
- if (character === "*" && source[index + 1] === "/") {
301
- mode = "code";
302
- index += 2;
303
- continue;
304
- }
305
- index += 1;
306
- continue;
307
- }
308
- if (character === "'") {
309
- mode = "single-quoted";
310
- index += 1;
311
- continue;
312
- }
313
- if (character === '"') {
314
- mode = "double-quoted";
315
- index += 1;
316
- continue;
317
- }
318
- if (character === "/" && source[index + 1] === "/") {
319
- mode = "line-comment";
320
- index += 2;
321
- continue;
322
- }
323
- if (character === "#" && source[index + 1] !== "[") {
324
- mode = "line-comment";
325
- index += 1;
326
- continue;
270
+ const scan = advancePhpScanner(source, index, scanner);
271
+ if (scan.ambiguous) {
272
+ return null;
327
273
  }
328
- if (character === "/" && source[index + 1] === "*") {
329
- mode = "block-comment";
330
- index += 2;
274
+ if (!scan.inCode) {
275
+ index = scan.index;
331
276
  continue;
332
277
  }
333
- if (character === "<") {
334
- const heredocStart = parsePhpHeredocStart(source, index);
335
- if (heredocStart) {
336
- mode = "heredoc";
337
- heredocDelimiter = heredocStart.delimiter;
338
- index = heredocStart.contentStart;
339
- continue;
340
- }
341
- }
278
+ const character = source[index];
342
279
  if (character === "{") {
343
280
  depth += 1;
344
281
  index += 1;
@@ -1,3 +1,5 @@
1
+ // Keep this list fixed so generated slugs and file paths do not drift when
2
+ // project config changes. Domain-specific acronyms should use separators.
1
3
  const COMMON_ACRONYM_PREFIXES = [
2
4
  'HTML',
3
5
  'HTTP',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.22.6",
3
+ "version": "0.22.7",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",