@wp-typia/project-tools 0.22.5 → 0.22.6

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 (62) hide show
  1. package/dist/runtime/ai-feature-capability.js +20 -0
  2. package/dist/runtime/cli-add-block.js +16 -11
  3. package/dist/runtime/cli-add-collision.js +213 -136
  4. package/dist/runtime/cli-add-help.js +1 -1
  5. package/dist/runtime/cli-add-kind-ids.d.ts +11 -0
  6. package/dist/runtime/cli-add-kind-ids.js +20 -0
  7. package/dist/runtime/cli-add-types.d.ts +2 -8
  8. package/dist/runtime/cli-add-types.js +1 -17
  9. package/dist/runtime/cli-add-workspace-ability-scaffold.d.ts +3 -1
  10. package/dist/runtime/cli-add-workspace-ability-scaffold.js +22 -5
  11. package/dist/runtime/cli-add-workspace-ability.d.ts +4 -0
  12. package/dist/runtime/cli-add-workspace-ability.js +5 -1
  13. package/dist/runtime/cli-add-workspace-admin-view-source.d.ts +7 -0
  14. package/dist/runtime/cli-add-workspace-admin-view-source.js +9 -10
  15. package/dist/runtime/cli-add-workspace-admin-view-types.d.ts +0 -2
  16. package/dist/runtime/cli-add-workspace-admin-view-types.js +0 -3
  17. package/dist/runtime/cli-add-workspace-ai-scaffold.js +14 -6
  18. package/dist/runtime/cli-doctor-workspace-bindings.js +2 -3
  19. package/dist/runtime/cli-doctor-workspace-blocks.js +2 -3
  20. package/dist/runtime/cli-doctor-workspace-features.js +6 -11
  21. package/dist/runtime/cli-doctor-workspace-shared.d.ts +8 -0
  22. package/dist/runtime/cli-doctor-workspace-shared.js +10 -0
  23. package/dist/runtime/cli-help.js +1 -1
  24. package/dist/runtime/cli-init-apply.d.ts +15 -0
  25. package/dist/runtime/cli-init-apply.js +99 -0
  26. package/dist/runtime/cli-init-package-json.d.ts +19 -0
  27. package/dist/runtime/cli-init-package-json.js +191 -0
  28. package/dist/runtime/cli-init-plan.d.ts +39 -0
  29. package/dist/runtime/cli-init-plan.js +375 -0
  30. package/dist/runtime/cli-init-templates.d.ts +27 -0
  31. package/dist/runtime/cli-init-templates.js +244 -0
  32. package/dist/runtime/cli-init-types.d.ts +84 -0
  33. package/dist/runtime/cli-init-types.js +3 -0
  34. package/dist/runtime/cli-init.d.ts +4 -100
  35. package/dist/runtime/cli-init.js +6 -878
  36. package/dist/runtime/fs-async.d.ts +28 -0
  37. package/dist/runtime/fs-async.js +53 -0
  38. package/dist/runtime/package-managers.js +1 -1
  39. package/dist/runtime/php-utils.d.ts +16 -0
  40. package/dist/runtime/php-utils.js +321 -1
  41. package/dist/runtime/scaffold-apply-utils.js +10 -20
  42. package/dist/runtime/scaffold-bootstrap.js +6 -8
  43. package/dist/runtime/scaffold-compatibility.d.ts +15 -3
  44. package/dist/runtime/scaffold-compatibility.js +42 -11
  45. package/dist/runtime/scaffold-documents.js +12 -0
  46. package/dist/runtime/scaffold-package-manager-files.js +4 -3
  47. package/dist/runtime/string-case.d.ts +5 -0
  48. package/dist/runtime/string-case.js +52 -2
  49. package/dist/runtime/template-source-cache.d.ts +19 -0
  50. package/dist/runtime/template-source-cache.js +164 -28
  51. package/dist/runtime/template-source-external.d.ts +7 -0
  52. package/dist/runtime/template-source-external.js +22 -5
  53. package/dist/runtime/template-source-normalization.d.ts +1 -1
  54. package/dist/runtime/template-source-normalization.js +12 -12
  55. package/dist/runtime/template-source-remote.d.ts +14 -0
  56. package/dist/runtime/template-source-remote.js +91 -15
  57. package/dist/runtime/template-source.js +35 -25
  58. package/dist/runtime/typia-llm.js +7 -0
  59. package/dist/runtime/version-floor.js +8 -2
  60. package/dist/runtime/workspace-inventory.d.ts +16 -14
  61. package/dist/runtime/workspace-inventory.js +58 -14
  62. package/package.json +6 -1
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Return whether a filesystem path exists without blocking the event loop.
3
+ *
4
+ * @param filePath Absolute or relative path to check.
5
+ * @returns `true` when the path can be accessed, otherwise `false`.
6
+ */
7
+ export declare function pathExists(filePath: string): Promise<boolean>;
8
+ /**
9
+ * Read a UTF-8 file when it exists and otherwise return `null`.
10
+ *
11
+ * @param filePath Absolute or relative file path to read.
12
+ * @returns The UTF-8 source, or `null` when the file is missing.
13
+ */
14
+ export declare function readOptionalUtf8File(filePath: string): Promise<string | null>;
15
+ /**
16
+ * Extract a Node.js error code from an unknown thrown value.
17
+ *
18
+ * @param error Unknown error value.
19
+ * @returns The string error code, or an empty string when unavailable.
20
+ */
21
+ export declare function getNodeErrorCode(error: unknown): string;
22
+ /**
23
+ * Return whether an unknown error represents a missing filesystem path.
24
+ *
25
+ * @param error Unknown error value.
26
+ * @returns `true` when the error has Node.js code `ENOENT`.
27
+ */
28
+ export declare function isFileNotFoundError(error: unknown): boolean;
@@ -0,0 +1,53 @@
1
+ import { promises as fsp } from "node:fs";
2
+ /**
3
+ * Return whether a filesystem path exists without blocking the event loop.
4
+ *
5
+ * @param filePath Absolute or relative path to check.
6
+ * @returns `true` when the path can be accessed, otherwise `false`.
7
+ */
8
+ export async function pathExists(filePath) {
9
+ try {
10
+ await fsp.access(filePath);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * Read a UTF-8 file when it exists and otherwise return `null`.
19
+ *
20
+ * @param filePath Absolute or relative file path to read.
21
+ * @returns The UTF-8 source, or `null` when the file is missing.
22
+ */
23
+ export async function readOptionalUtf8File(filePath) {
24
+ try {
25
+ return await fsp.readFile(filePath, "utf8");
26
+ }
27
+ catch (error) {
28
+ if (isFileNotFoundError(error)) {
29
+ return null;
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ /**
35
+ * Extract a Node.js error code from an unknown thrown value.
36
+ *
37
+ * @param error Unknown error value.
38
+ * @returns The string error code, or an empty string when unavailable.
39
+ */
40
+ export function getNodeErrorCode(error) {
41
+ return typeof error === "object" && error !== null && "code" in error
42
+ ? String(error.code)
43
+ : "";
44
+ }
45
+ /**
46
+ * Return whether an unknown error represents a missing filesystem path.
47
+ *
48
+ * @param error Unknown error value.
49
+ * @returns `true` when the error has Node.js code `ENOENT`.
50
+ */
51
+ export function isFileNotFoundError(error) {
52
+ return getNodeErrorCode(error) === "ENOENT";
53
+ }
@@ -12,7 +12,7 @@ const PACKAGE_MANAGER_DATA = [
12
12
  id: "npm",
13
13
  label: "npm",
14
14
  packageManagerField: "npm@11.6.1",
15
- installCommand: "npm install",
15
+ installCommand: "npm install --no-audit",
16
16
  frozenInstallCommand: "npm ci",
17
17
  },
18
18
  {
@@ -12,5 +12,21 @@ export type ReplacePhpFunctionDefinitionOptions = PhpFunctionRangeOptions & {
12
12
  export declare function escapeRegex(value: string): string;
13
13
  export declare function quotePhpString(value: string): string;
14
14
  export declare function hasPhpFunctionDefinition(source: string, functionName: string): boolean;
15
+ /**
16
+ * Detect a PHP function call outside strings, comments, heredoc, and nowdoc blocks.
17
+ *
18
+ * @param source PHP source to scan.
19
+ * @param functionName Literal PHP function identifier to find.
20
+ * @returns Whether `source` contains a code-mode call to `functionName`.
21
+ */
22
+ export declare function hasPhpFunctionCall(source: string, functionName: string): boolean;
23
+ /**
24
+ * Locate a PHP function body without counting braces in non-code regions.
25
+ *
26
+ * @param source PHP source to scan.
27
+ * @param functionName Literal PHP function identifier to locate.
28
+ * @param options Range options such as trailing newline inclusion.
29
+ * @returns The matched {@link PhpFunctionRange}, or `null` when no safe range exists.
30
+ */
15
31
  export declare function findPhpFunctionRange(source: string, functionName: string, options?: PhpFunctionRangeOptions): PhpFunctionRange | null;
16
32
  export declare function replacePhpFunctionDefinition(source: string, functionName: string, replacement: string, options?: ReplacePhpFunctionDefinitionOptions): string | null;
@@ -7,6 +7,243 @@ export function quotePhpString(value) {
7
7
  export function hasPhpFunctionDefinition(source, functionName) {
8
8
  return new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\(`, "u").test(source);
9
9
  }
10
+ function isPhpIdentifierStart(character) {
11
+ return /^[A-Za-z_]$/u.test(character ?? "");
12
+ }
13
+ function isPhpIdentifierPart(character) {
14
+ return /^[A-Za-z0-9_]$/u.test(character ?? "");
15
+ }
16
+ function isPhpLineStart(source, index) {
17
+ return index === 0 || source[index - 1] === "\n";
18
+ }
19
+ function isPhpHorizontalWhitespace(character) {
20
+ return character === " " || character === "\t";
21
+ }
22
+ function isPhpWhitespace(character) {
23
+ return typeof character === "string" && /\s/u.test(character);
24
+ }
25
+ function findPhpLineBoundary(source, index) {
26
+ const newlineIndex = source.indexOf("\n", index);
27
+ if (newlineIndex === -1) {
28
+ return {
29
+ contentEnd: source.endsWith("\r") ? source.length - 1 : source.length,
30
+ nextStart: source.length,
31
+ };
32
+ }
33
+ return {
34
+ contentEnd: source[newlineIndex - 1] === "\r" ? newlineIndex - 1 : newlineIndex,
35
+ nextStart: newlineIndex + 1,
36
+ };
37
+ }
38
+ function parsePhpHeredocStart(source, index) {
39
+ if (!source.startsWith("<<<", index)) {
40
+ return null;
41
+ }
42
+ let cursor = index + 3;
43
+ while (isPhpHorizontalWhitespace(source[cursor])) {
44
+ cursor += 1;
45
+ }
46
+ const quote = source[cursor] === "'" || source[cursor] === '"' ? source[cursor] : "";
47
+ if (quote) {
48
+ cursor += 1;
49
+ }
50
+ if (!isPhpIdentifierStart(source[cursor])) {
51
+ return null;
52
+ }
53
+ const delimiterStart = cursor;
54
+ cursor += 1;
55
+ while (isPhpIdentifierPart(source[cursor])) {
56
+ cursor += 1;
57
+ }
58
+ const delimiter = source.slice(delimiterStart, cursor);
59
+ if (quote) {
60
+ if (source[cursor] !== quote) {
61
+ return null;
62
+ }
63
+ cursor += 1;
64
+ }
65
+ const lineBoundary = findPhpLineBoundary(source, cursor);
66
+ if (source.slice(cursor, lineBoundary.contentEnd).trim() !== "") {
67
+ return null;
68
+ }
69
+ return {
70
+ contentStart: lineBoundary.nextStart,
71
+ delimiter,
72
+ };
73
+ }
74
+ function findPhpHeredocClosingEnd(source, index, delimiter) {
75
+ if (!isPhpLineStart(source, index)) {
76
+ return null;
77
+ }
78
+ let cursor = index;
79
+ while (isPhpHorizontalWhitespace(source[cursor])) {
80
+ cursor += 1;
81
+ }
82
+ if (!source.startsWith(delimiter, cursor)) {
83
+ return null;
84
+ }
85
+ cursor += delimiter.length;
86
+ if (isPhpIdentifierPart(source[cursor])) {
87
+ return null;
88
+ }
89
+ let continuationCursor = cursor;
90
+ while (isPhpHorizontalWhitespace(source[continuationCursor])) {
91
+ continuationCursor += 1;
92
+ }
93
+ const continuation = source[continuationCursor];
94
+ if (continuationCursor >= source.length ||
95
+ continuation === "\r" ||
96
+ continuation === "\n" ||
97
+ !isPhpIdentifierPart(continuation)) {
98
+ return cursor;
99
+ }
100
+ return null;
101
+ }
102
+ function skipPhpCallTrivia(source, index) {
103
+ let cursor = index;
104
+ while (cursor < source.length) {
105
+ while (isPhpWhitespace(source[cursor])) {
106
+ cursor += 1;
107
+ }
108
+ if (source[cursor] === "/" && source[cursor + 1] === "*") {
109
+ const commentEnd = source.indexOf("*/", cursor + 2);
110
+ if (commentEnd === -1) {
111
+ return null;
112
+ }
113
+ cursor = commentEnd + 2;
114
+ continue;
115
+ }
116
+ if (source[cursor] === "/" && source[cursor + 1] === "/") {
117
+ cursor = findPhpLineBoundary(source, cursor + 2).nextStart;
118
+ continue;
119
+ }
120
+ if (source[cursor] === "#" && source[cursor + 1] !== "[") {
121
+ cursor = findPhpLineBoundary(source, cursor + 1).nextStart;
122
+ continue;
123
+ }
124
+ return cursor;
125
+ }
126
+ return cursor;
127
+ }
128
+ function matchesPhpFunctionCallAt(source, index, functionName) {
129
+ if (!source.startsWith(functionName, index)) {
130
+ return false;
131
+ }
132
+ if (isPhpIdentifierPart(source[index - 1])) {
133
+ return false;
134
+ }
135
+ const cursor = index + functionName.length;
136
+ if (isPhpIdentifierPart(source[cursor])) {
137
+ return false;
138
+ }
139
+ const callStart = skipPhpCallTrivia(source, cursor);
140
+ return callStart !== null && source[callStart] === "(";
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;
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;
202
+ }
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;
216
+ continue;
217
+ }
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
+ if (matchesPhpFunctionCallAt(source, index, functionName)) {
233
+ return true;
234
+ }
235
+ index += 1;
236
+ }
237
+ return false;
238
+ }
239
+ /**
240
+ * Locate a PHP function body without counting braces in non-code regions.
241
+ *
242
+ * @param source PHP source to scan.
243
+ * @param functionName Literal PHP function identifier to locate.
244
+ * @param options Range options such as trailing newline inclusion.
245
+ * @returns The matched {@link PhpFunctionRange}, or `null` when no safe range exists.
246
+ */
10
247
  export function findPhpFunctionRange(source, functionName, options = {}) {
11
248
  const signaturePattern = new RegExp(`function\\s+${escapeRegex(functionName)}\\s*\\([^)]*\\)\\s*(?::\\s*[^{};]+)?\\s*\\{`, "u");
12
249
  const signatureMatch = signaturePattern.exec(source);
@@ -20,13 +257,95 @@ export function findPhpFunctionRange(source, functionName, options = {}) {
20
257
  }
21
258
  const openBraceIndex = functionStart + openBraceOffset;
22
259
  let depth = 0;
23
- for (let index = openBraceIndex; index < source.length; index += 1) {
260
+ let mode = "code";
261
+ let heredocDelimiter = "";
262
+ let index = openBraceIndex;
263
+ while (index < source.length) {
24
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;
327
+ }
328
+ if (character === "/" && source[index + 1] === "*") {
329
+ mode = "block-comment";
330
+ index += 2;
331
+ continue;
332
+ }
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
+ }
25
342
  if (character === "{") {
26
343
  depth += 1;
344
+ index += 1;
27
345
  continue;
28
346
  }
29
347
  if (character !== "}") {
348
+ index += 1;
30
349
  continue;
31
350
  }
32
351
  depth -= 1;
@@ -43,6 +362,7 @@ export function findPhpFunctionRange(source, functionName, options = {}) {
43
362
  start: functionStart,
44
363
  };
45
364
  }
365
+ index += 1;
46
366
  }
47
367
  return null;
48
368
  }
@@ -1,4 +1,3 @@
1
- import fs from "node:fs";
2
1
  import { promises as fsp } from "node:fs";
3
2
  import path from "node:path";
4
3
  import { execSync } from "node:child_process";
@@ -12,6 +11,7 @@ import { formatNonEmptyTargetDirectoryError, } from "./scaffold-bootstrap.js";
12
11
  import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
13
12
  import { copyInterpolatedDirectory } from "./template-render.js";
14
13
  import { formatInstallCommand, transformPackageManagerText, } from "./package-managers.js";
14
+ 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";
@@ -28,10 +28,7 @@ const LOCKFILES = {
28
28
  yarn: ["yarn.lock"],
29
29
  };
30
30
  export async function ensureDirectory(targetDir, allowExisting = false) {
31
- if (!fs.existsSync(targetDir)) {
32
- await fsp.mkdir(targetDir, { recursive: true });
33
- return;
34
- }
31
+ await fsp.mkdir(targetDir, { recursive: true });
35
32
  if (allowExisting) {
36
33
  return;
37
34
  }
@@ -68,7 +65,7 @@ async function writeBuiltInCodeArtifacts(targetDir, codeArtifacts) {
68
65
  await fsp.writeFile(destinationPath, artifact.source, "utf8");
69
66
  }
70
67
  }
71
- function resolveScaffoldGeneratorNodeModulesPath() {
68
+ async function resolveScaffoldGeneratorNodeModulesPath() {
72
69
  const projectToolsPackageRoot = path.resolve(__dirname, "..", "..");
73
70
  const candidates = [
74
71
  path.join(projectToolsPackageRoot, "node_modules"),
@@ -76,7 +73,7 @@ function resolveScaffoldGeneratorNodeModulesPath() {
76
73
  path.resolve(projectToolsPackageRoot, "..", "..", "node_modules"),
77
74
  ];
78
75
  for (const candidate of candidates) {
79
- if (fs.existsSync(path.join(candidate, "typia", "package.json"))) {
76
+ if (await pathExists(path.join(candidate, "typia", "package.json"))) {
80
77
  return candidate;
81
78
  }
82
79
  }
@@ -84,11 +81,11 @@ function resolveScaffoldGeneratorNodeModulesPath() {
84
81
  }
85
82
  async function withEphemeralScaffoldNodeModules(targetDir, callback) {
86
83
  const targetNodeModulesPath = path.join(targetDir, "node_modules");
87
- if (fs.existsSync(targetNodeModulesPath)) {
84
+ if (await pathExists(targetNodeModulesPath)) {
88
85
  await callback();
89
86
  return;
90
87
  }
91
- const sourceNodeModulesPath = resolveScaffoldGeneratorNodeModulesPath();
88
+ const sourceNodeModulesPath = await resolveScaffoldGeneratorNodeModulesPath();
92
89
  if (!sourceNodeModulesPath) {
93
90
  throw new Error("Unable to resolve a node_modules directory with typia for scaffold-time REST artifact generation.");
94
91
  }
@@ -134,9 +131,7 @@ export async function normalizePackageManagerFiles(targetDir, packageManagerId)
134
131
  await fsp.writeFile(yarnRcPath, "nodeLinker: node-modules\n", "utf8");
135
132
  return;
136
133
  }
137
- if (fs.existsSync(yarnRcPath)) {
138
- await fsp.rm(yarnRcPath, { force: true });
139
- }
134
+ await fsp.rm(yarnRcPath, { force: true });
140
135
  }
141
136
  export async function removeQueryLoopPlaceholderFiles(projectDir, templateId) {
142
137
  if (templateId !== "query-loop") {
@@ -153,10 +148,7 @@ export async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
153
148
  if (keep.has(filename)) {
154
149
  return;
155
150
  }
156
- const filePath = path.join(targetDir, filename);
157
- if (fs.existsSync(filePath)) {
158
- await fsp.rm(filePath, { force: true });
159
- }
151
+ await fsp.rm(path.join(targetDir, filename), { force: true });
160
152
  }));
161
153
  }
162
154
  /**
@@ -251,7 +243,7 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
251
243
  title: "Finalizing scaffold output",
252
244
  });
253
245
  const readmePath = path.join(projectDir, "README.md");
254
- if (!fs.existsSync(readmePath)) {
246
+ if (!(await pathExists(readmePath))) {
255
247
  await fsp.writeFile(readmePath, readmeContent ??
256
248
  buildReadme(templateId, variables, packageManager, {
257
249
  withMigrationUi,
@@ -260,9 +252,7 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
260
252
  }), "utf8");
261
253
  }
262
254
  const gitignorePath = path.join(projectDir, ".gitignore");
263
- const existingGitignore = fs.existsSync(gitignorePath)
264
- ? await fsp.readFile(gitignorePath, "utf8")
265
- : "";
255
+ const existingGitignore = await readOptionalUtf8File(gitignorePath) ?? "";
266
256
  await fsp.writeFile(gitignorePath, mergeTextLines(gitignoreContent ?? buildGitignore(), existingGitignore), "utf8");
267
257
  await normalizePackageJson(projectDir, packageManager);
268
258
  await applyGeneratedProjectDxPackageJson({
@@ -8,6 +8,7 @@ import { getPackageVersions } from "./package-versions.js";
8
8
  import { getStarterManifestFiles, stringifyStarterManifest } from "./starter-manifests.js";
9
9
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, } from "./template-registry.js";
10
10
  import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
11
+ import { pathExists } from "./fs-async.js";
11
12
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
12
13
  /**
13
14
  * Ensures the scaffold target directory exists and is empty unless explicitly allowed.
@@ -17,10 +18,7 @@ const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junctio
17
18
  * @returns A promise that resolves once the directory precondition is satisfied.
18
19
  */
19
20
  export async function ensureScaffoldDirectory(targetDir, allowExisting = false) {
20
- if (!fs.existsSync(targetDir)) {
21
- await fsp.mkdir(targetDir, { recursive: true });
22
- return;
23
- }
21
+ await fsp.mkdir(targetDir, { recursive: true });
24
22
  if (allowExisting) {
25
23
  return;
26
24
  }
@@ -165,14 +163,14 @@ export async function applyWorkspaceMigrationCapability(projectDir, packageManag
165
163
  *
166
164
  * @returns The first matching path, or `null` when no candidate contains `typia`.
167
165
  */
168
- function resolveScaffoldGeneratorNodeModulesPath() {
166
+ async function resolveScaffoldGeneratorNodeModulesPath() {
169
167
  const candidates = [
170
168
  path.join(PROJECT_TOOLS_PACKAGE_ROOT, "node_modules"),
171
169
  path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", ".."),
172
170
  path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", "..", "node_modules"),
173
171
  ];
174
172
  for (const candidate of candidates) {
175
- if (fs.existsSync(path.join(candidate, "typia", "package.json"))) {
173
+ if (await pathExists(path.join(candidate, "typia", "package.json"))) {
176
174
  return candidate;
177
175
  }
178
176
  }
@@ -193,11 +191,11 @@ function resolveScaffoldGeneratorNodeModulesPath() {
193
191
  */
194
192
  async function withEphemeralScaffoldNodeModules(targetDir, callback) {
195
193
  const targetNodeModulesPath = path.join(targetDir, "node_modules");
196
- if (fs.existsSync(targetNodeModulesPath)) {
194
+ if (await pathExists(targetNodeModulesPath)) {
197
195
  await callback();
198
196
  return;
199
197
  }
200
- const sourceNodeModulesPath = resolveScaffoldGeneratorNodeModulesPath();
198
+ const sourceNodeModulesPath = await resolveScaffoldGeneratorNodeModulesPath();
201
199
  if (!sourceNodeModulesPath) {
202
200
  throw new Error("Unable to resolve a node_modules directory with typia for scaffold-time REST artifact generation.");
203
201
  }
@@ -32,6 +32,16 @@ export interface ScaffoldCompatibilityConfig {
32
32
  requiredFeatures: string[];
33
33
  runtimeGates: string[];
34
34
  }
35
+ /**
36
+ * Optional hooks for surfacing user-authored compatibility header repairs.
37
+ */
38
+ export interface UpdatePluginHeaderCompatibilityOptions {
39
+ /**
40
+ * Receives warnings when a malformed existing plugin header value is
41
+ * replaced by the resolved policy floor.
42
+ */
43
+ onWarning?: (warning: string) => void;
44
+ }
35
45
  /**
36
46
  * Baseline headers used by scaffold output before optional features are added.
37
47
  */
@@ -61,7 +71,9 @@ export declare function renderScaffoldCompatibilityConfig(policy: ScaffoldCompat
61
71
  /**
62
72
  * Patch a generated plugin bootstrap header without lowering custom floors.
63
73
  *
64
- * Preserves the original header line endings while replacing empty or invalid
65
- * version strings with the policy values.
74
+ * Preserves the original header line endings while replacing empty version
75
+ * strings with policy values. Malformed user-authored values are reported
76
+ * through `options.onWarning`; without a warning handler they throw instead of
77
+ * falling back silently.
66
78
  */
67
- export declare function updatePluginHeaderCompatibility(source: string, policy: ScaffoldCompatibilityPolicy): string;
79
+ export declare function updatePluginHeaderCompatibility(source: string, policy: ScaffoldCompatibilityPolicy, options?: UpdatePluginHeaderCompatibilityOptions): string;