@ykdz/template 0.0.5 → 0.0.6

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.
@@ -98,6 +98,14 @@ const devcontainerJsonRootKeyOrder = [
98
98
  "image",
99
99
  "customizations",
100
100
  ];
101
+ const tsconfigJsonRootKeyOrder = [
102
+ "extends",
103
+ "compilerOptions",
104
+ "files",
105
+ "references",
106
+ "include",
107
+ "exclude",
108
+ ];
101
109
  function compareJsonKeys(left, right, pathSegments, rootKeyOrder) {
102
110
  if (pathSegments.length === 0 && rootKeyOrder) {
103
111
  const leftOrder = rootKeyOrder.get(left) ?? Number.POSITIVE_INFINITY;
@@ -112,6 +120,9 @@ function rootKeyOrderForPath(toPath) {
112
120
  if (path.basename(toPath) === "package.json") {
113
121
  return new Map(packageJsonRootKeyOrder.map((key, index) => [key, index]));
114
122
  }
123
+ if (/^tsconfig(?:\..*)?\.json$/.test(path.basename(toPath))) {
124
+ return new Map(tsconfigJsonRootKeyOrder.map((key, index) => [key, index]));
125
+ }
115
126
  if (toPath.split(path.sep).join("/") === ".devcontainer/devcontainer.json") {
116
127
  return new Map(devcontainerJsonRootKeyOrder.map((key, index) => [key, index]));
117
128
  }
@@ -200,6 +211,13 @@ function assertFoundationTextPath(relativePath) {
200
211
  }
201
212
  throw new Error(`Text output is limited to foundation files: ${relativePath}`);
202
213
  }
214
+ function assertTextTemplatePath(relativePath) {
215
+ const normalizedPath = relativePath.split(path.sep).join("/");
216
+ if (/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/(?:playwright|vite|vitest|oxfmt|oxlint)\.config\.ts$/.test(normalizedPath)) {
217
+ return;
218
+ }
219
+ assertFoundationTextPath(relativePath);
220
+ }
203
221
  async function renderWriteText(operation, options) {
204
222
  const toPath = expandOperationPath(operation.to, options);
205
223
  assertFoundationTextPath(toPath);
@@ -250,7 +268,7 @@ async function renderWriteTextTemplate(operation, options) {
250
268
  throw new Error(`Unknown renderer source root: ${operation.sourceRoot}`);
251
269
  }
252
270
  const toPath = expandOperationPath(operation.to, options);
253
- assertFoundationTextPath(toPath);
271
+ assertTextTemplatePath(toPath);
254
272
  const from = resolveContainedPath(sourceRoot, expandTemplatePath(operation.from, options.variables ?? {}));
255
273
  const to = resolveContainedPath(options.targetRoot, toPath);
256
274
  const sourceText = await readFile(from, "utf8");
@@ -1,4 +1,3 @@
1
- import type { PresetProjectionPlan } from "./preset-projection.js";
2
1
  import type { RenderOperation } from "./renderer.js";
3
2
  export type TemplateBoundaryDebt = {
4
3
  readonly preset: string;
@@ -17,7 +16,11 @@ export type TemplateBoundaryViolation = {
17
16
  export type TemplateBoundaryCheckProjection = {
18
17
  readonly name: string;
19
18
  readonly sourceFilePath: string;
20
- readonly plan: PresetProjectionPlan;
19
+ readonly plan: {
20
+ readonly sourceRoot: string;
21
+ readonly sourceRoots?: Record<string, string>;
22
+ readonly operations: readonly RenderOperation[];
23
+ };
21
24
  };
22
25
  export type TemplateBoundaryCheckResult = {
23
26
  readonly ok: boolean;
@@ -27,8 +30,9 @@ export type TemplateBoundaryCheckResult = {
27
30
  };
28
31
  export type CheckTemplateSourceBoundaryOptions = {
29
32
  readonly projections: readonly TemplateBoundaryCheckProjection[];
33
+ readonly manifestReferencedSourceFiles?: readonly string[];
30
34
  readonly allowlist?: readonly TemplateBoundaryDebt[];
31
35
  };
32
36
  export declare const templateBoundaryDebtAllowlist: readonly TemplateBoundaryDebt[];
33
37
  export declare function isProtectedGeneratedPath(generatedPath: string): boolean;
34
- export declare function checkTemplateSourceBoundary({ projections, allowlist, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;
38
+ export declare function checkTemplateSourceBoundary({ projections, manifestReferencedSourceFiles, allowlist, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
2
3
  import ts from "typescript";
3
4
  const inlineOperationKinds = new Set([
4
5
  "writeText",
@@ -18,6 +19,14 @@ function isInlineProtectedOperation(operation) {
18
19
  inlineOperationKinds.has(operation.kind) &&
19
20
  isProtectedGeneratedPath(generatedPath));
20
21
  }
22
+ function isProtectedSourceBackedOperation(operation) {
23
+ const generatedPath = operationTarget(operation);
24
+ return (generatedPath !== undefined &&
25
+ (operation.kind === "copyFile" ||
26
+ operation.kind === "writeTextTemplate" ||
27
+ operation.kind === "writeTextFromFragments") &&
28
+ isProtectedGeneratedPath(generatedPath));
29
+ }
21
30
  export function isProtectedGeneratedPath(generatedPath) {
22
31
  const normalizedPath = generatedPath.split("\\").join("/");
23
32
  const fileName = normalizedPath.split("/").at(-1);
@@ -36,20 +45,28 @@ export function isProtectedGeneratedPath(generatedPath) {
36
45
  fileName === "vite.config.ts" ||
37
46
  fileName === "vitest.config.ts");
38
47
  }
39
- function propertyStringValue(objectLiteral, name) {
40
- const initializer = propertyInitializer(objectLiteral, name);
41
- if (!initializer) {
42
- return undefined;
48
+ function expressionStringValue(expression) {
49
+ if (ts.isParenthesizedExpression(expression) ||
50
+ ts.isAsExpression(expression) ||
51
+ ts.isSatisfiesExpression(expression) ||
52
+ ts.isTypeAssertionExpression(expression)) {
53
+ return expressionStringValue(expression.expression);
43
54
  }
44
- if (ts.isStringLiteralLike(initializer)) {
45
- return initializer.text;
55
+ if (ts.isStringLiteralLike(expression)) {
56
+ return expression.text;
46
57
  }
47
- if (ts.isNoSubstitutionTemplateLiteral(initializer) ||
48
- ts.isTemplateExpression(initializer)) {
49
- return initializer.getText();
58
+ if (ts.isNoSubstitutionTemplateLiteral(expression) ||
59
+ ts.isTemplateExpression(expression)) {
60
+ return expression.getText();
50
61
  }
51
62
  return undefined;
52
63
  }
64
+ function propertyStringValue(objectLiteral, name) {
65
+ const initializer = propertyInitializer(objectLiteral, name);
66
+ return initializer === undefined
67
+ ? undefined
68
+ : expressionStringValue(initializer);
69
+ }
53
70
  function propertyInitializer(objectLiteral, name) {
54
71
  const property = objectLiteral.properties.find((candidate) => {
55
72
  return (ts.isPropertyAssignment(candidate) &&
@@ -270,6 +287,30 @@ function findOwningFunctionsForOperation(sourceFile, operation) {
270
287
  }
271
288
  return ["<unknown>"];
272
289
  }
290
+ function findOwningFunctionsForProtectedOperation(sourceFile, operation) {
291
+ const generatedPath = operationTarget(operation);
292
+ if (generatedPath === undefined) {
293
+ return ["<unknown>"];
294
+ }
295
+ const targetPath = generatedPath;
296
+ const owners = new Set();
297
+ function visit(node) {
298
+ if (!ts.isObjectLiteralExpression(node)) {
299
+ ts.forEachChild(node, visit);
300
+ return;
301
+ }
302
+ if (propertyStringValue(node, "kind") === operation.kind &&
303
+ propertyPathMatches(node, "to", targetPath)) {
304
+ owners.add(owningFunctionName(node));
305
+ }
306
+ ts.forEachChild(node, visit);
307
+ }
308
+ visit(sourceFile);
309
+ if (owners.size > 0) {
310
+ return [...owners];
311
+ }
312
+ return ["<unknown>"];
313
+ }
273
314
  function isPropertyAccess(expression, objectName, propertyName) {
274
315
  return (ts.isPropertyAccessExpression(expression) &&
275
316
  ts.isIdentifier(expression.expression) &&
@@ -392,7 +433,49 @@ function isAllowedStructuralMachineDeclaration(sourceFile, operation) {
392
433
  function allowlistKey(preset, generatedPath, owningFunction) {
393
434
  return `${preset}\0${owningFunction}\0${generatedPath}`;
394
435
  }
395
- export async function checkTemplateSourceBoundary({ projections, allowlist = [], }) {
436
+ function resolveOperationSourceRoot(projection, sourceRootName) {
437
+ if (sourceRootName === undefined) {
438
+ return projection.plan.sourceRoot;
439
+ }
440
+ return projection.plan.sourceRoots?.[sourceRootName];
441
+ }
442
+ function sourceBackedOperationSourceFiles(projection, operation) {
443
+ if (operation.kind === "writeTextFromFragments") {
444
+ return operation.fragments.map((fragment) => {
445
+ const root = resolveOperationSourceRoot(projection, fragment.sourceRoot);
446
+ return root === undefined
447
+ ? `missing-source-root:${fragment.sourceRoot}`
448
+ : path.resolve(root, fragment.from);
449
+ });
450
+ }
451
+ const root = resolveOperationSourceRoot(projection, operation.sourceRoot);
452
+ return [
453
+ root === undefined
454
+ ? `missing-source-root:${operation.sourceRoot}`
455
+ : path.resolve(root, operation.from),
456
+ ];
457
+ }
458
+ function operationUsesManifestReferencedSource(projection, operation, manifestReferencedSourceFiles) {
459
+ return sourceBackedOperationSourceFiles(projection, operation).every((sourceFile) => manifestReferencedSourceFiles.has(sourceFile));
460
+ }
461
+ function pushSourceBackedOperationViolations(options) {
462
+ const generatedPath = operationTarget(options.operation);
463
+ if (generatedPath === undefined) {
464
+ return;
465
+ }
466
+ const owningFunctions = findOwningFunctionsForProtectedOperation(options.sourceFile, options.operation);
467
+ for (const owningFunction of owningFunctions) {
468
+ options.violations.push({
469
+ preset: options.projection.name,
470
+ generatedPath,
471
+ owningFunction,
472
+ operationKind: options.operation.kind,
473
+ sourceFilePath: options.projection.sourceFilePath,
474
+ });
475
+ }
476
+ }
477
+ export async function checkTemplateSourceBoundary({ projections, manifestReferencedSourceFiles = [], allowlist = [], }) {
478
+ const manifestReferencedSourceFileSet = new Set(manifestReferencedSourceFiles.map((sourceFile) => path.resolve(sourceFile)));
396
479
  const allowed = new Map(allowlist.map((entry) => [
397
480
  allowlistKey(entry.preset, entry.generatedPath, entry.owningFunction),
398
481
  entry.reason,
@@ -405,6 +488,17 @@ export async function checkTemplateSourceBoundary({ projections, allowlist = [],
405
488
  const sourceText = await readFile(projection.sourceFilePath, "utf8");
406
489
  const sourceFile = ts.createSourceFile(projection.sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
407
490
  for (const operation of projection.plan.operations) {
491
+ if (isProtectedSourceBackedOperation(operation)) {
492
+ if (!operationUsesManifestReferencedSource(projection, operation, manifestReferencedSourceFileSet)) {
493
+ pushSourceBackedOperationViolations({
494
+ projection,
495
+ sourceFile,
496
+ operation,
497
+ violations,
498
+ });
499
+ }
500
+ continue;
501
+ }
408
502
  if (!isInlineProtectedOperation(operation)) {
409
503
  continue;
410
504
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykdz/template",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "private": false,
5
5
  "description": "Project template generator for YKDZ starter projects.",
6
6
  "keywords": [
@@ -53,9 +53,10 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
56
- "check": "pnpm run check:format && pnpm run typecheck && pnpm run test && pnpm run check:templates",
56
+ "check": "pnpm run check:format && pnpm run typecheck && pnpm run test && pnpm run check:generated && pnpm run check:templates",
57
57
  "check:format": "oxfmt --check --config templates/shared/oxc/oxfmt.config.ts --ignore-path .oxfmtignore .",
58
58
  "check:fixtures": "tsx scripts/check-fixtures.ts",
59
+ "check:generated": "tsx scripts/check-generated.ts",
59
60
  "check:templates": "pnpm run check:templates:shared-oxc && pnpm run check:templates:github-yaml && pnpm run check:templates:boundary && pnpm run check:templates:static-source",
60
61
  "check:toolchain:online": "tsx scripts/check-online-toolchain-resolution-contract.ts",
61
62
  "check:templates:boundary": "tsx scripts/check-template-boundary.ts",