@wp-typia/project-tools 0.19.2 → 0.19.3
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/cli-diagnostics.d.ts +2 -0
- package/dist/runtime/cli-diagnostics.js +10 -1
- package/dist/runtime/cli-help.js +3 -0
- package/dist/runtime/cli-init.d.ts +49 -0
- package/dist/runtime/cli-init.js +354 -0
- package/dist/runtime/cli-scaffold.d.ts +1 -0
- package/dist/runtime/cli-scaffold.js +5 -1
- package/dist/runtime/cli-templates.js +36 -6
- package/dist/runtime/external-template-guards.d.ts +31 -0
- package/dist/runtime/external-template-guards.js +132 -0
- package/dist/runtime/package-managers.d.ts +8 -0
- package/dist/runtime/package-managers.js +13 -0
- package/dist/runtime/package-versions.d.ts +8 -0
- package/dist/runtime/package-versions.js +84 -19
- package/dist/runtime/scaffold-documents.js +19 -1
- package/dist/runtime/scaffold-onboarding.d.ts +4 -0
- package/dist/runtime/scaffold-onboarding.js +25 -1
- package/dist/runtime/scaffold.js +2 -5
- package/dist/runtime/template-registry.d.ts +23 -1
- package/dist/runtime/template-registry.js +37 -3
- package/dist/runtime/template-source-external.js +9 -3
- package/dist/runtime/template-source-remote.js +5 -0
- package/dist/runtime/template-source-seeds.js +40 -6
- package/package.json +7 -2
- package/templates/_shared/base/package.json.mustache +0 -1
- package/templates/_shared/compound/core/package.json.mustache +0 -1
- package/templates/_shared/compound/persistence/package.json.mustache +0 -1
- package/templates/_shared/persistence/core/package.json.mustache +0 -1
- package/templates/interactivity/package.json.mustache +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export const TEMPLATE_SOURCE_TIMEOUT_CODE = "template-source-timeout";
|
|
3
|
+
export const TEMPLATE_SOURCE_TOO_LARGE_CODE = "template-source-too-large";
|
|
4
|
+
const DEFAULT_EXTERNAL_TEMPLATE_TIMEOUT_MS = 20000;
|
|
5
|
+
const DEFAULT_EXTERNAL_TEMPLATE_CONFIG_MAX_BYTES = 256 * 1024;
|
|
6
|
+
const DEFAULT_EXTERNAL_TEMPLATE_PACKAGE_JSON_MAX_BYTES = 1 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_EXTERNAL_TEMPLATE_METADATA_MAX_BYTES = 1 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_EXTERNAL_TEMPLATE_TARBALL_MAX_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
function parsePositiveIntegerEnv(value, fallback) {
|
|
10
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number.parseInt(value, 10);
|
|
14
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
15
|
+
}
|
|
16
|
+
function createTemplateGuardError(code, message) {
|
|
17
|
+
const error = new Error(message);
|
|
18
|
+
error.code = code;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
export function getExternalTemplateTimeoutMs() {
|
|
22
|
+
return parsePositiveIntegerEnv(process.env.WP_TYPIA_EXTERNAL_TEMPLATE_TIMEOUT_MS, DEFAULT_EXTERNAL_TEMPLATE_TIMEOUT_MS);
|
|
23
|
+
}
|
|
24
|
+
export function getExternalTemplateConfigMaxBytes() {
|
|
25
|
+
return parsePositiveIntegerEnv(process.env.WP_TYPIA_EXTERNAL_TEMPLATE_CONFIG_MAX_BYTES, DEFAULT_EXTERNAL_TEMPLATE_CONFIG_MAX_BYTES);
|
|
26
|
+
}
|
|
27
|
+
export function getExternalTemplatePackageJsonMaxBytes() {
|
|
28
|
+
return parsePositiveIntegerEnv(process.env.WP_TYPIA_EXTERNAL_TEMPLATE_PACKAGE_JSON_MAX_BYTES, DEFAULT_EXTERNAL_TEMPLATE_PACKAGE_JSON_MAX_BYTES);
|
|
29
|
+
}
|
|
30
|
+
export function getExternalTemplateMetadataMaxBytes() {
|
|
31
|
+
return parsePositiveIntegerEnv(process.env.WP_TYPIA_EXTERNAL_TEMPLATE_METADATA_MAX_BYTES, DEFAULT_EXTERNAL_TEMPLATE_METADATA_MAX_BYTES);
|
|
32
|
+
}
|
|
33
|
+
export function getExternalTemplateTarballMaxBytes() {
|
|
34
|
+
return parsePositiveIntegerEnv(process.env.WP_TYPIA_EXTERNAL_TEMPLATE_TARBALL_MAX_BYTES, DEFAULT_EXTERNAL_TEMPLATE_TARBALL_MAX_BYTES);
|
|
35
|
+
}
|
|
36
|
+
export function createExternalTemplateTimeoutError(label, timeoutMs) {
|
|
37
|
+
return createTemplateGuardError(TEMPLATE_SOURCE_TIMEOUT_CODE, `Timed out while ${label} after ${timeoutMs}ms.`);
|
|
38
|
+
}
|
|
39
|
+
export function createExternalTemplateTooLargeError(label, maxBytes) {
|
|
40
|
+
return createTemplateGuardError(TEMPLATE_SOURCE_TOO_LARGE_CODE, `${label} exceeded the external template size limit (${maxBytes} bytes).`);
|
|
41
|
+
}
|
|
42
|
+
export function assertExternalTemplateFileSize(filePath, options) {
|
|
43
|
+
const stats = fs.statSync(filePath);
|
|
44
|
+
if (stats.size > options.maxBytes) {
|
|
45
|
+
throw createExternalTemplateTooLargeError(options.label, options.maxBytes);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function withExternalTemplateTimeout(label, task, timeoutMs = getExternalTemplateTimeoutMs()) {
|
|
49
|
+
let timeoutHandle;
|
|
50
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
51
|
+
timeoutHandle = setTimeout(() => {
|
|
52
|
+
reject(createExternalTemplateTimeoutError(label, timeoutMs));
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const pendingTask = typeof task === "function" ? task() : task;
|
|
57
|
+
return await Promise.race([pendingTask, timeoutPromise]);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (timeoutHandle) {
|
|
61
|
+
clearTimeout(timeoutHandle);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function fetchWithExternalTemplateTimeout(input, options) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeoutMs = options.timeoutMs ?? getExternalTemplateTimeoutMs();
|
|
68
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
69
|
+
try {
|
|
70
|
+
return await fetch(input, {
|
|
71
|
+
...(options.init ?? {}),
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof Error &&
|
|
77
|
+
(error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
78
|
+
throw createExternalTemplateTimeoutError(options.label, timeoutMs);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timeoutHandle);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function readResponseBodyWithLimit(response, options) {
|
|
87
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
88
|
+
if (typeof contentLengthHeader === "string") {
|
|
89
|
+
const declaredLength = Number.parseInt(contentLengthHeader, 10);
|
|
90
|
+
if (Number.isFinite(declaredLength) && declaredLength > options.maxBytes) {
|
|
91
|
+
throw createExternalTemplateTooLargeError(options.label, options.maxBytes);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!response.body) {
|
|
95
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
96
|
+
if (buffer.length > options.maxBytes) {
|
|
97
|
+
throw createExternalTemplateTooLargeError(options.label, options.maxBytes);
|
|
98
|
+
}
|
|
99
|
+
return buffer;
|
|
100
|
+
}
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
const chunks = [];
|
|
103
|
+
let totalBytes = 0;
|
|
104
|
+
while (true) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (!value) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
totalBytes += value.byteLength;
|
|
113
|
+
if (totalBytes > options.maxBytes) {
|
|
114
|
+
await reader.cancel();
|
|
115
|
+
throw createExternalTemplateTooLargeError(options.label, options.maxBytes);
|
|
116
|
+
}
|
|
117
|
+
chunks.push(Buffer.from(value));
|
|
118
|
+
}
|
|
119
|
+
return Buffer.concat(chunks);
|
|
120
|
+
}
|
|
121
|
+
export async function readJsonResponseWithLimit(response, options) {
|
|
122
|
+
const buffer = await readResponseBodyWithLimit(response, options);
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
throw new Error(`Failed to parse ${options.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export async function readBufferResponseWithLimit(response, options) {
|
|
131
|
+
return readResponseBodyWithLimit(response, options);
|
|
132
|
+
}
|
|
@@ -16,6 +16,14 @@ export declare function getPackageManagerSelectOptions(): Array<{
|
|
|
16
16
|
}>;
|
|
17
17
|
export declare function formatRunScript(packageManagerId: PackageManagerId, scriptName: string, extraArgs?: string): string;
|
|
18
18
|
export declare function formatInstallCommand(packageManagerId: PackageManagerId): string;
|
|
19
|
+
/**
|
|
20
|
+
* Format a package-manager-specific devDependency install command.
|
|
21
|
+
*
|
|
22
|
+
* @param packageManagerId Package manager identifier.
|
|
23
|
+
* @param packages Package specifiers to add as devDependencies.
|
|
24
|
+
* @returns Command string suitable for shell execution.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatAddDevDependenciesCommand(packageManagerId: PackageManagerId, packages: string[]): string;
|
|
19
27
|
/**
|
|
20
28
|
* Format a package-manager-specific one-off package execution command.
|
|
21
29
|
*
|
|
@@ -67,6 +67,19 @@ export function formatRunScript(packageManagerId, scriptName, extraArgs = "") {
|
|
|
67
67
|
export function formatInstallCommand(packageManagerId) {
|
|
68
68
|
return getPackageManager(packageManagerId).installCommand;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Format a package-manager-specific devDependency install command.
|
|
72
|
+
*
|
|
73
|
+
* @param packageManagerId Package manager identifier.
|
|
74
|
+
* @param packages Package specifiers to add as devDependencies.
|
|
75
|
+
* @returns Command string suitable for shell execution.
|
|
76
|
+
*/
|
|
77
|
+
export function formatAddDevDependenciesCommand(packageManagerId, packages) {
|
|
78
|
+
if (packages.length === 0) {
|
|
79
|
+
return formatInstallCommand(packageManagerId);
|
|
80
|
+
}
|
|
81
|
+
return `${packageManagerId} ${DEV_INSTALL_FLAGS[packageManagerId]} ${packages.join(" ")}`;
|
|
82
|
+
}
|
|
70
83
|
/**
|
|
71
84
|
* Format a package-manager-specific one-off package execution command.
|
|
72
85
|
*
|
|
@@ -7,5 +7,13 @@ interface PackageVersions {
|
|
|
7
7
|
wpTypiaPackageExactVersion: string;
|
|
8
8
|
wpTypiaPackageVersion: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Clears the in-memory cache used by `getPackageVersions()`.
|
|
12
|
+
*
|
|
13
|
+
* Long-lived processes can call this after regenerating or updating package
|
|
14
|
+
* manifests when they want the next lookup to recompute version metadata
|
|
15
|
+
* synchronously from disk.
|
|
16
|
+
*/
|
|
17
|
+
export declare function invalidatePackageVersionsCache(): void;
|
|
10
18
|
export declare function getPackageVersions(): PackageVersions;
|
|
11
19
|
export {};
|
|
@@ -31,55 +31,120 @@ function normalizeExactVersion(value) {
|
|
|
31
31
|
}
|
|
32
32
|
return trimmed.replace(/^[~^<>=]+/, "");
|
|
33
33
|
}
|
|
34
|
-
function
|
|
34
|
+
function createContentFingerprint(source) {
|
|
35
|
+
let hash = 2166136261;
|
|
36
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
37
|
+
hash ^= source.charCodeAt(index);
|
|
38
|
+
hash = Math.imul(hash, 16777619);
|
|
39
|
+
}
|
|
40
|
+
return (hash >>> 0).toString(16);
|
|
41
|
+
}
|
|
42
|
+
function resolvePackageManifestLocation(packageJsonPath) {
|
|
35
43
|
try {
|
|
36
|
-
|
|
44
|
+
const stats = fs.statSync(packageJsonPath);
|
|
45
|
+
const source = fs.readFileSync(packageJsonPath, "utf8");
|
|
46
|
+
return {
|
|
47
|
+
cacheKey: `file:${packageJsonPath}:${stats.ino}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.size}:${createContentFingerprint(source)}`,
|
|
48
|
+
packageJsonPath,
|
|
49
|
+
source,
|
|
50
|
+
};
|
|
37
51
|
}
|
|
38
52
|
catch (error) {
|
|
39
53
|
if (getErrorCode(error) === "ENOENT") {
|
|
40
|
-
return
|
|
54
|
+
return {
|
|
55
|
+
cacheKey: `missing-file:${packageJsonPath}`,
|
|
56
|
+
packageJsonPath: null,
|
|
57
|
+
source: null,
|
|
58
|
+
};
|
|
41
59
|
}
|
|
42
60
|
throw error;
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
|
-
function
|
|
63
|
+
function readPackageManifest(location) {
|
|
64
|
+
if (!location.packageJsonPath || location.source === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return JSON.parse(location.source);
|
|
68
|
+
}
|
|
69
|
+
function resolveInstalledPackageManifestLocation(packageName) {
|
|
46
70
|
try {
|
|
47
|
-
return
|
|
71
|
+
return resolvePackageManifestLocation(require.resolve(`${packageName}/package.json`));
|
|
48
72
|
}
|
|
49
73
|
catch (error) {
|
|
50
74
|
if (getErrorCode(error) === "MODULE_NOT_FOUND") {
|
|
51
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
cacheKey: `missing-module:${packageName}`,
|
|
77
|
+
packageJsonPath: null,
|
|
78
|
+
source: null,
|
|
79
|
+
};
|
|
52
80
|
}
|
|
53
81
|
throw error;
|
|
54
82
|
}
|
|
55
83
|
}
|
|
84
|
+
function composePackageVersionsCacheKey(locations) {
|
|
85
|
+
return locations.map((location) => location.cacheKey).join("|");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clears the in-memory cache used by `getPackageVersions()`.
|
|
89
|
+
*
|
|
90
|
+
* Long-lived processes can call this after regenerating or updating package
|
|
91
|
+
* manifests when they want the next lookup to recompute version metadata
|
|
92
|
+
* synchronously from disk.
|
|
93
|
+
*/
|
|
94
|
+
export function invalidatePackageVersionsCache() {
|
|
95
|
+
cachedPackageVersions = null;
|
|
96
|
+
}
|
|
56
97
|
export function getPackageVersions() {
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
const createManifestLocation = resolvePackageManifestLocation(path.join(PROJECT_TOOLS_PACKAGE_ROOT, "package.json"));
|
|
99
|
+
const blockRuntimeManifestLocation = resolvePackageManifestLocation(path.join(PROJECT_TOOLS_PACKAGE_ROOT, "..", "wp-typia-block-runtime", "package.json"));
|
|
100
|
+
const wpTypiaManifestLocation = resolvePackageManifestLocation(path.join(PROJECT_TOOLS_PACKAGE_ROOT, "..", "wp-typia", "package.json"));
|
|
101
|
+
const installedProjectToolsManifestLocation = resolveInstalledPackageManifestLocation("@wp-typia/project-tools");
|
|
102
|
+
const installedApiClientManifestLocation = resolveInstalledPackageManifestLocation("@wp-typia/api-client");
|
|
103
|
+
const installedBlockRuntimeManifestLocation = resolveInstalledPackageManifestLocation("@wp-typia/block-runtime");
|
|
104
|
+
const installedBlockTypesManifestLocation = resolveInstalledPackageManifestLocation("@wp-typia/block-types");
|
|
105
|
+
const installedRestManifestLocation = resolveInstalledPackageManifestLocation("@wp-typia/rest");
|
|
106
|
+
const installedWpTypiaManifestLocation = resolveInstalledPackageManifestLocation("wp-typia");
|
|
107
|
+
const cacheKey = composePackageVersionsCacheKey([
|
|
108
|
+
createManifestLocation,
|
|
109
|
+
blockRuntimeManifestLocation,
|
|
110
|
+
wpTypiaManifestLocation,
|
|
111
|
+
installedProjectToolsManifestLocation,
|
|
112
|
+
installedApiClientManifestLocation,
|
|
113
|
+
installedBlockRuntimeManifestLocation,
|
|
114
|
+
installedBlockTypesManifestLocation,
|
|
115
|
+
installedRestManifestLocation,
|
|
116
|
+
installedWpTypiaManifestLocation,
|
|
117
|
+
]);
|
|
118
|
+
if (cachedPackageVersions?.cacheKey === cacheKey) {
|
|
119
|
+
return cachedPackageVersions.versions;
|
|
59
120
|
}
|
|
60
|
-
const createManifest = readPackageManifest(
|
|
61
|
-
|
|
121
|
+
const createManifest = readPackageManifest(createManifestLocation) ??
|
|
122
|
+
readPackageManifest(installedProjectToolsManifestLocation) ??
|
|
62
123
|
{};
|
|
63
|
-
const blockRuntimeManifest = readPackageManifest(
|
|
64
|
-
|
|
124
|
+
const blockRuntimeManifest = readPackageManifest(blockRuntimeManifestLocation) ??
|
|
125
|
+
readPackageManifest(installedBlockRuntimeManifestLocation) ??
|
|
65
126
|
{};
|
|
66
|
-
const wpTypiaManifest = readPackageManifest(
|
|
67
|
-
|
|
127
|
+
const wpTypiaManifest = readPackageManifest(wpTypiaManifestLocation) ??
|
|
128
|
+
readPackageManifest(installedWpTypiaManifestLocation) ??
|
|
68
129
|
{};
|
|
69
130
|
const blockRuntimeDependencyVersion = normalizeVersionRange(createManifest.dependencies?.["@wp-typia/block-runtime"]);
|
|
70
|
-
|
|
131
|
+
const versions = {
|
|
71
132
|
apiClientPackageVersion: normalizeVersionRange(createManifest.dependencies?.["@wp-typia/api-client"] ??
|
|
72
|
-
|
|
133
|
+
readPackageManifest(installedApiClientManifestLocation)?.version),
|
|
73
134
|
blockRuntimePackageVersion: blockRuntimeDependencyVersion !== DEFAULT_VERSION_RANGE
|
|
74
135
|
? blockRuntimeDependencyVersion
|
|
75
136
|
: normalizeVersionRange(blockRuntimeManifest.version),
|
|
76
137
|
blockTypesPackageVersion: normalizeVersionRange(createManifest.dependencies?.["@wp-typia/block-types"] ??
|
|
77
|
-
|
|
138
|
+
readPackageManifest(installedBlockTypesManifestLocation)?.version),
|
|
78
139
|
projectToolsPackageVersion: normalizeVersionRange(createManifest.version),
|
|
79
140
|
restPackageVersion: normalizeVersionRange(createManifest.dependencies?.["@wp-typia/rest"] ??
|
|
80
|
-
|
|
141
|
+
readPackageManifest(installedRestManifestLocation)?.version),
|
|
81
142
|
wpTypiaPackageExactVersion: normalizeExactVersion(wpTypiaManifest.version),
|
|
82
143
|
wpTypiaPackageVersion: normalizeVersionRange(wpTypiaManifest.version),
|
|
83
144
|
};
|
|
84
|
-
|
|
145
|
+
cachedPackageVersions = {
|
|
146
|
+
cacheKey,
|
|
147
|
+
versions,
|
|
148
|
+
};
|
|
149
|
+
return versions;
|
|
85
150
|
}
|
|
@@ -3,6 +3,24 @@ import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getIniti
|
|
|
3
3
|
import { formatPackageExecCommand, formatInstallCommand, formatRunScript, } from './package-managers.js';
|
|
4
4
|
import { getPackageVersions } from './package-versions.js';
|
|
5
5
|
import { getScaffoldTemplateVariableGroups } from './scaffold-template-variable-groups.js';
|
|
6
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_ALIAS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, normalizeTemplateLookupId, } from './template-registry.js';
|
|
7
|
+
function formatReadmeTemplateIdentity(templateId) {
|
|
8
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
9
|
+
if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
|
|
10
|
+
return [
|
|
11
|
+
`- Alias: ${OFFICIAL_WORKSPACE_TEMPLATE_ALIAS}`,
|
|
12
|
+
`- Package: ${OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE}`,
|
|
13
|
+
'- Type: official workspace scaffold',
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
16
|
+
if (normalizedTemplateId === 'query-loop') {
|
|
17
|
+
return ['- Family: query-loop', '- Type: create-time core/query variation scaffold'].join('\n');
|
|
18
|
+
}
|
|
19
|
+
if (isBuiltInTemplateId(normalizedTemplateId)) {
|
|
20
|
+
return [`- Family: ${normalizedTemplateId}`, '- Type: built-in block scaffold'].join('\n');
|
|
21
|
+
}
|
|
22
|
+
return [`- Template id: ${templateId}`, '- Type: custom or external scaffold'].join('\n');
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* Builds the generated README markdown for one scaffolded project.
|
|
8
26
|
*
|
|
@@ -73,7 +91,7 @@ ${variables.description}
|
|
|
73
91
|
|
|
74
92
|
## Template
|
|
75
93
|
|
|
76
|
-
${templateId}
|
|
94
|
+
${formatReadmeTemplateIdentity(templateId)}
|
|
77
95
|
|
|
78
96
|
## Quick Start
|
|
79
97
|
|
|
@@ -22,6 +22,10 @@ export declare function getQuickStartWorkflowNote(packageManager: PackageManager
|
|
|
22
22
|
* Returns the onboarding note explaining when manual sync is optional.
|
|
23
23
|
*/
|
|
24
24
|
export declare function getOptionalOnboardingNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns a shorter optional onboarding note suitable for create completion output.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getOptionalOnboardingShortNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
25
29
|
/**
|
|
26
30
|
* Returns the recommended version-control commands for a fresh scaffold.
|
|
27
31
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatPackageExecCommand, formatRunScript, } from "./package-managers.js";
|
|
2
2
|
import { getPackageVersions } from "./package-versions.js";
|
|
3
3
|
import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
|
|
4
|
-
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
4
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, normalizeTemplateLookupId, } from "./template-registry.js";
|
|
5
5
|
const INITIAL_COMMIT_COMMANDS = [
|
|
6
6
|
"git init",
|
|
7
7
|
"git add .",
|
|
@@ -99,6 +99,30 @@ export function getOptionalOnboardingNote(packageManager, templateId = "basic",
|
|
|
99
99
|
}
|
|
100
100
|
return `You usually do not need to run ${syncCommand} during a normal ${formatRunScript(packageManager, developmentScript)} session. Run ${syncCommand} before ${formatRunScript(packageManager, "build")}, ${typecheckCommand}, or ${doctorCommand} when you want a reviewable refresh. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` or \`${strictSyncCommand}\` for stricter CI checks.${advancedPersistenceNote} Generated syncs do not create migration history, so refresh before your first commit if this directory is new.`;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns a shorter optional onboarding note suitable for create completion output.
|
|
104
|
+
*/
|
|
105
|
+
export function getOptionalOnboardingShortNote(packageManager, templateId = "basic", options = {}) {
|
|
106
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
107
|
+
const doctorCommand = getDoctorVerificationCommand(packageManager);
|
|
108
|
+
if (normalizedTemplateId === "query-loop") {
|
|
109
|
+
return `No sync step is generated for this Query Loop scaffold. Edit the variation files directly, then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "typecheck")}, or ${doctorCommand} when you want a review pass.`;
|
|
110
|
+
}
|
|
111
|
+
const optionalSyncScripts = getOptionalSyncScriptNames(normalizedTemplateId, options);
|
|
112
|
+
const developmentScript = getPrimaryDevelopmentScript(normalizedTemplateId);
|
|
113
|
+
const devCommand = formatRunScript(packageManager, developmentScript);
|
|
114
|
+
const isCustomTemplate = !isBuiltInTemplateId(normalizedTemplateId) &&
|
|
115
|
+
normalizedTemplateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE;
|
|
116
|
+
if (isCustomTemplate && optionalSyncScripts.length === 0) {
|
|
117
|
+
return `Follow the template's own artifact-refresh guidance, then use ${doctorCommand} for a quick environment and workspace sanity check.`;
|
|
118
|
+
}
|
|
119
|
+
if (isCustomTemplate && optionalSyncScripts.length > 0) {
|
|
120
|
+
const syncSteps = optionalSyncScripts.map((scriptName) => formatRunScript(packageManager, scriptName));
|
|
121
|
+
return `Run ${syncSteps.join(" then ")} before build, typecheck, or ${doctorCommand} when you want a reviewable refresh.`;
|
|
122
|
+
}
|
|
123
|
+
const syncCommand = formatRunScript(packageManager, optionalSyncScripts.includes("sync") ? "sync" : "sync-types");
|
|
124
|
+
return `Skip ${syncCommand} during normal ${devCommand} work. Re-run it before build, typecheck, or ${doctorCommand} when you want a reviewable refresh.`;
|
|
125
|
+
}
|
|
102
126
|
/**
|
|
103
127
|
* Returns the recommended version-control commands for a fresh scaffold.
|
|
104
128
|
*/
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -8,13 +8,12 @@ import { applyMigrationUiCapability } from "./migration-ui-capability.js";
|
|
|
8
8
|
import { applyWorkspaceMigrationCapability, ensureScaffoldDirectory, isWorkspaceProject, seedBuiltInPersistenceArtifacts, writeStarterManifestFiles, } from "./scaffold-bootstrap.js";
|
|
9
9
|
import { defaultInstallDependencies, normalizePackageJson, normalizePackageManagerFiles, removeUnexpectedLockfiles, } from "./scaffold-package-manager-files.js";
|
|
10
10
|
import { copyInterpolatedDirectory } from "./template-render.js";
|
|
11
|
-
import {
|
|
11
|
+
import { isBuiltInTemplateId, normalizeTemplateLookupId, } from "./template-registry.js";
|
|
12
12
|
import { resolveTemplateSource } from "./template-source.js";
|
|
13
13
|
import { BlockGeneratorService, } from "./block-generator-service.js";
|
|
14
14
|
import { getTemplateVariables } from "./scaffold-template-variables.js";
|
|
15
15
|
import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
|
|
16
16
|
import { assertExternalLayerCompositionOptions, } from "./cli-validation.js";
|
|
17
|
-
const WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
18
17
|
export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
|
|
19
18
|
export const PERSISTENCE_POLICIES = ["authenticated", "public"];
|
|
20
19
|
export { buildBlockCssClassName } from "./scaffold-identifiers.js";
|
|
@@ -28,9 +27,7 @@ export function isPersistencePolicy(value) {
|
|
|
28
27
|
return PERSISTENCE_POLICIES.includes(value);
|
|
29
28
|
}
|
|
30
29
|
function normalizeTemplateSelection(templateId) {
|
|
31
|
-
return templateId
|
|
32
|
-
? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
33
|
-
: templateId;
|
|
30
|
+
return normalizeTemplateLookupId(templateId);
|
|
34
31
|
}
|
|
35
32
|
async function reportScaffoldProgress(onProgress, event) {
|
|
36
33
|
await onProgress?.(event);
|
|
@@ -24,6 +24,7 @@ export declare const SHARED_TEST_PRESET_TEMPLATE_ROOT: string;
|
|
|
24
24
|
export declare const SHARED_WP_ENV_PRESET_TEMPLATE_ROOT: string;
|
|
25
25
|
export declare const BUILTIN_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound", "query-loop"];
|
|
26
26
|
export declare const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
27
|
+
export declare const OFFICIAL_WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
27
28
|
export type BuiltInTemplateId = (typeof BUILTIN_TEMPLATE_IDS)[number];
|
|
28
29
|
export type PersistencePolicy = "authenticated" | "public";
|
|
29
30
|
export interface TemplateDefinition {
|
|
@@ -36,10 +37,31 @@ export interface TemplateDefinition {
|
|
|
36
37
|
export declare const TEMPLATE_REGISTRY: readonly TemplateDefinition[];
|
|
37
38
|
export declare const TEMPLATE_IDS: BuiltInTemplateId[];
|
|
38
39
|
export declare function isBuiltInTemplateId(templateId: string): templateId is BuiltInTemplateId;
|
|
40
|
+
/**
|
|
41
|
+
* Returns whether a template id matches the user-facing official workspace alias.
|
|
42
|
+
*
|
|
43
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
44
|
+
* @returns `true` when the id is the `workspace` alias.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isOfficialWorkspaceTemplateAlias(templateId: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Converts user-facing template aliases into the registry lookup id.
|
|
49
|
+
*
|
|
50
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
51
|
+
* @returns The registry id used for template resolution.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeTemplateLookupId(templateId: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Converts internal template ids into the id shown in human-facing output.
|
|
56
|
+
*
|
|
57
|
+
* @param templateId Template id stored in the registry.
|
|
58
|
+
* @returns The user-facing template id or alias for display.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getUserFacingTemplateId(templateId: string): string;
|
|
39
61
|
export declare function listTemplates(): readonly TemplateDefinition[];
|
|
40
62
|
export declare function getTemplateById(templateId: string): TemplateDefinition;
|
|
41
63
|
export declare function getTemplateSelectOptions(): Array<{
|
|
42
64
|
label: string;
|
|
43
|
-
value:
|
|
65
|
+
value: string;
|
|
44
66
|
hint: string;
|
|
45
67
|
}>;
|
|
@@ -81,6 +81,7 @@ export const BUILTIN_TEMPLATE_IDS = [
|
|
|
81
81
|
"query-loop",
|
|
82
82
|
];
|
|
83
83
|
export const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
84
|
+
export const OFFICIAL_WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
84
85
|
export const TEMPLATE_REGISTRY = Object.freeze([
|
|
85
86
|
{
|
|
86
87
|
id: "basic",
|
|
@@ -129,14 +130,47 @@ export const TEMPLATE_IDS = [...BUILTIN_TEMPLATE_IDS];
|
|
|
129
130
|
export function isBuiltInTemplateId(templateId) {
|
|
130
131
|
return BUILTIN_TEMPLATE_IDS.includes(templateId);
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns whether a template id matches the user-facing official workspace alias.
|
|
135
|
+
*
|
|
136
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
137
|
+
* @returns `true` when the id is the `workspace` alias.
|
|
138
|
+
*/
|
|
139
|
+
export function isOfficialWorkspaceTemplateAlias(templateId) {
|
|
140
|
+
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Converts user-facing template aliases into the registry lookup id.
|
|
144
|
+
*
|
|
145
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
146
|
+
* @returns The registry id used for template resolution.
|
|
147
|
+
*/
|
|
148
|
+
export function normalizeTemplateLookupId(templateId) {
|
|
149
|
+
return isOfficialWorkspaceTemplateAlias(templateId)
|
|
150
|
+
? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
151
|
+
: templateId;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Converts internal template ids into the id shown in human-facing output.
|
|
155
|
+
*
|
|
156
|
+
* @param templateId Template id stored in the registry.
|
|
157
|
+
* @returns The user-facing template id or alias for display.
|
|
158
|
+
*/
|
|
159
|
+
export function getUserFacingTemplateId(templateId) {
|
|
160
|
+
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
161
|
+
? OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
|
|
162
|
+
: templateId;
|
|
163
|
+
}
|
|
132
164
|
export function listTemplates() {
|
|
133
165
|
return TEMPLATE_REGISTRY;
|
|
134
166
|
}
|
|
135
167
|
export function getTemplateById(templateId) {
|
|
136
|
-
const
|
|
168
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
169
|
+
const template = TEMPLATE_REGISTRY.find((entry) => entry.id === normalizedTemplateId);
|
|
137
170
|
if (!template) {
|
|
138
171
|
throw new Error(`Unknown template "${templateId}". Expected one of: ${[
|
|
139
172
|
...TEMPLATE_IDS,
|
|
173
|
+
OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
|
|
140
174
|
OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE,
|
|
141
175
|
].join(", ")}`);
|
|
142
176
|
}
|
|
@@ -144,8 +178,8 @@ export function getTemplateById(templateId) {
|
|
|
144
178
|
}
|
|
145
179
|
export function getTemplateSelectOptions() {
|
|
146
180
|
return TEMPLATE_REGISTRY.map((template) => ({
|
|
147
|
-
label: template.id,
|
|
148
|
-
value: template.id,
|
|
181
|
+
label: getUserFacingTemplateId(template.id),
|
|
182
|
+
value: getUserFacingTemplateId(template.id),
|
|
149
183
|
hint: template.features.join(", "),
|
|
150
184
|
}));
|
|
151
185
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { assertExternalTemplateFileSize, getExternalTemplateConfigMaxBytes, withExternalTemplateTimeout, } from './external-template-guards.js';
|
|
5
6
|
import { isPlainObject } from './object-utils.js';
|
|
6
7
|
import { toSegmentPascalCase } from './string-case.js';
|
|
7
8
|
import { createManagedTempRoot } from './temp-roots.js';
|
|
@@ -47,8 +48,12 @@ async function loadExternalTemplateConfig(sourceDir) {
|
|
|
47
48
|
if (!entryPath) {
|
|
48
49
|
throw new Error(`No external template config entry found in ${sourceDir}.`);
|
|
49
50
|
}
|
|
51
|
+
assertExternalTemplateFileSize(entryPath, {
|
|
52
|
+
label: `External template config "${entryPath}"`,
|
|
53
|
+
maxBytes: getExternalTemplateConfigMaxBytes(),
|
|
54
|
+
});
|
|
50
55
|
const moduleUrl = `${pathToFileURL(entryPath).href}?mtime=${fs.statSync(entryPath).mtimeMs}`;
|
|
51
|
-
const loadedModule = (await import(moduleUrl));
|
|
56
|
+
const loadedModule = (await withExternalTemplateTimeout(`loading external template config "${entryPath}"`, () => import(moduleUrl)));
|
|
52
57
|
const loadedConfig = loadedModule.default ?? loadedModule;
|
|
53
58
|
if (!isPlainObject(loadedConfig)) {
|
|
54
59
|
throw new Error(`External template config must export an object: ${entryPath}`);
|
|
@@ -150,10 +155,11 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
|
|
|
150
155
|
mergedView.variant = selectedVariant;
|
|
151
156
|
mergedView[getVariantFlagName(selectedVariant)] = true;
|
|
152
157
|
}
|
|
153
|
-
|
|
158
|
+
const transformer = config.transformer;
|
|
159
|
+
if (!transformer) {
|
|
154
160
|
return mergedView;
|
|
155
161
|
}
|
|
156
|
-
const transformed = await
|
|
162
|
+
const transformed = await withExternalTemplateTimeout(`running external template transformer for "${context.slug}"`, () => Promise.resolve(transformer(mergedView)));
|
|
157
163
|
if (!isPlainObject(transformed)) {
|
|
158
164
|
throw new Error('External template transformer(view) must return an object.');
|
|
159
165
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { promises as fsp } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { assertExternalTemplateFileSize, getExternalTemplatePackageJsonMaxBytes, } from './external-template-guards.js';
|
|
4
5
|
import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from './template-builtins.js';
|
|
5
6
|
import { copyRawDirectory } from './template-render.js';
|
|
6
7
|
import { createManagedTempRoot } from './temp-roots.js';
|
|
@@ -66,6 +67,10 @@ function readTemplatePackageJson(sourceDir) {
|
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
try {
|
|
70
|
+
assertExternalTemplateFileSize(candidate, {
|
|
71
|
+
label: `Template metadata file "${candidate}"`,
|
|
72
|
+
maxBytes: getExternalTemplatePackageJsonMaxBytes(),
|
|
73
|
+
});
|
|
69
74
|
return {
|
|
70
75
|
packageJson: JSON.parse(fs.readFileSync(candidate, 'utf8')),
|
|
71
76
|
sourcePath: candidate,
|