@wp-typia/project-tools 0.16.7 → 0.16.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/README.md +16 -1
- package/dist/runtime/block-generator-service.d.ts +5 -1
- package/dist/runtime/block-generator-service.js +132 -15
- package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
- package/dist/runtime/block-generator-tool-contract.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/template-builtins.d.ts +11 -1
- package/dist/runtime/template-builtins.js +118 -25
- package/dist/runtime/template-layers.d.ts +31 -0
- package/dist/runtime/template-layers.js +171 -0
- package/dist/runtime/template-render.d.ts +15 -0
- package/dist/runtime/template-render.js +36 -0
- package/dist/runtime/template-source.d.ts +17 -0
- package/dist/runtime/template-source.js +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Package roles:
|
|
|
6
6
|
|
|
7
7
|
- `wp-typia` owns the CLI, help, TUI, completions, skills, MCP, and bin entry.
|
|
8
8
|
- `@wp-typia/project-tools` owns scaffold, add-block, migrate, template, doctor, and schema project helpers.
|
|
9
|
-
It also owns the typed generator boundary via `BlockSpec
|
|
9
|
+
It also owns the typed generator boundary via `BlockSpec`, `BlockGeneratorService`,
|
|
10
|
+
and `inspectBlockGeneration(...)`,
|
|
10
11
|
plus the emitter-owned built-in structural/code path where built-in
|
|
11
12
|
templates no longer ship structural, TS/TSX, style, or block-local `render.php`
|
|
12
13
|
Mustache files.
|
|
@@ -50,4 +51,18 @@ files and starter `typia.manifest.json` now come from the emitter path, while
|
|
|
50
51
|
project bootstrap/package-manager files, sync scripts, shared REST helpers, and
|
|
51
52
|
the remaining non-block assets still come from Mustache-backed template copy.
|
|
52
53
|
|
|
54
|
+
The higher-level generator architecture record, including the current phase map
|
|
55
|
+
and the non-mutating `plan -> validate -> render -> apply` tool-facing usage
|
|
56
|
+
model, lives in
|
|
57
|
+
[`docs/block-generator-architecture.md`](../../docs/block-generator-architecture.md).
|
|
58
|
+
The public non-mutating controller/tool contract now lives in
|
|
59
|
+
[`docs/block-generator-tool-contract.md`](../../docs/block-generator-tool-contract.md).
|
|
60
|
+
|
|
61
|
+
Reusable external layer packages on top of the built-in shared scaffold model
|
|
62
|
+
are now available programmatically through `scaffoldProject(...)`,
|
|
63
|
+
`BlockGeneratorService`, and `inspectBlockGeneration(...)` via
|
|
64
|
+
`externalLayerSource` and optional `externalLayerId`. The RFC/CLI UX record
|
|
65
|
+
still lives in
|
|
66
|
+
[`docs/external-template-layer-composition.md`](../../docs/external-template-layer-composition.md).
|
|
67
|
+
|
|
53
68
|
If you need metadata sync, editor helpers, validation helpers, or other generated-project runtime utilities, import them directly from `@wp-typia/block-runtime/*`.
|
|
@@ -41,6 +41,8 @@ export interface BlockSpec {
|
|
|
41
41
|
export interface BlockGenerationTarget {
|
|
42
42
|
allowExistingDir: boolean;
|
|
43
43
|
cwd: string;
|
|
44
|
+
externalLayerId?: string;
|
|
45
|
+
externalLayerSource?: string;
|
|
44
46
|
noInstall: boolean;
|
|
45
47
|
packageManager: PackageManagerId;
|
|
46
48
|
projectDir: string;
|
|
@@ -51,6 +53,8 @@ export interface PlanBlockInput {
|
|
|
51
53
|
answers: ScaffoldAnswers;
|
|
52
54
|
cwd?: string;
|
|
53
55
|
dataStorageMode?: DataStorageMode;
|
|
56
|
+
externalLayerId?: string;
|
|
57
|
+
externalLayerSource?: string;
|
|
54
58
|
noInstall?: boolean;
|
|
55
59
|
packageManager: PackageManagerId;
|
|
56
60
|
persistencePolicy?: PersistencePolicy;
|
|
@@ -95,7 +99,7 @@ export interface ApplyBlockInput {
|
|
|
95
99
|
export declare function createBuiltInBlockSpec({ answers, dataStorageMode, persistencePolicy, templateId, withMigrationUi, withTestPreset, withWpEnv, }: Omit<PlanBlockInput, "allowExistingDir" | "cwd" | "noInstall" | "packageManager" | "projectDir" | "variant">): BlockSpec;
|
|
96
100
|
export declare function buildTemplateVariablesFromBlockSpec(spec: BlockSpec): ScaffoldTemplateVariables;
|
|
97
101
|
export declare class BlockGeneratorService {
|
|
98
|
-
plan({ allowExistingDir, answers, cwd, dataStorageMode, noInstall, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }: PlanBlockInput): Promise<PlanBlockResult>;
|
|
102
|
+
plan({ allowExistingDir, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, noInstall, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }: PlanBlockInput): Promise<PlanBlockResult>;
|
|
99
103
|
validate({ plan }: ValidateBlockInput): Promise<ValidateBlockResult>;
|
|
100
104
|
render({ validated }: RenderBlockInput): Promise<RenderBlockResult>;
|
|
101
105
|
apply({ rendered, installDependencies, }: ApplyBlockInput): Promise<ScaffoldProjectResult>;
|
|
@@ -7,10 +7,62 @@ import { applyBuiltInScaffoldProjectFiles, buildGitignore, buildReadme, } from "
|
|
|
7
7
|
import { buildBlockCssClassName, buildFrontendCssClassName, resolveScaffoldIdentifiers, } from "./scaffold-identifiers.js";
|
|
8
8
|
import { buildBuiltInBlockArtifacts, } from "./built-in-block-artifacts.js";
|
|
9
9
|
import { buildBuiltInCodeArtifacts, } from "./built-in-block-code-artifacts.js";
|
|
10
|
+
import { getStarterManifestFiles } from "./starter-manifests.js";
|
|
11
|
+
import { resolveTemplateSeed, parseTemplateLocator } from "./template-source.js";
|
|
12
|
+
import { assertExternalTemplateLayersDoNotWriteProtectedOutputs, resolveExternalTemplateLayers, } from "./template-layers.js";
|
|
13
|
+
import { getBuiltInTemplateOverlayDir, getBuiltInTemplateSharedLayerDirs, resolveBuiltInTemplateSourceFromLayerDirs, } from "./template-builtins.js";
|
|
10
14
|
const renderedArtifactCache = new WeakMap();
|
|
11
15
|
function createVariablesFingerprint(variables) {
|
|
12
16
|
return JSON.stringify(variables);
|
|
13
17
|
}
|
|
18
|
+
function buildProtectedTemplateOutputPaths({ codeArtifacts, spec, variables, artifacts, }) {
|
|
19
|
+
const protectedOutputs = new Set([
|
|
20
|
+
".gitignore",
|
|
21
|
+
"package.json",
|
|
22
|
+
"scripts/add-compound-child.ts",
|
|
23
|
+
"scripts/block-config.ts",
|
|
24
|
+
"scripts/sync-project.ts",
|
|
25
|
+
"scripts/sync-rest-contracts.ts",
|
|
26
|
+
"scripts/sync-types-to-block-json.ts",
|
|
27
|
+
"tsconfig.json",
|
|
28
|
+
"webpack.config.js",
|
|
29
|
+
`${variables.slugKebabCase}.php`,
|
|
30
|
+
]);
|
|
31
|
+
for (const artifact of codeArtifacts) {
|
|
32
|
+
protectedOutputs.add(artifact.relativePath);
|
|
33
|
+
}
|
|
34
|
+
for (const artifact of artifacts) {
|
|
35
|
+
protectedOutputs.add(`${artifact.relativeDir}/block.json`);
|
|
36
|
+
protectedOutputs.add(`${artifact.relativeDir}/types.ts`);
|
|
37
|
+
}
|
|
38
|
+
for (const manifest of getStarterManifestFiles(spec.template.family, variables)) {
|
|
39
|
+
protectedOutputs.add(manifest.relativePath);
|
|
40
|
+
}
|
|
41
|
+
return protectedOutputs;
|
|
42
|
+
}
|
|
43
|
+
function buildCombinedTemplateLayerDirs({ baseLayerDirs, externalEntries, templateId, }) {
|
|
44
|
+
const orderedLayerDirs = [];
|
|
45
|
+
const seenLayerDirs = new Set();
|
|
46
|
+
for (const layerDir of baseLayerDirs) {
|
|
47
|
+
if (seenLayerDirs.has(layerDir)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
orderedLayerDirs.push(layerDir);
|
|
51
|
+
seenLayerDirs.add(layerDir);
|
|
52
|
+
}
|
|
53
|
+
for (const entry of externalEntries) {
|
|
54
|
+
if (seenLayerDirs.has(entry.dir)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
orderedLayerDirs.push(entry.dir);
|
|
58
|
+
seenLayerDirs.add(entry.dir);
|
|
59
|
+
}
|
|
60
|
+
const overlayDir = getBuiltInTemplateOverlayDir(templateId);
|
|
61
|
+
if (!seenLayerDirs.has(overlayDir)) {
|
|
62
|
+
orderedLayerDirs.push(overlayDir);
|
|
63
|
+
}
|
|
64
|
+
return orderedLayerDirs;
|
|
65
|
+
}
|
|
14
66
|
function getBuiltInPersistenceSpec({ templateId, dataStorageMode, persistencePolicy, }) {
|
|
15
67
|
if (templateId === "persistence") {
|
|
16
68
|
return {
|
|
@@ -146,7 +198,7 @@ export function buildTemplateVariablesFromBlockSpec(spec) {
|
|
|
146
198
|
};
|
|
147
199
|
}
|
|
148
200
|
export class BlockGeneratorService {
|
|
149
|
-
async plan({ allowExistingDir = false, answers, cwd = process.cwd(), dataStorageMode, noInstall = false, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
|
|
201
|
+
async plan({ allowExistingDir = false, answers, cwd = process.cwd(), dataStorageMode, externalLayerId, externalLayerSource, noInstall = false, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
|
|
150
202
|
return {
|
|
151
203
|
spec: createBuiltInBlockSpec({
|
|
152
204
|
answers,
|
|
@@ -160,6 +212,8 @@ export class BlockGeneratorService {
|
|
|
160
212
|
target: {
|
|
161
213
|
allowExistingDir,
|
|
162
214
|
cwd,
|
|
215
|
+
externalLayerId,
|
|
216
|
+
externalLayerSource,
|
|
163
217
|
noInstall,
|
|
164
218
|
packageManager,
|
|
165
219
|
projectDir,
|
|
@@ -168,21 +222,90 @@ export class BlockGeneratorService {
|
|
|
168
222
|
};
|
|
169
223
|
}
|
|
170
224
|
async validate({ plan }) {
|
|
225
|
+
if (plan.target.externalLayerId && !plan.target.externalLayerSource) {
|
|
226
|
+
throw new Error("externalLayerId requires externalLayerSource when composing built-in template layers.");
|
|
227
|
+
}
|
|
171
228
|
if (plan.target.variant) {
|
|
172
229
|
throw new Error(`--variant is only supported for official external template configs. Received variant "${plan.target.variant}" for built-in template "${plan.spec.template.family}".`);
|
|
173
230
|
}
|
|
174
231
|
return plan;
|
|
175
232
|
}
|
|
176
233
|
async render({ validated }) {
|
|
177
|
-
const
|
|
178
|
-
|
|
234
|
+
const variables = buildTemplateVariablesFromBlockSpec(validated.spec);
|
|
235
|
+
const persistenceEnabled = validated.spec.persistence.enabled;
|
|
236
|
+
const artifacts = buildBuiltInBlockArtifacts({
|
|
237
|
+
templateId: validated.spec.template.family,
|
|
238
|
+
variables,
|
|
239
|
+
});
|
|
240
|
+
const codeArtifacts = buildBuiltInCodeArtifacts({
|
|
241
|
+
templateId: validated.spec.template.family,
|
|
242
|
+
variables,
|
|
243
|
+
});
|
|
244
|
+
const templateVariantOptions = {
|
|
245
|
+
persistenceEnabled,
|
|
179
246
|
persistencePolicy: validated.spec.persistence.enabled &&
|
|
180
247
|
validated.spec.persistence.persistencePolicy === "public"
|
|
181
248
|
? "public"
|
|
182
249
|
: "authenticated",
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const
|
|
250
|
+
};
|
|
251
|
+
let templateSource = await resolveBuiltInTemplateSource(validated.spec.template.family, templateVariantOptions);
|
|
252
|
+
const warnings = [...(templateSource.warnings ?? [])];
|
|
253
|
+
if (validated.target.externalLayerSource) {
|
|
254
|
+
let layerSeed;
|
|
255
|
+
try {
|
|
256
|
+
layerSeed = await resolveTemplateSeed(parseTemplateLocator(validated.target.externalLayerSource), validated.target.cwd);
|
|
257
|
+
const resolvedLayers = await resolveExternalTemplateLayers({
|
|
258
|
+
externalLayerId: validated.target.externalLayerId,
|
|
259
|
+
sourceRoot: layerSeed.rootDir,
|
|
260
|
+
});
|
|
261
|
+
const baseLayerDirs = getBuiltInTemplateSharedLayerDirs(validated.spec.template.family, templateVariantOptions);
|
|
262
|
+
await assertExternalTemplateLayersDoNotWriteProtectedOutputs({
|
|
263
|
+
externalEntries: resolvedLayers.entries,
|
|
264
|
+
protectedOutputPaths: buildProtectedTemplateOutputPaths({
|
|
265
|
+
artifacts,
|
|
266
|
+
codeArtifacts,
|
|
267
|
+
spec: validated.spec,
|
|
268
|
+
variables,
|
|
269
|
+
}),
|
|
270
|
+
view: variables,
|
|
271
|
+
});
|
|
272
|
+
await templateSource.cleanup?.();
|
|
273
|
+
templateSource = await resolveBuiltInTemplateSourceFromLayerDirs(validated.spec.template.family, buildCombinedTemplateLayerDirs({
|
|
274
|
+
baseLayerDirs,
|
|
275
|
+
externalEntries: resolvedLayers.entries,
|
|
276
|
+
templateId: validated.spec.template.family,
|
|
277
|
+
}));
|
|
278
|
+
const layerSourceCleanup = layerSeed.cleanup;
|
|
279
|
+
const templateCleanup = templateSource.cleanup;
|
|
280
|
+
templateSource.cleanup = async () => {
|
|
281
|
+
const cleanupErrors = [];
|
|
282
|
+
try {
|
|
283
|
+
await templateCleanup?.();
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await layerSourceCleanup?.();
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
293
|
+
}
|
|
294
|
+
if (cleanupErrors.length > 0) {
|
|
295
|
+
throw new Error([
|
|
296
|
+
"Failed to cleanup composed template sources.",
|
|
297
|
+
...cleanupErrors.map((error) => `- ${error.message}`),
|
|
298
|
+
].join("\n"));
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
warnings.push(`Applied external layer "${resolvedLayers.selectedLayerId}" from "${validated.target.externalLayerSource}".`);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
await templateSource.cleanup?.();
|
|
305
|
+
await layerSeed?.cleanup?.();
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
186
309
|
const rendered = {
|
|
187
310
|
...validated,
|
|
188
311
|
cleanup: templateSource.cleanup,
|
|
@@ -202,17 +325,11 @@ export class BlockGeneratorService {
|
|
|
202
325
|
selectedVariant: null,
|
|
203
326
|
templateDir: templateSource.templateDir,
|
|
204
327
|
variables,
|
|
205
|
-
warnings
|
|
328
|
+
warnings,
|
|
206
329
|
};
|
|
207
330
|
renderedArtifactCache.set(rendered, {
|
|
208
|
-
artifacts
|
|
209
|
-
|
|
210
|
-
variables,
|
|
211
|
-
}),
|
|
212
|
-
codeArtifacts: buildBuiltInCodeArtifacts({
|
|
213
|
-
templateId: validated.spec.template.family,
|
|
214
|
-
variables,
|
|
215
|
-
}),
|
|
331
|
+
artifacts,
|
|
332
|
+
codeArtifacts,
|
|
216
333
|
variablesFingerprint: createVariablesFingerprint(variables),
|
|
217
334
|
});
|
|
218
335
|
return rendered;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BlockGeneratorService, type PlanBlockInput, type PlanBlockResult, type RenderBlockResult, type ValidateBlockResult } from "./block-generator-service.js";
|
|
2
|
+
import type { ManifestDocument } from "./migration-types.js";
|
|
3
|
+
import type { BuiltInTemplateId } from "./template-registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Semantic version marker for the public block generation tool contract.
|
|
6
|
+
*
|
|
7
|
+
* Increment this number whenever the serialized inspection payload changes in a
|
|
8
|
+
* breaking way.
|
|
9
|
+
*/
|
|
10
|
+
export declare const BLOCK_GENERATION_TOOL_CONTRACT_VERSION: 1;
|
|
11
|
+
/**
|
|
12
|
+
* Staged execution points for the non-mutating inspection workflow.
|
|
13
|
+
*
|
|
14
|
+
* - `"plan"` returns the normalized `BlockSpec` and target metadata.
|
|
15
|
+
* - `"validate"` returns the validated generation stage without rendering.
|
|
16
|
+
* - `"render"` returns the full non-mutating preview, including copied and
|
|
17
|
+
* emitted file snapshots.
|
|
18
|
+
*/
|
|
19
|
+
export type BlockGenerationToolStage = "plan" | "validate" | "render";
|
|
20
|
+
/**
|
|
21
|
+
* Input for the staged, non-mutating block generation inspection entrypoint.
|
|
22
|
+
*
|
|
23
|
+
* Extends `PlanBlockInput` with an optional `stopAfter` selector so callers can
|
|
24
|
+
* stop at `plan`, `validate`, or continue through the full `render` preview.
|
|
25
|
+
*/
|
|
26
|
+
export interface InspectBlockGenerationInput extends PlanBlockInput {
|
|
27
|
+
stopAfter?: BlockGenerationToolStage;
|
|
28
|
+
}
|
|
29
|
+
export interface BlockGenerationTemplateCopyPreview {
|
|
30
|
+
owner: "template-copy";
|
|
31
|
+
relativePath: string;
|
|
32
|
+
}
|
|
33
|
+
export interface BlockGenerationEmittedFilePreview {
|
|
34
|
+
kind: "generated-source" | "starter-manifest" | "structural";
|
|
35
|
+
owner: "emitter";
|
|
36
|
+
relativePath: string;
|
|
37
|
+
source: string;
|
|
38
|
+
}
|
|
39
|
+
export interface BlockGenerationStarterManifestPreview {
|
|
40
|
+
document: ManifestDocument;
|
|
41
|
+
owner: "emitter";
|
|
42
|
+
relativePath: string;
|
|
43
|
+
source: string;
|
|
44
|
+
}
|
|
45
|
+
export interface BlockGenerationRenderPreview {
|
|
46
|
+
copiedTemplateFiles: BlockGenerationTemplateCopyPreview[];
|
|
47
|
+
emittedFiles: BlockGenerationEmittedFilePreview[];
|
|
48
|
+
postRender: RenderBlockResult["postRender"] & {
|
|
49
|
+
installsDependencies: boolean;
|
|
50
|
+
};
|
|
51
|
+
selectedVariant: null;
|
|
52
|
+
starterManifestFiles: BlockGenerationStarterManifestPreview[];
|
|
53
|
+
template: {
|
|
54
|
+
description: string;
|
|
55
|
+
family: BuiltInTemplateId;
|
|
56
|
+
features: string[];
|
|
57
|
+
format: "wp-typia";
|
|
58
|
+
};
|
|
59
|
+
warnings: string[];
|
|
60
|
+
readmeContent: string;
|
|
61
|
+
gitignoreContent: string;
|
|
62
|
+
}
|
|
63
|
+
interface BlockGenerationInspectionBase {
|
|
64
|
+
contractVersion: typeof BLOCK_GENERATION_TOOL_CONTRACT_VERSION;
|
|
65
|
+
mutatesWorkspace: false;
|
|
66
|
+
stage: BlockGenerationToolStage;
|
|
67
|
+
}
|
|
68
|
+
export interface InspectBlockGenerationPlanResult extends BlockGenerationInspectionBase {
|
|
69
|
+
plan: PlanBlockResult;
|
|
70
|
+
stage: "plan";
|
|
71
|
+
}
|
|
72
|
+
export interface InspectBlockGenerationValidateResult extends BlockGenerationInspectionBase {
|
|
73
|
+
plan: PlanBlockResult;
|
|
74
|
+
stage: "validate";
|
|
75
|
+
validated: ValidateBlockResult;
|
|
76
|
+
}
|
|
77
|
+
export interface InspectBlockGenerationRenderResult extends BlockGenerationInspectionBase {
|
|
78
|
+
plan: PlanBlockResult;
|
|
79
|
+
rendered: BlockGenerationRenderPreview;
|
|
80
|
+
stage: "render";
|
|
81
|
+
validated: ValidateBlockResult;
|
|
82
|
+
}
|
|
83
|
+
export type InspectBlockGenerationResult = InspectBlockGenerationPlanResult | InspectBlockGenerationValidateResult | InspectBlockGenerationRenderResult;
|
|
84
|
+
export declare function inspectBlockGeneration(input: InspectBlockGenerationInput & {
|
|
85
|
+
stopAfter: "plan";
|
|
86
|
+
}, service?: BlockGeneratorService): Promise<InspectBlockGenerationPlanResult>;
|
|
87
|
+
export declare function inspectBlockGeneration(input: InspectBlockGenerationInput & {
|
|
88
|
+
stopAfter: "validate";
|
|
89
|
+
}, service?: BlockGeneratorService): Promise<InspectBlockGenerationValidateResult>;
|
|
90
|
+
export declare function inspectBlockGeneration(input: InspectBlockGenerationInput & {
|
|
91
|
+
stopAfter?: "render" | undefined;
|
|
92
|
+
}, service?: BlockGeneratorService): Promise<InspectBlockGenerationRenderResult>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { buildBuiltInBlockArtifacts, stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
|
|
2
|
+
import { buildBuiltInCodeArtifacts, } from "./built-in-block-code-artifacts.js";
|
|
3
|
+
import { BlockGeneratorService, } from "./block-generator-service.js";
|
|
4
|
+
import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
|
|
5
|
+
import { listInterpolatedDirectoryOutputs } from "./template-render.js";
|
|
6
|
+
/**
|
|
7
|
+
* Semantic version marker for the public block generation tool contract.
|
|
8
|
+
*
|
|
9
|
+
* Increment this number whenever the serialized inspection payload changes in a
|
|
10
|
+
* breaking way.
|
|
11
|
+
*/
|
|
12
|
+
export const BLOCK_GENERATION_TOOL_CONTRACT_VERSION = 1;
|
|
13
|
+
function buildStarterManifestPreviews(templateId, variables, artifacts) {
|
|
14
|
+
const starterManifests = getStarterManifestFiles(templateId, variables);
|
|
15
|
+
const artifactManifests = new Map(artifacts.map((artifact) => [
|
|
16
|
+
`${artifact.relativeDir}/typia.manifest.json`,
|
|
17
|
+
artifact.manifestDocument,
|
|
18
|
+
]));
|
|
19
|
+
return starterManifests
|
|
20
|
+
.map((entry) => {
|
|
21
|
+
const document = artifactManifests.get(entry.relativePath) ?? entry.document;
|
|
22
|
+
return {
|
|
23
|
+
document,
|
|
24
|
+
owner: "emitter",
|
|
25
|
+
relativePath: entry.relativePath,
|
|
26
|
+
source: stringifyStarterManifest(document),
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
30
|
+
}
|
|
31
|
+
function buildStructuralArtifactPreviews(artifacts) {
|
|
32
|
+
return artifacts
|
|
33
|
+
.flatMap((artifact) => [
|
|
34
|
+
{
|
|
35
|
+
kind: "structural",
|
|
36
|
+
owner: "emitter",
|
|
37
|
+
relativePath: `${artifact.relativeDir}/block.json`,
|
|
38
|
+
source: stringifyBuiltInBlockJsonDocument(artifact.blockJsonDocument),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: "structural",
|
|
42
|
+
owner: "emitter",
|
|
43
|
+
relativePath: `${artifact.relativeDir}/types.ts`,
|
|
44
|
+
source: artifact.typesSource,
|
|
45
|
+
},
|
|
46
|
+
])
|
|
47
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
48
|
+
}
|
|
49
|
+
function buildCodeArtifactPreviews(codeArtifacts) {
|
|
50
|
+
return codeArtifacts
|
|
51
|
+
.map((artifact) => ({
|
|
52
|
+
kind: "generated-source",
|
|
53
|
+
owner: "emitter",
|
|
54
|
+
relativePath: artifact.relativePath,
|
|
55
|
+
source: artifact.source,
|
|
56
|
+
}))
|
|
57
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
58
|
+
}
|
|
59
|
+
async function buildRenderPreview(rendered) {
|
|
60
|
+
const artifacts = buildBuiltInBlockArtifacts({
|
|
61
|
+
templateId: rendered.spec.template.family,
|
|
62
|
+
variables: rendered.variables,
|
|
63
|
+
});
|
|
64
|
+
const codeArtifacts = buildBuiltInCodeArtifacts({
|
|
65
|
+
templateId: rendered.spec.template.family,
|
|
66
|
+
variables: rendered.variables,
|
|
67
|
+
});
|
|
68
|
+
const copiedTemplateFiles = await listInterpolatedDirectoryOutputs(rendered.templateDir, rendered.variables);
|
|
69
|
+
return {
|
|
70
|
+
copiedTemplateFiles: copiedTemplateFiles.map((relativePath) => ({
|
|
71
|
+
owner: "template-copy",
|
|
72
|
+
relativePath,
|
|
73
|
+
})),
|
|
74
|
+
emittedFiles: [
|
|
75
|
+
...buildStructuralArtifactPreviews(artifacts),
|
|
76
|
+
...buildCodeArtifactPreviews(codeArtifacts),
|
|
77
|
+
].sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
78
|
+
gitignoreContent: rendered.gitignoreContent,
|
|
79
|
+
postRender: {
|
|
80
|
+
...rendered.postRender,
|
|
81
|
+
installsDependencies: !rendered.target.noInstall,
|
|
82
|
+
},
|
|
83
|
+
readmeContent: rendered.readmeContent,
|
|
84
|
+
selectedVariant: rendered.selectedVariant,
|
|
85
|
+
starterManifestFiles: buildStarterManifestPreviews(rendered.spec.template.family, rendered.variables, artifacts),
|
|
86
|
+
template: {
|
|
87
|
+
description: rendered.spec.template.description,
|
|
88
|
+
family: rendered.spec.template.family,
|
|
89
|
+
features: [...rendered.spec.template.features],
|
|
90
|
+
format: "wp-typia",
|
|
91
|
+
},
|
|
92
|
+
warnings: [...rendered.warnings],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Inspects built-in block generation through the staged generator boundary
|
|
97
|
+
* without mutating the destination workspace.
|
|
98
|
+
*
|
|
99
|
+
* Use `stopAfter` to halt after the `plan`, `validate`, or `render` stage.
|
|
100
|
+
* The render stage includes copied template file paths, emitter-owned source
|
|
101
|
+
* previews, starter manifest previews, and post-render intent metadata. Any
|
|
102
|
+
* temporary render state is cleaned up before the promise resolves.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const inspection = await inspectBlockGeneration({
|
|
107
|
+
* answers,
|
|
108
|
+
* noInstall: true,
|
|
109
|
+
* packageManager: "bun",
|
|
110
|
+
* projectDir: "demo-block",
|
|
111
|
+
* templateId: "basic",
|
|
112
|
+
* stopAfter: "render",
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @param input - Planning input plus an optional `stopAfter` stage selector.
|
|
117
|
+
* @param service - Optional generator service instance. Defaults to a new
|
|
118
|
+
* `BlockGeneratorService`.
|
|
119
|
+
* @returns The serialized inspection result for the reached stage.
|
|
120
|
+
* @throws Propagates planning, validation, or render failures from
|
|
121
|
+
* `BlockGeneratorService`.
|
|
122
|
+
*/
|
|
123
|
+
export async function inspectBlockGeneration({ stopAfter = "render", ...planInput }, service = new BlockGeneratorService()) {
|
|
124
|
+
const plan = await service.plan(planInput);
|
|
125
|
+
if (stopAfter === "plan") {
|
|
126
|
+
return {
|
|
127
|
+
contractVersion: BLOCK_GENERATION_TOOL_CONTRACT_VERSION,
|
|
128
|
+
mutatesWorkspace: false,
|
|
129
|
+
plan,
|
|
130
|
+
stage: "plan",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const validated = await service.validate({ plan });
|
|
134
|
+
if (stopAfter === "validate") {
|
|
135
|
+
return {
|
|
136
|
+
contractVersion: BLOCK_GENERATION_TOOL_CONTRACT_VERSION,
|
|
137
|
+
mutatesWorkspace: false,
|
|
138
|
+
plan,
|
|
139
|
+
stage: "validate",
|
|
140
|
+
validated,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const rendered = await service.render({ validated });
|
|
144
|
+
try {
|
|
145
|
+
return {
|
|
146
|
+
contractVersion: BLOCK_GENERATION_TOOL_CONTRACT_VERSION,
|
|
147
|
+
mutatesWorkspace: false,
|
|
148
|
+
plan,
|
|
149
|
+
rendered: await buildRenderPreview(rendered),
|
|
150
|
+
stage: "render",
|
|
151
|
+
validated,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await rendered.cleanup?.();
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
|
|
13
13
|
export { BlockGeneratorService } from "./block-generator-service.js";
|
|
14
14
|
export type { ApplyBlockInput, BlockGenerationTarget, BlockSpec, PlanBlockInput, PlanBlockResult, RenderBlockInput, RenderBlockResult, ValidateBlockInput, ValidateBlockResult, } from "./block-generator-service.js";
|
|
15
|
+
export { BLOCK_GENERATION_TOOL_CONTRACT_VERSION, inspectBlockGeneration, } from "./block-generator-tool-contract.js";
|
|
16
|
+
export type { BlockGenerationEmittedFilePreview, BlockGenerationRenderPreview, BlockGenerationStarterManifestPreview, BlockGenerationTemplateCopyPreview, BlockGenerationToolStage, InspectBlockGenerationInput, InspectBlockGenerationPlanResult, InspectBlockGenerationRenderResult, InspectBlockGenerationResult, InspectBlockGenerationValidateResult, } from "./block-generator-tool-contract.js";
|
|
15
17
|
export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
|
|
16
18
|
export { parseWorkspacePackageManagerId, resolveWorkspaceProject, tryResolveWorkspaceProject, } from "./workspace-project.js";
|
|
17
19
|
export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJsonSchema, manifestToOpenApi, normalizeEndpointAuthDefinition, } from "./schema-core.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { scaffoldProject, collectScaffoldAnswers, getDefaultAnswers, getTemplateVariables, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
|
|
13
13
|
export { BlockGeneratorService } from "./block-generator-service.js";
|
|
14
|
+
export { BLOCK_GENERATION_TOOL_CONTRACT_VERSION, inspectBlockGeneration, } from "./block-generator-tool-contract.js";
|
|
14
15
|
export { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand, } from "./migrations.js";
|
|
15
16
|
export { parseWorkspacePackageManagerId, resolveWorkspaceProject, tryResolveWorkspaceProject, } from "./workspace-project.js";
|
|
16
17
|
export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJsonSchema, manifestToOpenApi, normalizeEndpointAuthDefinition, } from "./schema-core.js";
|
|
@@ -19,6 +19,15 @@ export interface MaterializedBuiltInTemplateSource {
|
|
|
19
19
|
selectedVariant?: string | null;
|
|
20
20
|
warnings?: string[];
|
|
21
21
|
}
|
|
22
|
+
export interface BuiltInSharedTemplateLayer {
|
|
23
|
+
dir: string;
|
|
24
|
+
id: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function listBuiltInSharedTemplateLayers(): readonly BuiltInSharedTemplateLayer[];
|
|
27
|
+
export declare function isBuiltInSharedTemplateLayerId(layerId: string): boolean;
|
|
28
|
+
export declare function getBuiltInSharedTemplateLayerDir(layerId: string): string;
|
|
29
|
+
export declare function getBuiltInTemplateOverlayDir(templateId: BuiltInTemplateId): string;
|
|
30
|
+
export declare function getBuiltInTemplateSharedLayerDirs(templateId: BuiltInTemplateId, { persistenceEnabled, persistencePolicy, }?: BuiltInTemplateVariantOptions): string[];
|
|
22
31
|
/**
|
|
23
32
|
* Returns the ordered overlay directories for a built-in template.
|
|
24
33
|
*
|
|
@@ -26,7 +35,7 @@ export interface MaterializedBuiltInTemplateSource {
|
|
|
26
35
|
* the selected policy layer, and the thin template overlay. All other built-ins
|
|
27
36
|
* resolve to the shared base plus their own template directory.
|
|
28
37
|
*/
|
|
29
|
-
export declare function getBuiltInTemplateLayerDirs(templateId: BuiltInTemplateId,
|
|
38
|
+
export declare function getBuiltInTemplateLayerDirs(templateId: BuiltInTemplateId, options?: BuiltInTemplateVariantOptions): string[];
|
|
30
39
|
/**
|
|
31
40
|
* Returns whether a missing built-in overlay directory is expected because the
|
|
32
41
|
* template family no longer ships any Mustache assets in that layer.
|
|
@@ -36,6 +45,7 @@ export declare function getBuiltInTemplateLayerDirs(templateId: BuiltInTemplateI
|
|
|
36
45
|
* @returns True when the missing layer can be skipped safely.
|
|
37
46
|
*/
|
|
38
47
|
export declare function isOmittableBuiltInTemplateLayerDir(templateId: BuiltInTemplateId, layerDir: string): boolean;
|
|
48
|
+
export declare function resolveBuiltInTemplateSourceFromLayerDirs(templateId: BuiltInTemplateId, layerDirs: readonly string[]): Promise<MaterializedBuiltInTemplateSource>;
|
|
39
49
|
/**
|
|
40
50
|
* Materializes a built-in template into a temporary directory by copying each
|
|
41
51
|
* resolved layer in order.
|
|
@@ -2,20 +2,96 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promises as fsp } from "node:fs";
|
|
5
|
-
import { getTemplateById, SHARED_BASE_TEMPLATE_ROOT, SHARED_COMPOUND_TEMPLATE_ROOT, SHARED_PERSISTENCE_TEMPLATE_ROOT, SHARED_REST_HELPER_TEMPLATE_ROOT, } from "./template-registry.js";
|
|
5
|
+
import { getTemplateById, SHARED_BASE_TEMPLATE_ROOT, SHARED_COMPOUND_TEMPLATE_ROOT, SHARED_MIGRATION_UI_TEMPLATE_ROOT, SHARED_PERSISTENCE_TEMPLATE_ROOT, SHARED_PRESET_TEMPLATE_ROOT, SHARED_REST_HELPER_TEMPLATE_ROOT, SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
|
|
6
|
+
const BUILT_IN_SHARED_TEMPLATE_LAYERS = Object.freeze([
|
|
7
|
+
{
|
|
8
|
+
dir: SHARED_BASE_TEMPLATE_ROOT,
|
|
9
|
+
id: "builtin:shared/base",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
dir: path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, "shared"),
|
|
13
|
+
id: "builtin:shared/rest-helpers/shared",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
dir: path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, "public"),
|
|
17
|
+
id: "builtin:shared/rest-helpers/public",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
dir: path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, "auth"),
|
|
21
|
+
id: "builtin:shared/rest-helpers/auth",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
dir: path.join(SHARED_PERSISTENCE_TEMPLATE_ROOT, "core"),
|
|
25
|
+
id: "builtin:shared/persistence/core",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
dir: path.join(SHARED_PERSISTENCE_TEMPLATE_ROOT, "public"),
|
|
29
|
+
id: "builtin:shared/persistence/public",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
dir: path.join(SHARED_PERSISTENCE_TEMPLATE_ROOT, "auth"),
|
|
33
|
+
id: "builtin:shared/persistence/auth",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
dir: path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "core"),
|
|
37
|
+
id: "builtin:shared/compound/core",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
dir: path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "persistence"),
|
|
41
|
+
id: "builtin:shared/compound/persistence",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
dir: path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "persistence-public"),
|
|
45
|
+
id: "builtin:shared/compound/persistence-public",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
dir: path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "persistence-auth"),
|
|
49
|
+
id: "builtin:shared/compound/persistence-auth",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
dir: path.join(SHARED_MIGRATION_UI_TEMPLATE_ROOT, "common"),
|
|
53
|
+
id: "builtin:shared/migration-ui/common",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
dir: path.join(SHARED_PRESET_TEMPLATE_ROOT, "wp-env"),
|
|
57
|
+
id: "builtin:shared/presets/wp-env",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
dir: path.join(SHARED_PRESET_TEMPLATE_ROOT, "test-preset"),
|
|
61
|
+
id: "builtin:shared/presets/test-preset",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
dir: path.join(SHARED_WORKSPACE_TEMPLATE_ROOT, "persistence-public"),
|
|
65
|
+
id: "builtin:shared/workspace/persistence-public",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
dir: path.join(SHARED_WORKSPACE_TEMPLATE_ROOT, "persistence-auth"),
|
|
69
|
+
id: "builtin:shared/workspace/persistence-auth",
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
const BUILT_IN_SHARED_TEMPLATE_LAYER_DIRS = new Map(BUILT_IN_SHARED_TEMPLATE_LAYERS.map((layer) => [layer.id, layer.dir]));
|
|
6
73
|
const OMITTABLE_BUILT_IN_OVERLAY_TEMPLATE_IDS = new Set([
|
|
7
74
|
"basic",
|
|
8
75
|
"persistence",
|
|
9
76
|
"compound",
|
|
10
77
|
]);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
78
|
+
export function listBuiltInSharedTemplateLayers() {
|
|
79
|
+
return BUILT_IN_SHARED_TEMPLATE_LAYERS;
|
|
80
|
+
}
|
|
81
|
+
export function isBuiltInSharedTemplateLayerId(layerId) {
|
|
82
|
+
return BUILT_IN_SHARED_TEMPLATE_LAYER_DIRS.has(layerId);
|
|
83
|
+
}
|
|
84
|
+
export function getBuiltInSharedTemplateLayerDir(layerId) {
|
|
85
|
+
const layerDir = BUILT_IN_SHARED_TEMPLATE_LAYER_DIRS.get(layerId);
|
|
86
|
+
if (!layerDir) {
|
|
87
|
+
throw new Error(`Unknown built-in shared template layer id: ${layerId}`);
|
|
88
|
+
}
|
|
89
|
+
return layerDir;
|
|
90
|
+
}
|
|
91
|
+
export function getBuiltInTemplateOverlayDir(templateId) {
|
|
92
|
+
return getTemplateById(templateId).templateDir;
|
|
93
|
+
}
|
|
94
|
+
export function getBuiltInTemplateSharedLayerDirs(templateId, { persistenceEnabled = false, persistencePolicy = "authenticated", } = {}) {
|
|
19
95
|
if (templateId === "persistence") {
|
|
20
96
|
return [
|
|
21
97
|
SHARED_BASE_TEMPLATE_ROOT,
|
|
@@ -23,21 +99,32 @@ export function getBuiltInTemplateLayerDirs(templateId, { persistenceEnabled = f
|
|
|
23
99
|
path.join(SHARED_PERSISTENCE_TEMPLATE_ROOT, "core"),
|
|
24
100
|
path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, persistencePolicy === "public" ? "public" : "auth"),
|
|
25
101
|
path.join(SHARED_PERSISTENCE_TEMPLATE_ROOT, persistencePolicy === "public" ? "public" : "auth"),
|
|
26
|
-
getTemplateById(templateId).templateDir,
|
|
27
102
|
];
|
|
28
103
|
}
|
|
29
104
|
if (templateId === "compound") {
|
|
30
105
|
const layers = [
|
|
31
106
|
SHARED_BASE_TEMPLATE_ROOT,
|
|
32
107
|
path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "core"),
|
|
33
|
-
getTemplateById(templateId).templateDir,
|
|
34
108
|
];
|
|
35
109
|
if (persistenceEnabled) {
|
|
36
110
|
layers.push(path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, "shared"), path.join(SHARED_COMPOUND_TEMPLATE_ROOT, "persistence"), path.join(SHARED_REST_HELPER_TEMPLATE_ROOT, persistencePolicy === "public" ? "public" : "auth"), path.join(SHARED_COMPOUND_TEMPLATE_ROOT, persistencePolicy === "public" ? "persistence-public" : "persistence-auth"));
|
|
37
111
|
}
|
|
38
112
|
return layers;
|
|
39
113
|
}
|
|
40
|
-
return [SHARED_BASE_TEMPLATE_ROOT
|
|
114
|
+
return [SHARED_BASE_TEMPLATE_ROOT];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the ordered overlay directories for a built-in template.
|
|
118
|
+
*
|
|
119
|
+
* Persistence templates include the shared base, the persistence core layer,
|
|
120
|
+
* the selected policy layer, and the thin template overlay. All other built-ins
|
|
121
|
+
* resolve to the shared base plus their own template directory.
|
|
122
|
+
*/
|
|
123
|
+
export function getBuiltInTemplateLayerDirs(templateId, options = {}) {
|
|
124
|
+
return [
|
|
125
|
+
...getBuiltInTemplateSharedLayerDirs(templateId, options),
|
|
126
|
+
getBuiltInTemplateOverlayDir(templateId),
|
|
127
|
+
];
|
|
41
128
|
}
|
|
42
129
|
/**
|
|
43
130
|
* Returns whether a missing built-in overlay directory is expected because the
|
|
@@ -51,8 +138,8 @@ export function isOmittableBuiltInTemplateLayerDir(templateId, layerDir) {
|
|
|
51
138
|
return (OMITTABLE_BUILT_IN_OVERLAY_TEMPLATE_IDS.has(templateId) &&
|
|
52
139
|
layerDir === getTemplateById(templateId).templateDir);
|
|
53
140
|
}
|
|
54
|
-
function
|
|
55
|
-
return
|
|
141
|
+
function resolveMaterializedTemplateLayerDirs(templateId, layerDirs) {
|
|
142
|
+
return layerDirs.flatMap((layerDir) => {
|
|
56
143
|
if (fs.existsSync(layerDir)) {
|
|
57
144
|
return [layerDir];
|
|
58
145
|
}
|
|
@@ -62,22 +149,14 @@ function resolveMaterializedBuiltInTemplateLayerDirs(templateId, options) {
|
|
|
62
149
|
throw new Error(`Built-in template layer is missing: ${layerDir}`);
|
|
63
150
|
});
|
|
64
151
|
}
|
|
65
|
-
|
|
66
|
-
* Materializes a built-in template into a temporary directory by copying each
|
|
67
|
-
* resolved layer in order.
|
|
68
|
-
*
|
|
69
|
-
* Callers should invoke the returned `cleanup` function when they no longer
|
|
70
|
-
* need the materialized directory. If copying fails, the temporary directory is
|
|
71
|
-
* removed before the error is rethrown.
|
|
72
|
-
*/
|
|
73
|
-
export async function resolveBuiltInTemplateSource(templateId, options = {}) {
|
|
152
|
+
async function materializeBuiltInTemplateSource(templateId, layerDirs) {
|
|
74
153
|
const template = getTemplateById(templateId);
|
|
75
|
-
const
|
|
154
|
+
const materializedLayerDirs = resolveMaterializedTemplateLayerDirs(templateId, layerDirs);
|
|
76
155
|
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-"));
|
|
77
156
|
const templateDir = path.join(tempRoot, templateId);
|
|
78
157
|
try {
|
|
79
158
|
await fsp.mkdir(templateDir, { recursive: true });
|
|
80
|
-
for (const layerDir of
|
|
159
|
+
for (const layerDir of materializedLayerDirs) {
|
|
81
160
|
await fsp.cp(layerDir, templateDir, {
|
|
82
161
|
recursive: true,
|
|
83
162
|
force: true,
|
|
@@ -100,3 +179,17 @@ export async function resolveBuiltInTemplateSource(templateId, options = {}) {
|
|
|
100
179
|
},
|
|
101
180
|
};
|
|
102
181
|
}
|
|
182
|
+
export async function resolveBuiltInTemplateSourceFromLayerDirs(templateId, layerDirs) {
|
|
183
|
+
return materializeBuiltInTemplateSource(templateId, layerDirs);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Materializes a built-in template into a temporary directory by copying each
|
|
187
|
+
* resolved layer in order.
|
|
188
|
+
*
|
|
189
|
+
* Callers should invoke the returned `cleanup` function when they no longer
|
|
190
|
+
* need the materialized directory. If copying fails, the temporary directory is
|
|
191
|
+
* removed before the error is rethrown.
|
|
192
|
+
*/
|
|
193
|
+
export async function resolveBuiltInTemplateSource(templateId, options = {}) {
|
|
194
|
+
return materializeBuiltInTemplateSource(templateId, getBuiltInTemplateLayerDirs(templateId, options));
|
|
195
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const TEMPLATE_LAYER_MANIFEST_FILENAME = "wp-typia.layers.json";
|
|
2
|
+
declare const TEMPLATE_LAYER_MANIFEST_VERSION: 1;
|
|
3
|
+
export interface ExternalTemplateLayerDefinition {
|
|
4
|
+
description?: string;
|
|
5
|
+
extends?: string[];
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ExternalTemplateLayerManifest {
|
|
9
|
+
layers: Record<string, ExternalTemplateLayerDefinition>;
|
|
10
|
+
version: typeof TEMPLATE_LAYER_MANIFEST_VERSION;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolvedTemplateLayerEntry {
|
|
13
|
+
dir: string;
|
|
14
|
+
id: string;
|
|
15
|
+
kind: "built-in" | "external";
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedExternalTemplateLayers {
|
|
18
|
+
entries: ResolvedTemplateLayerEntry[];
|
|
19
|
+
selectedLayerId: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function loadExternalTemplateLayerManifest(sourceRoot: string): Promise<ExternalTemplateLayerManifest | null>;
|
|
22
|
+
export declare function resolveExternalTemplateLayers({ externalLayerId, sourceRoot, }: {
|
|
23
|
+
externalLayerId?: string;
|
|
24
|
+
sourceRoot: string;
|
|
25
|
+
}): Promise<ResolvedExternalTemplateLayers>;
|
|
26
|
+
export declare function assertExternalTemplateLayersDoNotWriteProtectedOutputs({ externalEntries, protectedOutputPaths, view, }: {
|
|
27
|
+
externalEntries: readonly ResolvedTemplateLayerEntry[];
|
|
28
|
+
protectedOutputPaths: ReadonlySet<string>;
|
|
29
|
+
view: Record<string, string>;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
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 getDefaultExternalLayerId(manifest) {
|
|
81
|
+
const layerIds = Object.keys(manifest.layers);
|
|
82
|
+
if (layerIds.length === 1) {
|
|
83
|
+
return layerIds[0];
|
|
84
|
+
}
|
|
85
|
+
const referencedExternalLayerIds = new Set();
|
|
86
|
+
for (const definition of Object.values(manifest.layers)) {
|
|
87
|
+
for (const ancestorId of definition.extends ?? []) {
|
|
88
|
+
if (ancestorId in manifest.layers) {
|
|
89
|
+
referencedExternalLayerIds.add(ancestorId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const publicLayerIds = layerIds.filter((layerId) => !referencedExternalLayerIds.has(layerId));
|
|
94
|
+
if (publicLayerIds.length === 1) {
|
|
95
|
+
return publicLayerIds[0];
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`External layer package defines multiple selectable layers (${layerIds.join(", ")}). Pass an explicit externalLayerId until a final CLI selection UX exists.`);
|
|
98
|
+
}
|
|
99
|
+
export async function resolveExternalTemplateLayers({ externalLayerId, sourceRoot, }) {
|
|
100
|
+
const manifest = await loadExternalTemplateLayerManifest(sourceRoot);
|
|
101
|
+
if (!manifest) {
|
|
102
|
+
throw new Error(`No ${TEMPLATE_LAYER_MANIFEST_FILENAME} manifest found in ${sourceRoot}.`);
|
|
103
|
+
}
|
|
104
|
+
const manifestDocument = manifest;
|
|
105
|
+
const selectedLayerId = externalLayerId ?? getDefaultExternalLayerId(manifestDocument);
|
|
106
|
+
if (!(selectedLayerId in manifestDocument.layers)) {
|
|
107
|
+
throw new Error(`Unknown external layer "${selectedLayerId}". Expected one of: ${Object.keys(manifestDocument.layers).join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
const entries = [];
|
|
110
|
+
const emittedLayerIds = new Set();
|
|
111
|
+
const visitingLayerIds = new Set();
|
|
112
|
+
async function visitLayer(layerId) {
|
|
113
|
+
if (isBuiltInSharedTemplateLayerId(layerId)) {
|
|
114
|
+
if (!emittedLayerIds.has(layerId)) {
|
|
115
|
+
entries.push({
|
|
116
|
+
dir: getBuiltInSharedTemplateLayerDir(layerId),
|
|
117
|
+
id: layerId,
|
|
118
|
+
kind: "built-in",
|
|
119
|
+
});
|
|
120
|
+
emittedLayerIds.add(layerId);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const definition = manifestDocument.layers[layerId];
|
|
125
|
+
if (!definition) {
|
|
126
|
+
throw new Error(`Layer "${layerId}" is not defined in ${TEMPLATE_LAYER_MANIFEST_FILENAME}.`);
|
|
127
|
+
}
|
|
128
|
+
if (visitingLayerIds.has(layerId)) {
|
|
129
|
+
throw new Error(`Detected a cycle while resolving external layer "${layerId}".`);
|
|
130
|
+
}
|
|
131
|
+
visitingLayerIds.add(layerId);
|
|
132
|
+
for (const ancestorId of definition.extends ?? []) {
|
|
133
|
+
await visitLayer(ancestorId);
|
|
134
|
+
}
|
|
135
|
+
visitingLayerIds.delete(layerId);
|
|
136
|
+
if (emittedLayerIds.has(layerId)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const layerDir = resolveLayerPath(sourceRoot, definition.path);
|
|
140
|
+
const stats = await fsp.stat(layerDir).catch(() => null);
|
|
141
|
+
if (!stats || !stats.isDirectory()) {
|
|
142
|
+
throw new Error(`Layer "${layerId}" points to a missing directory: ${definition.path}`);
|
|
143
|
+
}
|
|
144
|
+
await assertNoSymlinks(layerDir);
|
|
145
|
+
entries.push({
|
|
146
|
+
dir: layerDir,
|
|
147
|
+
id: layerId,
|
|
148
|
+
kind: "external",
|
|
149
|
+
});
|
|
150
|
+
emittedLayerIds.add(layerId);
|
|
151
|
+
}
|
|
152
|
+
await visitLayer(selectedLayerId);
|
|
153
|
+
return {
|
|
154
|
+
entries,
|
|
155
|
+
selectedLayerId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export async function assertExternalTemplateLayersDoNotWriteProtectedOutputs({ externalEntries, protectedOutputPaths, view, }) {
|
|
159
|
+
for (const entry of externalEntries) {
|
|
160
|
+
if (entry.kind !== "external") {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const renderedOutputPaths = await listInterpolatedDirectoryOutputs(entry.dir, view);
|
|
164
|
+
for (const relativePath of renderedOutputPaths) {
|
|
165
|
+
if (!protectedOutputPaths.has(relativePath)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`External layer "${entry.id}" writes protected output "${relativePath}".`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -26,4 +26,19 @@ export declare function renderMustacheTemplateString(template: string, view: Tem
|
|
|
26
26
|
export declare function copyRawDirectory(sourceDir: string, targetDir: string, options?: CopyRawDirectoryOptions): Promise<void>;
|
|
27
27
|
export declare function copyRenderedDirectory(sourceDir: string, targetDir: string, view: TemplateRenderView): Promise<void>;
|
|
28
28
|
export declare function copyInterpolatedDirectory(sourceDir: string, targetDir: string, view: Record<string, string>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Lists the output file paths produced by an interpolated template directory
|
|
31
|
+
* without writing any files to disk.
|
|
32
|
+
*
|
|
33
|
+
* This walks the source directory, applies the same filename interpolation and
|
|
34
|
+
* `.mustache` stripping rules as `copyInterpolatedDirectory(...)`, and returns
|
|
35
|
+
* normalized output-relative paths under a virtual root.
|
|
36
|
+
*
|
|
37
|
+
* @param sourceDir - The template directory to traverse.
|
|
38
|
+
* @param view - The interpolation map used when resolving file and directory
|
|
39
|
+
* names.
|
|
40
|
+
* @returns A sorted array of normalized output paths relative to a virtual
|
|
41
|
+
* preview root.
|
|
42
|
+
*/
|
|
43
|
+
export declare function listInterpolatedDirectoryOutputs(sourceDir: string, view: Record<string, string>): Promise<string[]>;
|
|
29
44
|
export declare function pathExists(targetPath: string): boolean;
|
|
@@ -118,6 +118,42 @@ export async function copyInterpolatedDirectory(sourceDir, targetDir, view) {
|
|
|
118
118
|
await fsp.writeFile(destinationPath, renderInterpolatedString(content, view), "utf8");
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Lists the output file paths produced by an interpolated template directory
|
|
123
|
+
* without writing any files to disk.
|
|
124
|
+
*
|
|
125
|
+
* This walks the source directory, applies the same filename interpolation and
|
|
126
|
+
* `.mustache` stripping rules as `copyInterpolatedDirectory(...)`, and returns
|
|
127
|
+
* normalized output-relative paths under a virtual root.
|
|
128
|
+
*
|
|
129
|
+
* @param sourceDir - The template directory to traverse.
|
|
130
|
+
* @param view - The interpolation map used when resolving file and directory
|
|
131
|
+
* names.
|
|
132
|
+
* @returns A sorted array of normalized output paths relative to a virtual
|
|
133
|
+
* preview root.
|
|
134
|
+
*/
|
|
135
|
+
export async function listInterpolatedDirectoryOutputs(sourceDir, view) {
|
|
136
|
+
const virtualRoot = path.resolve("/wp-typia-template-preview");
|
|
137
|
+
const outputs = [];
|
|
138
|
+
async function visit(currentSourceDir, currentTargetDir) {
|
|
139
|
+
const entries = await fsp.readdir(currentSourceDir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const sourcePath = path.join(currentSourceDir, entry.name);
|
|
142
|
+
const destinationNameTemplate = entry.name.endsWith(".mustache")
|
|
143
|
+
? entry.name.slice(0, -".mustache".length)
|
|
144
|
+
: entry.name;
|
|
145
|
+
const destinationName = renderInterpolatedString(destinationNameTemplate, view);
|
|
146
|
+
const destinationPath = resolveRenderedPath(currentTargetDir, destinationName);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
await visit(sourcePath, destinationPath);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
outputs.push(path.relative(virtualRoot, destinationPath).replace(/\\/g, "/"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
await visit(sourceDir, virtualRoot);
|
|
155
|
+
return outputs.sort((left, right) => left.localeCompare(right));
|
|
156
|
+
}
|
|
121
157
|
export function pathExists(targetPath) {
|
|
122
158
|
return fs.existsSync(targetPath);
|
|
123
159
|
}
|
|
@@ -52,6 +52,14 @@ interface NpmTemplateLocator {
|
|
|
52
52
|
rawSpec: string;
|
|
53
53
|
type: string;
|
|
54
54
|
}
|
|
55
|
+
interface SeedSource {
|
|
56
|
+
assetsDir?: string;
|
|
57
|
+
blockDir: string;
|
|
58
|
+
cleanup?: () => Promise<void>;
|
|
59
|
+
rootDir: string;
|
|
60
|
+
selectedVariant?: string | null;
|
|
61
|
+
warnings?: string[];
|
|
62
|
+
}
|
|
55
63
|
type RemoteTemplateLocator = {
|
|
56
64
|
kind: "github";
|
|
57
65
|
locator: GitHubTemplateLocator;
|
|
@@ -65,6 +73,15 @@ type RemoteTemplateLocator = {
|
|
|
65
73
|
export declare function parseGitHubTemplateLocator(templateId: string): GitHubTemplateLocator | null;
|
|
66
74
|
export declare function parseNpmTemplateLocator(templateId: string): NpmTemplateLocator | null;
|
|
67
75
|
export declare function parseTemplateLocator(templateId: string): RemoteTemplateLocator;
|
|
76
|
+
/**
|
|
77
|
+
* Resolves a template locator into a local seed source directory.
|
|
78
|
+
*
|
|
79
|
+
* @param locator Remote template locator describing a local path, GitHub source, or npm package.
|
|
80
|
+
* @param cwd Current working directory used to resolve local template paths.
|
|
81
|
+
* @returns A local seed source containing the resolved root and block directory, plus optional cleanup.
|
|
82
|
+
* @throws When the locator is invalid, the source cannot be fetched, or filesystem validation fails.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveTemplateSeed(locator: RemoteTemplateLocator, cwd: string): Promise<SeedSource>;
|
|
68
85
|
export declare function resolveTemplateSource(templateId: string, cwd: string, variables: {
|
|
69
86
|
[key: string]: string;
|
|
70
87
|
}, variant?: string): Promise<ResolvedTemplateSource>;
|
|
@@ -13,6 +13,7 @@ import { BUILTIN_TEMPLATE_IDS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOL
|
|
|
13
13
|
import { isPlainObject } from "./object-utils.js";
|
|
14
14
|
import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
|
|
15
15
|
import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, resolveBuiltInTemplateSource, } from "./template-builtins.js";
|
|
16
|
+
import { loadExternalTemplateLayerManifest } from "./template-layers.js";
|
|
16
17
|
import { getPackageVersions } from "./package-versions.js";
|
|
17
18
|
import { toSegmentPascalCase } from "./string-case.js";
|
|
18
19
|
import { copyRawDirectory, copyRenderedDirectory } from "./template-render.js";
|
|
@@ -315,6 +316,9 @@ async function detectTemplateSourceFormat(sourceDir) {
|
|
|
315
316
|
if (fs.existsSync(path.join(sourceDir, "package.json.mustache"))) {
|
|
316
317
|
return "wp-typia";
|
|
317
318
|
}
|
|
319
|
+
if (await loadExternalTemplateLayerManifest(sourceDir)) {
|
|
320
|
+
throw new Error(`Template source at ${sourceDir} is an external layer package. External layers currently compose only through built-in scaffolds via the runtime API, not as standalone template ids.`);
|
|
321
|
+
}
|
|
318
322
|
if (getExternalTemplateEntry(sourceDir)) {
|
|
319
323
|
return "create-block-external";
|
|
320
324
|
}
|
|
@@ -728,7 +732,15 @@ async function resolveGitHubTemplateSource(locator) {
|
|
|
728
732
|
throw error;
|
|
729
733
|
}
|
|
730
734
|
}
|
|
731
|
-
|
|
735
|
+
/**
|
|
736
|
+
* Resolves a template locator into a local seed source directory.
|
|
737
|
+
*
|
|
738
|
+
* @param locator Remote template locator describing a local path, GitHub source, or npm package.
|
|
739
|
+
* @param cwd Current working directory used to resolve local template paths.
|
|
740
|
+
* @returns A local seed source containing the resolved root and block directory, plus optional cleanup.
|
|
741
|
+
* @throws When the locator is invalid, the source cannot be fetched, or filesystem validation fails.
|
|
742
|
+
*/
|
|
743
|
+
export async function resolveTemplateSeed(locator, cwd) {
|
|
732
744
|
if (locator.kind === "path") {
|
|
733
745
|
const sourceDir = path.resolve(cwd, locator.templatePath);
|
|
734
746
|
if (!fs.existsSync(sourceDir)) {
|