@wp-typia/project-tools 0.22.6 → 0.22.8

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 (47) hide show
  1. package/dist/runtime/block-targets.d.ts +40 -0
  2. package/dist/runtime/block-targets.js +71 -0
  3. package/dist/runtime/built-in-block-artifact-types.js +2 -1
  4. package/dist/runtime/built-in-block-attribute-specs.js +2 -1
  5. package/dist/runtime/built-in-block-code-artifacts.js +2 -0
  6. package/dist/runtime/built-in-block-non-ts-family-artifacts.js +12 -9
  7. package/dist/runtime/built-in-block-non-ts-render-utils.js +2 -0
  8. package/dist/runtime/cli-add-block-config.js +2 -1
  9. package/dist/runtime/cli-add-block-json.d.ts +2 -2
  10. package/dist/runtime/cli-add-block-json.js +5 -4
  11. package/dist/runtime/cli-add-block.js +4 -3
  12. package/dist/runtime/cli-add-workspace-ability-scaffold.js +21 -15
  13. package/dist/runtime/cli-add-workspace-ability.js +2 -2
  14. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +17 -13
  15. package/dist/runtime/cli-add-workspace-admin-view.js +2 -2
  16. package/dist/runtime/cli-add-workspace-ai.js +2 -2
  17. package/dist/runtime/cli-add-workspace-assets.js +42 -48
  18. package/dist/runtime/cli-add-workspace-rest.js +2 -2
  19. package/dist/runtime/cli-add-workspace.js +14 -49
  20. package/dist/runtime/cli-diagnostics.js +6 -0
  21. package/dist/runtime/cli-init-plan-presentation.d.ts +16 -0
  22. package/dist/runtime/cli-init-plan-presentation.js +74 -0
  23. package/dist/runtime/cli-init-plan.js +5 -77
  24. package/dist/runtime/cli-scaffold.js +2 -1
  25. package/dist/runtime/create-template-validation.d.ts +10 -0
  26. package/dist/runtime/create-template-validation.js +121 -0
  27. package/dist/runtime/package-versions.d.ts +1 -1
  28. package/dist/runtime/package-versions.js +16 -3
  29. package/dist/runtime/php-utils.js +151 -148
  30. package/dist/runtime/scaffold-answer-resolution.js +5 -108
  31. package/dist/runtime/scaffold-apply-utils.js +3 -2
  32. package/dist/runtime/scaffold-identifiers.js +4 -3
  33. package/dist/runtime/scaffold-template-assertions.d.ts +6 -0
  34. package/dist/runtime/scaffold-template-assertions.js +33 -0
  35. package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
  36. package/dist/runtime/scaffold-template-variable-groups.js +7 -0
  37. package/dist/runtime/string-case.d.ts +2 -4
  38. package/dist/runtime/string-case.js +15 -4
  39. package/dist/runtime/template-source-cache-policy.d.ts +53 -0
  40. package/dist/runtime/template-source-cache-policy.js +135 -0
  41. package/dist/runtime/template-source-cache.d.ts +1 -45
  42. package/dist/runtime/template-source-cache.js +9 -152
  43. package/dist/runtime/ts-property-names.d.ts +11 -0
  44. package/dist/runtime/ts-property-names.js +16 -0
  45. package/dist/runtime/workspace-inventory.d.ts +13 -1
  46. package/dist/runtime/workspace-inventory.js +35 -9
  47. package/package.json +6 -1
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from "./cli-diagnostics.js";
3
+ import { OFFICIAL_WORKSPACE_TEMPLATE_ALIAS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
4
+ import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
5
+ import { parseNpmTemplateLocator } from "./template-source-locators.js";
6
+ export const CREATE_TEMPLATE_SELECTION_HINT = `--template <${[
7
+ ...TEMPLATE_IDS,
8
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
9
+ ].join("|")}|./path|github:owner/repo/path[#ref]|npm-package>`;
10
+ const TEMPLATE_SUGGESTION_IDS = [
11
+ ...TEMPLATE_IDS,
12
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
13
+ ];
14
+ const USER_FACING_TEMPLATE_IDS = [
15
+ ...TEMPLATE_IDS,
16
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
17
+ ];
18
+ function normalizeCreateTemplateSelection(templateId) {
19
+ return templateId === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
20
+ ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
21
+ : templateId;
22
+ }
23
+ function looksLikeWindowsAbsoluteTemplatePath(templateId) {
24
+ return /^[a-z]:[\\/]/iu.test(templateId) || /^\\\\[^\\]+\\[^\\]+/u.test(templateId);
25
+ }
26
+ function looksLikeExplicitNonNpmExternalTemplateLocator(templateId) {
27
+ return (path.isAbsolute(templateId) ||
28
+ looksLikeWindowsAbsoluteTemplatePath(templateId) ||
29
+ templateId.startsWith("./") ||
30
+ templateId.startsWith(".\\") ||
31
+ templateId.startsWith("../") ||
32
+ templateId.startsWith("..\\") ||
33
+ templateId.startsWith("@") ||
34
+ templateId.startsWith("github:") ||
35
+ templateId.includes("/") ||
36
+ templateId.includes("\\"));
37
+ }
38
+ function looksLikeExplicitCreateExternalTemplateLocator(templateId) {
39
+ return (looksLikeExplicitNonNpmExternalTemplateLocator(templateId) ||
40
+ parseNpmTemplateLocator(templateId) !== null);
41
+ }
42
+ function getEditDistance(left, right) {
43
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
44
+ const current = new Array(right.length + 1);
45
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
46
+ current[0] = leftIndex + 1;
47
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
48
+ const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
49
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
50
+ }
51
+ for (let index = 0; index < current.length; index += 1) {
52
+ previous[index] = current[index];
53
+ }
54
+ }
55
+ return previous[right.length];
56
+ }
57
+ function findMistypedBuiltInTemplateSuggestion(templateId) {
58
+ const normalizedTemplateId = templateId.trim().toLowerCase();
59
+ if (normalizedTemplateId.length === 0 ||
60
+ looksLikeExplicitNonNpmExternalTemplateLocator(normalizedTemplateId)) {
61
+ return null;
62
+ }
63
+ let bestCandidate = null;
64
+ for (const candidateId of TEMPLATE_SUGGESTION_IDS) {
65
+ const distance = getEditDistance(normalizedTemplateId, candidateId);
66
+ if (bestCandidate === null || distance < bestCandidate.distance) {
67
+ bestCandidate = {
68
+ distance,
69
+ id: candidateId,
70
+ };
71
+ }
72
+ }
73
+ return bestCandidate && bestCandidate.distance <= 2
74
+ ? bestCandidate.id
75
+ : null;
76
+ }
77
+ function getMistypedBuiltInTemplateMessage(templateId) {
78
+ const suggestion = findMistypedBuiltInTemplateSuggestion(templateId);
79
+ if (!suggestion) {
80
+ return null;
81
+ }
82
+ const suggestionDescription = suggestion === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
83
+ ? "official workspace scaffold"
84
+ : "built-in scaffold";
85
+ 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.`;
86
+ }
87
+ function getUnknownTemplateMessage(templateId) {
88
+ return [
89
+ `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(", ")}.`,
90
+ "Run `wp-typia templates list` to inspect available templates.",
91
+ "Pass an explicit external template locator such as `./path`, `github:owner/repo/path[#ref]`, or `@scope/template` for custom templates.",
92
+ ].join(" ");
93
+ }
94
+ /**
95
+ * Validate an explicitly supplied create template id before entering the full
96
+ * scaffold flow.
97
+ *
98
+ * Built-in template ids and the workspace alias resolve immediately, common
99
+ * built-in typos keep suggestion diagnostics, and explicit external template
100
+ * locators remain deferred to the external template resolver.
101
+ */
102
+ export function validateExplicitCreateTemplateId(templateId) {
103
+ const normalizedTemplateId = normalizeCreateTemplateSelection(templateId);
104
+ if (isRemovedBuiltInTemplateId(templateId)) {
105
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getRemovedBuiltInTemplateMessage(templateId));
106
+ }
107
+ if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
108
+ return normalizedTemplateId;
109
+ }
110
+ if (isBuiltInTemplateId(normalizedTemplateId)) {
111
+ return getTemplateById(normalizedTemplateId).id;
112
+ }
113
+ const mistypedBuiltInTemplateMessage = getMistypedBuiltInTemplateMessage(templateId);
114
+ if (mistypedBuiltInTemplateMessage) {
115
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, mistypedBuiltInTemplateMessage);
116
+ }
117
+ if (!looksLikeExplicitCreateExternalTemplateLocator(normalizedTemplateId)) {
118
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getUnknownTemplateMessage(templateId));
119
+ }
120
+ return normalizedTemplateId;
121
+ }
@@ -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
@@ -58,10 +58,23 @@ function createContentFingerprint(source) {
58
58
  }
59
59
  return (hash >>> 0).toString(16);
60
60
  }
61
+ function readPackageManifestFile(packageJsonPath) {
62
+ const fileDescriptor = fs.openSync(packageJsonPath, 'r');
63
+ try {
64
+ // Keep cache metadata and manifest contents tied to one opened file, so a
65
+ // concurrent path replacement cannot mix stats from one manifest with
66
+ // contents from another. The content fingerprint remains the final guard.
67
+ const stats = fs.fstatSync(fileDescriptor);
68
+ const source = fs.readFileSync(fileDescriptor, 'utf8');
69
+ return { source, stats };
70
+ }
71
+ finally {
72
+ fs.closeSync(fileDescriptor);
73
+ }
74
+ }
61
75
  function resolvePackageManifestLocation(packageJsonPath) {
62
76
  try {
63
- const stats = fs.statSync(packageJsonPath);
64
- const source = fs.readFileSync(packageJsonPath, 'utf8');
77
+ const { source, stats } = readPackageManifestFile(packageJsonPath);
65
78
  return {
66
79
  cacheKey: `file:${packageJsonPath}:${stats.ino}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.size}:${createContentFingerprint(source)}`,
67
80
  packageJsonPath,
@@ -139,95 +139,168 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
139
139
  const callStart = skipPhpCallTrivia(source, cursor);
140
140
  return callStart !== null && source[callStart] === "(";
141
141
  }
142
- /**
143
- * Detect a PHP function call outside strings, comments, heredoc, and nowdoc blocks.
144
- *
145
- * @param source PHP source to scan.
146
- * @param functionName Literal PHP function identifier to find.
147
- * @returns Whether `source` contains a code-mode call to `functionName`.
148
- */
149
- export function hasPhpFunctionCall(source, functionName) {
150
- let mode = "code";
151
- let heredocDelimiter = "";
152
- let index = 0;
153
- 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;
142
+ function createPhpScannerState() {
143
+ return {
144
+ heredocDelimiter: "",
145
+ interpolationComment: "",
146
+ interpolationDepth: 0,
147
+ interpolationQuote: "",
148
+ mode: "code",
149
+ };
150
+ }
151
+ function advancePhpScanner(source, index, state) {
152
+ const character = source[index];
153
+ if (state.mode === "heredoc") {
154
+ const closingEnd = findPhpHeredocClosingEnd(source, index, state.heredocDelimiter);
155
+ if (closingEnd !== null) {
156
+ state.mode = "code";
157
+ state.heredocDelimiter = "";
158
+ return { ambiguous: false, inCode: false, index: closingEnd };
159
+ }
160
+ const nextLineStart = findPhpLineBoundary(source, index).nextStart;
161
+ if (nextLineStart <= index) {
162
+ return { ambiguous: true, inCode: false, index };
163
+ }
164
+ return { ambiguous: false, inCode: false, index: nextLineStart };
165
+ }
166
+ if (state.mode === "single-quoted" || state.mode === "double-quoted") {
167
+ const quote = state.mode === "single-quoted" ? "'" : '"';
168
+ if (character === "\\") {
169
+ return { ambiguous: false, inCode: false, index: index + 2 };
170
+ }
171
+ if (state.mode === "double-quoted" &&
172
+ character === "{" &&
173
+ source[index + 1] === "$") {
174
+ state.mode = "double-quoted-interpolation";
175
+ state.interpolationComment = "";
176
+ state.interpolationDepth = 1;
177
+ state.interpolationQuote = "";
178
+ return { ambiguous: false, inCode: false, index: index + 2 };
169
179
  }
170
- if (mode === "single-quoted" || mode === "double-quoted") {
171
- const quote = mode === "single-quoted" ? "'" : '"';
180
+ if (character === quote) {
181
+ state.mode = "code";
182
+ }
183
+ return { ambiguous: false, inCode: false, index: index + 1 };
184
+ }
185
+ if (state.mode === "double-quoted-interpolation") {
186
+ if (state.interpolationQuote) {
172
187
  if (character === "\\") {
173
- index += 2;
174
- continue;
188
+ return { ambiguous: false, inCode: false, index: index + 2 };
175
189
  }
176
- if (character === quote) {
177
- mode = "code";
190
+ if (character === state.interpolationQuote) {
191
+ state.interpolationQuote = "";
178
192
  }
179
- index += 1;
180
- continue;
193
+ return { ambiguous: false, inCode: false, index: index + 1 };
181
194
  }
182
- if (mode === "line-comment") {
195
+ if (state.interpolationComment === "line") {
183
196
  if (character === "\r" || character === "\n") {
184
- mode = "code";
197
+ state.interpolationComment = "";
185
198
  }
186
- index += 1;
187
- continue;
199
+ return { ambiguous: false, inCode: false, index: index + 1 };
188
200
  }
189
- if (mode === "block-comment") {
201
+ if (state.interpolationComment === "block") {
190
202
  if (character === "*" && source[index + 1] === "/") {
191
- mode = "code";
192
- index += 2;
193
- continue;
203
+ state.interpolationComment = "";
204
+ return { ambiguous: false, inCode: false, index: index + 2 };
194
205
  }
195
- index += 1;
196
- continue;
197
- }
198
- if (character === "'") {
199
- mode = "single-quoted";
200
- index += 1;
201
- continue;
202
- }
203
- if (character === '"') {
204
- mode = "double-quoted";
205
- index += 1;
206
- continue;
206
+ return { ambiguous: false, inCode: false, index: index + 1 };
207
207
  }
208
208
  if (character === "/" && source[index + 1] === "/") {
209
- mode = "line-comment";
210
- index += 2;
211
- continue;
209
+ state.interpolationComment = "line";
210
+ return { ambiguous: false, inCode: false, index: index + 2 };
212
211
  }
213
212
  if (character === "#" && source[index + 1] !== "[") {
214
- mode = "line-comment";
215
- index += 1;
216
- continue;
213
+ state.interpolationComment = "line";
214
+ return { ambiguous: false, inCode: false, index: index + 1 };
217
215
  }
218
216
  if (character === "/" && source[index + 1] === "*") {
219
- mode = "block-comment";
220
- index += 2;
221
- continue;
217
+ state.interpolationComment = "block";
218
+ return { ambiguous: false, inCode: false, index: index + 2 };
219
+ }
220
+ if (character === "'" || character === '"') {
221
+ state.interpolationQuote = character;
222
+ return { ambiguous: false, inCode: false, index: index + 1 };
223
+ }
224
+ if (character === "{") {
225
+ state.interpolationDepth += 1;
226
+ return { ambiguous: false, inCode: false, index: index + 1 };
222
227
  }
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;
228
+ if (character === "}") {
229
+ state.interpolationDepth -= 1;
230
+ if (state.interpolationDepth <= 0) {
231
+ state.interpolationComment = "";
232
+ state.interpolationDepth = 0;
233
+ state.mode = "double-quoted";
230
234
  }
235
+ return { ambiguous: false, inCode: false, index: index + 1 };
236
+ }
237
+ return { ambiguous: false, inCode: false, index: index + 1 };
238
+ }
239
+ if (state.mode === "line-comment") {
240
+ if (character === "\r" || character === "\n") {
241
+ state.mode = "code";
242
+ }
243
+ return { ambiguous: false, inCode: false, index: index + 1 };
244
+ }
245
+ if (state.mode === "block-comment") {
246
+ if (character === "*" && source[index + 1] === "/") {
247
+ state.mode = "code";
248
+ return { ambiguous: false, inCode: false, index: index + 2 };
249
+ }
250
+ return { ambiguous: false, inCode: false, index: index + 1 };
251
+ }
252
+ if (character === "'") {
253
+ state.mode = "single-quoted";
254
+ return { ambiguous: false, inCode: false, index: index + 1 };
255
+ }
256
+ if (character === '"') {
257
+ state.mode = "double-quoted";
258
+ return { ambiguous: false, inCode: false, index: index + 1 };
259
+ }
260
+ if (character === "/" && source[index + 1] === "/") {
261
+ state.mode = "line-comment";
262
+ return { ambiguous: false, inCode: false, index: index + 2 };
263
+ }
264
+ if (character === "#" && source[index + 1] !== "[") {
265
+ state.mode = "line-comment";
266
+ return { ambiguous: false, inCode: false, index: index + 1 };
267
+ }
268
+ if (character === "/" && source[index + 1] === "*") {
269
+ state.mode = "block-comment";
270
+ return { ambiguous: false, inCode: false, index: index + 2 };
271
+ }
272
+ if (character === "<") {
273
+ const heredocStart = parsePhpHeredocStart(source, index);
274
+ if (heredocStart) {
275
+ state.mode = "heredoc";
276
+ state.heredocDelimiter = heredocStart.delimiter;
277
+ return {
278
+ ambiguous: false,
279
+ inCode: false,
280
+ index: heredocStart.contentStart,
281
+ };
282
+ }
283
+ }
284
+ return { ambiguous: false, inCode: true, index };
285
+ }
286
+ /**
287
+ * Detect a PHP function call outside strings, comments, heredoc, and nowdoc blocks.
288
+ *
289
+ * @param source PHP source to scan.
290
+ * @param functionName Literal PHP function identifier to find.
291
+ * @returns Whether `source` contains a code-mode call to `functionName`.
292
+ */
293
+ export function hasPhpFunctionCall(source, functionName) {
294
+ const scanner = createPhpScannerState();
295
+ let index = 0;
296
+ while (index < source.length) {
297
+ const scan = advancePhpScanner(source, index, scanner);
298
+ if (scan.ambiguous) {
299
+ return false;
300
+ }
301
+ if (!scan.inCode) {
302
+ index = scan.index;
303
+ continue;
231
304
  }
232
305
  if (matchesPhpFunctionCallAt(source, index, functionName)) {
233
306
  return true;
@@ -257,88 +330,18 @@ export function findPhpFunctionRange(source, functionName, options = {}) {
257
330
  }
258
331
  const openBraceIndex = functionStart + openBraceOffset;
259
332
  let depth = 0;
260
- let mode = "code";
261
- let heredocDelimiter = "";
333
+ const scanner = createPhpScannerState();
262
334
  let index = openBraceIndex;
263
335
  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;
336
+ const scan = advancePhpScanner(source, index, scanner);
337
+ if (scan.ambiguous) {
338
+ return null;
327
339
  }
328
- if (character === "/" && source[index + 1] === "*") {
329
- mode = "block-comment";
330
- index += 2;
340
+ if (!scan.inCode) {
341
+ index = scan.index;
331
342
  continue;
332
343
  }
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
- }
344
+ const character = source[index];
342
345
  if (character === "{") {
343
346
  depth += 1;
344
347
  index += 1;
@@ -1,23 +1,11 @@
1
1
  import { execSync } from 'node:child_process';
2
- import path from 'node:path';
3
2
  import { PACKAGE_MANAGER_IDS, getPackageManager, } from './package-managers.js';
4
3
  import { normalizeBlockSlug, resolveScaffoldIdentifiers, validateBlockSlug, validateNamespace, } from './scaffold-identifiers.js';
5
4
  import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from './cli-diagnostics.js';
6
- import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
7
- import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
8
- import { parseNpmTemplateLocator } from './template-source-locators.js';
5
+ import { getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
6
+ import { CREATE_TEMPLATE_SELECTION_HINT, validateExplicitCreateTemplateId, } from './create-template-validation.js';
9
7
  import { toSnakeCase, toTitleCase, } from './string-case.js';
10
- const WORKSPACE_TEMPLATE_ALIAS = 'workspace';
11
- const TEMPLATE_SELECTION_HINT = `--template <${[
12
- ...TEMPLATE_IDS,
13
- WORKSPACE_TEMPLATE_ALIAS,
14
- ].join('|')}|./path|github:owner/repo/path[#ref]|npm-package>`;
15
- const TEMPLATE_SUGGESTION_IDS = [...TEMPLATE_IDS, WORKSPACE_TEMPLATE_ALIAS];
16
8
  const QUERY_POST_TYPE_RULE = 'Use lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
17
- const USER_FACING_TEMPLATE_IDS = [
18
- ...TEMPLATE_IDS,
19
- WORKSPACE_TEMPLATE_ALIAS,
20
- ];
21
9
  /**
22
10
  * Detect the current author name from local Git config.
23
11
  *
@@ -80,80 +68,6 @@ function normalizeQueryPostType(value) {
80
68
  }
81
69
  return value.trim().toLowerCase();
82
70
  }
83
- function normalizeTemplateSelection(templateId) {
84
- return templateId === WORKSPACE_TEMPLATE_ALIAS
85
- ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
86
- : templateId;
87
- }
88
- function looksLikeWindowsAbsoluteTemplatePath(templateId) {
89
- return /^[a-z]:[\\/]/iu.test(templateId) || /^\\\\[^\\]+\\[^\\]+/u.test(templateId);
90
- }
91
- function looksLikeExplicitNonNpmExternalTemplateLocator(templateId) {
92
- return (path.isAbsolute(templateId) ||
93
- looksLikeWindowsAbsoluteTemplatePath(templateId) ||
94
- templateId.startsWith('./') ||
95
- templateId.startsWith('../') ||
96
- templateId.startsWith('@') ||
97
- templateId.startsWith('github:') ||
98
- templateId.includes('/'));
99
- }
100
- function looksLikeExplicitExternalTemplateLocator(templateId) {
101
- return (looksLikeExplicitNonNpmExternalTemplateLocator(templateId) ||
102
- parseNpmTemplateLocator(templateId) !== null);
103
- }
104
- function getEditDistance(left, right) {
105
- const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
106
- const current = new Array(right.length + 1);
107
- for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
108
- current[0] = leftIndex + 1;
109
- for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
110
- const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
111
- current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
112
- }
113
- for (let index = 0; index < current.length; index += 1) {
114
- previous[index] = current[index];
115
- }
116
- }
117
- return previous[right.length];
118
- }
119
- function findMistypedBuiltInTemplateSuggestion(templateId) {
120
- const normalizedTemplateId = templateId.trim().toLowerCase();
121
- if (normalizedTemplateId.length === 0 ||
122
- looksLikeExplicitNonNpmExternalTemplateLocator(normalizedTemplateId)) {
123
- return null;
124
- }
125
- let bestCandidate = null;
126
- for (const candidateId of TEMPLATE_SUGGESTION_IDS) {
127
- const distance = getEditDistance(normalizedTemplateId, candidateId);
128
- if (bestCandidate === null ||
129
- distance < bestCandidate.distance) {
130
- bestCandidate = {
131
- distance,
132
- id: candidateId,
133
- };
134
- }
135
- }
136
- return bestCandidate && bestCandidate.distance <= 2
137
- ? bestCandidate.id
138
- : null;
139
- }
140
- function getMistypedBuiltInTemplateMessage(templateId) {
141
- const suggestion = findMistypedBuiltInTemplateSuggestion(templateId);
142
- if (!suggestion) {
143
- return null;
144
- }
145
- const suggestionDescription = suggestion === WORKSPACE_TEMPLATE_ALIAS
146
- ? 'official workspace scaffold'
147
- : 'built-in scaffold';
148
- 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.`;
149
- }
150
- function getUnknownTemplateMessage(templateId) {
151
- return [
152
- `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(', ')}.`,
153
- 'Run `wp-typia templates list` to inspect available templates.',
154
- 'Pass an explicit external template locator such as `./path`, `github:owner/repo/path[#ref]`, or `@scope/template` for custom templates.',
155
- ].join(' ');
156
- }
157
71
  /**
158
72
  * Resolve the scaffold template id from flags, defaults, and interactive selection.
159
73
  *
@@ -162,32 +76,15 @@ function getUnknownTemplateMessage(templateId) {
162
76
  */
163
77
  export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
164
78
  if (templateId) {
165
- const normalizedTemplateId = normalizeTemplateSelection(templateId);
166
- if (isRemovedBuiltInTemplateId(templateId)) {
167
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getRemovedBuiltInTemplateMessage(templateId));
168
- }
169
- if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
170
- return normalizedTemplateId;
171
- }
172
- if (isBuiltInTemplateId(normalizedTemplateId)) {
173
- return getTemplateById(normalizedTemplateId).id;
174
- }
175
- const mistypedBuiltInTemplateMessage = getMistypedBuiltInTemplateMessage(templateId);
176
- if (mistypedBuiltInTemplateMessage) {
177
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, mistypedBuiltInTemplateMessage);
178
- }
179
- if (!looksLikeExplicitExternalTemplateLocator(normalizedTemplateId)) {
180
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getUnknownTemplateMessage(templateId));
181
- }
182
- return normalizedTemplateId;
79
+ return validateExplicitCreateTemplateId(templateId);
183
80
  }
184
81
  if (yes) {
185
82
  return 'basic';
186
83
  }
187
84
  if (!isInteractive || !selectTemplate) {
188
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `Template is required in non-interactive mode. Use ${TEMPLATE_SELECTION_HINT}.`);
85
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `Template is required in non-interactive mode. Use ${CREATE_TEMPLATE_SELECTION_HINT}.`);
189
86
  }
190
- return normalizeTemplateSelection(await selectTemplate());
87
+ return validateExplicitCreateTemplateId(await selectTemplate());
191
88
  }
192
89
  /**
193
90
  * Resolve the package manager id from flags, defaults, and interactive selection.
@@ -15,6 +15,7 @@ import { pathExists, readOptionalUtf8File, } from "./fs-async.js";
15
15
  import { normalizePackageJson } from "./scaffold-package-manager-files.js";
16
16
  export { applyWorkspaceMigrationCapability, isOfficialWorkspaceProject, } from "./scaffold-bootstrap.js";
17
17
  import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
18
+ import { isCompoundPersistenceEnabled } from "./scaffold-template-variable-groups.js";
18
19
  export { buildGitignore, buildReadme, mergeTextLines } from "./scaffold-documents.js";
19
20
  async function reportScaffoldProgress(onProgress, event) {
20
21
  await onProgress?.(event);
@@ -103,7 +104,7 @@ async function withEphemeralScaffoldNodeModules(targetDir, callback) {
103
104
  */
104
105
  export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, variables) {
105
106
  const needsPersistenceArtifacts = templateId === "persistence" ||
106
- (templateId === "compound" && variables.compoundPersistenceEnabled === "true");
107
+ (templateId === "compound" && isCompoundPersistenceEnabled(variables));
107
108
  if (!needsPersistenceArtifacts) {
108
109
  return;
109
110
  }
@@ -256,7 +257,7 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
256
257
  await fsp.writeFile(gitignorePath, mergeTextLines(gitignoreContent ?? buildGitignore(), existingGitignore), "utf8");
257
258
  await normalizePackageJson(projectDir, packageManager);
258
259
  await applyGeneratedProjectDxPackageJson({
259
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
260
+ compoundPersistenceEnabled: isCompoundPersistenceEnabled(variables),
260
261
  packageManager,
261
262
  projectDir,
262
263
  templateId,