@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.
- package/dist/runtime/block-targets.d.ts +40 -0
- package/dist/runtime/block-targets.js +71 -0
- package/dist/runtime/built-in-block-artifact-types.js +2 -1
- package/dist/runtime/built-in-block-attribute-specs.js +2 -1
- package/dist/runtime/built-in-block-code-artifacts.js +2 -0
- package/dist/runtime/built-in-block-non-ts-family-artifacts.js +12 -9
- package/dist/runtime/built-in-block-non-ts-render-utils.js +2 -0
- package/dist/runtime/cli-add-block-config.js +2 -1
- package/dist/runtime/cli-add-block-json.d.ts +2 -2
- package/dist/runtime/cli-add-block-json.js +5 -4
- package/dist/runtime/cli-add-block.js +4 -3
- package/dist/runtime/cli-add-workspace-ability-scaffold.js +21 -15
- package/dist/runtime/cli-add-workspace-ability.js +2 -2
- package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +17 -13
- package/dist/runtime/cli-add-workspace-admin-view.js +2 -2
- package/dist/runtime/cli-add-workspace-ai.js +2 -2
- package/dist/runtime/cli-add-workspace-assets.js +42 -48
- package/dist/runtime/cli-add-workspace-rest.js +2 -2
- package/dist/runtime/cli-add-workspace.js +14 -49
- package/dist/runtime/cli-diagnostics.js +6 -0
- package/dist/runtime/cli-init-plan-presentation.d.ts +16 -0
- package/dist/runtime/cli-init-plan-presentation.js +74 -0
- package/dist/runtime/cli-init-plan.js +5 -77
- package/dist/runtime/cli-scaffold.js +2 -1
- package/dist/runtime/create-template-validation.d.ts +10 -0
- package/dist/runtime/create-template-validation.js +121 -0
- package/dist/runtime/package-versions.d.ts +1 -1
- package/dist/runtime/package-versions.js +16 -3
- package/dist/runtime/php-utils.js +151 -148
- package/dist/runtime/scaffold-answer-resolution.js +5 -108
- package/dist/runtime/scaffold-apply-utils.js +3 -2
- package/dist/runtime/scaffold-identifiers.js +4 -3
- package/dist/runtime/scaffold-template-assertions.d.ts +6 -0
- package/dist/runtime/scaffold-template-assertions.js +33 -0
- package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
- package/dist/runtime/scaffold-template-variable-groups.js +7 -0
- package/dist/runtime/string-case.d.ts +2 -4
- package/dist/runtime/string-case.js +15 -4
- package/dist/runtime/template-source-cache-policy.d.ts +53 -0
- package/dist/runtime/template-source-cache-policy.js +135 -0
- package/dist/runtime/template-source-cache.d.ts +1 -45
- package/dist/runtime/template-source-cache.js +9 -152
- package/dist/runtime/ts-property-names.d.ts +11 -0
- package/dist/runtime/ts-property-names.js +16 -0
- package/dist/runtime/workspace-inventory.d.ts +13 -1
- package/dist/runtime/workspace-inventory.js +35 -9
- 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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 (
|
|
171
|
-
|
|
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
|
|
174
|
-
continue;
|
|
188
|
+
return { ambiguous: false, inCode: false, index: index + 2 };
|
|
175
189
|
}
|
|
176
|
-
if (character ===
|
|
177
|
-
|
|
190
|
+
if (character === state.interpolationQuote) {
|
|
191
|
+
state.interpolationQuote = "";
|
|
178
192
|
}
|
|
179
|
-
index
|
|
180
|
-
continue;
|
|
193
|
+
return { ambiguous: false, inCode: false, index: index + 1 };
|
|
181
194
|
}
|
|
182
|
-
if (
|
|
195
|
+
if (state.interpolationComment === "line") {
|
|
183
196
|
if (character === "\r" || character === "\n") {
|
|
184
|
-
|
|
197
|
+
state.interpolationComment = "";
|
|
185
198
|
}
|
|
186
|
-
index
|
|
187
|
-
continue;
|
|
199
|
+
return { ambiguous: false, inCode: false, index: index + 1 };
|
|
188
200
|
}
|
|
189
|
-
if (
|
|
201
|
+
if (state.interpolationComment === "block") {
|
|
190
202
|
if (character === "*" && source[index + 1] === "/") {
|
|
191
|
-
|
|
192
|
-
index
|
|
193
|
-
continue;
|
|
203
|
+
state.interpolationComment = "";
|
|
204
|
+
return { ambiguous: false, inCode: false, index: index + 2 };
|
|
194
205
|
}
|
|
195
|
-
index
|
|
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
|
-
|
|
210
|
-
index
|
|
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
|
-
|
|
215
|
-
index
|
|
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
|
-
|
|
220
|
-
index
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
261
|
-
let heredocDelimiter = "";
|
|
333
|
+
const scanner = createPhpScannerState();
|
|
262
334
|
let index = openBraceIndex;
|
|
263
335
|
while (index < source.length) {
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
266
|
-
|
|
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 (
|
|
329
|
-
|
|
330
|
-
index += 2;
|
|
340
|
+
if (!scan.inCode) {
|
|
341
|
+
index = scan.index;
|
|
331
342
|
continue;
|
|
332
343
|
}
|
|
333
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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 ${
|
|
85
|
+
throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `Template is required in non-interactive mode. Use ${CREATE_TEMPLATE_SELECTION_HINT}.`);
|
|
189
86
|
}
|
|
190
|
-
return
|
|
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
|
|
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
|
|
260
|
+
compoundPersistenceEnabled: isCompoundPersistenceEnabled(variables),
|
|
260
261
|
packageManager,
|
|
261
262
|
projectDir,
|
|
262
263
|
templateId,
|