@wp-typia/project-tools 0.16.7 → 0.16.9
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/README.md +26 -1
- package/dist/runtime/block-generator-service.d.ts +9 -1
- package/dist/runtime/block-generator-service.js +137 -16
- package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
- package/dist/runtime/block-generator-tool-contract.js +157 -0
- package/dist/runtime/built-in-block-artifacts.js +171 -423
- package/dist/runtime/built-in-block-code-artifacts.js +96 -46
- package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
- package/dist/runtime/built-in-block-code-templates.js +2233 -0
- package/dist/runtime/cli-add-block.d.ts +2 -1
- package/dist/runtime/cli-add-block.js +163 -25
- package/dist/runtime/cli-add-shared.d.ts +7 -0
- package/dist/runtime/cli-add-shared.js +4 -6
- package/dist/runtime/cli-add-workspace.js +56 -17
- package/dist/runtime/cli-core.d.ts +4 -0
- package/dist/runtime/cli-core.js +3 -0
- package/dist/runtime/cli-diagnostics.d.ts +58 -0
- package/dist/runtime/cli-diagnostics.js +101 -0
- package/dist/runtime/cli-doctor.d.ts +2 -1
- package/dist/runtime/cli-doctor.js +16 -5
- package/dist/runtime/cli-help.js +4 -4
- package/dist/runtime/cli-scaffold.d.ts +5 -1
- package/dist/runtime/cli-scaffold.js +138 -111
- package/dist/runtime/external-layer-selection.d.ts +14 -0
- package/dist/runtime/external-layer-selection.js +35 -0
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/migration-render.d.ts +23 -1
- package/dist/runtime/migration-render.js +58 -10
- package/dist/runtime/migration-ui-capability.js +17 -8
- package/dist/runtime/migration-utils.d.ts +7 -6
- package/dist/runtime/migration-utils.js +76 -73
- package/dist/runtime/migrations.js +2 -2
- package/dist/runtime/object-utils.d.ts +8 -1
- package/dist/runtime/object-utils.js +21 -1
- package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
- package/dist/runtime/scaffold-apply-utils.js +19 -6
- package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
- package/dist/runtime/scaffold-repository-reference.js +119 -0
- package/dist/runtime/scaffold.d.ts +5 -1
- package/dist/runtime/scaffold.js +15 -37
- package/dist/runtime/template-builtins.d.ts +11 -1
- package/dist/runtime/template-builtins.js +118 -25
- package/dist/runtime/template-layers.d.ts +37 -0
- package/dist/runtime/template-layers.js +184 -0
- package/dist/runtime/template-render.d.ts +28 -2
- package/dist/runtime/template-render.js +122 -55
- package/dist/runtime/template-source.d.ts +23 -5
- package/dist/runtime/template-source.js +296 -217
- package/package.json +8 -3
- package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +58 -12
- package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
- package/templates/_shared/migration-ui/common/src/migrations/index.ts +40 -11
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fsp } from "node:fs";
|
|
4
|
+
import { isPlainObject } from "./object-utils.js";
|
|
5
|
+
import { getBuiltInSharedTemplateLayerDir, isBuiltInSharedTemplateLayerId, } from "./template-builtins.js";
|
|
6
|
+
import { listInterpolatedDirectoryOutputs } from "./template-render.js";
|
|
7
|
+
export const TEMPLATE_LAYER_MANIFEST_FILENAME = "wp-typia.layers.json";
|
|
8
|
+
const TEMPLATE_LAYER_MANIFEST_VERSION = 1;
|
|
9
|
+
function resolveLayerPath(sourceRoot, relativePath) {
|
|
10
|
+
const targetPath = path.resolve(sourceRoot, relativePath);
|
|
11
|
+
const relativeTarget = path.relative(sourceRoot, targetPath);
|
|
12
|
+
if (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget)) {
|
|
13
|
+
throw new Error(`Template layer path "${relativePath}" must stay within ${sourceRoot}.`);
|
|
14
|
+
}
|
|
15
|
+
return targetPath;
|
|
16
|
+
}
|
|
17
|
+
async function assertNoSymlinks(sourceDir) {
|
|
18
|
+
const stats = await fsp.lstat(sourceDir);
|
|
19
|
+
if (stats.isSymbolicLink()) {
|
|
20
|
+
throw new Error(`Template layer packages may not include symbolic links: ${sourceDir}`);
|
|
21
|
+
}
|
|
22
|
+
if (!stats.isDirectory()) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of await fsp.readdir(sourceDir)) {
|
|
26
|
+
await assertNoSymlinks(path.join(sourceDir, entry));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function parseLayerDefinition(layerId, value) {
|
|
30
|
+
if (isBuiltInSharedTemplateLayerId(layerId)) {
|
|
31
|
+
throw new Error(`Layer "${layerId}" uses a reserved built-in shared layer id and cannot be redefined in ${TEMPLATE_LAYER_MANIFEST_FILENAME}.`);
|
|
32
|
+
}
|
|
33
|
+
if (!isPlainObject(value)) {
|
|
34
|
+
throw new Error(`Layer "${layerId}" in ${TEMPLATE_LAYER_MANIFEST_FILENAME} must be an object.`);
|
|
35
|
+
}
|
|
36
|
+
const layerPath = value.path;
|
|
37
|
+
if (typeof layerPath !== "string" || layerPath.trim().length === 0) {
|
|
38
|
+
throw new Error(`Layer "${layerId}" must define a non-empty string "path".`);
|
|
39
|
+
}
|
|
40
|
+
const layerExtends = value.extends;
|
|
41
|
+
if (typeof layerExtends !== "undefined" &&
|
|
42
|
+
(!Array.isArray(layerExtends) ||
|
|
43
|
+
!layerExtends.every((entry) => typeof entry === "string" && entry.trim().length > 0))) {
|
|
44
|
+
throw new Error(`Layer "${layerId}" must define "extends" as an array of non-empty strings when present.`);
|
|
45
|
+
}
|
|
46
|
+
const description = value.description;
|
|
47
|
+
if (typeof description !== "undefined" &&
|
|
48
|
+
(typeof description !== "string" || description.trim().length === 0)) {
|
|
49
|
+
throw new Error(`Layer "${layerId}" must define "description" as a non-empty string when present.`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
description: typeof description === "string" ? description : undefined,
|
|
53
|
+
extends: Array.isArray(layerExtends) ? [...layerExtends] : undefined,
|
|
54
|
+
path: layerPath,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function loadExternalTemplateLayerManifest(sourceRoot) {
|
|
58
|
+
const manifestPath = path.join(sourceRoot, TEMPLATE_LAYER_MANIFEST_FILENAME);
|
|
59
|
+
if (!fs.existsSync(manifestPath)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const raw = JSON.parse(await fsp.readFile(manifestPath, "utf8"));
|
|
63
|
+
if (!isPlainObject(raw)) {
|
|
64
|
+
throw new Error(`${TEMPLATE_LAYER_MANIFEST_FILENAME} must export a JSON object.`);
|
|
65
|
+
}
|
|
66
|
+
if (raw.version !== TEMPLATE_LAYER_MANIFEST_VERSION) {
|
|
67
|
+
throw new Error(`${TEMPLATE_LAYER_MANIFEST_FILENAME} must declare "version": ${TEMPLATE_LAYER_MANIFEST_VERSION}.`);
|
|
68
|
+
}
|
|
69
|
+
if (!isPlainObject(raw.layers) || Object.keys(raw.layers).length === 0) {
|
|
70
|
+
throw new Error(`${TEMPLATE_LAYER_MANIFEST_FILENAME} must define a non-empty "layers" object.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
layers: Object.fromEntries(Object.entries(raw.layers).map(([layerId, value]) => [
|
|
74
|
+
layerId,
|
|
75
|
+
parseLayerDefinition(layerId, value),
|
|
76
|
+
])),
|
|
77
|
+
version: TEMPLATE_LAYER_MANIFEST_VERSION,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getSelectableExternalLayers(manifest) {
|
|
81
|
+
const layerIds = Object.keys(manifest.layers);
|
|
82
|
+
const referencedExternalLayerIds = new Set();
|
|
83
|
+
for (const definition of Object.values(manifest.layers)) {
|
|
84
|
+
for (const ancestorId of definition.extends ?? []) {
|
|
85
|
+
if (ancestorId in manifest.layers) {
|
|
86
|
+
referencedExternalLayerIds.add(ancestorId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const publicLayerIds = layerIds.filter((layerId) => !referencedExternalLayerIds.has(layerId));
|
|
91
|
+
return publicLayerIds.map((layerId) => ({
|
|
92
|
+
description: manifest.layers[layerId]?.description,
|
|
93
|
+
extends: [...(manifest.layers[layerId]?.extends ?? [])],
|
|
94
|
+
id: layerId,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
function getDefaultExternalLayerId(manifest) {
|
|
98
|
+
const selectableLayers = getSelectableExternalLayers(manifest);
|
|
99
|
+
if (selectableLayers.length === 1) {
|
|
100
|
+
return selectableLayers[0].id;
|
|
101
|
+
}
|
|
102
|
+
const layerIds = Object.keys(manifest.layers);
|
|
103
|
+
throw new Error(`External layer package defines multiple selectable layers (${layerIds.join(", ")}). Pass an explicit externalLayerId or rerun through the interactive CLI selector.`);
|
|
104
|
+
}
|
|
105
|
+
export async function listSelectableExternalTemplateLayers(sourceRoot) {
|
|
106
|
+
const manifest = await loadExternalTemplateLayerManifest(sourceRoot);
|
|
107
|
+
if (!manifest) {
|
|
108
|
+
throw new Error(`No ${TEMPLATE_LAYER_MANIFEST_FILENAME} manifest found in ${sourceRoot}.`);
|
|
109
|
+
}
|
|
110
|
+
return getSelectableExternalLayers(manifest);
|
|
111
|
+
}
|
|
112
|
+
export async function resolveExternalTemplateLayers({ externalLayerId, sourceRoot, }) {
|
|
113
|
+
const manifest = await loadExternalTemplateLayerManifest(sourceRoot);
|
|
114
|
+
if (!manifest) {
|
|
115
|
+
throw new Error(`No ${TEMPLATE_LAYER_MANIFEST_FILENAME} manifest found in ${sourceRoot}.`);
|
|
116
|
+
}
|
|
117
|
+
const manifestDocument = manifest;
|
|
118
|
+
const selectedLayerId = externalLayerId ?? getDefaultExternalLayerId(manifestDocument);
|
|
119
|
+
if (!(selectedLayerId in manifestDocument.layers)) {
|
|
120
|
+
throw new Error(`Unknown external layer "${selectedLayerId}". Expected one of: ${Object.keys(manifestDocument.layers).join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
const entries = [];
|
|
123
|
+
const emittedLayerIds = new Set();
|
|
124
|
+
const visitingLayerIds = new Set();
|
|
125
|
+
async function visitLayer(layerId) {
|
|
126
|
+
if (isBuiltInSharedTemplateLayerId(layerId)) {
|
|
127
|
+
if (!emittedLayerIds.has(layerId)) {
|
|
128
|
+
entries.push({
|
|
129
|
+
dir: getBuiltInSharedTemplateLayerDir(layerId),
|
|
130
|
+
id: layerId,
|
|
131
|
+
kind: "built-in",
|
|
132
|
+
});
|
|
133
|
+
emittedLayerIds.add(layerId);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const definition = manifestDocument.layers[layerId];
|
|
138
|
+
if (!definition) {
|
|
139
|
+
throw new Error(`Layer "${layerId}" is not defined in ${TEMPLATE_LAYER_MANIFEST_FILENAME}.`);
|
|
140
|
+
}
|
|
141
|
+
if (visitingLayerIds.has(layerId)) {
|
|
142
|
+
throw new Error(`Detected a cycle while resolving external layer "${layerId}".`);
|
|
143
|
+
}
|
|
144
|
+
visitingLayerIds.add(layerId);
|
|
145
|
+
for (const ancestorId of definition.extends ?? []) {
|
|
146
|
+
await visitLayer(ancestorId);
|
|
147
|
+
}
|
|
148
|
+
visitingLayerIds.delete(layerId);
|
|
149
|
+
if (emittedLayerIds.has(layerId)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const layerDir = resolveLayerPath(sourceRoot, definition.path);
|
|
153
|
+
const stats = await fsp.stat(layerDir).catch(() => null);
|
|
154
|
+
if (!stats || !stats.isDirectory()) {
|
|
155
|
+
throw new Error(`Layer "${layerId}" points to a missing directory: ${definition.path}`);
|
|
156
|
+
}
|
|
157
|
+
await assertNoSymlinks(layerDir);
|
|
158
|
+
entries.push({
|
|
159
|
+
dir: layerDir,
|
|
160
|
+
id: layerId,
|
|
161
|
+
kind: "external",
|
|
162
|
+
});
|
|
163
|
+
emittedLayerIds.add(layerId);
|
|
164
|
+
}
|
|
165
|
+
await visitLayer(selectedLayerId);
|
|
166
|
+
return {
|
|
167
|
+
entries,
|
|
168
|
+
selectedLayerId,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export async function assertExternalTemplateLayersDoNotWriteProtectedOutputs({ externalEntries, protectedOutputPaths, view, }) {
|
|
172
|
+
for (const entry of externalEntries) {
|
|
173
|
+
if (entry.kind !== "external") {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const renderedOutputPaths = await listInterpolatedDirectoryOutputs(entry.dir, view);
|
|
177
|
+
for (const relativePath of renderedOutputPaths) {
|
|
178
|
+
if (!protectedOutputPaths.has(relativePath)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`External layer "${entry.id}" writes protected output "${relativePath}".`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -16,14 +16,40 @@ export interface CopyRawDirectoryOptions {
|
|
|
16
16
|
filter?: (sourcePath: string, targetPath: string, entry: fs.Dirent) => boolean | Promise<boolean>;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Render a Mustache template
|
|
20
|
-
*
|
|
19
|
+
* Render a Mustache template with full Mustache semantics while leaving values
|
|
20
|
+
* unescaped for scaffold source generation.
|
|
21
21
|
*/
|
|
22
22
|
export declare function renderMustacheTemplateString(template: string, view: TemplateRenderView): string;
|
|
23
23
|
/**
|
|
24
24
|
* Recursively copies a directory tree without rendering template contents.
|
|
25
25
|
*/
|
|
26
26
|
export declare function copyRawDirectory(sourceDir: string, targetDir: string, options?: CopyRawDirectoryOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Copy a template directory using full Mustache semantics for filenames and
|
|
29
|
+
* text-file contents while leaving rendered values unescaped.
|
|
30
|
+
*/
|
|
27
31
|
export declare function copyRenderedDirectory(sourceDir: string, targetDir: string, view: TemplateRenderView): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Copy a template directory using direct `{{key}}` replacement only.
|
|
34
|
+
*
|
|
35
|
+
* This intentionally preserves literal Mustache sections, partials, and other
|
|
36
|
+
* advanced constructs so built-in scaffold copy paths keep their historical
|
|
37
|
+
* interpolation behavior.
|
|
38
|
+
*/
|
|
28
39
|
export declare function copyInterpolatedDirectory(sourceDir: string, targetDir: string, view: Record<string, string>): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Lists the output file paths produced by an interpolated template directory
|
|
42
|
+
* without writing any files to disk.
|
|
43
|
+
*
|
|
44
|
+
* This walks the source directory, applies the same filename interpolation and
|
|
45
|
+
* `.mustache` stripping rules as `copyInterpolatedDirectory(...)`, and returns
|
|
46
|
+
* normalized output-relative paths under a virtual root.
|
|
47
|
+
*
|
|
48
|
+
* @param sourceDir - The template directory to traverse.
|
|
49
|
+
* @param view - The interpolation map used when resolving file and directory
|
|
50
|
+
* names.
|
|
51
|
+
* @returns A sorted array of normalized output paths relative to a virtual
|
|
52
|
+
* preview root.
|
|
53
|
+
*/
|
|
54
|
+
export declare function listInterpolatedDirectoryOutputs(sourceDir: string, view: Record<string, string>): Promise<string[]>;
|
|
29
55
|
export declare function pathExists(targetPath: string): boolean;
|
|
@@ -19,25 +19,32 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
19
19
|
".woff",
|
|
20
20
|
".woff2",
|
|
21
21
|
]);
|
|
22
|
+
const IDENTITY_MUSTACHE_ESCAPE = (value) => value;
|
|
22
23
|
/**
|
|
23
|
-
* Render a Mustache template
|
|
24
|
-
*
|
|
24
|
+
* Render a Mustache template with full Mustache semantics while leaving values
|
|
25
|
+
* unescaped for scaffold source generation.
|
|
25
26
|
*/
|
|
26
27
|
export function renderMustacheTemplateString(template, view) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
Mustache.escape = originalEscape;
|
|
34
|
-
}
|
|
28
|
+
const mustacheModule = Mustache;
|
|
29
|
+
const writer = new mustacheModule.Writer();
|
|
30
|
+
return writer.render(template, view, undefined, {
|
|
31
|
+
escape: IDENTITY_MUSTACHE_ESCAPE,
|
|
32
|
+
});
|
|
35
33
|
}
|
|
36
34
|
function escapeRegExp(value) {
|
|
37
35
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Render direct `{{key}}` placeholders without enabling sections, partials, or
|
|
39
|
+
* any other Mustache features.
|
|
40
|
+
*/
|
|
39
41
|
function renderInterpolatedString(template, view) {
|
|
40
|
-
|
|
42
|
+
const keys = Object.keys(view);
|
|
43
|
+
if (keys.length === 0) {
|
|
44
|
+
return template;
|
|
45
|
+
}
|
|
46
|
+
const placeholderPattern = new RegExp(keys.map((key) => `({{${escapeRegExp(key)}}})`).join("|"), "g");
|
|
47
|
+
return template.replace(placeholderPattern, (match) => view[match.slice(2, -2)] ?? match);
|
|
41
48
|
}
|
|
42
49
|
function isBinaryTemplateFile(filePath) {
|
|
43
50
|
return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
@@ -51,6 +58,59 @@ function resolveRenderedPath(targetDir, destinationName) {
|
|
|
51
58
|
}
|
|
52
59
|
return resolvedDestinationPath;
|
|
53
60
|
}
|
|
61
|
+
function stripTemplateExtension(entryName) {
|
|
62
|
+
return entryName.endsWith(".mustache")
|
|
63
|
+
? entryName.slice(0, -".mustache".length)
|
|
64
|
+
: entryName;
|
|
65
|
+
}
|
|
66
|
+
function renderTemplateDestinationName(entryName, view, renderString) {
|
|
67
|
+
return renderString(stripTemplateExtension(entryName), view);
|
|
68
|
+
}
|
|
69
|
+
async function traverseTemplateDirectory({ prepareDirectory, renderString, sourceDir, targetDir, view, visitFile, }) {
|
|
70
|
+
const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
73
|
+
const destinationName = renderTemplateDestinationName(entry.name, view, renderString);
|
|
74
|
+
const destinationPath = resolveRenderedPath(targetDir, destinationName);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
await prepareDirectory?.(destinationPath);
|
|
77
|
+
await traverseTemplateDirectory({
|
|
78
|
+
prepareDirectory,
|
|
79
|
+
renderString,
|
|
80
|
+
sourceDir: sourcePath,
|
|
81
|
+
targetDir: destinationPath,
|
|
82
|
+
view,
|
|
83
|
+
visitFile,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await visitFile({
|
|
88
|
+
destinationPath,
|
|
89
|
+
sourcePath,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function copyTemplateDirectory({ renderString, sourceDir, targetDir, view, }) {
|
|
94
|
+
await fsp.mkdir(targetDir, { recursive: true });
|
|
95
|
+
await traverseTemplateDirectory({
|
|
96
|
+
prepareDirectory: async (directoryPath) => {
|
|
97
|
+
await fsp.mkdir(directoryPath, { recursive: true });
|
|
98
|
+
},
|
|
99
|
+
renderString,
|
|
100
|
+
sourceDir,
|
|
101
|
+
targetDir,
|
|
102
|
+
view,
|
|
103
|
+
visitFile: async ({ destinationPath, sourcePath }) => {
|
|
104
|
+
await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
105
|
+
if (isBinaryTemplateFile(sourcePath)) {
|
|
106
|
+
await fsp.copyFile(sourcePath, destinationPath);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const content = await fsp.readFile(sourcePath, "utf8");
|
|
110
|
+
await fsp.writeFile(destinationPath, renderString(content, view), "utf8");
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
54
114
|
/**
|
|
55
115
|
* Recursively copies a directory tree without rendering template contents.
|
|
56
116
|
*/
|
|
@@ -70,53 +130,60 @@ export async function copyRawDirectory(sourceDir, targetDir, options = {}) {
|
|
|
70
130
|
await fsp.copyFile(sourcePath, targetPath);
|
|
71
131
|
}
|
|
72
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Copy a template directory using full Mustache semantics for filenames and
|
|
135
|
+
* text-file contents while leaving rendered values unescaped.
|
|
136
|
+
*/
|
|
73
137
|
export async function copyRenderedDirectory(sourceDir, targetDir, view) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const destinationName = renderMustacheTemplateString(destinationNameTemplate, view);
|
|
81
|
-
const destinationPath = resolveRenderedPath(targetDir, destinationName);
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
await fsp.mkdir(destinationPath, { recursive: true });
|
|
84
|
-
await copyRenderedDirectory(sourcePath, destinationPath, view);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (isBinaryTemplateFile(sourcePath)) {
|
|
88
|
-
await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
89
|
-
await fsp.copyFile(sourcePath, destinationPath);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const content = await fsp.readFile(sourcePath, "utf8");
|
|
93
|
-
await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
94
|
-
await fsp.writeFile(destinationPath, renderMustacheTemplateString(content, view), "utf8");
|
|
95
|
-
}
|
|
138
|
+
await copyTemplateDirectory({
|
|
139
|
+
renderString: renderMustacheTemplateString,
|
|
140
|
+
sourceDir,
|
|
141
|
+
targetDir,
|
|
142
|
+
view,
|
|
143
|
+
});
|
|
96
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Copy a template directory using direct `{{key}}` replacement only.
|
|
147
|
+
*
|
|
148
|
+
* This intentionally preserves literal Mustache sections, partials, and other
|
|
149
|
+
* advanced constructs so built-in scaffold copy paths keep their historical
|
|
150
|
+
* interpolation behavior.
|
|
151
|
+
*/
|
|
97
152
|
export async function copyInterpolatedDirectory(sourceDir, targetDir, view) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
await copyTemplateDirectory({
|
|
154
|
+
renderString: renderInterpolatedString,
|
|
155
|
+
sourceDir,
|
|
156
|
+
targetDir,
|
|
157
|
+
view,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Lists the output file paths produced by an interpolated template directory
|
|
162
|
+
* without writing any files to disk.
|
|
163
|
+
*
|
|
164
|
+
* This walks the source directory, applies the same filename interpolation and
|
|
165
|
+
* `.mustache` stripping rules as `copyInterpolatedDirectory(...)`, and returns
|
|
166
|
+
* normalized output-relative paths under a virtual root.
|
|
167
|
+
*
|
|
168
|
+
* @param sourceDir - The template directory to traverse.
|
|
169
|
+
* @param view - The interpolation map used when resolving file and directory
|
|
170
|
+
* names.
|
|
171
|
+
* @returns A sorted array of normalized output paths relative to a virtual
|
|
172
|
+
* preview root.
|
|
173
|
+
*/
|
|
174
|
+
export async function listInterpolatedDirectoryOutputs(sourceDir, view) {
|
|
175
|
+
const virtualRoot = path.resolve("/wp-typia-template-preview");
|
|
176
|
+
const outputs = [];
|
|
177
|
+
await traverseTemplateDirectory({
|
|
178
|
+
renderString: renderInterpolatedString,
|
|
179
|
+
sourceDir,
|
|
180
|
+
targetDir: virtualRoot,
|
|
181
|
+
view,
|
|
182
|
+
visitFile: async ({ destinationPath }) => {
|
|
183
|
+
outputs.push(path.relative(virtualRoot, destinationPath).replace(/\\/g, "/"));
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return outputs.sort((left, right) => left.localeCompare(right));
|
|
120
187
|
}
|
|
121
188
|
export function pathExists(targetPath) {
|
|
122
189
|
return fs.existsSync(targetPath);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { type UnknownRecord } from './object-utils.js';
|
|
2
|
+
type TemplateSourceFormat = 'wp-typia' | 'create-block-external' | 'create-block-subset';
|
|
2
3
|
/**
|
|
3
4
|
* Public template variables exposed to external template seeds before wp-typia
|
|
4
5
|
* normalizes them into a scaffold project.
|
|
5
6
|
*/
|
|
6
|
-
export interface TemplateVariableContext extends
|
|
7
|
+
export interface TemplateVariableContext extends UnknownRecord {
|
|
7
8
|
/** Version string for `@wp-typia/api-client` used in generated dependencies. */
|
|
8
9
|
apiClientPackageVersion: string;
|
|
9
10
|
/** Version string for `@wp-typia/block-runtime` used in generated dependencies. */
|
|
@@ -52,19 +53,36 @@ interface NpmTemplateLocator {
|
|
|
52
53
|
rawSpec: string;
|
|
53
54
|
type: string;
|
|
54
55
|
}
|
|
56
|
+
interface SeedSource {
|
|
57
|
+
assetsDir?: string;
|
|
58
|
+
blockDir: string;
|
|
59
|
+
cleanup?: () => Promise<void>;
|
|
60
|
+
rootDir: string;
|
|
61
|
+
selectedVariant?: string | null;
|
|
62
|
+
warnings?: string[];
|
|
63
|
+
}
|
|
55
64
|
type RemoteTemplateLocator = {
|
|
56
|
-
kind:
|
|
65
|
+
kind: 'github';
|
|
57
66
|
locator: GitHubTemplateLocator;
|
|
58
67
|
} | {
|
|
59
|
-
kind:
|
|
68
|
+
kind: 'npm';
|
|
60
69
|
locator: NpmTemplateLocator;
|
|
61
70
|
} | {
|
|
62
|
-
kind:
|
|
71
|
+
kind: 'path';
|
|
63
72
|
templatePath: string;
|
|
64
73
|
};
|
|
65
74
|
export declare function parseGitHubTemplateLocator(templateId: string): GitHubTemplateLocator | null;
|
|
66
75
|
export declare function parseNpmTemplateLocator(templateId: string): NpmTemplateLocator | null;
|
|
67
76
|
export declare function parseTemplateLocator(templateId: string): RemoteTemplateLocator;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves a template locator into a local seed source directory.
|
|
79
|
+
*
|
|
80
|
+
* @param locator Remote template locator describing a local path, GitHub source, or npm package.
|
|
81
|
+
* @param cwd Current working directory used to resolve local template paths.
|
|
82
|
+
* @returns A local seed source containing the resolved root and block directory, plus optional cleanup.
|
|
83
|
+
* @throws When the locator is invalid, the source cannot be fetched, or filesystem validation fails.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveTemplateSeed(locator: RemoteTemplateLocator, cwd: string): Promise<SeedSource>;
|
|
68
86
|
export declare function resolveTemplateSource(templateId: string, cwd: string, variables: {
|
|
69
87
|
[key: string]: string;
|
|
70
88
|
}, variant?: string): Promise<ResolvedTemplateSource>;
|