@wp-typia/project-tools 0.16.6 → 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 +25 -8
- 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/built-in-block-code-artifacts.js +5 -5
- package/dist/runtime/cli-add-block.d.ts +36 -0
- package/dist/runtime/cli-add-block.js +518 -0
- package/dist/runtime/cli-add-shared.d.ts +93 -0
- package/dist/runtime/cli-add-shared.js +201 -0
- package/dist/runtime/cli-add-workspace.d.ts +81 -0
- package/dist/runtime/cli-add-workspace.js +582 -0
- package/dist/runtime/cli-add.d.ts +11 -131
- package/dist/runtime/cli-add.js +10 -1250
- package/dist/runtime/cli-prompt.d.ts +25 -0
- package/dist/runtime/cli-prompt.js +32 -20
- package/dist/runtime/cli-scaffold.js +1 -2
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/migration-types.d.ts +9 -53
- package/dist/runtime/scaffold.js +1 -2
- 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 +14 -3
- package/package.json +6 -8
- package/templates/_shared/base/package.json.mustache +1 -0
- package/templates/_shared/compound/core/package.json.mustache +1 -1
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +5 -5
- package/templates/_shared/compound/persistence/package.json.mustache +1 -1
- package/templates/_shared/persistence/core/package.json.mustache +1 -0
- package/templates/interactivity/package.json.mustache +2 -1
|
@@ -12,10 +12,35 @@ export interface ReadlinePrompt {
|
|
|
12
12
|
select<T extends string>(message: string, options: PromptOption<T>[], defaultValue?: number): Promise<T>;
|
|
13
13
|
text(message: string, defaultValue: string, validate?: ValidateInput): Promise<string>;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Adapter interface for readline-style prompt interactions.
|
|
17
|
+
*
|
|
18
|
+
* Public CLI code uses the native readline implementation, while tests can
|
|
19
|
+
* substitute lightweight doubles that expose the same `question` and `close`
|
|
20
|
+
* methods.
|
|
21
|
+
*/
|
|
22
|
+
export interface ReadlineQuestionAdapter {
|
|
23
|
+
/** Close the underlying prompt interface and release any open handles. */
|
|
24
|
+
close(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Render one prompt and resolve with the collected answer string.
|
|
27
|
+
*
|
|
28
|
+
* @param query Prompt text written to the active output stream.
|
|
29
|
+
* @param callback Callback that receives the entered answer.
|
|
30
|
+
*/
|
|
31
|
+
question(query: string, callback: (answer: string) => void): void;
|
|
32
|
+
}
|
|
15
33
|
/**
|
|
16
34
|
* Create the default readline-backed prompt implementation for the CLI.
|
|
17
35
|
*
|
|
18
36
|
* @returns A prompt adapter that reads from stdin and writes to stdout.
|
|
19
37
|
*/
|
|
20
38
|
export declare function createReadlinePrompt(): ReadlinePrompt;
|
|
39
|
+
/**
|
|
40
|
+
* Build a prompt adapter around a supplied readline-style question interface.
|
|
41
|
+
*
|
|
42
|
+
* This keeps the production CLI path unchanged while letting tests validate
|
|
43
|
+
* retry behavior without stubbing global stdin/stdout.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createReadlinePromptWithInterface(rl: ReadlineQuestionAdapter): ReadlinePrompt;
|
|
21
46
|
export {};
|
|
@@ -9,21 +9,32 @@ export function createReadlinePrompt() {
|
|
|
9
9
|
input: process.stdin,
|
|
10
10
|
output: process.stdout,
|
|
11
11
|
});
|
|
12
|
+
return createReadlinePromptWithInterface(rl);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a prompt adapter around a supplied readline-style question interface.
|
|
16
|
+
*
|
|
17
|
+
* This keeps the production CLI path unchanged while letting tests validate
|
|
18
|
+
* retry behavior without stubbing global stdin/stdout.
|
|
19
|
+
*/
|
|
20
|
+
export function createReadlinePromptWithInterface(rl) {
|
|
12
21
|
return {
|
|
13
22
|
async text(message, defaultValue, validate) {
|
|
14
23
|
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
while (true) {
|
|
25
|
+
const answer = await new Promise((resolve) => {
|
|
26
|
+
rl.question(`${message}${suffix}: `, resolve);
|
|
27
|
+
});
|
|
28
|
+
const value = String(answer).trim() || defaultValue;
|
|
29
|
+
if (validate) {
|
|
30
|
+
const result = validate(value);
|
|
31
|
+
if (result !== true) {
|
|
32
|
+
console.error(`❌ ${typeof result === "string" ? result : "Invalid input"}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
24
35
|
}
|
|
36
|
+
return value;
|
|
25
37
|
}
|
|
26
|
-
return value;
|
|
27
38
|
},
|
|
28
39
|
async select(message, options, defaultValue = 1) {
|
|
29
40
|
if (options.length === 0) {
|
|
@@ -34,17 +45,18 @@ export function createReadlinePrompt() {
|
|
|
34
45
|
const hint = option.hint ? ` - ${option.hint}` : "";
|
|
35
46
|
console.log(` ${index + 1}. ${option.label}${hint}`);
|
|
36
47
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
while (true) {
|
|
49
|
+
const answer = await this.text("Choice", String(defaultValue));
|
|
50
|
+
const numericChoice = Number(answer);
|
|
51
|
+
if (!Number.isNaN(numericChoice) && options[numericChoice - 1]) {
|
|
52
|
+
return options[numericChoice - 1].value;
|
|
53
|
+
}
|
|
54
|
+
const directChoice = options.find((option) => option.value === answer);
|
|
55
|
+
if (directChoice) {
|
|
56
|
+
return directChoice.value;
|
|
57
|
+
}
|
|
58
|
+
console.error(`❌ Invalid selection: ${answer}`);
|
|
45
59
|
}
|
|
46
|
-
console.error(`❌ Invalid selection: ${answer}`);
|
|
47
|
-
return this.select(message, options, defaultValue);
|
|
48
60
|
},
|
|
49
61
|
close() {
|
|
50
62
|
rl.close();
|
|
@@ -4,8 +4,7 @@ import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDat
|
|
|
4
4
|
import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
|
|
5
5
|
import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
|
|
6
6
|
import { getOptionalOnboardingNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
|
|
7
|
-
import { isBuiltInTemplateId } from "./template-registry.js";
|
|
8
|
-
const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
7
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
9
8
|
function templateUsesPersistenceSettings(templateId, options) {
|
|
10
9
|
if (templateId === "persistence") {
|
|
11
10
|
return true;
|
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";
|
|
@@ -1,56 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
maxLength?: number | null;
|
|
11
|
-
maxItems?: number | null;
|
|
12
|
-
maximum?: number | null;
|
|
13
|
-
minLength?: number | null;
|
|
14
|
-
minItems?: number | null;
|
|
15
|
-
minimum?: number | null;
|
|
16
|
-
multipleOf?: number | null;
|
|
17
|
-
pattern?: string | null;
|
|
18
|
-
typeTag?: string | null;
|
|
19
|
-
}
|
|
20
|
-
export interface ManifestUnionMetadata {
|
|
21
|
-
discriminator: string;
|
|
22
|
-
branches: Record<string, ManifestAttribute>;
|
|
23
|
-
}
|
|
24
|
-
export interface ManifestTsMetadata {
|
|
25
|
-
items?: ManifestAttribute | null;
|
|
26
|
-
kind: ManifestTsKind;
|
|
27
|
-
properties?: Record<string, ManifestAttribute> | null;
|
|
28
|
-
required?: boolean;
|
|
29
|
-
union?: ManifestUnionMetadata | null;
|
|
30
|
-
}
|
|
31
|
-
export interface ManifestTypiaMetadata {
|
|
32
|
-
constraints: ManifestConstraints;
|
|
33
|
-
defaultValue?: JsonValue | null;
|
|
34
|
-
hasDefault?: boolean;
|
|
35
|
-
}
|
|
36
|
-
export interface ManifestWpMetadata {
|
|
37
|
-
defaultValue?: JsonValue | null;
|
|
38
|
-
enum?: JsonValue[] | null;
|
|
39
|
-
hasDefault?: boolean;
|
|
40
|
-
selector?: string | null;
|
|
41
|
-
source?: "html" | "text" | "rich-text" | null;
|
|
42
|
-
type?: string | null;
|
|
43
|
-
}
|
|
44
|
-
export interface ManifestAttribute {
|
|
45
|
-
ts: ManifestTsMetadata;
|
|
46
|
-
typia: ManifestTypiaMetadata;
|
|
47
|
-
wp: ManifestWpMetadata;
|
|
48
|
-
}
|
|
49
|
-
export interface ManifestDocument {
|
|
50
|
-
attributes?: Record<string, ManifestAttribute>;
|
|
51
|
-
manifestVersion?: number | null;
|
|
52
|
-
sourceType?: string | null;
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared migration contract facade for project-tools runtime.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports canonical manifest and migration contracts from
|
|
5
|
+
* `@wp-typia/block-runtime/migration-types` and layers project-tools-specific
|
|
6
|
+
* migration workspace types on top.
|
|
7
|
+
*/
|
|
8
|
+
import type { JsonValue, ManifestAttribute, ManifestDocument } from "@wp-typia/block-runtime/migration-types";
|
|
9
|
+
export type { JsonPrimitive, JsonValue, ManifestAttribute, ManifestConstraints, ManifestDocument, ManifestTsKind, ManifestTsMetadata, ManifestTypiaMetadata, ManifestUnionMetadata, ManifestWpMetadata, } from "@wp-typia/block-runtime/migration-types";
|
|
54
10
|
export interface ManifestSummaryAttribute {
|
|
55
11
|
constraints: ManifestAttribute["typia"]["constraints"];
|
|
56
12
|
defaultValue: ManifestAttribute["typia"]["defaultValue"] | null;
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -13,13 +13,12 @@ import { getStarterManifestFiles, stringifyStarterManifest } from "./starter-man
|
|
|
13
13
|
import { toKebabCase, toPascalCase, toSnakeCase, toTitleCase, } from "./string-case.js";
|
|
14
14
|
import { BUILTIN_BLOCK_METADATA_VERSION, COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS, getBuiltInTemplateMetadataDefaults, getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
|
|
15
15
|
import { copyInterpolatedDirectory } from "./template-render.js";
|
|
16
|
-
import { PROJECT_TOOLS_PACKAGE_ROOT, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
|
|
16
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
|
|
17
17
|
import { resolveTemplateSource } from "./template-source.js";
|
|
18
18
|
import { BlockGeneratorService, buildTemplateVariablesFromBlockSpec, createBuiltInBlockSpec, } from "./block-generator-service.js";
|
|
19
19
|
const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
20
20
|
const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
|
|
21
21
|
const PHP_PREFIX_MAX_LENGTH = 50;
|
|
22
|
-
const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
23
22
|
const WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
24
23
|
const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
|
|
25
24
|
const LOCKFILES = {
|
|
@@ -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
|
+
}
|