@ykdz/template 0.0.32 → 0.0.35

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.
Files changed (40) hide show
  1. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.d.ts +2 -0
  2. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +40 -14
  3. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +1 -0
  4. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +17 -0
  5. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +1 -1
  6. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +9 -10
  7. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +6 -1
  8. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +6 -1
  9. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment-diagnostics.yml +81 -0
  10. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment.yml +55 -0
  11. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-diagnostics.yml +64 -0
  12. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check.yml +24 -5
  13. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
  14. package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/github-cli.Dockerfile +9 -0
  15. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/src/readiness.ts +1 -1
  16. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/global-setup.ts +21 -0
  17. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +6 -1
  18. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/vitest.config.ts +7 -0
  19. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/drizzle.config.ts +5 -1
  20. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +103 -22
  21. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/playwright-teardown.ts +12 -0
  22. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vite.config.ts +39 -1
  23. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/playwright.config.ts +50 -8
  24. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/playwright.config.ts +61 -8
  25. package/node_modules/@ykdz/template-core/dist/ci-diagnostic-artifact.d.ts +19 -0
  26. package/node_modules/@ykdz/template-core/dist/ci-diagnostic-artifact.js +68 -0
  27. package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +3 -0
  28. package/node_modules/@ykdz/template-core/dist/package-contribution.js +7 -0
  29. package/node_modules/@ykdz/template-core/dist/project-github.d.ts +50 -6
  30. package/node_modules/@ykdz/template-core/dist/project-github.js +83 -47
  31. package/node_modules/@ykdz/template-core/dist/template-boundary-check.d.ts +15 -1
  32. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +162 -1
  33. package/package.json +1 -1
  34. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-playwright.yml +0 -20
  35. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-rust.yml +0 -23
  36. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check.dynamic.template +0 -19
  37. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/scripts/prepare-database.ts +0 -45
  38. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/run-playwright.ts +0 -151
  39. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/scripts/run-playwright.ts +0 -62
  40. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/scripts/run-playwright.ts +0 -103
@@ -1,73 +1,109 @@
1
+ import { composeCiDiagnosticArtifacts, } from "./ci-diagnostic-artifact.js";
2
+ function validatedDiagnosticArtifacts(options) {
3
+ return composeCiDiagnosticArtifacts({
4
+ packagePaths: options.packagePaths,
5
+ declarations: options.artifacts,
6
+ });
7
+ }
1
8
  const defaultCiCapability = {
2
9
  workflowName: "Check",
3
10
  jobName: "check",
11
+ jobDisplayName: "Root Check",
4
12
  runner: "ubuntu-latest",
13
+ timeoutMinutes: 30,
5
14
  };
6
15
  const pnpmTaskLayer = {
7
- installCommand: "pnpm install",
16
+ installCommand: "pnpm install --frozen-lockfile",
8
17
  checkCommand: "pnpm run check",
9
18
  };
10
- export function projectCheckWorkflow(options) {
19
+ /** Capability facts for the Foundation-owned GitHub Actions Template Source. */
20
+ export function projectCheckWorkflowPlan(options) {
11
21
  const capability = options.capability ?? defaultCiCapability;
12
22
  const environmentPreparation = {
13
23
  nodeFromPackageMetadata: true,
24
+ packageManagerFromPackageMetadata: true,
25
+ pnpmStoreCache: true,
14
26
  ...options.environmentPreparation,
15
27
  };
16
28
  const taskLayer = options.taskLayer ?? pnpmTaskLayer;
17
29
  const hasDeploymentTask = options.hasDeploymentTask === true;
18
30
  const needsDocker = (options.deploymentEnvironmentNeeds ?? []).some((need) => need.kind === "docker-engine");
19
- const lines = [
20
- `name: ${capability.workflowName}`,
21
- "",
22
- "on:",
23
- " pull_request:",
24
- " push:",
25
- " branches:",
26
- " - main",
27
- "",
28
- "jobs:",
29
- ` ${capability.jobName}:`,
30
- ` runs-on: ${capability.runner}`,
31
- ...(hasDeploymentTask
32
- ? [" strategy:", " matrix:", " check: [root, deployment]"]
33
- : []),
34
- " steps:",
35
- " - uses: actions/checkout@v7",
36
- ];
37
- if (environmentPreparation.nodeFromPackageMetadata) {
38
- lines.push(" - uses: actions/setup-node@v7", " with:", " node-version-file: package.json", " - run: corepack enable");
39
- }
40
- if (hasDeploymentTask && needsDocker) {
41
- lines.push(" - uses: docker/setup-buildx-action@v3", " if: matrix.check == 'deployment'");
31
+ if (hasDeploymentTask && !needsDocker) {
32
+ throw new Error("Deployment Check requires a Docker environment need");
42
33
  }
43
- lines.push(` - run: ${taskLayer.installCommand}`);
44
- lines.push(` - run: ${taskLayer.checkCommand}`);
45
- if (hasDeploymentTask) {
46
- lines.push(" if: matrix.check == 'root'");
34
+ const packagePaths = [...options.packagePaths];
35
+ const diagnosticArtifacts = validatedDiagnosticArtifacts({
36
+ packagePaths,
37
+ artifacts: options.diagnosticArtifacts ?? [],
38
+ });
39
+ return {
40
+ workflowName: capability.workflowName,
41
+ triggers: { pullRequest: true, pushBranches: ["main"] },
42
+ permissions: { contents: "read" },
43
+ concurrency: {
44
+ group: "${{ github.workflow }}-${{ github.ref }}",
45
+ cancelInProgress: true,
46
+ },
47
+ rootCheck: capability,
48
+ matrix: hasDeploymentTask
49
+ ? {
50
+ failFast: false,
51
+ include: [
52
+ {
53
+ capability: "root",
54
+ jobDisplayName: capability.jobDisplayName,
55
+ taskEntrypoint: taskLayer.checkCommand,
56
+ timeoutMinutes: capability.timeoutMinutes,
57
+ requiresDocker: false,
58
+ },
59
+ {
60
+ capability: "deployment",
61
+ jobDisplayName: "Deployment Check",
62
+ taskEntrypoint: "pnpm run check:deployment",
63
+ timeoutMinutes: 45,
64
+ requiresDocker: true,
65
+ },
66
+ ],
67
+ }
68
+ : undefined,
69
+ environmentPreparation,
70
+ taskLayer,
71
+ packagePaths,
72
+ diagnosticArtifacts,
73
+ };
74
+ }
75
+ /** Selects a complete Foundation-owned workflow Template Source. */
76
+ export function projectCheckWorkflowTemplateSource(options) {
77
+ const hasDeploymentTask = options.hasDeploymentTask === true;
78
+ const needsDocker = (options.deploymentEnvironmentNeeds ?? []).some((need) => need.kind === "docker-engine");
79
+ if (hasDeploymentTask && !needsDocker) {
80
+ throw new Error("Deployment Check requires a Docker environment need");
47
81
  }
82
+ const hasDiagnostics = validatedDiagnosticArtifacts({
83
+ packagePaths: options.packagePaths,
84
+ artifacts: options.diagnosticArtifacts ?? [],
85
+ }).length > 0;
48
86
  if (hasDeploymentTask) {
49
- lines.push(" - run: pnpm run check:deployment", " if: matrix.check == 'deployment'");
87
+ return hasDiagnostics
88
+ ? ".github/workflows/check-deployment-diagnostics.yml"
89
+ : ".github/workflows/check-deployment.yml";
50
90
  }
51
- lines.push("");
52
- return lines.join("\n");
91
+ return hasDiagnostics
92
+ ? ".github/workflows/check-diagnostics.yml"
93
+ : ".github/workflows/check.yml";
53
94
  }
54
- /** Limited substitutions for the Foundation-owned workflow Template Source. */
95
+ /** Limited, validated facts for static Foundation diagnostic staging. */
55
96
  export function projectCheckWorkflowTemplateReplacements(options) {
56
- const hasDeploymentTask = options.hasDeploymentTask === true;
57
- const needsDocker = (options.deploymentEnvironmentNeeds ?? []).some((need) => need.kind === "docker-engine");
97
+ const declarations = validatedDiagnosticArtifacts({
98
+ packagePaths: options.packagePaths,
99
+ artifacts: options.diagnosticArtifacts ?? [],
100
+ });
101
+ if (declarations.length === 0)
102
+ return {};
58
103
  return {
59
- DEPLOYMENT_MATRIX: hasDeploymentTask
60
- ? "\n strategy:\n matrix:\n check: [root, deployment]"
61
- : "",
62
- DEPLOYMENT_DOCKER_PREPARATION: hasDeploymentTask && needsDocker
63
- ? "\n - uses: docker/setup-buildx-action@v3\n if: matrix.check == 'deployment'"
64
- : "",
65
- ROOT_CHECK_CONDITION: hasDeploymentTask
66
- ? "\n if: matrix.check == 'root'"
67
- : "",
68
- DEPLOYMENT_CHECK: hasDeploymentTask
69
- ? "\n - run: pnpm run check:deployment\n if: matrix.check == 'deployment'"
70
- : "",
104
+ DIAGNOSTIC_OWNER_PATHS: declarations
105
+ .map((declaration) => declaration.owner.path)
106
+ .join("\n "),
71
107
  };
72
108
  }
73
109
  export function projectDependabotConfig(policy) {
@@ -1,3 +1,4 @@
1
+ import type { ProjectBlueprintV2 } from "./project-blueprint-v2.ts";
1
2
  import { type RenderOperation } from "./renderer.ts";
2
3
  export type TemplateBoundaryViolation = {
3
4
  readonly preset: string;
@@ -34,6 +35,19 @@ export type CheckTemplateSourceBoundaryOptions = {
34
35
  readonly projections: readonly TemplateBoundaryCheckProjection[];
35
36
  readonly templateSourceContexts?: readonly TemplateSourceContext[];
36
37
  readonly manifestReferencedSourceFiles?: readonly string[];
38
+ /** Complete Built-in plans whose Foundation workflow is a protected output. */
39
+ readonly protectedWorkflowPlans?: readonly {
40
+ readonly name: string;
41
+ readonly sourceFilePath: string;
42
+ readonly definitionName?: string;
43
+ readonly planningContribution?: string;
44
+ readonly generatedPath: ".github/workflows/check.yml";
45
+ /** The original Blueprint, before Foundation composes workflow facts. */
46
+ readonly blueprint: ProjectBlueprintV2;
47
+ /** Raw Package Contribution declarations, not the composed plan field. */
48
+ readonly diagnosticArtifactDeclarations: readonly unknown[];
49
+ readonly operations: readonly RenderOperation[];
50
+ }[];
37
51
  };
38
52
  /**
39
53
  * Reads each registered Template Source context before any plan is inspected.
@@ -41,4 +55,4 @@ export type CheckTemplateSourceBoundaryOptions = {
41
55
  */
42
56
  export declare function checkTemplateSourceContexts(contexts: readonly Omit<TemplateSourceContext, "checkedFiles">[]): Promise<readonly TemplateSourceContext[]>;
43
57
  export declare function isProtectedGeneratedPath(generatedPath: string): boolean;
44
- export declare function checkTemplateSourceBoundary({ projections, templateSourceContexts, manifestReferencedSourceFiles, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;
58
+ export declare function checkTemplateSourceBoundary({ projections, templateSourceContexts, manifestReferencedSourceFiles, protectedWorkflowPlans, }: CheckTemplateSourceBoundaryOptions): Promise<TemplateBoundaryCheckResult>;
@@ -589,9 +589,170 @@ function pushSourceBackedOperationViolations(options) {
589
589
  });
590
590
  }
591
591
  }
592
- export async function checkTemplateSourceBoundary({ projections, templateSourceContexts = [], manifestReferencedSourceFiles = [], }) {
592
+ const workflowTemplateVariants = new Map([
593
+ [
594
+ ".github/workflows/check.yml",
595
+ { operationKind: "copyFile", replacementKeys: [] },
596
+ ],
597
+ [
598
+ ".github/workflows/check-deployment.yml",
599
+ { operationKind: "copyFile", replacementKeys: [] },
600
+ ],
601
+ [
602
+ ".github/workflows/check-diagnostics.yml",
603
+ {
604
+ operationKind: "writeTextTemplate",
605
+ replacementKeys: ["DIAGNOSTIC_OWNER_PATHS"],
606
+ },
607
+ ],
608
+ [
609
+ ".github/workflows/check-deployment-diagnostics.yml",
610
+ {
611
+ operationKind: "writeTextTemplate",
612
+ replacementKeys: ["DIAGNOSTIC_OWNER_PATHS"],
613
+ },
614
+ ],
615
+ ]);
616
+ function hasExactReplacementKeys(replacements, expectedKeys) {
617
+ const keys = Object.keys(replacements).toSorted();
618
+ const expected = [...expectedKeys].toSorted();
619
+ return (keys.length === expected.length &&
620
+ keys.every((key, index) => key === expected[index]));
621
+ }
622
+ const reservedWorkspaceCollections = new Set([
623
+ ".git",
624
+ ".github",
625
+ ".devcontainer",
626
+ ".template",
627
+ "node_modules",
628
+ "dist",
629
+ "target",
630
+ ]);
631
+ function isSafePackageBoundaryPath(value) {
632
+ return (/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/u.test(value) &&
633
+ !reservedWorkspaceCollections.has(value.split("/", 1)[0]));
634
+ }
635
+ /**
636
+ * Independent oracle for the Foundation workflow's limited substitution.
637
+ * It deliberately consumes raw Blueprint and Package Contribution facts rather
638
+ * than the production CI artifact composer or its composed plan output.
639
+ */
640
+ function expectedDiagnosticOwnerPaths(options) {
641
+ const blueprintPackagePaths = options.blueprint.packages.map((packageDefinition) => packageDefinition.path);
642
+ if (!blueprintPackagePaths.every(isSafePackageBoundaryPath)) {
643
+ return { valid: false, value: "" };
644
+ }
645
+ const blueprintOwners = new Set(blueprintPackagePaths);
646
+ const owners = new Set();
647
+ for (const declaration of options.diagnosticArtifactDeclarations) {
648
+ if (!isRecord(declaration) ||
649
+ !hasExactKeys(declaration, ["kind", "owner"])) {
650
+ return { valid: false, value: "" };
651
+ }
652
+ if (declaration.kind !== "playwright" || !isRecord(declaration.owner)) {
653
+ return { valid: false, value: "" };
654
+ }
655
+ if (!hasExactKeys(declaration.owner, ["kind", "path"])) {
656
+ return { valid: false, value: "" };
657
+ }
658
+ const ownerPath = declaration.owner.path;
659
+ if (declaration.owner.kind !== "package-boundary" ||
660
+ typeof ownerPath !== "string" ||
661
+ !isSafePackageBoundaryPath(ownerPath) ||
662
+ !blueprintOwners.has(ownerPath)) {
663
+ return { valid: false, value: "" };
664
+ }
665
+ owners.add(ownerPath);
666
+ }
667
+ return {
668
+ valid: true,
669
+ value: [...owners].toSorted().join("\n "),
670
+ };
671
+ }
672
+ function sourceFileForWorkflowOperation(options) {
673
+ return options.operation?.provenance?.plannerSourceFile ?? options.fallback;
674
+ }
675
+ function findWorkflowOperationOwners(sourceFile, operation) {
676
+ if (operation === undefined)
677
+ return ["<unknown>"];
678
+ const targetPath = operationTarget(operation);
679
+ if (targetPath === undefined)
680
+ return ["<unknown>"];
681
+ const expectedTargetPath = targetPath;
682
+ const operationKind = operation.kind;
683
+ const owners = new Set();
684
+ function visit(node) {
685
+ if (ts.isObjectLiteralExpression(node)) {
686
+ if (propertyStringValue(node, "kind") === operationKind &&
687
+ propertyPathMatches(node, "to", expectedTargetPath)) {
688
+ owners.add(owningFunctionName(node));
689
+ }
690
+ }
691
+ ts.forEachChild(node, visit);
692
+ }
693
+ visit(sourceFile);
694
+ return owners.size === 0 ? ["<unknown>"] : [...owners];
695
+ }
696
+ async function protectedWorkflowProjectionViolations(plans) {
697
+ const violations = [];
698
+ for (const plan of plans) {
699
+ const matchingOperations = plan.operations.filter((operation) => operationTarget(operation) === plan.generatedPath);
700
+ const operation = matchingOperations[0];
701
+ const sourceFilePath = sourceFileForWorkflowOperation({
702
+ operation,
703
+ fallback: plan.sourceFilePath,
704
+ });
705
+ const sourceText = await readFile(sourceFilePath, "utf8");
706
+ const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
707
+ const expectedDiagnosticOwners = expectedDiagnosticOwnerPaths(plan);
708
+ const variant = operation !== undefined &&
709
+ (operation.kind === "copyFile" || operation.kind === "writeTextTemplate")
710
+ ? workflowTemplateVariants.get(operation.from)
711
+ : undefined;
712
+ const replacementValid = operation?.kind === "writeTextTemplate" &&
713
+ hasExactReplacementKeys(operation.replacements, [
714
+ "DIAGNOSTIC_OWNER_PATHS",
715
+ ]) &&
716
+ operation.replacements.DIAGNOSTIC_OWNER_PATHS ===
717
+ expectedDiagnosticOwners.value;
718
+ const sourceBackedExactlyOnce = matchingOperations.length === 1 &&
719
+ operation !== undefined &&
720
+ (operation.kind === "copyFile" || operation.kind === "writeTextTemplate");
721
+ const diagnosticsExpected = expectedDiagnosticOwners.value !== "";
722
+ const operationValid = expectedDiagnosticOwners.valid &&
723
+ sourceBackedExactlyOnce &&
724
+ variant !== undefined &&
725
+ operation?.kind === variant.operationKind &&
726
+ (operation.kind === "copyFile"
727
+ ? variant.replacementKeys.length === 0 && !diagnosticsExpected
728
+ : variant.replacementKeys.length === 1 &&
729
+ diagnosticsExpected &&
730
+ replacementValid);
731
+ if (operationValid)
732
+ continue;
733
+ for (const owningFunction of findWorkflowOperationOwners(sourceFile, operation)) {
734
+ violations.push({
735
+ preset: plan.name,
736
+ generatedPath: plan.generatedPath,
737
+ owningFunction,
738
+ operationKind: operation?.kind ?? "writeTextTemplate",
739
+ sourceFilePath,
740
+ ...(plan.definitionName === undefined
741
+ ? {}
742
+ : { definitionName: plan.definitionName }),
743
+ ...(plan.planningContribution === undefined
744
+ ? {}
745
+ : { planningContribution: plan.planningContribution }),
746
+ ownershipRule: "Foundation workflow projections require exactly one source-backed template operation with closed diagnostic owner facts",
747
+ });
748
+ }
749
+ }
750
+ return violations;
751
+ }
752
+ export async function checkTemplateSourceBoundary({ projections, templateSourceContexts = [], manifestReferencedSourceFiles = [], protectedWorkflowPlans = [], }) {
593
753
  const manifestReferencedSourceFileSet = new Set(manifestReferencedSourceFiles.map((sourceFile) => path.resolve(sourceFile)));
594
754
  const violations = [];
755
+ violations.push(...(await protectedWorkflowProjectionViolations(protectedWorkflowPlans)));
595
756
  for (const projection of projections) {
596
757
  const sourceText = await readFile(projection.sourceFilePath, "utf8");
597
758
  const sourceFile = ts.createSourceFile(projection.sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykdz/template",
3
- "version": "0.0.32",
3
+ "version": "0.0.35",
4
4
  "private": false,
5
5
  "description": "Project template generator for YKDZ starter projects.",
6
6
  "keywords": [
@@ -1,20 +0,0 @@
1
- name: Check
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - main
8
-
9
- jobs:
10
- check:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v7
14
- - uses: actions/setup-node@v7
15
- with:
16
- node-version-file: package.json
17
- - run: corepack enable
18
- - run: pnpm install
19
- - run: pnpm --filter ./apps/web exec playwright install --with-deps chromium
20
- - run: pnpm run check
@@ -1,23 +0,0 @@
1
- name: Check
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - main
8
-
9
- jobs:
10
- check:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v7
14
- - uses: actions/setup-node@v7
15
- with:
16
- node-version-file: package.json
17
- - run: corepack enable
18
- - uses: dtolnay/rust-toolchain@stable
19
- with:
20
- components: rustfmt, clippy
21
- - uses: Swatinem/rust-cache@v2
22
- - run: pnpm install
23
- - run: pnpm run check
@@ -1,19 +0,0 @@
1
- name: Check
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - main
8
-
9
- jobs:
10
- check:
11
- runs-on: ubuntu-latest{{DEPLOYMENT_MATRIX}}
12
- steps:
13
- - uses: actions/checkout@v7
14
- - uses: actions/setup-node@v7
15
- with:
16
- node-version-file: package.json
17
- - run: corepack enable{{DEPLOYMENT_DOCKER_PREPARATION}}
18
- - run: pnpm install
19
- - run: pnpm run check{{ROOT_CHECK_CONDITION}}{{DEPLOYMENT_CHECK}}
@@ -1,45 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { mkdirSync, rmSync } from "node:fs";
3
- import path from "node:path";
4
-
5
- const mode = process.argv[2];
6
-
7
- if (mode !== "dev" && mode !== "test") {
8
- throw new Error("Usage: node scripts/prepare-database.ts <dev|test>");
9
- }
10
-
11
- const packageDirectory = process.cwd();
12
- const databaseFile = process.env.DATABASE_FILE
13
- ? path.resolve(
14
- process.env.INIT_CWD ?? packageDirectory,
15
- process.env.DATABASE_FILE,
16
- )
17
- : path.resolve(packageDirectory, "../../apps/web/data/app.sqlite");
18
- const environment = { ...process.env, DATABASE_FILE: databaseFile };
19
-
20
- mkdirSync(path.dirname(databaseFile), { recursive: true });
21
- if (mode === "test") {
22
- rmSync(databaseFile, { force: true });
23
- }
24
-
25
- execFileSync("pnpm", ["run", "db:push"], {
26
- cwd: packageDirectory,
27
- env: environment,
28
- stdio: "inherit",
29
- });
30
- const databasePackageName = process.env.DATABASE_PACKAGE_NAME;
31
- if (!databasePackageName) {
32
- throw new Error(
33
- "DATABASE_PACKAGE_NAME must name the database Package Boundary",
34
- );
35
- }
36
-
37
- execFileSync(
38
- "pnpm",
39
- ["--filter", databasePackageName, "run", "db:seed:example"],
40
- {
41
- cwd: packageDirectory,
42
- env: environment,
43
- stdio: "inherit",
44
- },
45
- );
@@ -1,151 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdir, rm } from "node:fs/promises";
3
- import { createServer } from "node:net";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const databaseFile = fileURLToPath(
8
- new URL("../node_modules/.tmp/e2e.sqlite", import.meta.url),
9
- );
10
- const externalBaseUrlName = "PLAYWRIGHT_EXTERNAL_BASE_URL";
11
- const externalReadinessTimeoutMs = 30_000;
12
-
13
- function externalBaseUrl(): string | undefined {
14
- if (!Object.hasOwn(process.env, externalBaseUrlName)) {
15
- return undefined;
16
- }
17
-
18
- const value = process.env[externalBaseUrlName]?.trim();
19
- if (!value) {
20
- throw new Error(`${externalBaseUrlName} must be a non-empty HTTP(S) URL`);
21
- }
22
-
23
- let url: URL;
24
- try {
25
- url = new URL(value);
26
- } catch {
27
- throw new Error(`${externalBaseUrlName} must be a valid HTTP(S) URL`);
28
- }
29
-
30
- if (url.protocol !== "http:" && url.protocol !== "https:") {
31
- throw new Error(`${externalBaseUrlName} must use HTTP or HTTPS`);
32
- }
33
-
34
- return url.toString();
35
- }
36
-
37
- async function availablePort(): Promise<string> {
38
- return await new Promise((resolve, reject) => {
39
- const server = createServer();
40
-
41
- server.once("error", reject);
42
- server.listen(0, "127.0.0.1", () => {
43
- const address = server.address();
44
-
45
- if (typeof address !== "object" || address === null) {
46
- server.close(() => {
47
- reject(new Error("Could not allocate a Playwright web port"));
48
- });
49
- return;
50
- }
51
-
52
- const port = String(address.port);
53
- server.close((error) => {
54
- if (error) {
55
- reject(error);
56
- return;
57
- }
58
-
59
- resolve(port);
60
- });
61
- });
62
- });
63
- }
64
-
65
- async function awaitExternalService(baseUrl: string): Promise<void> {
66
- const deadline = Date.now() + externalReadinessTimeoutMs;
67
-
68
- while (Date.now() < deadline) {
69
- try {
70
- const response = await fetch(baseUrl, {
71
- signal: AbortSignal.timeout(2_000),
72
- });
73
- if (response.ok) {
74
- return;
75
- }
76
- } catch {
77
- // The externally managed service may still be starting.
78
- }
79
-
80
- await new Promise((resolve) => setTimeout(resolve, 250));
81
- }
82
-
83
- throw new Error(
84
- `External Playwright service at ${baseUrl} was not ready within ${externalReadinessTimeoutMs}ms`,
85
- );
86
- }
87
-
88
- async function localPlaywrightEnv(): Promise<NodeJS.ProcessEnv> {
89
- return {
90
- ...process.env,
91
- DATABASE_FILE: process.env.DATABASE_FILE ?? databaseFile,
92
- PLAYWRIGHT_WEB_PORT:
93
- process.env.PLAYWRIGHT_WEB_PORT ?? (await availablePort()),
94
- };
95
- }
96
-
97
- const externalServiceUrl = externalBaseUrl();
98
- const env = externalServiceUrl
99
- ? { ...process.env }
100
- : await localPlaywrightEnv();
101
- const localDatabaseFile = externalServiceUrl ? undefined : env.DATABASE_FILE;
102
- delete env.NO_COLOR;
103
-
104
- if (externalServiceUrl) {
105
- await awaitExternalService(externalServiceUrl);
106
- } else {
107
- await mkdir(path.dirname(localDatabaseFile!), { recursive: true });
108
- await rm(localDatabaseFile!, { force: true });
109
- }
110
-
111
- const command = process.platform === "win32" ? "playwright.cmd" : "playwright";
112
- const child = spawn(command, ["test", ...process.argv.slice(2)], {
113
- env,
114
- stdio: "inherit",
115
- });
116
-
117
- const forwardedSignals = ["SIGINT", "SIGTERM"] as const;
118
- const forwardSignal = (signal: NodeJS.Signals): void => {
119
- child.kill(signal);
120
- };
121
- for (const signal of forwardedSignals) {
122
- process.once(signal, forwardSignal);
123
- }
124
-
125
- try {
126
- const result = await new Promise<{
127
- code: number | null;
128
- signal: NodeJS.Signals | null;
129
- }>((resolve, reject) => {
130
- child.once("error", reject);
131
- child.once("exit", (code, signal) => resolve({ code, signal }));
132
- });
133
-
134
- if (result.signal) {
135
- console.error(`Playwright exited with signal ${result.signal}`);
136
- process.exitCode = 1;
137
- } else {
138
- process.exitCode = result.code ?? 1;
139
- }
140
- } catch (error) {
141
- console.error(error);
142
- process.exitCode = 1;
143
- } finally {
144
- for (const signal of forwardedSignals) {
145
- process.removeListener(signal, forwardSignal);
146
- }
147
-
148
- if (localDatabaseFile) {
149
- await rm(localDatabaseFile, { force: true });
150
- }
151
- }