@ykdz/template 0.0.3 → 0.0.5
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 +1 -1
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +26 -2
- package/dist/src/declarations.d.ts +38 -3
- package/dist/src/declarations.js +75 -36
- package/dist/src/dependency-catalog.d.ts +9 -1
- package/dist/src/dependency-catalog.js +69 -0
- package/dist/src/devcontainer.d.ts +49 -9
- package/dist/src/devcontainer.js +192 -14
- package/dist/src/editor-customization.d.ts +0 -1
- package/dist/src/editor-customization.js +54 -47
- package/dist/src/generation-context.d.ts +0 -1
- package/dist/src/generation-context.js +4 -1
- package/dist/src/module-graph.d.ts +8 -5
- package/dist/src/module-graph.js +20 -2
- package/dist/src/next-step-instructions.d.ts +0 -1
- package/dist/src/next-step-instructions.js +35 -0
- package/dist/src/package-addition-support.d.ts +5 -0
- package/dist/src/package-addition-support.js +4 -0
- package/dist/src/package-addition.d.ts +2 -1
- package/dist/src/package-addition.js +557 -55
- package/dist/src/package-linking.d.ts +55 -0
- package/dist/src/package-linking.js +116 -0
- package/dist/src/preset-projection.d.ts +4 -2
- package/dist/src/project-github.d.ts +3 -2
- package/dist/src/project-github.js +24 -6
- package/dist/src/renderer.d.ts +16 -2
- package/dist/src/renderer.js +96 -12
- package/dist/src/runtime-paths.d.ts +0 -1
- package/dist/src/template-boundary-check.d.ts +34 -0
- package/dist/src/template-boundary-check.js +448 -0
- package/dist/src/toolchain-resolution.d.ts +0 -1
- package/dist/templates/hono-api/projection.d.ts +2 -3
- package/dist/templates/hono-api/projection.js +258 -76
- package/dist/templates/projection-plans.d.ts +0 -1
- package/dist/templates/registry.d.ts +0 -1
- package/dist/templates/registry.js +5 -2
- package/dist/templates/rust-bin/projection.d.ts +2 -3
- package/dist/templates/rust-bin/projection.js +171 -53
- package/dist/templates/ts-lib/projection.d.ts +2 -3
- package/dist/templates/ts-lib/projection.js +251 -87
- package/dist/templates/vue-app/projection.d.ts +2 -3
- package/dist/templates/vue-app/projection.js +266 -94
- package/dist/templates/vue-hono-app/projection.d.ts +0 -1
- package/dist/templates/vue-hono-app/projection.js +232 -108
- package/package.json +10 -5
- package/pnpm-workspace.yaml +1 -0
- package/templates/hono-api/.github/dependabot.yml +13 -0
- package/templates/hono-api/src/server.ts +1 -1
- package/templates/hono-api/test/app.test.ts +1 -1
- package/templates/rust-bin/.github/dependabot.yml +18 -1
- package/templates/rust-bin/rust-toolchain.toml +3 -0
- package/templates/rust-bin/rustfmt.toml +2 -0
- package/templates/shared/devcontainer/browser-test.Dockerfile +2 -0
- package/templates/shared/devcontainer/node-pnpm.Dockerfile +6 -0
- package/templates/shared/devcontainer/rust.Dockerfile +16 -0
- package/templates/shared/editor-customization/capabilities.json +56 -0
- package/templates/ts-lib/.github/dependabot.yml +13 -0
- package/templates/ts-lib/src/index.ts +5 -1
- package/templates/ts-lib/src/name-schema.ts +3 -0
- package/templates/vue-app/.github/dependabot.yml +13 -0
- package/templates/vue-app/.github/workflows/check.yml +1 -1
- package/templates/vue-app/src/App.vue +1 -1
- package/templates/vue-app/test/app.test.ts +1 -1
- package/templates/vue-hono-app/.github/dependabot.yml +13 -0
- package/templates/vue-hono-app/web/src/App.vue +2 -2
- package/templates/vue-hono-app/web/test/app.test.ts +1 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/declarations.d.ts.map +0 -1
- package/dist/src/dependency-catalog.d.ts.map +0 -1
- package/dist/src/devcontainer.d.ts.map +0 -1
- package/dist/src/editor-customization.d.ts.map +0 -1
- package/dist/src/generation-context.d.ts.map +0 -1
- package/dist/src/module-graph.d.ts.map +0 -1
- package/dist/src/next-step-instructions.d.ts.map +0 -1
- package/dist/src/package-addition.d.ts.map +0 -1
- package/dist/src/preset-projection.d.ts.map +0 -1
- package/dist/src/project-github.d.ts.map +0 -1
- package/dist/src/renderer.d.ts.map +0 -1
- package/dist/src/runtime-paths.d.ts.map +0 -1
- package/dist/src/toolchain-resolution.d.ts.map +0 -1
- package/dist/templates/hono-api/projection.d.ts.map +0 -1
- package/dist/templates/projection-plans.d.ts.map +0 -1
- package/dist/templates/registry.d.ts.map +0 -1
- package/dist/templates/rust-bin/projection.d.ts.map +0 -1
- package/dist/templates/ts-lib/projection.d.ts.map +0 -1
- package/dist/templates/vue-app/projection.d.ts.map +0 -1
- package/dist/templates/vue-hono-app/projection.d.ts.map +0 -1
- package/templates/hono-api/projection.ts +0 -453
- package/templates/projection-plans.ts +0 -135
- package/templates/registry.ts +0 -59
- package/templates/rust-bin/projection.ts +0 -346
- package/templates/ts-lib/projection.ts +0 -454
- package/templates/vue-app/projection.ts +0 -687
- package/templates/vue-hono-app/projection.ts +0 -669
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type PackageRole = "runtime-service" | "shared-library";
|
|
2
|
+
export type PackageSourcePreset = "hono-api" | "ts-lib" | "vue-app";
|
|
3
|
+
export type PackageDefinition = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly role?: PackageRole;
|
|
7
|
+
readonly sourcePreset?: PackageSourcePreset;
|
|
8
|
+
};
|
|
9
|
+
export type PackageLinkIntent = {
|
|
10
|
+
readonly consumerPackagePath: string;
|
|
11
|
+
readonly providerPackagePath: string;
|
|
12
|
+
};
|
|
13
|
+
export type JitSourcePackageExposure = {
|
|
14
|
+
readonly kind: "jit-source";
|
|
15
|
+
readonly entrypoint: string;
|
|
16
|
+
readonly packageLocalImportPattern: string;
|
|
17
|
+
readonly packageLocalImportTarget: string;
|
|
18
|
+
};
|
|
19
|
+
export type CompiledPackageExposure = {
|
|
20
|
+
readonly kind: "compiled";
|
|
21
|
+
readonly entrypoint: string;
|
|
22
|
+
readonly sourceTypes: string;
|
|
23
|
+
readonly packageLocalImportPattern: string;
|
|
24
|
+
readonly packageLocalImportRuntimeTarget: string;
|
|
25
|
+
readonly packageLocalImportTypesTarget: string;
|
|
26
|
+
};
|
|
27
|
+
export type PackageExposure = CompiledPackageExposure | JitSourcePackageExposure;
|
|
28
|
+
export type PackageLinkPlan = {
|
|
29
|
+
readonly exposuresByPackagePath: ReadonlyMap<string, PackageExposure>;
|
|
30
|
+
readonly manifestDependenciesByPackagePath: ReadonlyMap<string, Readonly<Record<string, "workspace:*">>>;
|
|
31
|
+
readonly turboTasks: TurboTaskGraph;
|
|
32
|
+
};
|
|
33
|
+
export type PackageManifestExposureFields = {
|
|
34
|
+
readonly exports: Record<string, unknown>;
|
|
35
|
+
readonly imports: Record<string, unknown>;
|
|
36
|
+
readonly types?: string;
|
|
37
|
+
};
|
|
38
|
+
export type PackageLinkBoundaryDirection = "consumer" | "provider";
|
|
39
|
+
export type TurboTaskDefinition = {
|
|
40
|
+
readonly dependsOn?: readonly string[];
|
|
41
|
+
readonly outputs?: readonly string[];
|
|
42
|
+
readonly cache?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export type TurboTaskGraph = Readonly<Record<string, TurboTaskDefinition>>;
|
|
45
|
+
export type PackageTurboTaskOptions = {
|
|
46
|
+
readonly dependencyBuildsRequired: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare function derivePackageExposure(definition: PackageDefinition): PackageExposure;
|
|
49
|
+
export declare function assertTypeScriptPackageBoundaryForLinkIntent(definition: PackageDefinition, direction: PackageLinkBoundaryDirection): asserts definition is PackageDefinition & {
|
|
50
|
+
readonly role: PackageRole;
|
|
51
|
+
readonly sourcePreset: PackageSourcePreset;
|
|
52
|
+
};
|
|
53
|
+
export declare function planPackageLinks(definitions: readonly PackageDefinition[], intents?: readonly PackageLinkIntent[]): PackageLinkPlan;
|
|
54
|
+
export declare function packageTurboTasks({ dependencyBuildsRequired, }: PackageTurboTaskOptions): TurboTaskGraph;
|
|
55
|
+
export declare function packageManifestExposureFields(exposure: PackageExposure): PackageManifestExposureFields;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export function derivePackageExposure(definition) {
|
|
2
|
+
if (definition.role === "shared-library" &&
|
|
3
|
+
definition.sourcePreset === "ts-lib") {
|
|
4
|
+
return {
|
|
5
|
+
kind: "jit-source",
|
|
6
|
+
entrypoint: "./src/index.ts",
|
|
7
|
+
packageLocalImportPattern: "#/*",
|
|
8
|
+
packageLocalImportTarget: "./src/*.ts",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (definition.role === "runtime-service" &&
|
|
12
|
+
definition.sourcePreset === "hono-api") {
|
|
13
|
+
return {
|
|
14
|
+
kind: "compiled",
|
|
15
|
+
entrypoint: "./dist/index.js",
|
|
16
|
+
sourceTypes: "./src/index.ts",
|
|
17
|
+
packageLocalImportPattern: "#/*",
|
|
18
|
+
packageLocalImportRuntimeTarget: "./dist/*.js",
|
|
19
|
+
packageLocalImportTypesTarget: "./src/*.ts",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Unsupported Package Exposure for ${definition.name} at ${definition.path}`);
|
|
23
|
+
}
|
|
24
|
+
export function assertTypeScriptPackageBoundaryForLinkIntent(definition, direction) {
|
|
25
|
+
if (definition.role !== undefined && definition.sourcePreset !== undefined) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const relationship = direction === "consumer" ? "from native package" : "to native package";
|
|
29
|
+
throw new Error(`Package Link Intent ${relationship} ${definition.path} is unsupported in V1 TypeScript-only Project Linking`);
|
|
30
|
+
}
|
|
31
|
+
export function planPackageLinks(definitions, intents = []) {
|
|
32
|
+
const definitionsByPath = new Map(definitions.map((definition) => [definition.path, definition]));
|
|
33
|
+
const manifestDependenciesByPackagePath = new Map();
|
|
34
|
+
for (const intent of intents) {
|
|
35
|
+
const consumer = definitionsByPath.get(intent.consumerPackagePath);
|
|
36
|
+
const provider = definitionsByPath.get(intent.providerPackagePath);
|
|
37
|
+
if (consumer !== undefined) {
|
|
38
|
+
assertTypeScriptPackageBoundaryForLinkIntent(consumer, "consumer");
|
|
39
|
+
}
|
|
40
|
+
if (provider === undefined) {
|
|
41
|
+
throw new Error(`Package Link Intent references unknown provider package at ${intent.providerPackagePath}`);
|
|
42
|
+
}
|
|
43
|
+
assertTypeScriptPackageBoundaryForLinkIntent(provider, "provider");
|
|
44
|
+
}
|
|
45
|
+
const exposuresByPackagePath = new Map(definitions.map((definition) => [
|
|
46
|
+
definition.path,
|
|
47
|
+
derivePackageExposure(definition),
|
|
48
|
+
]));
|
|
49
|
+
let dependencyBuildsRequired = false;
|
|
50
|
+
for (const intent of intents) {
|
|
51
|
+
const provider = definitionsByPath.get(intent.providerPackagePath);
|
|
52
|
+
if (provider === undefined) {
|
|
53
|
+
throw new Error(`Package Link Intent references unknown provider package at ${intent.providerPackagePath}`);
|
|
54
|
+
}
|
|
55
|
+
if (exposuresByPackagePath.get(provider.path)?.kind === "compiled") {
|
|
56
|
+
dependencyBuildsRequired = true;
|
|
57
|
+
}
|
|
58
|
+
const dependencies = manifestDependenciesByPackagePath.get(intent.consumerPackagePath) ?? {};
|
|
59
|
+
dependencies[provider.name] = "workspace:*";
|
|
60
|
+
manifestDependenciesByPackagePath.set(intent.consumerPackagePath, dependencies);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
exposuresByPackagePath,
|
|
64
|
+
manifestDependenciesByPackagePath,
|
|
65
|
+
turboTasks: packageTurboTasks({ dependencyBuildsRequired }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function packageTurboTasks({ dependencyBuildsRequired, }) {
|
|
69
|
+
return {
|
|
70
|
+
typecheck: { dependsOn: ["^typecheck"] },
|
|
71
|
+
build: dependencyBuildsRequired
|
|
72
|
+
? { dependsOn: ["^build"], outputs: ["dist/**"] }
|
|
73
|
+
: { outputs: ["dist/**"] },
|
|
74
|
+
test: { dependsOn: ["^typecheck"] },
|
|
75
|
+
"test:e2e": {
|
|
76
|
+
dependsOn: dependencyBuildsRequired ? ["build", "^build"] : ["build"],
|
|
77
|
+
},
|
|
78
|
+
check: { dependsOn: ["typecheck", "build", "test", "test:e2e"] },
|
|
79
|
+
fix: { cache: false },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function packageManifestExposureFields(exposure) {
|
|
83
|
+
switch (exposure.kind) {
|
|
84
|
+
case "jit-source":
|
|
85
|
+
return {
|
|
86
|
+
imports: {
|
|
87
|
+
[exposure.packageLocalImportPattern]: {
|
|
88
|
+
default: exposure.packageLocalImportTarget,
|
|
89
|
+
types: exposure.packageLocalImportTarget,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
exports: {
|
|
93
|
+
".": {
|
|
94
|
+
default: exposure.entrypoint,
|
|
95
|
+
types: exposure.entrypoint,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
case "compiled":
|
|
100
|
+
return {
|
|
101
|
+
types: exposure.sourceTypes,
|
|
102
|
+
imports: {
|
|
103
|
+
[exposure.packageLocalImportPattern]: {
|
|
104
|
+
default: exposure.packageLocalImportRuntimeTarget,
|
|
105
|
+
types: exposure.packageLocalImportTypesTarget,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
exports: {
|
|
109
|
+
".": {
|
|
110
|
+
default: exposure.entrypoint,
|
|
111
|
+
types: exposure.sourceTypes,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { BuiltInPreset, ProjectBlueprint } from "./declarations.js";
|
|
|
2
2
|
import type { GenerationContext } from "./generation-context.js";
|
|
3
3
|
import { type CheckPlan, type FixPlan } from "./module-graph.js";
|
|
4
4
|
import { type NextStepInstruction } from "./next-step-instructions.js";
|
|
5
|
+
import type { PackageRole, PackageSourcePreset } from "./package-linking.js";
|
|
5
6
|
import type { DependencyMaintenancePolicy } from "./project-github.js";
|
|
6
7
|
import type { RenderOperation } from "./renderer.js";
|
|
7
8
|
export type PresetPackageAdditionOptions = {
|
|
@@ -9,12 +10,14 @@ export type PresetPackageAdditionOptions = {
|
|
|
9
10
|
readonly blueprint: ProjectBlueprint;
|
|
10
11
|
readonly packageLeafName: string;
|
|
11
12
|
readonly packageName: string;
|
|
13
|
+
readonly packagePath: string;
|
|
12
14
|
readonly nodeVersion: string;
|
|
13
15
|
};
|
|
14
16
|
export type PresetPackageAdditionPlan = {
|
|
15
17
|
readonly packagePath: string;
|
|
16
18
|
readonly workspacePackageGlob: string;
|
|
17
|
-
readonly
|
|
19
|
+
readonly packageRole: PackageRole;
|
|
20
|
+
readonly packageSourcePreset: PackageSourcePreset;
|
|
18
21
|
readonly sourceRoot: string;
|
|
19
22
|
readonly sourceRoots?: Record<string, string>;
|
|
20
23
|
readonly operations: readonly RenderOperation[];
|
|
@@ -63,4 +66,3 @@ export declare function planNextStepInstructionsForProjection(options: {
|
|
|
63
66
|
readonly targetDir: string;
|
|
64
67
|
readonly plan: PresetProjectionPlan;
|
|
65
68
|
}): readonly NextStepInstruction[];
|
|
66
|
-
//# sourceMappingURL=preset-projection.d.ts.map
|
|
@@ -12,9 +12,11 @@ export type PnpmTaskLayer = {
|
|
|
12
12
|
readonly installCommand: "pnpm install";
|
|
13
13
|
readonly checkCommand: "pnpm run check";
|
|
14
14
|
};
|
|
15
|
-
export type DependencyEcosystem = "npm" | "cargo" | "github-actions";
|
|
15
|
+
export type DependencyEcosystem = "npm" | "cargo" | "github-actions" | "docker" | "rust-toolchain";
|
|
16
|
+
export type DependabotDirectory = `/${string}`;
|
|
16
17
|
export type DependencyMaintenancePolicy = {
|
|
17
18
|
readonly ecosystems: DependencyEcosystem[];
|
|
19
|
+
readonly directories?: Partial<Record<DependencyEcosystem, DependabotDirectory>>;
|
|
18
20
|
readonly interval: "weekly";
|
|
19
21
|
};
|
|
20
22
|
type ProjectCheckWorkflowOptions = {
|
|
@@ -26,4 +28,3 @@ type ProjectCheckWorkflowOptions = {
|
|
|
26
28
|
export declare function projectCheckWorkflow(options: ProjectCheckWorkflowOptions): string;
|
|
27
29
|
export declare function projectDependabotConfig(policy: DependencyMaintenancePolicy): string;
|
|
28
30
|
export {};
|
|
29
|
-
//# sourceMappingURL=project-github.d.ts.map
|
|
@@ -44,16 +44,34 @@ export function projectCheckWorkflow(options) {
|
|
|
44
44
|
export function projectDependabotConfig(policy) {
|
|
45
45
|
return [
|
|
46
46
|
"version: 2",
|
|
47
|
+
"",
|
|
47
48
|
"updates:",
|
|
48
|
-
...policy.ecosystems.flatMap((ecosystem) => [
|
|
49
|
-
|
|
50
|
-
" directory: /",
|
|
51
|
-
" schedule:",
|
|
52
|
-
` interval: ${policy.interval}`,
|
|
53
|
-
]),
|
|
49
|
+
...policy.ecosystems.flatMap((ecosystem) => renderDependabotUpdate(ecosystem, policy.directories?.[ecosystem] ??
|
|
50
|
+
defaultDependabotDirectory(ecosystem), policy.interval)),
|
|
54
51
|
"",
|
|
55
52
|
].join("\n");
|
|
56
53
|
}
|
|
54
|
+
function renderDependabotUpdate(ecosystem, directory, interval) {
|
|
55
|
+
const lines = [
|
|
56
|
+
` - package-ecosystem: ${ecosystem}`,
|
|
57
|
+
` directory: ${renderDependabotDirectory(ecosystem, directory)}`,
|
|
58
|
+
" schedule:",
|
|
59
|
+
` interval: ${interval}`,
|
|
60
|
+
];
|
|
61
|
+
if (ecosystem === "npm") {
|
|
62
|
+
lines.push(" ignore:", ' - dependency-name: "@types/node"', " update-types:", " - version-update:semver-major");
|
|
63
|
+
}
|
|
64
|
+
if (ecosystem === "docker") {
|
|
65
|
+
lines.push(" ignore:", " - dependency-name: mcr.microsoft.com/devcontainers/typescript-node", " update-types:", " - version-update:semver-major");
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
function renderDependabotDirectory(ecosystem, directory) {
|
|
70
|
+
return ecosystem === "cargo" ? JSON.stringify(directory) : directory;
|
|
71
|
+
}
|
|
72
|
+
function defaultDependabotDirectory(ecosystem) {
|
|
73
|
+
return ecosystem === "docker" ? "/.devcontainer" : "/";
|
|
74
|
+
}
|
|
57
75
|
function renderCiEnvironmentNeedCommand(need) {
|
|
58
76
|
if (need.kind === "playwright-browser-assets" &&
|
|
59
77
|
need.owner.path === "apps/web") {
|
package/dist/src/renderer.d.ts
CHANGED
|
@@ -21,6 +21,21 @@ export type WriteTextOperation = {
|
|
|
21
21
|
to: string;
|
|
22
22
|
text: string;
|
|
23
23
|
};
|
|
24
|
+
export type WriteTextFromFragmentsOperation = {
|
|
25
|
+
kind: "writeTextFromFragments";
|
|
26
|
+
to: string;
|
|
27
|
+
fragments: readonly {
|
|
28
|
+
from: string;
|
|
29
|
+
sourceRoot?: string;
|
|
30
|
+
}[];
|
|
31
|
+
};
|
|
32
|
+
export type WriteTextTemplateOperation = {
|
|
33
|
+
kind: "writeTextTemplate";
|
|
34
|
+
from: string;
|
|
35
|
+
to: string;
|
|
36
|
+
sourceRoot?: string;
|
|
37
|
+
replacements: Record<string, string>;
|
|
38
|
+
};
|
|
24
39
|
export type SetExecutableOperation = {
|
|
25
40
|
kind: "setExecutable";
|
|
26
41
|
path: string;
|
|
@@ -32,7 +47,7 @@ export type ReplaceAnchorsOperation = {
|
|
|
32
47
|
language: "typescript";
|
|
33
48
|
replacements: Record<string, string>;
|
|
34
49
|
};
|
|
35
|
-
export type RenderOperation = CopyFileOperation | WriteJsonOperation | MergeJsonOperation | WriteTextOperation | SetExecutableOperation | ReplaceAnchorsOperation;
|
|
50
|
+
export type RenderOperation = CopyFileOperation | WriteJsonOperation | MergeJsonOperation | WriteTextOperation | WriteTextFromFragmentsOperation | WriteTextTemplateOperation | SetExecutableOperation | ReplaceAnchorsOperation;
|
|
36
51
|
export type RenderProjectOptions = {
|
|
37
52
|
sourceRoot: string;
|
|
38
53
|
sourceRoots?: Record<string, string>;
|
|
@@ -43,4 +58,3 @@ export type RenderProjectOptions = {
|
|
|
43
58
|
export declare function renderProject(options: RenderProjectOptions): Promise<void>;
|
|
44
59
|
export declare function renderNewProject(options: RenderProjectOptions): Promise<void>;
|
|
45
60
|
export {};
|
|
46
|
-
//# sourceMappingURL=renderer.d.ts.map
|
package/dist/src/renderer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
function expandTemplatePath(templatePath, variables) {
|
|
5
5
|
return templatePath.replaceAll(/\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g, (_, name) => {
|
|
@@ -19,7 +19,8 @@ function resolveContainedPath(root, relativePath) {
|
|
|
19
19
|
}
|
|
20
20
|
const resolvedRoot = path.resolve(root);
|
|
21
21
|
const resolvedPath = path.resolve(resolvedRoot, relativePath);
|
|
22
|
-
const insideRoot = resolvedPath === resolvedRoot ||
|
|
22
|
+
const insideRoot = resolvedPath === resolvedRoot ||
|
|
23
|
+
resolvedPath.startsWith(`${resolvedRoot}${path.sep}`);
|
|
23
24
|
if (!insideRoot) {
|
|
24
25
|
throw new Error(`Renderer path escapes its root: ${relativePath}`);
|
|
25
26
|
}
|
|
@@ -47,7 +48,7 @@ function isRecord(value) {
|
|
|
47
48
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
49
|
}
|
|
49
50
|
function isScalarJsonValue(value) {
|
|
50
|
-
return value === null || ["boolean", "number", "string"].includes(typeof value);
|
|
51
|
+
return (value === null || ["boolean", "number", "string"].includes(typeof value));
|
|
51
52
|
}
|
|
52
53
|
function serializeJsonValue(value, indentation, pathSegments, multilineArrays, rootKeyOrder) {
|
|
53
54
|
if (Array.isArray(value)) {
|
|
@@ -81,6 +82,7 @@ const packageJsonRootKeyOrder = [
|
|
|
81
82
|
"files",
|
|
82
83
|
"type",
|
|
83
84
|
"types",
|
|
85
|
+
"imports",
|
|
84
86
|
"exports",
|
|
85
87
|
"scripts",
|
|
86
88
|
"dependencies",
|
|
@@ -88,7 +90,13 @@ const packageJsonRootKeyOrder = [
|
|
|
88
90
|
"peerDependencies",
|
|
89
91
|
"optionalDependencies",
|
|
90
92
|
"engines",
|
|
91
|
-
"packageManager"
|
|
93
|
+
"packageManager",
|
|
94
|
+
];
|
|
95
|
+
const devcontainerJsonRootKeyOrder = [
|
|
96
|
+
"name",
|
|
97
|
+
"build",
|
|
98
|
+
"image",
|
|
99
|
+
"customizations",
|
|
92
100
|
];
|
|
93
101
|
function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
|
|
94
102
|
if (pathSegments.length === 0 && rootKeyOrder) {
|
|
@@ -101,10 +109,13 @@ function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
|
|
|
101
109
|
return left.localeCompare(right);
|
|
102
110
|
}
|
|
103
111
|
function rootKeyOrderForPath(toPath) {
|
|
104
|
-
if (path.basename(toPath)
|
|
105
|
-
return
|
|
112
|
+
if (path.basename(toPath) === "package.json") {
|
|
113
|
+
return new Map(packageJsonRootKeyOrder.map((key, index) => [key, index]));
|
|
114
|
+
}
|
|
115
|
+
if (toPath.split(path.sep).join("/") === ".devcontainer/devcontainer.json") {
|
|
116
|
+
return new Map(devcontainerJsonRootKeyOrder.map((key, index) => [key, index]));
|
|
106
117
|
}
|
|
107
|
-
return
|
|
118
|
+
return undefined;
|
|
108
119
|
}
|
|
109
120
|
function serializeJson(value, multilineArrays = [], rootKeyOrder) {
|
|
110
121
|
return `${serializeJsonValue(value, 0, [], new Set(multilineArrays), rootKeyOrder)}\n`;
|
|
@@ -151,7 +162,7 @@ const foundationTextFiles = new Set([
|
|
|
151
162
|
"README",
|
|
152
163
|
"README.md",
|
|
153
164
|
"LICENSE",
|
|
154
|
-
"CHANGELOG.md"
|
|
165
|
+
"CHANGELOG.md",
|
|
155
166
|
]);
|
|
156
167
|
function assertFoundationTextPath(relativePath) {
|
|
157
168
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
@@ -162,11 +173,20 @@ function assertFoundationTextPath(relativePath) {
|
|
|
162
173
|
if (isRootLevel && relativePath.endsWith(".md")) {
|
|
163
174
|
return;
|
|
164
175
|
}
|
|
165
|
-
if (isRootLevel &&
|
|
176
|
+
if (isRootLevel &&
|
|
177
|
+
(relativePath.endsWith(".yaml") || relativePath.endsWith(".yml"))) {
|
|
166
178
|
return;
|
|
167
179
|
}
|
|
168
180
|
if (isRootLevel &&
|
|
169
|
-
[
|
|
181
|
+
[
|
|
182
|
+
"Cargo.toml",
|
|
183
|
+
"Cargo.lock",
|
|
184
|
+
"rustfmt.toml",
|
|
185
|
+
"rust-toolchain.toml",
|
|
186
|
+
].includes(normalizedPath)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (/^packages\/[A-Za-z0-9._-]+\/(?:Cargo\.toml|Cargo\.lock|rustfmt\.toml)$/.test(normalizedPath)) {
|
|
170
190
|
return;
|
|
171
191
|
}
|
|
172
192
|
if (/^\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml$/.test(normalizedPath)) {
|
|
@@ -175,6 +195,9 @@ function assertFoundationTextPath(relativePath) {
|
|
|
175
195
|
if (/^\.github\/dependabot\.ya?ml$/.test(normalizedPath)) {
|
|
176
196
|
return;
|
|
177
197
|
}
|
|
198
|
+
if (normalizedPath === ".devcontainer/Dockerfile") {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
178
201
|
throw new Error(`Text output is limited to foundation files: ${relativePath}`);
|
|
179
202
|
}
|
|
180
203
|
async function renderWriteText(operation, options) {
|
|
@@ -184,6 +207,56 @@ async function renderWriteText(operation, options) {
|
|
|
184
207
|
await mkdir(path.dirname(to), { recursive: true });
|
|
185
208
|
await writeGeneratedFile(to, operation.text);
|
|
186
209
|
}
|
|
210
|
+
async function renderWriteTextFromFragments(operation, options) {
|
|
211
|
+
const toPath = expandOperationPath(operation.to, options);
|
|
212
|
+
assertFoundationTextPath(toPath);
|
|
213
|
+
const to = resolveContainedPath(options.targetRoot, toPath);
|
|
214
|
+
const texts = await Promise.all(operation.fragments.map(async (fragment) => {
|
|
215
|
+
const sourceRoot = fragment.sourceRoot === undefined
|
|
216
|
+
? options.sourceRoot
|
|
217
|
+
: options.sourceRoots?.[fragment.sourceRoot];
|
|
218
|
+
if (sourceRoot === undefined) {
|
|
219
|
+
throw new Error(`Unknown renderer source root: ${fragment.sourceRoot}`);
|
|
220
|
+
}
|
|
221
|
+
const fromPath = expandTemplatePath(fragment.from, options.variables ?? {});
|
|
222
|
+
const from = resolveContainedPath(sourceRoot, fromPath);
|
|
223
|
+
return readFile(from, "utf8");
|
|
224
|
+
}));
|
|
225
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
226
|
+
await writeGeneratedFile(to, texts.map((text) => text.trimEnd()).join("\n\n") + "\n");
|
|
227
|
+
}
|
|
228
|
+
function replaceTextTemplateVariables(sourceText, replacements) {
|
|
229
|
+
const used = new Set();
|
|
230
|
+
const rendered = sourceText.replaceAll(/\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g, (_placeholder, name) => {
|
|
231
|
+
const replacement = replacements[name];
|
|
232
|
+
if (replacement === undefined) {
|
|
233
|
+
throw new Error(`Missing text template variable: ${name}`);
|
|
234
|
+
}
|
|
235
|
+
used.add(name);
|
|
236
|
+
return replacement;
|
|
237
|
+
});
|
|
238
|
+
for (const name of Object.keys(replacements)) {
|
|
239
|
+
if (!used.has(name)) {
|
|
240
|
+
throw new Error(`Unused text template variable: ${name}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return rendered;
|
|
244
|
+
}
|
|
245
|
+
async function renderWriteTextTemplate(operation, options) {
|
|
246
|
+
const sourceRoot = operation.sourceRoot === undefined
|
|
247
|
+
? options.sourceRoot
|
|
248
|
+
: options.sourceRoots?.[operation.sourceRoot];
|
|
249
|
+
if (sourceRoot === undefined) {
|
|
250
|
+
throw new Error(`Unknown renderer source root: ${operation.sourceRoot}`);
|
|
251
|
+
}
|
|
252
|
+
const toPath = expandOperationPath(operation.to, options);
|
|
253
|
+
assertFoundationTextPath(toPath);
|
|
254
|
+
const from = resolveContainedPath(sourceRoot, expandTemplatePath(operation.from, options.variables ?? {}));
|
|
255
|
+
const to = resolveContainedPath(options.targetRoot, toPath);
|
|
256
|
+
const sourceText = await readFile(from, "utf8");
|
|
257
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
258
|
+
await writeGeneratedFile(to, replaceTextTemplateVariables(sourceText, operation.replacements));
|
|
259
|
+
}
|
|
187
260
|
async function renderSetExecutable(operation, options) {
|
|
188
261
|
const filePath = resolveContainedPath(options.targetRoot, expandOperationPath(operation.path, options));
|
|
189
262
|
const currentMode = (await stat(filePath)).mode;
|
|
@@ -224,7 +297,7 @@ async function findTypeScriptAnchorRanges(sourceText) {
|
|
|
224
297
|
anchors.push({
|
|
225
298
|
name,
|
|
226
299
|
start: comment.pos,
|
|
227
|
-
end: comment.end
|
|
300
|
+
end: comment.end,
|
|
228
301
|
});
|
|
229
302
|
}
|
|
230
303
|
ts.forEachChild(node, collectNodeAnchors);
|
|
@@ -296,7 +369,10 @@ async function copyGeneratedFile(from, to) {
|
|
|
296
369
|
}
|
|
297
370
|
async function writeGeneratedFile(to, text, overwrite = false) {
|
|
298
371
|
try {
|
|
299
|
-
await writeFile(to, text, {
|
|
372
|
+
await writeFile(to, text, {
|
|
373
|
+
encoding: "utf8",
|
|
374
|
+
flag: overwrite ? "w" : "wx",
|
|
375
|
+
});
|
|
300
376
|
}
|
|
301
377
|
catch (error) {
|
|
302
378
|
if (isNodeError(error) && error.code === "EEXIST") {
|
|
@@ -323,6 +399,14 @@ export async function renderProject(options) {
|
|
|
323
399
|
await renderWriteText(operation, options);
|
|
324
400
|
continue;
|
|
325
401
|
}
|
|
402
|
+
if (operation.kind === "writeTextFromFragments") {
|
|
403
|
+
await renderWriteTextFromFragments(operation, options);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (operation.kind === "writeTextTemplate") {
|
|
407
|
+
await renderWriteTextTemplate(operation, options);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
326
410
|
if (operation.kind === "setExecutable") {
|
|
327
411
|
await renderSetExecutable(operation, options);
|
|
328
412
|
continue;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PresetProjectionPlan } from "./preset-projection.js";
|
|
2
|
+
import type { RenderOperation } from "./renderer.js";
|
|
3
|
+
export type TemplateBoundaryDebt = {
|
|
4
|
+
readonly preset: string;
|
|
5
|
+
readonly generatedPath: string;
|
|
6
|
+
readonly owningFunction: string;
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
};
|
|
9
|
+
export type TemplateBoundaryViolation = {
|
|
10
|
+
readonly preset: string;
|
|
11
|
+
readonly generatedPath: string;
|
|
12
|
+
readonly owningFunction: string;
|
|
13
|
+
readonly operationKind: RenderOperation["kind"];
|
|
14
|
+
readonly sourceFilePath: string;
|
|
15
|
+
readonly allowlistReason?: string;
|
|
16
|
+
};
|
|
17
|
+
export type TemplateBoundaryCheckProjection = {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly sourceFilePath: string;
|
|
20
|
+
readonly plan: PresetProjectionPlan;
|
|
21
|
+
};
|
|
22
|
+
export type TemplateBoundaryCheckResult = {
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
readonly violations: readonly TemplateBoundaryViolation[];
|
|
25
|
+
readonly allowlistedDebt: readonly TemplateBoundaryViolation[];
|
|
26
|
+
readonly unusedAllowlistEntries: readonly TemplateBoundaryDebt[];
|
|
27
|
+
};
|
|
28
|
+
export type CheckTemplateSourceBoundaryOptions = {
|
|
29
|
+
readonly projections: readonly TemplateBoundaryCheckProjection[];
|
|
30
|
+
readonly allowlist?: readonly TemplateBoundaryDebt[];
|
|
31
|
+
};
|
|
32
|
+
export declare const templateBoundaryDebtAllowlist: readonly TemplateBoundaryDebt[];
|
|
33
|
+
export declare function isProtectedGeneratedPath(generatedPath: string): boolean;
|
|
34
|
+
export declare function checkTemplateSourceBoundary({ projections, allowlist, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;
|