Skip to content

Commit 0b1e0b0

Browse files
ynamiteclaude
andcommitted
feat(installer): run ydeploy:diff around the dev server when ydeploy is installed
When ydeploy is installed, rewrite the project package.json dev script from "vite" to "<console> ydeploy:diff && cross-env NODE_ENV=development vite && <console> ydeploy:diff" so schema changes made during development surface as migrations both before Vite starts and after it exits. - New pure helper src/utils/patch-dev-script.ts (only the exact stub/preset "dev": "vite" value is rewritten — idempotent, won't clobber a custom script). - New pipeline task src/tasks/patch-dev-script.ts, run after "Apply preset files" (the default preset overwrites the stub package.json verbatim) and before the initial commit. ydeploy presence is read from disk so it covers both fresh (baseline ydeploy) and augmented installs. - Console path is layout-aware (bin/console modern, redaxo/bin/console classic). - cross-env already ships as a devDependency in viterex_addon's stub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e6aa2cf commit 0b1e0b0

6 files changed

Lines changed: 183 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ lives under `[Unreleased]`.
88

99
## [Unreleased]
1010

11+
### Added (2026-06-02)
12+
13+
- **`pnpm dev` now diffs ydeploy migrations around the Vite dev server when ydeploy is installed.** A new pipeline task (`src/tasks/patch-dev-script.ts`, "Patch dev script for ydeploy") rewrites the project `package.json` `dev` script from `"vite"` to `"<console> ydeploy:diff && cross-env NODE_ENV=development vite && <console> ydeploy:diff"`, so schema changes made during development surface as migrations both before Vite starts and after it exits. `<console>` is layout-aware (`bin/console` in modern, `redaxo/bin/console` in classic / classic+theme); `cross-env` already ships as a devDependency in `viterex_addon`'s stub `package.json`. "ydeploy installed" is read from disk (the addon's `package.yml`), covering both a fresh install (ydeploy is part of the always-included baseline) and an augmented existing install. The task runs **after** *Apply preset files* (the default preset overwrites the stub `package.json` verbatim, so the patch must land on the final file) and **before** the initial commit. Idempotent and surgical: only the exact stub/preset `"dev": "vite"` value is rewritten, so a re-run or a customised dev script is left untouched. New pure helper `src/utils/patch-dev-script.ts` (unit-tested); ordering locked in `src/__tests__/pipeline-order.test.ts` (task count 18 → 19).
14+
1115
## [3.0.0-alpha.2] - 2026-06-02
1216

1317
### Added (2026-06-02)

src/__tests__/pipeline-order.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ describe("pipeline task order", () => {
3636
);
3737
});
3838

39-
it("still has 18 tasks", () => {
40-
expect(tasks).toHaveLength(18);
39+
it("patches the dev script AFTER applying preset files (so it lands on the final package.json)", () => {
40+
expect(idx("Apply preset files")).toBeLessThan(idx("Patch dev script for ydeploy"));
41+
});
42+
43+
it("patches the dev script BEFORE the initial commit (so the change is committed)", () => {
44+
expect(idx("Patch dev script for ydeploy")).toBeLessThan(idx("Git initial commit"));
45+
});
46+
47+
it("still has 19 tasks", () => {
48+
expect(tasks).toHaveLength(19);
4149
});
4250
});

src/pipeline.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { installRedaxo } from "./tasks/install-redaxo.js";
1010
import { installAddons } from "./tasks/install-addons.js";
1111
import { scaffoldFrontend } from "./tasks/scaffold-frontend.js";
1212
import { applyPresetFiles } from "./tasks/apply-preset-files.js";
13+
import { patchDevScript } from "./tasks/patch-dev-script.js";
1314
import { buildFrontend } from "./tasks/build-frontend.js";
1415
import { clearCache } from "./tasks/clear-cache.js";
1516
import { importSql } from "./tasks/import-sql.js";
@@ -77,6 +78,13 @@ export const tasks: Task[] = [
7778
skip: (c) => !c.presetFilesDir,
7879
run: applyPresetFiles,
7980
},
81+
// Must come AFTER "Apply preset files" — the default preset overwrites the
82+
// stub package.json verbatim, so the dev-script patch has to land on the
83+
// final file. No-ops unless ydeploy is installed (checked on disk).
84+
{
85+
name: "Patch dev script for ydeploy",
86+
run: patchDevScript,
87+
},
8088
{
8189
name: "Seed database",
8290
skip: (c) => isAugment(c) || c.skipDb || !c.seedFile,

src/tasks/patch-dev-script.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import path from "node:path";
2+
import fs from "fs-extra";
3+
import * as p from "@clack/prompts";
4+
import { consolePathFor, srcAddonsDirFor } from "../utils/detect.js";
5+
import { patchDevScriptForYdeploy } from "../utils/patch-dev-script.js";
6+
import type { ViterexConfig } from "../types.js";
7+
8+
/**
9+
* When ydeploy is installed, rewrite the project's `dev` npm script so that
10+
* `pnpm dev` brackets the Vite dev server with `ydeploy:diff` calls:
11+
*
12+
* <console> ydeploy:diff && cross-env NODE_ENV=development vite && <console> ydeploy:diff
13+
*
14+
* "ydeploy installed" is read from the filesystem (the addon directory holds a
15+
* package.yml) so it covers both a fresh install — where ydeploy is part of the
16+
* always-included baseline — and an augmented existing install where ydeploy
17+
* was already present.
18+
*
19+
* Runs AFTER "Apply preset files" so it patches the final package.json (the
20+
* default preset overwrites the stub package.json verbatim), and BEFORE the
21+
* initial commit so the change is committed on a clean tree.
22+
*
23+
* Idempotent: only the exact stub/preset `"dev": "vite"` value is rewritten;
24+
* an already-patched or customised script is left untouched.
25+
*/
26+
export async function patchDevScript(config: ViterexConfig): Promise<void> {
27+
const { projectDir, layout, verbose } = config;
28+
29+
const ydeployManifest = path.join(
30+
projectDir,
31+
srcAddonsDirFor(layout),
32+
"ydeploy",
33+
"package.yml",
34+
);
35+
if (!(await fs.pathExists(ydeployManifest))) {
36+
if (verbose) p.log.info("ydeploy not installed — leaving dev script unchanged.");
37+
return;
38+
}
39+
40+
const pkgPath = path.join(projectDir, "package.json");
41+
if (!(await fs.pathExists(pkgPath))) return;
42+
43+
const pkg = (await fs.readJSON(pkgPath)) as Record<string, unknown>;
44+
const { pkg: patched, changed } = patchDevScriptForYdeploy(pkg, consolePathFor(layout));
45+
46+
if (changed) {
47+
await fs.writeFile(pkgPath, `${JSON.stringify(patched, null, 2)}\n`);
48+
p.log.info("Patched dev script to run ydeploy:diff around the Vite dev server.");
49+
}
50+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
import { patchDevScriptForYdeploy } from "../patch-dev-script.js";
3+
4+
const MODERN = "bin/console";
5+
const CLASSIC = "redaxo/bin/console";
6+
7+
describe("patchDevScriptForYdeploy", () => {
8+
it("rewrites a plain `vite` dev script to the ydeploy variant (modern console path)", () => {
9+
const { pkg, changed } = patchDevScriptForYdeploy(
10+
{ scripts: { dev: "vite", build: "vite build" } },
11+
MODERN,
12+
);
13+
expect(changed).toBe(true);
14+
expect((pkg.scripts as Record<string, string>).dev).toBe(
15+
"bin/console ydeploy:diff && cross-env NODE_ENV=development vite && bin/console ydeploy:diff",
16+
);
17+
// leaves sibling scripts untouched
18+
expect((pkg.scripts as Record<string, string>).build).toBe("vite build");
19+
});
20+
21+
it("uses the classic console path when given one", () => {
22+
const { pkg, changed } = patchDevScriptForYdeploy(
23+
{ scripts: { dev: "vite" } },
24+
CLASSIC,
25+
);
26+
expect(changed).toBe(true);
27+
expect((pkg.scripts as Record<string, string>).dev).toBe(
28+
"redaxo/bin/console ydeploy:diff && cross-env NODE_ENV=development vite && redaxo/bin/console ydeploy:diff",
29+
);
30+
});
31+
32+
it("is idempotent — re-patching an already-patched dev script is a no-op", () => {
33+
const first = patchDevScriptForYdeploy({ scripts: { dev: "vite" } }, MODERN);
34+
const second = patchDevScriptForYdeploy(first.pkg, MODERN);
35+
expect(second.changed).toBe(false);
36+
expect(second.pkg).toEqual(first.pkg);
37+
});
38+
39+
it("leaves a customised dev script alone (only the exact `vite` value is patched)", () => {
40+
const { pkg, changed } = patchDevScriptForYdeploy(
41+
{ scripts: { dev: "vite --host" } },
42+
MODERN,
43+
);
44+
expect(changed).toBe(false);
45+
expect((pkg.scripts as Record<string, string>).dev).toBe("vite --host");
46+
});
47+
48+
it("does nothing when there is no scripts.dev entry", () => {
49+
const { changed } = patchDevScriptForYdeploy(
50+
{ scripts: { build: "vite build" } },
51+
MODERN,
52+
);
53+
expect(changed).toBe(false);
54+
});
55+
56+
it("does nothing when there is no scripts section at all", () => {
57+
const { pkg, changed } = patchDevScriptForYdeploy({ name: "x" }, MODERN);
58+
expect(changed).toBe(false);
59+
expect(pkg).toEqual({ name: "x" });
60+
});
61+
62+
it("does not mutate the input package object", () => {
63+
const input = { scripts: { dev: "vite" } };
64+
patchDevScriptForYdeploy(input, MODERN);
65+
expect(input.scripts.dev).toBe("vite");
66+
});
67+
});

src/utils/patch-dev-script.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Rewrite the `dev` npm script so that running it diffs ydeploy migrations
3+
* before and after the Vite dev server.
4+
*
5+
* When ydeploy is installed, `pnpm dev` should bracket the dev server with
6+
* `ydeploy:diff` so schema changes made during development are surfaced as
7+
* migrations (once before starting Vite, once after it exits):
8+
*
9+
* <console> ydeploy:diff && cross-env NODE_ENV=development vite && <console> ydeploy:diff
10+
*
11+
* `<console>` is the layout-aware console binary (`bin/console` in modern,
12+
* `redaxo/bin/console` in classic / classic+theme). `cross-env` already ships
13+
* as a devDependency in viterex_addon's stub package.json.
14+
*
15+
* Only the exact stub/preset value `"vite"` is rewritten — a customised dev
16+
* script is left untouched, which also makes the patch idempotent (the
17+
* rewritten value is no longer `"vite"`, so a second run is a no-op).
18+
*
19+
* Pure — does no I/O. The caller reads and writes package.json.
20+
*/
21+
export function patchDevScriptForYdeploy(
22+
pkg: Record<string, unknown>,
23+
consolePath: string,
24+
): { pkg: Record<string, unknown>; changed: boolean } {
25+
const scripts = pkg.scripts;
26+
if (typeof scripts !== "object" || scripts === null) {
27+
return { pkg, changed: false };
28+
}
29+
30+
const current = (scripts as Record<string, unknown>).dev;
31+
if (current !== "vite") {
32+
return { pkg, changed: false };
33+
}
34+
35+
const dev = `${consolePath} ydeploy:diff && cross-env NODE_ENV=development vite && ${consolePath} ydeploy:diff`;
36+
37+
return {
38+
pkg: {
39+
...pkg,
40+
scripts: { ...(scripts as Record<string, string>), dev },
41+
},
42+
changed: true,
43+
};
44+
}

0 commit comments

Comments
 (0)