@ykdz/template 0.0.20 → 0.0.21

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.
@@ -5,6 +5,11 @@ updates:
5
5
  directory: /
6
6
  schedule:
7
7
  interval: weekly
8
+ groups:
9
+ drizzle:
10
+ patterns:
11
+ - "drizzle-*"
12
+ - "drizzle-orm"
8
13
  ignore:
9
14
  - dependency-name: "@types/node"
10
15
  update-types:
@@ -5,6 +5,11 @@ updates:
5
5
  directory: /
6
6
  schedule:
7
7
  interval: weekly
8
+ groups:
9
+ drizzle:
10
+ patterns:
11
+ - "drizzle-*"
12
+ - "drizzle-orm"
8
13
  ignore:
9
14
  - dependency-name: "@types/node"
10
15
  update-types:
@@ -5,6 +5,11 @@ updates:
5
5
  directory: /
6
6
  schedule:
7
7
  interval: weekly
8
+ groups:
9
+ drizzle:
10
+ patterns:
11
+ - "drizzle-*"
12
+ - "drizzle-orm"
8
13
  ignore:
9
14
  - dependency-name: "@types/node"
10
15
  update-types:
@@ -2,13 +2,16 @@ import { mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { drizzle } from "drizzle-orm/node-sqlite";
5
+ import { defineRelations } from "drizzle-orm/relations";
5
6
 
6
7
  import * as schema from "#db/schema";
7
8
 
9
+ const relations = defineRelations(schema);
10
+
8
11
  export type Database = ReturnType<typeof createDatabase>;
9
12
 
10
13
  export function createDatabase(databaseFile = process.env.DATABASE_FILE) {
11
14
  const file = databaseFile ?? "./data/app.sqlite";
12
15
  mkdirSync(path.dirname(file), { recursive: true });
13
- return drizzle(file, { schema });
16
+ return drizzle(file, { relations });
14
17
  }
@@ -14,5 +14,8 @@ describe("todo queries", () => {
14
14
  await expect(listTodos(db)).resolves.toEqual(
15
15
  expect.arrayContaining([expect.objectContaining({ title })]),
16
16
  );
17
+ await expect(db.query.todos.findMany()).resolves.toEqual(
18
+ expect.arrayContaining([expect.objectContaining({ title })]),
19
+ );
17
20
  });
18
21
  });
@@ -5,6 +5,11 @@ updates:
5
5
  directory: /
6
6
  schedule:
7
7
  interval: weekly
8
+ groups:
9
+ drizzle:
10
+ patterns:
11
+ - "drizzle-*"
12
+ - "drizzle-orm"
8
13
  ignore:
9
14
  - dependency-name: "@types/node"
10
15
  update-types:
@@ -5,6 +5,11 @@ updates:
5
5
  directory: /
6
6
  schedule:
7
7
  interval: weekly
8
+ groups:
9
+ drizzle:
10
+ patterns:
11
+ - "drizzle-*"
12
+ - "drizzle-orm"
8
13
  ignore:
9
14
  - dependency-name: "@types/node"
10
15
  update-types:
@@ -46,10 +46,16 @@ export type GeneratedScenarioRunnerOptions = {
46
46
  readonly reporter?: GeneratedScenarioReporter;
47
47
  readonly deterministicToolchainEnv?: Record<string, string>;
48
48
  readonly rootCheckLock?: ExclusiveLock;
49
+ readonly replayCache?: GeneratedScenarioReplayCache | undefined;
49
50
  };
50
51
  export type ExclusiveLock = {
51
52
  readonly run: <T>(callback: () => Promise<T>) => Promise<T>;
52
53
  };
54
+ export type GeneratedScenarioReplayCache = {
55
+ readonly directory: string;
56
+ readonly read: boolean;
57
+ readonly write: boolean;
58
+ };
53
59
  export declare function generatedScenarioChildProcessEnv(env?: Record<string, string>): NodeJS.ProcessEnv;
54
60
  export declare function createExclusiveLock(): ExclusiveLock;
55
61
  export declare function generatedScenarioId(scenario: Pick<GeneratedScenario, "basePreset" | "addedPreset" | "linkFrom">): string;
@@ -1,4 +1,5 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { PackageAdditionSupport } from "@ykdz/template-shared";
4
5
  import { execa } from "execa";
@@ -13,6 +14,7 @@ const defaultDeterministicToolchainEnv = {
13
14
  TEMPLATE_TOOLCHAIN_RESOLUTION: "bundled-fallback",
14
15
  };
15
16
  const defaultFixtureConcurrency = 2;
17
+ const scenarioFingerprintVersion = 1;
16
18
  const storedBlueprintSchema = v.object({
17
19
  packages: v.optional(v.array(v.object({
18
20
  name: v.string(),
@@ -171,6 +173,68 @@ function packageLeaf(packagePath) {
171
173
  function hasSourceDirectoryPath(sourceFile) {
172
174
  return sourceFile.startsWith("src/") || sourceFile.includes("/src/");
173
175
  }
176
+ function scenarioFingerprintPrefix(scenario) {
177
+ return JSON.stringify({
178
+ version: scenarioFingerprintVersion,
179
+ scenario,
180
+ platform: process.platform,
181
+ arch: process.arch,
182
+ nodeMajor: process.versions.node.split(".")[0],
183
+ qualityGateProtocol: "lockfile-only-snapshot-then-fix-check",
184
+ });
185
+ }
186
+ async function hashGeneratedScenarioDirectory(scenario, projectDir) {
187
+ const hash = createHash("sha256");
188
+ hash.update(scenarioFingerprintPrefix(scenario));
189
+ await hashDirectoryEntries(projectDir, projectDir, hash);
190
+ return hash.digest("hex");
191
+ }
192
+ async function hashDirectoryEntries(rootDir, currentDir, hash) {
193
+ const entries = await readdir(currentDir, { withFileTypes: true });
194
+ for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
195
+ if (entry.name === "node_modules" || entry.name === ".git") {
196
+ continue;
197
+ }
198
+ const entryPath = path.join(currentDir, entry.name);
199
+ const relativePath = path
200
+ .relative(rootDir, entryPath)
201
+ .split(path.sep)
202
+ .join("/");
203
+ if (entry.isDirectory()) {
204
+ hash.update(`dir\0${relativePath}\0`);
205
+ await hashDirectoryEntries(rootDir, entryPath, hash);
206
+ continue;
207
+ }
208
+ if (!entry.isFile()) {
209
+ const fileStat = await stat(entryPath);
210
+ hash.update(`special\0${relativePath}\0${fileStat.mode.toString(8)}\0`);
211
+ continue;
212
+ }
213
+ const fileStat = await stat(entryPath);
214
+ hash.update(`file\0${relativePath}\0${fileStat.mode.toString(8)}\0`);
215
+ hash.update(await readFile(entryPath));
216
+ hash.update("\0");
217
+ }
218
+ }
219
+ async function scenarioReplayMarkerExists(cache, fingerprint) {
220
+ try {
221
+ await readFile(path.join(cache.directory, `${fingerprint}.passed`), "utf8");
222
+ return true;
223
+ }
224
+ catch (error) {
225
+ if (error &&
226
+ typeof error === "object" &&
227
+ "code" in error &&
228
+ error.code === "ENOENT") {
229
+ return false;
230
+ }
231
+ throw error;
232
+ }
233
+ }
234
+ async function writeScenarioReplayMarker(cache, fingerprint, scenario) {
235
+ await mkdir(cache.directory, { recursive: true });
236
+ await writeFile(path.join(cache.directory, `${fingerprint}.passed`), `${JSON.stringify({ scenario, fingerprint })}\n`, "utf8");
237
+ }
174
238
  export function packageLeafNameForAddedPreset(manifest, presetName) {
175
239
  const preset = manifest.presets.find((entry) => entry.name === presetName);
176
240
  if (!preset) {
@@ -383,9 +447,14 @@ export function generatedScenarioQualityGateSteps(manifest, scenario, projectDir
383
447
  async function runGeneratedScenarioQualityGate(manifest, scenario, projectDir, addedPackagePath, options) {
384
448
  const steps = generatedScenarioQualityGateSteps(manifest, scenario, projectDir, addedPackagePath, options);
385
449
  const requiresSerializedPlaywrightRootCheck = generatedScenarioRequiresSerializedRootCheck(steps);
450
+ let replayFingerprint;
386
451
  for (const step of steps) {
387
452
  if (step.id === "install-dependencies") {
388
- await runGeneratedScenarioInstallStep(step, scenario, options);
453
+ const installResult = await runGeneratedScenarioInstallStep(step, scenario, options);
454
+ replayFingerprint = installResult.fingerprint;
455
+ if (installResult.replayed) {
456
+ return;
457
+ }
389
458
  continue;
390
459
  }
391
460
  const env = step.id === "run-root-check" ? { CI: "1" } : undefined;
@@ -401,13 +470,27 @@ async function runGeneratedScenarioQualityGate(manifest, scenario, projectDir, a
401
470
  }
402
471
  await runStep();
403
472
  }
473
+ if (options.replayCache?.write && replayFingerprint !== undefined) {
474
+ await writeScenarioReplayMarker(options.replayCache, replayFingerprint, scenario);
475
+ options.reporter.info?.(`-- Stored passed fixture replay marker for ${scenario.label}: ${replayFingerprint}`);
476
+ }
404
477
  }
405
478
  async function runGeneratedScenarioInstallStep(step, scenario, options) {
406
479
  await options.runCommand(step.command, ["install", "--lockfile-only", "--prefer-offline", "--no-frozen-lockfile"], step.cwd, { logPrefix: scenario.label });
480
+ let replayFingerprint;
481
+ if (options.replayCache) {
482
+ replayFingerprint = await hashGeneratedScenarioDirectory(scenario, step.cwd);
483
+ if (options.replayCache.read &&
484
+ (await scenarioReplayMarkerExists(options.replayCache, replayFingerprint))) {
485
+ options.reporter.info?.(`-- Replayed passed fixture ${scenario.label}: ${replayFingerprint}`);
486
+ return { fingerprint: replayFingerprint, replayed: true };
487
+ }
488
+ }
407
489
  await options.runCommand(step.command, ["fetch"], step.cwd, {
408
490
  logPrefix: scenario.label,
409
491
  });
410
492
  await options.runCommand(step.command, ["install", "--offline", "--frozen-lockfile"], step.cwd, { logPrefix: scenario.label });
493
+ return { fingerprint: replayFingerprint, replayed: false };
411
494
  }
412
495
  async function checkGeneratedScenario(manifest, scenario, workspace, options) {
413
496
  options.reporter.info?.(`\n== ${scenario.label} ==`);
@@ -446,6 +529,7 @@ function normalizeRunnerOptions(options) {
446
529
  },
447
530
  deterministicToolchainEnv: options.deterministicToolchainEnv ?? defaultDeterministicToolchainEnv,
448
531
  rootCheckLock: options.rootCheckLock ?? createExclusiveLock(),
532
+ replayCache: options.replayCache,
449
533
  };
450
534
  }
451
535
  export async function runGeneratedScenariosConcurrently(manifest, scenarios, workspace, concurrency, options) {
@@ -12,16 +12,16 @@ catalog:
12
12
  "@vitejs/plugin-vue": ^6.0.7
13
13
  "@vikejs/hono": ^0.2.1
14
14
  "@vue/tsconfig": ^0.9.1
15
- drizzle-kit: 1.0.0-beta.22
16
- drizzle-orm: 1.0.0-beta.22
15
+ drizzle-kit: 1.0.0-rc.4
16
+ drizzle-orm: 1.0.0-rc.4
17
17
  execa: ^9.6.1
18
18
  hono: ^4.12.27
19
- oxfmt: ^0.57.0
19
+ oxfmt: ^0.58.0
20
20
  oxlint: ^1.72.0
21
21
  oxlint-tsgolint: ^0.24.0
22
22
  pinia: ^3.0.4
23
23
  tailwindcss: ^4.3.2
24
- telefunc: ^0.2.21
24
+ telefunc: ^0.2.22
25
25
  tsc-alias: ^1.8.17
26
26
  tsx: ^4.22.4
27
27
  turbo: 2.10.2
@@ -30,7 +30,7 @@ catalog:
30
30
  vite: ^8.1.2
31
31
  vike: ^0.4.260
32
32
  vike-vue: ^0.9.12
33
- vitest: ^4.1.9
33
+ vitest: ^4.1.10
34
34
  vue: ^3.5.39
35
35
  vue-tsc: ^3.3.6
36
36
  yaml: ^2.9.0
@@ -60,7 +60,7 @@ function renderDependabotUpdate(ecosystem, directory, interval) {
60
60
  ` interval: ${interval}`,
61
61
  ];
62
62
  if (ecosystem === "npm") {
63
- lines.push(" ignore:", ' - dependency-name: "@types/node"', " update-types:", " - version-update:semver-major");
63
+ lines.push(" groups:", " drizzle:", " patterns:", ' - "drizzle-*"', ' - "drizzle-orm"', " ignore:", ' - dependency-name: "@types/node"', " update-types:", " - version-update:semver-major");
64
64
  }
65
65
  if (ecosystem === "docker") {
66
66
  lines.push(" ignore:", " - dependency-name: mcr.microsoft.com/devcontainers/typescript-node", " update-types:", " - version-update:semver-major");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykdz/template",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "private": false,
5
5
  "description": "Project template generator for YKDZ starter projects.",
6
6
  "keywords": [
@@ -34,13 +34,13 @@
34
34
  "dependencies": {
35
35
  "typescript": "^6.0.3",
36
36
  "valibot": "^1.4.2",
37
- "@ykdz/template-builtin-source": "0.0.0",
38
37
  "@ykdz/template-core": "0.0.0",
38
+ "@ykdz/template-builtin-source": "0.0.0",
39
39
  "@ykdz/template-shared": "0.0.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^24.0.0",
43
- "oxfmt": "^0.57.0",
43
+ "oxfmt": "^0.58.0",
44
44
  "oxlint": "^1.72.0",
45
45
  "oxlint-tsgolint": "^0.24.0"
46
46
  },