@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.
Files changed (54) hide show
  1. package/README.md +26 -1
  2. package/dist/runtime/block-generator-service.d.ts +9 -1
  3. package/dist/runtime/block-generator-service.js +137 -16
  4. package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
  5. package/dist/runtime/block-generator-tool-contract.js +157 -0
  6. package/dist/runtime/built-in-block-artifacts.js +171 -423
  7. package/dist/runtime/built-in-block-code-artifacts.js +96 -46
  8. package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
  9. package/dist/runtime/built-in-block-code-templates.js +2233 -0
  10. package/dist/runtime/cli-add-block.d.ts +2 -1
  11. package/dist/runtime/cli-add-block.js +163 -25
  12. package/dist/runtime/cli-add-shared.d.ts +7 -0
  13. package/dist/runtime/cli-add-shared.js +4 -6
  14. package/dist/runtime/cli-add-workspace.js +56 -17
  15. package/dist/runtime/cli-core.d.ts +4 -0
  16. package/dist/runtime/cli-core.js +3 -0
  17. package/dist/runtime/cli-diagnostics.d.ts +58 -0
  18. package/dist/runtime/cli-diagnostics.js +101 -0
  19. package/dist/runtime/cli-doctor.d.ts +2 -1
  20. package/dist/runtime/cli-doctor.js +16 -5
  21. package/dist/runtime/cli-help.js +4 -4
  22. package/dist/runtime/cli-scaffold.d.ts +5 -1
  23. package/dist/runtime/cli-scaffold.js +138 -111
  24. package/dist/runtime/external-layer-selection.d.ts +14 -0
  25. package/dist/runtime/external-layer-selection.js +35 -0
  26. package/dist/runtime/index.d.ts +4 -2
  27. package/dist/runtime/index.js +2 -1
  28. package/dist/runtime/migration-render.d.ts +23 -1
  29. package/dist/runtime/migration-render.js +58 -10
  30. package/dist/runtime/migration-ui-capability.js +17 -8
  31. package/dist/runtime/migration-utils.d.ts +7 -6
  32. package/dist/runtime/migration-utils.js +76 -73
  33. package/dist/runtime/migrations.js +2 -2
  34. package/dist/runtime/object-utils.d.ts +8 -1
  35. package/dist/runtime/object-utils.js +21 -1
  36. package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
  37. package/dist/runtime/scaffold-apply-utils.js +19 -6
  38. package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
  39. package/dist/runtime/scaffold-repository-reference.js +119 -0
  40. package/dist/runtime/scaffold.d.ts +5 -1
  41. package/dist/runtime/scaffold.js +15 -37
  42. package/dist/runtime/template-builtins.d.ts +11 -1
  43. package/dist/runtime/template-builtins.js +118 -25
  44. package/dist/runtime/template-layers.d.ts +37 -0
  45. package/dist/runtime/template-layers.js +184 -0
  46. package/dist/runtime/template-render.d.ts +28 -2
  47. package/dist/runtime/template-render.js +122 -55
  48. package/dist/runtime/template-source.d.ts +23 -5
  49. package/dist/runtime/template-source.js +296 -217
  50. package/package.json +8 -3
  51. package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
  52. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +58 -12
  53. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
  54. 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 while keeping HTML escaping disabled only for the
20
- * current render call.
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 while keeping HTML escaping disabled only for the
24
- * current render call.
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 originalEscape = Mustache.escape;
28
- Mustache.escape = (value) => value;
29
- try {
30
- return Mustache.render(template, view);
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
- return Object.entries(view).reduce((output, [key, value]) => output.replace(new RegExp(`{{${escapeRegExp(key)}}}`, "g"), value), template);
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
- const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
75
- for (const entry of entries) {
76
- const sourcePath = path.join(sourceDir, entry.name);
77
- const destinationNameTemplate = entry.name.endsWith(".mustache")
78
- ? entry.name.slice(0, -".mustache".length)
79
- : entry.name;
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
- const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
99
- for (const entry of entries) {
100
- const sourcePath = path.join(sourceDir, entry.name);
101
- const destinationNameTemplate = entry.name.endsWith(".mustache")
102
- ? entry.name.slice(0, -".mustache".length)
103
- : entry.name;
104
- const destinationName = renderInterpolatedString(destinationNameTemplate, view);
105
- const destinationPath = resolveRenderedPath(targetDir, destinationName);
106
- if (entry.isDirectory()) {
107
- await fsp.mkdir(destinationPath, { recursive: true });
108
- await copyInterpolatedDirectory(sourcePath, destinationPath, view);
109
- continue;
110
- }
111
- if (isBinaryTemplateFile(sourcePath)) {
112
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
113
- await fsp.copyFile(sourcePath, destinationPath);
114
- continue;
115
- }
116
- const content = await fsp.readFile(sourcePath, "utf8");
117
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
118
- await fsp.writeFile(destinationPath, renderInterpolatedString(content, view), "utf8");
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
- type TemplateSourceFormat = "wp-typia" | "create-block-external" | "create-block-subset";
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 Record<string, unknown> {
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: "github";
65
+ kind: 'github';
57
66
  locator: GitHubTemplateLocator;
58
67
  } | {
59
- kind: "npm";
68
+ kind: 'npm';
60
69
  locator: NpmTemplateLocator;
61
70
  } | {
62
- kind: "path";
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>;