@ykdz/template 0.0.3 → 0.0.4
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 +14 -2
- package/dist/src/declarations.d.ts +2 -3
- package/dist/src/declarations.js +46 -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 +10 -0
- package/dist/src/next-step-instructions.d.ts +0 -1
- package/dist/src/next-step-instructions.js +35 -0
- package/dist/src/package-addition.d.ts +1 -1
- package/dist/src/package-addition.js +170 -15
- package/dist/src/preset-projection.d.ts +1 -1
- 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 +95 -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 +230 -69
- package/dist/templates/projection-plans.d.ts +0 -1
- package/dist/templates/registry.d.ts +0 -1
- package/dist/templates/registry.js +2 -2
- package/dist/templates/rust-bin/projection.d.ts +2 -3
- package/dist/templates/rust-bin/projection.js +161 -53
- package/dist/templates/ts-lib/projection.d.ts +2 -3
- package/dist/templates/ts-lib/projection.js +215 -64
- package/dist/templates/vue-app/projection.d.ts +2 -3
- package/dist/templates/vue-app/projection.js +246 -83
- package/dist/templates/vue-hono-app/projection.d.ts +0 -1
- package/dist/templates/vue-hono-app/projection.js +191 -87
- package/package.json +10 -5
- package/pnpm-workspace.yaml +1 -0
- package/templates/hono-api/.github/dependabot.yml +13 -0
- 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/vue-app/.github/dependabot.yml +13 -0
- package/templates/vue-app/.github/workflows/check.yml +1 -1
- package/templates/vue-hono-app/.github/dependabot.yml +13 -0
- 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
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { findBuiltInPresetProjection } from "../templates/registry.js";
|
|
4
4
|
import { validateProjectBlueprint, } from "./declarations.js";
|
|
5
|
+
import { collectGeneratedManifestCatalogReferences, pnpmWorkspaceYamlWithCatalogDependencies, } from "./dependency-catalog.js";
|
|
5
6
|
import { renderProject } from "./renderer.js";
|
|
6
7
|
function projectNameFromBlueprint(blueprint) {
|
|
7
8
|
const firstPackage = blueprint.packages?.[0];
|
|
@@ -16,13 +17,80 @@ function assertSafePackageLeaf(name) {
|
|
|
16
17
|
throw new Error("--name must be a lowercase package leaf name using letters, numbers, and hyphens");
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
const reservedPackagePathCollections = new Set([
|
|
21
|
+
".devcontainer",
|
|
22
|
+
".git",
|
|
23
|
+
".github",
|
|
24
|
+
".template",
|
|
25
|
+
"build",
|
|
26
|
+
"coverage",
|
|
27
|
+
"dist",
|
|
28
|
+
"node_modules",
|
|
29
|
+
"out",
|
|
30
|
+
"target",
|
|
31
|
+
]);
|
|
32
|
+
function assertSafePackagePathSegment(packagePath, segment) {
|
|
33
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(segment)) {
|
|
34
|
+
throw new Error(`Package Path ${packagePath} must use safe path segments with lowercase letters, numbers, and hyphens`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function validateExplicitPackagePath(packagePath) {
|
|
38
|
+
if (path.isAbsolute(packagePath)) {
|
|
39
|
+
throw new Error(`Package Path ${packagePath} must be relative`);
|
|
40
|
+
}
|
|
41
|
+
if (packagePath !== packagePath.trim()) {
|
|
42
|
+
throw new Error(`Package Path ${packagePath} must not contain whitespace`);
|
|
43
|
+
}
|
|
44
|
+
if (packagePath === ".." ||
|
|
45
|
+
packagePath.startsWith("../") ||
|
|
46
|
+
packagePath.endsWith("/..") ||
|
|
47
|
+
packagePath.includes("/../")) {
|
|
48
|
+
throw new Error(`Package Path ${packagePath} must not escape the workspace`);
|
|
49
|
+
}
|
|
50
|
+
const normalized = path.posix.normalize(packagePath);
|
|
51
|
+
if (normalized !== packagePath || packagePath.includes("\\")) {
|
|
52
|
+
throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
|
|
53
|
+
}
|
|
54
|
+
const segments = packagePath.split("/");
|
|
55
|
+
if (segments.length !== 2) {
|
|
56
|
+
throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
|
|
57
|
+
}
|
|
58
|
+
const [collection, packageDirectory] = segments;
|
|
59
|
+
if (!collection || !packageDirectory) {
|
|
60
|
+
throw new Error(`Package Path ${packagePath} must be exactly two safe path segments`);
|
|
61
|
+
}
|
|
62
|
+
if (reservedPackagePathCollections.has(collection)) {
|
|
63
|
+
throw new Error(`Package Path ${packagePath} uses reserved collection ${collection}`);
|
|
64
|
+
}
|
|
65
|
+
assertSafePackagePathSegment(packagePath, collection);
|
|
66
|
+
assertSafePackagePathSegment(packagePath, packageDirectory);
|
|
67
|
+
if (reservedPackagePathCollections.has(packageDirectory)) {
|
|
68
|
+
throw new Error(`Package Path ${packagePath} uses reserved package directory ${packageDirectory}`);
|
|
69
|
+
}
|
|
70
|
+
return packagePath;
|
|
71
|
+
}
|
|
72
|
+
function defaultPackagePathForPreset(preset, packageLeafName) {
|
|
73
|
+
return preset === "ts-lib"
|
|
74
|
+
? `packages/${packageLeafName}`
|
|
75
|
+
: `apps/${packageLeafName}`;
|
|
76
|
+
}
|
|
77
|
+
function assertNoPackageConflict(blueprint, packageName, packagePath) {
|
|
78
|
+
const existingByName = blueprint.packages?.find((pkg) => pkg.name === packageName);
|
|
79
|
+
if (existingByName) {
|
|
80
|
+
throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByName.name} at ${existingByName.path}`);
|
|
81
|
+
}
|
|
82
|
+
const existingByPath = blueprint.packages?.find((pkg) => pkg.path === packagePath);
|
|
83
|
+
if (existingByPath) {
|
|
84
|
+
throw new Error(`Package Path ${packagePath} conflicts with existing package ${existingByPath.name} at ${existingByPath.path}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
19
87
|
async function readJson(filePath) {
|
|
20
88
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
21
89
|
}
|
|
22
90
|
async function writeJson(filePath, value) {
|
|
23
91
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
24
92
|
}
|
|
25
|
-
async function
|
|
93
|
+
async function assertMissingPackagePath(packagePath, targetPath) {
|
|
26
94
|
try {
|
|
27
95
|
await stat(targetPath);
|
|
28
96
|
}
|
|
@@ -32,7 +100,7 @@ async function assertMissing(targetPath) {
|
|
|
32
100
|
}
|
|
33
101
|
throw error;
|
|
34
102
|
}
|
|
35
|
-
throw new Error(`Package
|
|
103
|
+
throw new Error(`Package Path ${packagePath} conflicts with existing filesystem path ${targetPath}`);
|
|
36
104
|
}
|
|
37
105
|
function resolveGeneratedPath(root, relativePath) {
|
|
38
106
|
if (path.isAbsolute(relativePath)) {
|
|
@@ -53,11 +121,39 @@ function isNodeError(error) {
|
|
|
53
121
|
function isRecord(value) {
|
|
54
122
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
123
|
}
|
|
124
|
+
function assertRootPackageJson(input) {
|
|
125
|
+
if (!isRecord(input)) {
|
|
126
|
+
throw new Error("Cannot update root Package Addition scripts: package.json must be an object");
|
|
127
|
+
}
|
|
128
|
+
if (!isRecord(input.scripts)) {
|
|
129
|
+
throw new Error("Cannot update root Package Addition scripts: scripts must be an object");
|
|
130
|
+
}
|
|
131
|
+
for (const scriptName of ["check", "fix"]) {
|
|
132
|
+
if (typeof input.scripts[scriptName] !== "string") {
|
|
133
|
+
throw new Error(`Cannot update root Package Addition scripts: scripts.${scriptName} must be a string`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
56
137
|
async function readGeneratedWorkspaceBlueprint(root) {
|
|
57
|
-
const blueprintPath = path.join(root, ".
|
|
58
|
-
|
|
138
|
+
const blueprintPath = path.join(root, ".template/blueprint.json");
|
|
139
|
+
let blueprintJson;
|
|
140
|
+
try {
|
|
141
|
+
blueprintJson = await readJson(blueprintPath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof SyntaxError ||
|
|
145
|
+
(isNodeError(error) && error.code === "ENOENT")) {
|
|
146
|
+
throw new Error("Package Addition requires a valid .template/blueprint.json");
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
const result = validateProjectBlueprint(blueprintJson);
|
|
59
151
|
if (!result.ok) {
|
|
60
|
-
|
|
152
|
+
if (isRecord(blueprintJson) &&
|
|
153
|
+
blueprintJson.projectKind === "single-package") {
|
|
154
|
+
throw new Error("Package Addition only supports existing workspace Generated Repositories");
|
|
155
|
+
}
|
|
156
|
+
throw new Error("Package Addition requires a valid .template/blueprint.json");
|
|
61
157
|
}
|
|
62
158
|
const blueprint = result.value;
|
|
63
159
|
if (blueprint.projectKind !== "multi-package") {
|
|
@@ -122,6 +218,20 @@ function rootTsconfigWithReferences(input, referencePaths) {
|
|
|
122
218
|
}
|
|
123
219
|
return { ...tsconfig, references };
|
|
124
220
|
}
|
|
221
|
+
function packageAdditionCatalogDependencies(operations) {
|
|
222
|
+
const manifests = [];
|
|
223
|
+
for (const operation of operations) {
|
|
224
|
+
if (operation.kind !== "writeJson" ||
|
|
225
|
+
path.basename(operation.to) !== "package.json") {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!isRecord(operation.value)) {
|
|
229
|
+
throw new Error(`Package Addition package manifest must be an object: ${operation.to}`);
|
|
230
|
+
}
|
|
231
|
+
manifests.push(operation.value);
|
|
232
|
+
}
|
|
233
|
+
return collectGeneratedManifestCatalogReferences(manifests);
|
|
234
|
+
}
|
|
125
235
|
function blueprintWithPackage(blueprint, packageName, packagePath) {
|
|
126
236
|
const nextBlueprint = {
|
|
127
237
|
...blueprint,
|
|
@@ -132,15 +242,57 @@ function blueprintWithPackage(blueprint, packageName, packagePath) {
|
|
|
132
242
|
};
|
|
133
243
|
const result = validateProjectBlueprint(nextBlueprint);
|
|
134
244
|
if (!result.ok) {
|
|
135
|
-
throw new Error("Package Addition would write an invalid .
|
|
245
|
+
throw new Error("Package Addition would write an invalid .template/blueprint.json");
|
|
136
246
|
}
|
|
137
247
|
return result.value;
|
|
138
248
|
}
|
|
139
|
-
|
|
140
|
-
const
|
|
249
|
+
function workspacePackageGlobsFromBlueprint(blueprint) {
|
|
250
|
+
const globs = [];
|
|
251
|
+
for (const packageDefinition of blueprint.packages ?? []) {
|
|
252
|
+
const [workspaceDir] = packageDefinition.path.split("/");
|
|
253
|
+
if (!workspaceDir) {
|
|
254
|
+
throw new Error(`Cannot update root Package Addition scripts: invalid package path ${packageDefinition.path}`);
|
|
255
|
+
}
|
|
256
|
+
const glob = `${workspaceDir}/*`;
|
|
257
|
+
if (!globs.includes(glob)) {
|
|
258
|
+
globs.push(glob);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return globs;
|
|
262
|
+
}
|
|
263
|
+
function turboPackageTaskCommand(task, workspacePackageGlobs) {
|
|
264
|
+
const filters = workspacePackageGlobs.map((glob) => `--filter './${glob}'`);
|
|
265
|
+
return [`turbo run ${task}`, ...filters].join(" ");
|
|
266
|
+
}
|
|
267
|
+
function rootScriptWithTurboPackageTask(script, task, workspacePackageGlobs) {
|
|
268
|
+
const commands = script.split(" && ");
|
|
269
|
+
const turboCommandIndex = commands.findIndex((command) => command.startsWith(`turbo run ${task}`));
|
|
270
|
+
if (turboCommandIndex === -1) {
|
|
271
|
+
throw new Error(`Cannot update root Package Addition scripts: scripts.${task} must run Turbo package tasks`);
|
|
272
|
+
}
|
|
273
|
+
commands[turboCommandIndex] = turboPackageTaskCommand(task, workspacePackageGlobs);
|
|
274
|
+
return commands.join(" && ");
|
|
275
|
+
}
|
|
276
|
+
function rootPackageJsonWithPackageTaskFilters(input, blueprint) {
|
|
277
|
+
assertRootPackageJson(input);
|
|
278
|
+
const workspacePackageGlobs = workspacePackageGlobsFromBlueprint(blueprint);
|
|
279
|
+
return {
|
|
280
|
+
...input,
|
|
281
|
+
scripts: {
|
|
282
|
+
...input.scripts,
|
|
283
|
+
check: rootScriptWithTurboPackageTask(input.scripts.check, "check", workspacePackageGlobs),
|
|
284
|
+
fix: rootScriptWithTurboPackageTask(input.scripts.fix, "fix", workspacePackageGlobs),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function planRootUpdates(root, blueprint, packageName, packagePath, workspacePackageGlob, rootTsconfigReferences, catalogDependencies) {
|
|
289
|
+
const nextBlueprint = blueprintWithPackage(blueprint, packageName, packagePath);
|
|
290
|
+
const workspaceText = pnpmWorkspaceYamlWithCatalogDependencies(workspaceTextWithPackageGlob(await readFile(path.join(root, "pnpm-workspace.yaml"), "utf8"), workspacePackageGlob), catalogDependencies);
|
|
141
291
|
const rootTsconfig = rootTsconfigWithReferences(await readJson(path.join(root, "tsconfig.json")), rootTsconfigReferences);
|
|
292
|
+
const rootPackageJson = rootPackageJsonWithPackageTaskFilters(await readJson(path.join(root, "package.json")), nextBlueprint);
|
|
142
293
|
return {
|
|
143
|
-
blueprint:
|
|
294
|
+
blueprint: nextBlueprint,
|
|
295
|
+
rootPackageJson,
|
|
144
296
|
rootTsconfig,
|
|
145
297
|
workspaceText,
|
|
146
298
|
};
|
|
@@ -152,6 +304,9 @@ export async function addPackage(options) {
|
|
|
152
304
|
const nodeVersion = await readGeneratedRepositoryNodeVersion(root);
|
|
153
305
|
const projectName = projectNameFromBlueprint(blueprint);
|
|
154
306
|
const packageName = `@${projectName}/${options.name}`;
|
|
307
|
+
const packagePath = options.path
|
|
308
|
+
? validateExplicitPackagePath(options.path)
|
|
309
|
+
: defaultPackagePathForPreset(options.preset, options.name);
|
|
155
310
|
const projection = findBuiltInPresetProjection(options.preset);
|
|
156
311
|
const packageAddition = projection?.capabilities?.packageAddition;
|
|
157
312
|
if (!projection) {
|
|
@@ -165,13 +320,12 @@ export async function addPackage(options) {
|
|
|
165
320
|
blueprint,
|
|
166
321
|
packageLeafName: options.name,
|
|
167
322
|
packageName,
|
|
323
|
+
packagePath,
|
|
168
324
|
nodeVersion,
|
|
169
325
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
await assertMissing(path.join(root, additionPlan.packagePath));
|
|
174
|
-
const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.workspacePackageGlob, additionPlan.rootTsconfigReferences);
|
|
326
|
+
assertNoPackageConflict(blueprint, packageName, additionPlan.packagePath);
|
|
327
|
+
await assertMissingPackagePath(additionPlan.packagePath, path.join(root, additionPlan.packagePath));
|
|
328
|
+
const rootUpdatePlan = await planRootUpdates(root, blueprint, packageName, additionPlan.packagePath, additionPlan.workspacePackageGlob, additionPlan.rootTsconfigReferences, packageAdditionCatalogDependencies(additionPlan.operations));
|
|
175
329
|
await mkdir(path.join(root, additionPlan.packagePath), { recursive: true });
|
|
176
330
|
await renderProject({
|
|
177
331
|
sourceRoot: additionPlan.sourceRoot,
|
|
@@ -183,6 +337,7 @@ export async function addPackage(options) {
|
|
|
183
337
|
await writeFile(resolveGeneratedPath(root, textFile.path), textFile.text, "utf8");
|
|
184
338
|
}
|
|
185
339
|
await writeFile(path.join(root, "pnpm-workspace.yaml"), rootUpdatePlan.workspaceText, "utf8");
|
|
340
|
+
await writeJson(path.join(root, "package.json"), rootUpdatePlan.rootPackageJson);
|
|
186
341
|
await writeJson(path.join(root, "tsconfig.json"), rootUpdatePlan.rootTsconfig);
|
|
187
|
-
await writeJson(path.join(root, ".
|
|
342
|
+
await writeJson(path.join(root, ".template/blueprint.json"), rootUpdatePlan.blueprint);
|
|
188
343
|
}
|
|
@@ -9,6 +9,7 @@ export type PresetPackageAdditionOptions = {
|
|
|
9
9
|
readonly blueprint: ProjectBlueprint;
|
|
10
10
|
readonly packageLeafName: string;
|
|
11
11
|
readonly packageName: string;
|
|
12
|
+
readonly packagePath: string;
|
|
12
13
|
readonly nodeVersion: string;
|
|
13
14
|
};
|
|
14
15
|
export type PresetPackageAdditionPlan = {
|
|
@@ -63,4 +64,3 @@ export declare function planNextStepInstructionsForProjection(options: {
|
|
|
63
64
|
readonly targetDir: string;
|
|
64
65
|
readonly plan: PresetProjectionPlan;
|
|
65
66
|
}): 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)) {
|
|
@@ -88,7 +89,13 @@ const packageJsonRootKeyOrder = [
|
|
|
88
89
|
"peerDependencies",
|
|
89
90
|
"optionalDependencies",
|
|
90
91
|
"engines",
|
|
91
|
-
"packageManager"
|
|
92
|
+
"packageManager",
|
|
93
|
+
];
|
|
94
|
+
const devcontainerJsonRootKeyOrder = [
|
|
95
|
+
"name",
|
|
96
|
+
"build",
|
|
97
|
+
"image",
|
|
98
|
+
"customizations",
|
|
92
99
|
];
|
|
93
100
|
function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
|
|
94
101
|
if (pathSegments.length === 0 && rootKeyOrder) {
|
|
@@ -101,10 +108,13 @@ function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
|
|
|
101
108
|
return left.localeCompare(right);
|
|
102
109
|
}
|
|
103
110
|
function rootKeyOrderForPath(toPath) {
|
|
104
|
-
if (path.basename(toPath)
|
|
105
|
-
return
|
|
111
|
+
if (path.basename(toPath) === "package.json") {
|
|
112
|
+
return new Map(packageJsonRootKeyOrder.map((key, index) => [key, index]));
|
|
113
|
+
}
|
|
114
|
+
if (toPath.split(path.sep).join("/") === ".devcontainer/devcontainer.json") {
|
|
115
|
+
return new Map(devcontainerJsonRootKeyOrder.map((key, index) => [key, index]));
|
|
106
116
|
}
|
|
107
|
-
return
|
|
117
|
+
return undefined;
|
|
108
118
|
}
|
|
109
119
|
function serializeJson(value, multilineArrays = [], rootKeyOrder) {
|
|
110
120
|
return `${serializeJsonValue(value, 0, [], new Set(multilineArrays), rootKeyOrder)}\n`;
|
|
@@ -151,7 +161,7 @@ const foundationTextFiles = new Set([
|
|
|
151
161
|
"README",
|
|
152
162
|
"README.md",
|
|
153
163
|
"LICENSE",
|
|
154
|
-
"CHANGELOG.md"
|
|
164
|
+
"CHANGELOG.md",
|
|
155
165
|
]);
|
|
156
166
|
function assertFoundationTextPath(relativePath) {
|
|
157
167
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
@@ -162,11 +172,20 @@ function assertFoundationTextPath(relativePath) {
|
|
|
162
172
|
if (isRootLevel && relativePath.endsWith(".md")) {
|
|
163
173
|
return;
|
|
164
174
|
}
|
|
165
|
-
if (isRootLevel &&
|
|
175
|
+
if (isRootLevel &&
|
|
176
|
+
(relativePath.endsWith(".yaml") || relativePath.endsWith(".yml"))) {
|
|
166
177
|
return;
|
|
167
178
|
}
|
|
168
179
|
if (isRootLevel &&
|
|
169
|
-
[
|
|
180
|
+
[
|
|
181
|
+
"Cargo.toml",
|
|
182
|
+
"Cargo.lock",
|
|
183
|
+
"rustfmt.toml",
|
|
184
|
+
"rust-toolchain.toml",
|
|
185
|
+
].includes(normalizedPath)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (/^packages\/[A-Za-z0-9._-]+\/(?:Cargo\.toml|Cargo\.lock|rustfmt\.toml)$/.test(normalizedPath)) {
|
|
170
189
|
return;
|
|
171
190
|
}
|
|
172
191
|
if (/^\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml$/.test(normalizedPath)) {
|
|
@@ -175,6 +194,9 @@ function assertFoundationTextPath(relativePath) {
|
|
|
175
194
|
if (/^\.github\/dependabot\.ya?ml$/.test(normalizedPath)) {
|
|
176
195
|
return;
|
|
177
196
|
}
|
|
197
|
+
if (normalizedPath === ".devcontainer/Dockerfile") {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
178
200
|
throw new Error(`Text output is limited to foundation files: ${relativePath}`);
|
|
179
201
|
}
|
|
180
202
|
async function renderWriteText(operation, options) {
|
|
@@ -184,6 +206,56 @@ async function renderWriteText(operation, options) {
|
|
|
184
206
|
await mkdir(path.dirname(to), { recursive: true });
|
|
185
207
|
await writeGeneratedFile(to, operation.text);
|
|
186
208
|
}
|
|
209
|
+
async function renderWriteTextFromFragments(operation, options) {
|
|
210
|
+
const toPath = expandOperationPath(operation.to, options);
|
|
211
|
+
assertFoundationTextPath(toPath);
|
|
212
|
+
const to = resolveContainedPath(options.targetRoot, toPath);
|
|
213
|
+
const texts = await Promise.all(operation.fragments.map(async (fragment) => {
|
|
214
|
+
const sourceRoot = fragment.sourceRoot === undefined
|
|
215
|
+
? options.sourceRoot
|
|
216
|
+
: options.sourceRoots?.[fragment.sourceRoot];
|
|
217
|
+
if (sourceRoot === undefined) {
|
|
218
|
+
throw new Error(`Unknown renderer source root: ${fragment.sourceRoot}`);
|
|
219
|
+
}
|
|
220
|
+
const fromPath = expandTemplatePath(fragment.from, options.variables ?? {});
|
|
221
|
+
const from = resolveContainedPath(sourceRoot, fromPath);
|
|
222
|
+
return readFile(from, "utf8");
|
|
223
|
+
}));
|
|
224
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
225
|
+
await writeGeneratedFile(to, texts.map((text) => text.trimEnd()).join("\n\n") + "\n");
|
|
226
|
+
}
|
|
227
|
+
function replaceTextTemplateVariables(sourceText, replacements) {
|
|
228
|
+
const used = new Set();
|
|
229
|
+
const rendered = sourceText.replaceAll(/\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g, (_placeholder, name) => {
|
|
230
|
+
const replacement = replacements[name];
|
|
231
|
+
if (replacement === undefined) {
|
|
232
|
+
throw new Error(`Missing text template variable: ${name}`);
|
|
233
|
+
}
|
|
234
|
+
used.add(name);
|
|
235
|
+
return replacement;
|
|
236
|
+
});
|
|
237
|
+
for (const name of Object.keys(replacements)) {
|
|
238
|
+
if (!used.has(name)) {
|
|
239
|
+
throw new Error(`Unused text template variable: ${name}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return rendered;
|
|
243
|
+
}
|
|
244
|
+
async function renderWriteTextTemplate(operation, options) {
|
|
245
|
+
const sourceRoot = operation.sourceRoot === undefined
|
|
246
|
+
? options.sourceRoot
|
|
247
|
+
: options.sourceRoots?.[operation.sourceRoot];
|
|
248
|
+
if (sourceRoot === undefined) {
|
|
249
|
+
throw new Error(`Unknown renderer source root: ${operation.sourceRoot}`);
|
|
250
|
+
}
|
|
251
|
+
const toPath = expandOperationPath(operation.to, options);
|
|
252
|
+
assertFoundationTextPath(toPath);
|
|
253
|
+
const from = resolveContainedPath(sourceRoot, expandTemplatePath(operation.from, options.variables ?? {}));
|
|
254
|
+
const to = resolveContainedPath(options.targetRoot, toPath);
|
|
255
|
+
const sourceText = await readFile(from, "utf8");
|
|
256
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
257
|
+
await writeGeneratedFile(to, replaceTextTemplateVariables(sourceText, operation.replacements));
|
|
258
|
+
}
|
|
187
259
|
async function renderSetExecutable(operation, options) {
|
|
188
260
|
const filePath = resolveContainedPath(options.targetRoot, expandOperationPath(operation.path, options));
|
|
189
261
|
const currentMode = (await stat(filePath)).mode;
|
|
@@ -224,7 +296,7 @@ async function findTypeScriptAnchorRanges(sourceText) {
|
|
|
224
296
|
anchors.push({
|
|
225
297
|
name,
|
|
226
298
|
start: comment.pos,
|
|
227
|
-
end: comment.end
|
|
299
|
+
end: comment.end,
|
|
228
300
|
});
|
|
229
301
|
}
|
|
230
302
|
ts.forEachChild(node, collectNodeAnchors);
|
|
@@ -296,7 +368,10 @@ async function copyGeneratedFile(from, to) {
|
|
|
296
368
|
}
|
|
297
369
|
async function writeGeneratedFile(to, text, overwrite = false) {
|
|
298
370
|
try {
|
|
299
|
-
await writeFile(to, text, {
|
|
371
|
+
await writeFile(to, text, {
|
|
372
|
+
encoding: "utf8",
|
|
373
|
+
flag: overwrite ? "w" : "wx",
|
|
374
|
+
});
|
|
300
375
|
}
|
|
301
376
|
catch (error) {
|
|
302
377
|
if (isNodeError(error) && error.code === "EEXIST") {
|
|
@@ -323,6 +398,14 @@ export async function renderProject(options) {
|
|
|
323
398
|
await renderWriteText(operation, options);
|
|
324
399
|
continue;
|
|
325
400
|
}
|
|
401
|
+
if (operation.kind === "writeTextFromFragments") {
|
|
402
|
+
await renderWriteTextFromFragments(operation, options);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (operation.kind === "writeTextTemplate") {
|
|
406
|
+
await renderWriteTextTemplate(operation, options);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
326
409
|
if (operation.kind === "setExecutable") {
|
|
327
410
|
await renderSetExecutable(operation, options);
|
|
328
411
|
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>;
|