@multiplatform.one/cli 7.10.0 → 7.15.0
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 +121 -111
- package/bin/createMultiplatformApp.mjs +39 -37
- package/lib/bin/multiplatformOne.mjs +65 -75
- package/lib/commands/addApp.mjs +78 -0
- package/lib/commands/adoptApp.mjs +11 -7
- package/lib/commands/doctypeTypes.mjs +148 -0
- package/lib/commands/e2e.mjs +2 -2
- package/lib/commands/init.mjs +142 -459
- package/lib/commands/merge.mjs +359 -0
- package/lib/commands/pieces.mjs +236 -0
- package/lib/commands/provenance.mjs +108 -0
- package/lib/commands/scaffold.mjs +105 -0
- package/lib/commands/updateApp.mjs +211 -528
- package/lib/source.json +5 -0
- package/lib/source.mjs +63 -0
- package/package.json +7 -7
- package/scripts/frappe-bootstrap.sh +29 -3
- package/scripts/frappe-dev.sh +1 -2
- package/scripts/frappe-helpers.sh +50 -2
- package/scripts/frappe.sh +5 -0
- package/scripts/scaffold.sh +937 -0
- package/scripts/stamp-source.mjs +51 -0
- package/scripts/stamp-source.spec.ts +86 -0
- package/src/bin/multiplatformOne.ts +116 -226
- package/src/commands/addApp.ts +102 -0
- package/src/commands/adoptApp.spec.ts +5 -1
- package/src/commands/adoptApp.ts +15 -10
- package/src/commands/doctypeTypes.spec.ts +155 -0
- package/src/commands/doctypeTypes.ts +214 -0
- package/src/commands/init.spec.ts +66 -0
- package/src/commands/init.ts +201 -643
- package/src/commands/merge.spec.ts +81 -0
- package/src/commands/merge.ts +411 -0
- package/src/commands/pieces.spec.ts +62 -0
- package/src/commands/pieces.ts +340 -0
- package/src/commands/provenance.spec.ts +92 -0
- package/src/commands/provenance.ts +169 -0
- package/src/commands/scaffold.ts +131 -0
- package/src/commands/updateApp.spec.ts +540 -331
- package/src/commands/updateApp.ts +255 -668
- package/src/source.spec.ts +45 -0
- package/src/source.ts +88 -0
- package/templates/AGENTS.md +161 -0
- package/templates/README.md +62 -0
- package/templates/pieces/frappe/universal/README.md.partial +7 -32
- package/templates/pieces/frappe/universal/env.example.partial +3 -3
- package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
- package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
- package/templates/pieces/tauri/universal/README.md.partial +7 -5
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/addApp.d.ts +20 -0
- package/types/commands/addApp.d.ts.map +1 -0
- package/types/commands/adoptApp.d.ts +4 -3
- package/types/commands/adoptApp.d.ts.map +1 -1
- package/types/commands/doctypeTypes.d.ts +40 -0
- package/types/commands/doctypeTypes.d.ts.map +1 -0
- package/types/commands/init.d.ts +30 -13
- package/types/commands/init.d.ts.map +1 -1
- package/types/commands/merge.d.ts +54 -0
- package/types/commands/merge.d.ts.map +1 -0
- package/types/commands/pieces.d.ts +54 -0
- package/types/commands/pieces.d.ts.map +1 -0
- package/types/commands/provenance.d.ts +52 -0
- package/types/commands/provenance.d.ts.map +1 -0
- package/types/commands/scaffold.d.ts +43 -0
- package/types/commands/scaffold.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +19 -27
- package/types/commands/updateApp.d.ts.map +1 -1
- package/types/source.d.ts +35 -0
- package/types/source.d.ts.map +1 -0
- package/lib/commands/initApp.mjs +0 -431
- package/lib/generateVscode.mjs +0 -184
- package/scripts/clone.sh +0 -17
- package/scripts/update.sh +0 -46
- package/src/commands/initApp.spec.ts +0 -720
- package/src/commands/initApp.ts +0 -706
- package/src/generateVscode.ts +0 -238
- package/templates/app/AGENTS.md +0 -142
- package/templates/app/README.md +0 -62
- package/templates/app/apps/__NAME__/index.html +0 -36
- package/templates/app/apps/__NAME__/package.json +0 -31
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +0 -9
- package/templates/app/apps/__NAME__/src/main.tsx +0 -14
- package/templates/app/apps/__NAME__/tsconfig.json +0 -8
- package/templates/app/apps/__NAME__/vite.config.ts +0 -17
- package/templates/app/features/__NAME__/package.json +0 -34
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
- package/templates/app/features/__NAME__/src/index.ts +0 -1
- package/templates/app/features/__NAME__/tsconfig.json +0 -5
- package/templates/app/gitignore +0 -9
- package/templates/app/npmrc +0 -2
- package/templates/app/oxlintrc.json +0 -39
- package/templates/app/package.json +0 -38
- package/templates/app/packages/config/config.json +0 -4
- package/templates/app/packages/config/index.ts +0 -5
- package/templates/app/packages/config/package.json +0 -17
- package/templates/app/packages/config/tsconfig.json +0 -5
- package/templates/app/pnpm-workspace.yaml +0 -4
- package/templates/app/tsconfig.base.json +0 -16
- package/templates/universal/AGENTS.md +0 -142
- package/templates/universal/README.md +0 -97
- package/templates/universal/apps/__NAME__/app.config.js +0 -31
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
- package/templates/universal/apps/__NAME__/package.json +0 -62
- package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
- package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
- package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
- package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
- package/templates/universal/apps/__NAME__/src/app.ts +0 -10
- package/templates/universal/apps/__NAME__/tamagui.css +0 -3
- package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
- package/templates/universal/apps/__NAME__/types.d.ts +0 -9
- package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
- package/templates/universal/features/__NAME__/package.json +0 -35
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
- package/templates/universal/features/__NAME__/src/index.ts +0 -4
- package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
- package/templates/universal/features/__NAME__/tsconfig.json +0 -5
- package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
- package/templates/universal/gitignore +0 -14
- package/templates/universal/npmrc +0 -3
- package/templates/universal/oxlintrc.json +0 -39
- package/templates/universal/package.json +0 -51
- package/templates/universal/packages/config/config.json +0 -4
- package/templates/universal/packages/config/index.ts +0 -5
- package/templates/universal/packages/config/package.json +0 -17
- package/templates/universal/packages/config/tsconfig.json +0 -5
- package/templates/universal/packages/i18n/en/common.json +0 -30
- package/templates/universal/packages/i18n/es/common.json +0 -30
- package/templates/universal/packages/i18n/i18n.d.ts +0 -16
- package/templates/universal/packages/i18n/index.ts +0 -26
- package/templates/universal/packages/i18n/package.json +0 -20
- package/templates/universal/packages/i18n/resources.ts +0 -13
- package/templates/universal/packages/i18n/tsconfig.json +0 -5
- package/templates/universal/packages/themes/accent.ts +0 -32
- package/templates/universal/packages/themes/base.ts +0 -50
- package/templates/universal/packages/themes/fonts.ts +0 -8
- package/templates/universal/packages/themes/index.ts +0 -33
- package/templates/universal/packages/themes/package.json +0 -23
- package/templates/universal/packages/themes/tamagui.config.ts +0 -14
- package/templates/universal/packages/themes/tsconfig.json +0 -5
- package/templates/universal/pnpm-workspace.yaml +0 -4
- package/templates/universal/tsconfig.base.json +0 -21
- package/templates/universal/types/env.d.ts +0 -9
- package/types/commands/initApp.d.ts +0 -114
- package/types/commands/initApp.d.ts.map +0 -1
- package/types/generateVscode.d.ts +0 -7
- package/types/generateVscode.d.ts.map +0 -1
|
@@ -1,720 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdtempSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
readdirSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
statSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { extname, join } from "node:path";
|
|
12
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
13
|
-
import { initApp, mergeJsonValues, readProvenance } from "./initApp";
|
|
14
|
-
|
|
15
|
-
const readJson = (path: string) => JSON.parse(readFileSync(path, "utf-8"));
|
|
16
|
-
|
|
17
|
-
/** Collect dependency ranges by scope for leakage assertions. */
|
|
18
|
-
function depRanges(pkg: {
|
|
19
|
-
dependencies?: Record<string, string>;
|
|
20
|
-
devDependencies?: Record<string, string>;
|
|
21
|
-
}): Record<string, string> {
|
|
22
|
-
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("initApp generator", () => {
|
|
26
|
-
let sandbox: string;
|
|
27
|
-
let previousCwd: string;
|
|
28
|
-
|
|
29
|
-
beforeAll(() => {
|
|
30
|
-
sandbox = mkdtempSync(join(tmpdir(), "mpo-initapp-spec-"));
|
|
31
|
-
previousCwd = process.cwd();
|
|
32
|
-
process.chdir(sandbox);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterAll(() => {
|
|
36
|
-
process.chdir(previousCwd);
|
|
37
|
-
rmSync(sandbox, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("universal template", () => {
|
|
41
|
-
const name = "uni-demo";
|
|
42
|
-
let root: string;
|
|
43
|
-
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
await initApp(name, {
|
|
46
|
-
template: "universal",
|
|
47
|
-
skipInstall: true,
|
|
48
|
-
version: "6.1.0",
|
|
49
|
-
yes: true,
|
|
50
|
-
});
|
|
51
|
-
root = join(sandbox, name);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("writes the workspace tree", () => {
|
|
55
|
-
for (const file of [
|
|
56
|
-
"package.json",
|
|
57
|
-
"pnpm-workspace.yaml",
|
|
58
|
-
"tsconfig.base.json",
|
|
59
|
-
"oxlintrc.json",
|
|
60
|
-
"AGENTS.md",
|
|
61
|
-
"README.md",
|
|
62
|
-
".gitignore",
|
|
63
|
-
".npmrc",
|
|
64
|
-
`apps/${name}/package.json`,
|
|
65
|
-
`apps/${name}/vite.config.ts`,
|
|
66
|
-
`apps/${name}/app.config.js`,
|
|
67
|
-
`apps/${name}/tamagui.css`,
|
|
68
|
-
`apps/${name}/types.d.ts`,
|
|
69
|
-
`apps/${name}/config/tamagui.config.ts`,
|
|
70
|
-
`apps/${name}/routes/_layout.tsx`,
|
|
71
|
-
`apps/${name}/routes/_layout.web.tsx`,
|
|
72
|
-
`apps/${name}/routes/index.tsx`,
|
|
73
|
-
`apps/${name}/routes/about.tsx`,
|
|
74
|
-
`apps/${name}/src/app.ts`,
|
|
75
|
-
`apps/${name}/polyfills.ts`,
|
|
76
|
-
`apps/${name}/polyfills.native.ts`,
|
|
77
|
-
`features/${name}/package.json`,
|
|
78
|
-
`features/${name}/vitest.config.mjs`,
|
|
79
|
-
`features/${name}/src/HomeScreen.tsx`,
|
|
80
|
-
`features/${name}/src/AboutScreen.tsx`,
|
|
81
|
-
`features/${name}/src/AppLayout.tsx`,
|
|
82
|
-
`features/${name}/src/ContactForm.tsx`,
|
|
83
|
-
`features/${name}/src/ContactForm.spec.tsx`,
|
|
84
|
-
"packages/config/package.json",
|
|
85
|
-
"packages/i18n/en/common.json",
|
|
86
|
-
"packages/i18n/es/common.json",
|
|
87
|
-
"packages/i18n/resources.ts",
|
|
88
|
-
"packages/themes/tamagui.config.ts",
|
|
89
|
-
"packages/themes/fonts.ts",
|
|
90
|
-
"packages/themes/base.ts",
|
|
91
|
-
"packages/themes/accent.ts",
|
|
92
|
-
"types/env.d.ts",
|
|
93
|
-
]) {
|
|
94
|
-
expect(existsSync(join(root, file)), `missing ${file}`).toBe(true);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("restores dotfile names stripped by npm pack", () => {
|
|
99
|
-
expect(readFileSync(join(root, ".npmrc"), "utf-8")).toContain("node-linker=hoisted");
|
|
100
|
-
expect(readFileSync(join(root, ".gitignore"), "utf-8")).toContain("node_modules");
|
|
101
|
-
expect(existsSync(join(root, "npmrc"))).toBe(false);
|
|
102
|
-
expect(existsSync(join(root, "gitignore"))).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("pins @multiplatform.one/* to semver and locals to workspace:*", () => {
|
|
106
|
-
for (const pkgPath of [
|
|
107
|
-
`apps/${name}/package.json`,
|
|
108
|
-
`features/${name}/package.json`,
|
|
109
|
-
"packages/i18n/package.json",
|
|
110
|
-
"packages/themes/package.json",
|
|
111
|
-
]) {
|
|
112
|
-
const ranges = depRanges(readJson(join(root, pkgPath)));
|
|
113
|
-
for (const [dep, range] of Object.entries(ranges)) {
|
|
114
|
-
if (dep.startsWith("@multiplatform.one/")) {
|
|
115
|
-
expect(range, `${pkgPath} ${dep}`).toBe("^6.1.0");
|
|
116
|
-
} else if (dep.startsWith("@features/") || dep.startsWith("@package/")) {
|
|
117
|
-
expect(range, `${pkgPath} ${dep}`).toBe("workspace:*");
|
|
118
|
-
} else {
|
|
119
|
-
expect(range, `${pkgPath} ${dep} leaked a workspace range`).not.toMatch(/^workspace:/u);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("renders template vars (no placeholders left)", () => {
|
|
126
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
127
|
-
expect(appPkg.name).toBe(`@app/${name}`);
|
|
128
|
-
const rootPkg = readJson(join(root, "package.json"));
|
|
129
|
-
expect(rootPkg.name).toBe(name);
|
|
130
|
-
const appConfig = readFileSync(join(root, `apps/${name}/app.config.js`), "utf-8");
|
|
131
|
-
expect(appConfig).toContain('name: "UniDemo"');
|
|
132
|
-
expect(appConfig).toContain("com.unidemo.app");
|
|
133
|
-
const viteConfig = readFileSync(join(root, `apps/${name}/vite.config.ts`), "utf-8");
|
|
134
|
-
expect(viteConfig).toContain('key: "UniDemo"');
|
|
135
|
-
for (const file of [
|
|
136
|
-
"package.json",
|
|
137
|
-
"README.md",
|
|
138
|
-
`apps/${name}/app.config.js`,
|
|
139
|
-
`features/${name}/src/HomeScreen.tsx`,
|
|
140
|
-
"packages/i18n/en/common.json",
|
|
141
|
-
]) {
|
|
142
|
-
expect(readFileSync(join(root, file), "utf-8")).not.toMatch(/__NAME(_PASCAL|_APPID)?__/u);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("wires universal scripts", () => {
|
|
147
|
-
const rootPkg = readJson(join(root, "package.json"));
|
|
148
|
-
for (const script of [
|
|
149
|
-
"dev",
|
|
150
|
-
"build",
|
|
151
|
-
"native",
|
|
152
|
-
"ios",
|
|
153
|
-
"android",
|
|
154
|
-
"test",
|
|
155
|
-
"lint",
|
|
156
|
-
"typecheck",
|
|
157
|
-
]) {
|
|
158
|
-
expect(rootPkg.scripts[script], `root script ${script}`).toBeDefined();
|
|
159
|
-
}
|
|
160
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
161
|
-
expect(appPkg.scripts.dev).toContain("one dev");
|
|
162
|
-
expect(appPkg.scripts.build).toBe("one build");
|
|
163
|
-
expect(appPkg.scripts["prebuild:native"]).toBe("one prebuild");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("app (web-only) template regression", () => {
|
|
168
|
-
const name = "web-demo";
|
|
169
|
-
let root: string;
|
|
170
|
-
|
|
171
|
-
beforeAll(async () => {
|
|
172
|
-
await initApp(name, {
|
|
173
|
-
template: "app",
|
|
174
|
-
skipInstall: true,
|
|
175
|
-
version: "6.1.0",
|
|
176
|
-
yes: true,
|
|
177
|
-
});
|
|
178
|
-
root = join(sandbox, name);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("keeps the web-only tree (no universal extras)", () => {
|
|
182
|
-
for (const file of [
|
|
183
|
-
"package.json",
|
|
184
|
-
".npmrc",
|
|
185
|
-
".gitignore",
|
|
186
|
-
`apps/${name}/vite.config.ts`,
|
|
187
|
-
`apps/${name}/src/main.tsx`,
|
|
188
|
-
`features/${name}/src/HomeScreen.tsx`,
|
|
189
|
-
"packages/config/package.json",
|
|
190
|
-
]) {
|
|
191
|
-
expect(existsSync(join(root, file)), `missing ${file}`).toBe(true);
|
|
192
|
-
}
|
|
193
|
-
expect(existsSync(join(root, "packages/i18n"))).toBe(false);
|
|
194
|
-
expect(existsSync(join(root, "packages/themes"))).toBe(false);
|
|
195
|
-
expect(existsSync(join(root, `apps/${name}/routes`))).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("keeps semver deps and web scripts", () => {
|
|
199
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
200
|
-
expect(appPkg.dependencies["@multiplatform.one/components"]).toBe("^6.1.0");
|
|
201
|
-
expect(appPkg.dependencies[`@features/${name}`]).toBe("workspace:*");
|
|
202
|
-
expect(appPkg.scripts.dev).toBe("vite");
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe("pieces-only template (adopted-project baselines)", () => {
|
|
207
|
-
const name = "adopt-base";
|
|
208
|
-
let root: string;
|
|
209
|
-
|
|
210
|
-
beforeAll(async () => {
|
|
211
|
-
await initApp(name, {
|
|
212
|
-
template: "none",
|
|
213
|
-
skipInstall: true,
|
|
214
|
-
skipGit: true,
|
|
215
|
-
version: "6.6.0",
|
|
216
|
-
yes: true,
|
|
217
|
-
pieces: ["webext"],
|
|
218
|
-
});
|
|
219
|
-
root = join(sandbox, name);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("overlays ONLY the piece fragments (no base template)", () => {
|
|
223
|
-
for (const file of [
|
|
224
|
-
`apps/${name}/vite.config.webext.ts`,
|
|
225
|
-
`apps/${name}/webext/manifest.ts`,
|
|
226
|
-
`apps/${name}/webext/background/main.ts`,
|
|
227
|
-
`apps/${name}/package.json`,
|
|
228
|
-
".gitignore",
|
|
229
|
-
"README.md",
|
|
230
|
-
"package.json",
|
|
231
|
-
]) {
|
|
232
|
-
expect(existsSync(join(root, file)), `missing ${file}`).toBe(true);
|
|
233
|
-
}
|
|
234
|
-
// Base-template content must NOT leak in.
|
|
235
|
-
for (const file of [
|
|
236
|
-
"pnpm-workspace.yaml",
|
|
237
|
-
"tsconfig.base.json",
|
|
238
|
-
`features/${name}`,
|
|
239
|
-
"packages/config",
|
|
240
|
-
`apps/${name}/routes`,
|
|
241
|
-
`apps/${name}/vite.config.ts`,
|
|
242
|
-
]) {
|
|
243
|
-
expect(existsSync(join(root, file)), `unexpected ${file}`).toBe(false);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("creates partial targets from the fragments alone", () => {
|
|
248
|
-
// Root package.json = the webext fragment only — no template fields.
|
|
249
|
-
const rootPkg = readJson(join(root, "package.json"));
|
|
250
|
-
expect(Object.keys(rootPkg)).toEqual(["scripts"]);
|
|
251
|
-
expect(rootPkg.scripts["build:webext"]).toBe(`pnpm --filter @app/${name} build:webext`);
|
|
252
|
-
const manifest = readFileSync(join(root, `apps/${name}/webext/manifest.ts`), "utf-8");
|
|
253
|
-
expect(manifest).toContain('const DISPLAY_NAME = "AdoptBase"');
|
|
254
|
-
expect(manifest).not.toMatch(/__NAME(_PASCAL|_APPID)?__/u);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("records template none in provenance", () => {
|
|
258
|
-
const provenance = readJson(join(root, ".mpo.json"));
|
|
259
|
-
expect(provenance.template).toBe("none");
|
|
260
|
-
expect(provenance.pieces).toEqual(["webext"]);
|
|
261
|
-
expect("reconciled" in provenance).toBe(false);
|
|
262
|
-
expect(readProvenance(root)?.template).toBe("none");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("refuses a pieces-only scaffold without pieces", async () => {
|
|
266
|
-
await expect(
|
|
267
|
-
initApp("no-pieces-base", {
|
|
268
|
-
template: "none",
|
|
269
|
-
skipInstall: true,
|
|
270
|
-
skipGit: true,
|
|
271
|
-
yes: true,
|
|
272
|
-
}),
|
|
273
|
-
).rejects.toThrow(/at least one piece/u);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("rejects invalid names", async () => {
|
|
278
|
-
await expect(
|
|
279
|
-
initApp("Bad Name", { template: "app", skipInstall: true, yes: true }),
|
|
280
|
-
).rejects.toThrow(/Invalid project name/u);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("refuses to scaffold into a non-empty directory", async () => {
|
|
284
|
-
await initApp("occupied", { template: "app", skipInstall: true, yes: true });
|
|
285
|
-
await expect(
|
|
286
|
-
initApp("occupied", { template: "app", skipInstall: true, yes: true }),
|
|
287
|
-
).rejects.toThrow(/already exists and is not empty/u);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
describe("composable pieces", () => {
|
|
291
|
-
describe("webext piece", () => {
|
|
292
|
-
const name = "ext-demo";
|
|
293
|
-
let root: string;
|
|
294
|
-
|
|
295
|
-
beforeAll(async () => {
|
|
296
|
-
await initApp(name, {
|
|
297
|
-
template: "universal",
|
|
298
|
-
skipInstall: true,
|
|
299
|
-
skipGit: true,
|
|
300
|
-
version: "6.1.0",
|
|
301
|
-
yes: true,
|
|
302
|
-
pieces: ["webext"],
|
|
303
|
-
});
|
|
304
|
-
root = join(sandbox, name);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("overlays the webext target files", () => {
|
|
308
|
-
for (const file of [
|
|
309
|
-
`apps/${name}/vite.config.webext.ts`,
|
|
310
|
-
`apps/${name}/vite.config.webext-background.ts`,
|
|
311
|
-
`apps/${name}/webext/env.ts`,
|
|
312
|
-
`apps/${name}/webext/manifest.ts`,
|
|
313
|
-
`apps/${name}/webext/prepare.ts`,
|
|
314
|
-
`apps/${name}/webext/tamagui.css`,
|
|
315
|
-
`apps/${name}/webext/tsconfig.json`,
|
|
316
|
-
`apps/${name}/webext/background/main.ts`,
|
|
317
|
-
`apps/${name}/webext/components/ErrorBoundary.tsx`,
|
|
318
|
-
`apps/${name}/webext/views/popup/index.html`,
|
|
319
|
-
`apps/${name}/webext/views/popup/main.tsx`,
|
|
320
|
-
`apps/${name}/webext/assets/fonts/inter-400.woff2`,
|
|
321
|
-
`apps/${name}/webext/assets/fonts/inter-700.woff2`,
|
|
322
|
-
`apps/${name}/webext/static/assets/icon-512.png`,
|
|
323
|
-
]) {
|
|
324
|
-
expect(existsSync(join(root, file)), `missing ${file}`).toBe(true);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("renders template vars in fragment files", () => {
|
|
329
|
-
const manifest = readFileSync(join(root, `apps/${name}/webext/manifest.ts`), "utf-8");
|
|
330
|
-
expect(manifest).toContain('const DISPLAY_NAME = "ExtDemo"');
|
|
331
|
-
for (const file of [
|
|
332
|
-
`apps/${name}/vite.config.webext.ts`,
|
|
333
|
-
`apps/${name}/webext/manifest.ts`,
|
|
334
|
-
`apps/${name}/webext/views/popup/main.tsx`,
|
|
335
|
-
]) {
|
|
336
|
-
expect(readFileSync(join(root, file), "utf-8")).not.toMatch(/__NAME(_PASCAL|_APPID)?__/u);
|
|
337
|
-
}
|
|
338
|
-
const popup = readFileSync(join(root, `apps/${name}/webext/views/popup/main.tsx`), "utf-8");
|
|
339
|
-
expect(popup).toContain(`from "@features/${name}"`);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("deep-merges package.json.partial into the app manifest", () => {
|
|
343
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
344
|
-
// base scripts survive, fragment scripts append
|
|
345
|
-
expect(appPkg.scripts.dev).toContain("one dev");
|
|
346
|
-
expect(appPkg.scripts["build:webext"]).toContain("vite.config.webext.ts");
|
|
347
|
-
expect(appPkg.scripts["typecheck:webext"]).toBe("tsc --noEmit -p webext/tsconfig.json");
|
|
348
|
-
// fragment deps merge in with rendered versions; base deps survive
|
|
349
|
-
expect(appPkg.dependencies["webextension-polyfill"]).toBe("^0.12.0");
|
|
350
|
-
expect(appPkg.dependencies["@multiplatform.one/router"]).toBe("^6.1.0");
|
|
351
|
-
expect(appPkg.dependencies[`@features/${name}`]).toBe("workspace:*");
|
|
352
|
-
expect(appPkg.devDependencies["@multiplatform.one/vite-plugin-webext"]).toBe("^6.1.0");
|
|
353
|
-
// dependency maps stay sorted after the merge
|
|
354
|
-
for (const field of ["dependencies", "devDependencies"]) {
|
|
355
|
-
const keys = Object.keys(appPkg[field]);
|
|
356
|
-
expect(keys, `${field} sorted`).toEqual([...keys].sort());
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("unions tsconfig types and root scripts, appends gitignore", () => {
|
|
361
|
-
const tsconfig = readJson(join(root, `apps/${name}/tsconfig.json`));
|
|
362
|
-
expect(tsconfig.compilerOptions.types).toEqual(["vite/client", "node"]);
|
|
363
|
-
const rootPkg = readJson(join(root, "package.json"));
|
|
364
|
-
expect(rootPkg.scripts["build:webext"]).toBe(`pnpm --filter @app/${name} build:webext`);
|
|
365
|
-
const gitignore = readFileSync(join(root, ".gitignore"), "utf-8");
|
|
366
|
-
expect(gitignore).toContain("node_modules");
|
|
367
|
-
expect(gitignore).toContain("dist-webext");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("records pieces in provenance", () => {
|
|
371
|
-
const provenance = readJson(join(root, ".mpo.json"));
|
|
372
|
-
expect(provenance.pieces).toEqual(["webext"]);
|
|
373
|
-
expect(readProvenance(root)?.pieces).toEqual(["webext"]);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
describe("frappe + keycloak pieces", () => {
|
|
378
|
-
const name = "backend-demo";
|
|
379
|
-
let root: string;
|
|
380
|
-
|
|
381
|
-
beforeAll(async () => {
|
|
382
|
-
await initApp(name, {
|
|
383
|
-
template: "universal",
|
|
384
|
-
skipInstall: true,
|
|
385
|
-
skipGit: true,
|
|
386
|
-
version: "6.1.0",
|
|
387
|
-
yes: true,
|
|
388
|
-
// intentionally unsorted — provenance must sort
|
|
389
|
-
pieces: ["keycloak", "frappe"],
|
|
390
|
-
});
|
|
391
|
-
root = join(sandbox, name);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("generates src/app.ts from the selected providers", () => {
|
|
395
|
-
const appTs = readFileSync(join(root, `apps/${name}/src/app.ts`), "utf-8");
|
|
396
|
-
expect(appTs).toContain('import { config } from "@multiplatform.one/platform"');
|
|
397
|
-
expect(appTs).toContain('frappe: { baseURL: config.get("FRAPPE_URL") }');
|
|
398
|
-
expect(appTs).toContain('keycloak: { baseUrl: config.get("KEYCLOAK_BASE_URL") }');
|
|
399
|
-
expect(appTs).toContain("theme: themeConfig");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("accumulates public config keys across pieces", () => {
|
|
403
|
-
const config = readJson(join(root, "packages/config/config.json"));
|
|
404
|
-
expect(config.public).toEqual([
|
|
405
|
-
"BASE_URL",
|
|
406
|
-
"DEBUG",
|
|
407
|
-
"I18N_DEFAULT_LANGUAGE",
|
|
408
|
-
"FRAPPE_ENABLED",
|
|
409
|
-
"FRAPPE_URL",
|
|
410
|
-
"KEYCLOAK_ENABLED",
|
|
411
|
-
"KEYCLOAK_BASE_URL",
|
|
412
|
-
"KEYCLOAK_REALM",
|
|
413
|
-
"KEYCLOAK_CLIENT_ID",
|
|
414
|
-
"KEYCLOAK_PUBLIC_CLIENT_ID",
|
|
415
|
-
]);
|
|
416
|
-
expect(config.private).toEqual(["SECRET"]);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it("writes env, docker, and README fragments", () => {
|
|
420
|
-
const env = readFileSync(join(root, ".env.example"), "utf-8");
|
|
421
|
-
expect(env).toContain("FRAPPE_ENABLED=1");
|
|
422
|
-
expect(env).toContain("KEYCLOAK_ENABLED=1");
|
|
423
|
-
expect(env).toContain(`KEYCLOAK_REALM=${name}`);
|
|
424
|
-
expect(existsSync(join(root, "docker/compose.frappe.yaml"))).toBe(true);
|
|
425
|
-
expect(existsSync(join(root, "docker/compose.keycloak.yaml"))).toBe(true);
|
|
426
|
-
const realm = readJson(join(root, "docker/keycloak/realm.json"));
|
|
427
|
-
expect(realm.realm).toBe(name);
|
|
428
|
-
expect(realm.clients[1].clientId).toBe(`${name}-public`);
|
|
429
|
-
const readme = readFileSync(join(root, "README.md"), "utf-8");
|
|
430
|
-
expect(readme).toContain("## Frappe backend");
|
|
431
|
-
expect(readme).toContain("## Keycloak auth");
|
|
432
|
-
// Fixtures are a supported way to run the generated app (MPO-228),
|
|
433
|
-
// so the README has to say how.
|
|
434
|
-
expect(readme).toContain("### Running against fixtures, with no bench");
|
|
435
|
-
expect(readme).toContain("frappe: { fixtures }");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Two measured first-boot failures, both cheap to reintroduce. The
|
|
439
|
-
// keycloak image ships /opt/keycloak/data (keycloak-owned) and no
|
|
440
|
-
// h2/ subdirectory: a named volume mounted at data/h2 materializes
|
|
441
|
-
// root-owned, and Keycloak (uid 1000) dies with H2 90028 on a FRESH
|
|
442
|
-
// volume — it reads like corruption and survives `down -v`, because
|
|
443
|
-
// the wipe recreates the same root-owned mount point. And a
|
|
444
|
-
// hardcoded 8080 collides with whatever already owns the most
|
|
445
|
-
// contested dev port; a yaml !override in a second compose file is
|
|
446
|
-
// not a safe alternative (compose versions without the tag drop the
|
|
447
|
-
// whole ports list silently — container healthy, unreachable).
|
|
448
|
-
it("keeps the dev keycloak bootable: data/ volume, parameterized port", () => {
|
|
449
|
-
const compose = readFileSync(join(root, "docker/compose.keycloak.yaml"), "utf-8");
|
|
450
|
-
expect(compose).toContain('"${KEYCLOAK_HTTP_PORT:-8080}:8080"');
|
|
451
|
-
expect(
|
|
452
|
-
compose,
|
|
453
|
-
"volume mounts data/, which the image ships with keycloak ownership",
|
|
454
|
-
).toMatch(/^\s*- keycloak:\/opt\/keycloak\/data$/mu);
|
|
455
|
-
expect(compose).not.toMatch(/^\s*- keycloak:\/opt\/keycloak\/data\/h2$/mu);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it("records pieces sorted in provenance", () => {
|
|
459
|
-
expect(readJson(join(root, ".mpo.json")).pieces).toEqual(["frappe", "keycloak"]);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
describe("kitchen sink (all pieces)", () => {
|
|
464
|
-
const name = "full-demo";
|
|
465
|
-
let root: string;
|
|
466
|
-
|
|
467
|
-
beforeAll(async () => {
|
|
468
|
-
await initApp(name, {
|
|
469
|
-
template: "universal",
|
|
470
|
-
skipInstall: true,
|
|
471
|
-
skipGit: true,
|
|
472
|
-
version: "6.1.0",
|
|
473
|
-
yes: true,
|
|
474
|
-
pieces: ["frappe", "gnome", "keycloak", "tauri", "vscode", "webext"],
|
|
475
|
-
});
|
|
476
|
-
root = join(sandbox, name);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it("overlays every target", () => {
|
|
480
|
-
for (const file of [
|
|
481
|
-
`apps/${name}/vite.config.gnome.ts`,
|
|
482
|
-
`apps/${name}/gnome/main.tsx`,
|
|
483
|
-
`apps/${name}/gnome/polyfills.ts`,
|
|
484
|
-
`apps/${name}/gnome/anchor.tsx`,
|
|
485
|
-
`apps/${name}/gnome/tamagui-barrel.ts`,
|
|
486
|
-
`apps/${name}/gnome/shims/components.ts`,
|
|
487
|
-
`apps/${name}/gnome/shims/theme.ts`,
|
|
488
|
-
`apps/${name}/gnome/shims/forms.ts`,
|
|
489
|
-
`apps/${name}/gnome/shims/frappe-ui.ts`,
|
|
490
|
-
`apps/${name}/gnome/shims/one.ts`,
|
|
491
|
-
`apps/${name}/gnome/shims/empty-module.ts`,
|
|
492
|
-
`apps/${name}/vite.config.vscode.ts`,
|
|
493
|
-
`apps/${name}/vscode/extension/index.ts`,
|
|
494
|
-
`apps/${name}/vscode/webview/main.tsx`,
|
|
495
|
-
`apps/${name}/vscode/webview/router.tsx`,
|
|
496
|
-
`apps/${name}/vscode/package.json`,
|
|
497
|
-
`apps/${name}/vscode/manifest.ts`,
|
|
498
|
-
`apps/${name}/vite.config.webext.ts`,
|
|
499
|
-
`apps/${name}/webext/manifest.ts`,
|
|
500
|
-
`apps/${name}/src-tauri/tauri.conf.json`,
|
|
501
|
-
`apps/${name}/src-tauri/Cargo.toml`,
|
|
502
|
-
`apps/${name}/src-tauri/build.rs`,
|
|
503
|
-
`apps/${name}/src-tauri/src/main.rs`,
|
|
504
|
-
`apps/${name}/src-tauri/src/lib.rs`,
|
|
505
|
-
`apps/${name}/src-tauri/capabilities/default.json`,
|
|
506
|
-
`apps/${name}/src-tauri/icons/icon.icns`,
|
|
507
|
-
"docker/compose.frappe.yaml",
|
|
508
|
-
"docker/compose.keycloak.yaml",
|
|
509
|
-
]) {
|
|
510
|
-
expect(existsSync(join(root, file)), `missing ${file}`).toBe(true);
|
|
511
|
-
}
|
|
512
|
-
const rootPkg = readJson(join(root, "package.json"));
|
|
513
|
-
for (const script of [
|
|
514
|
-
"gnome",
|
|
515
|
-
"gnome:capture",
|
|
516
|
-
"build:vscode",
|
|
517
|
-
"build:webext",
|
|
518
|
-
"tauri:dev",
|
|
519
|
-
"tauri:build",
|
|
520
|
-
]) {
|
|
521
|
-
expect(rootPkg.scripts[script], `root script ${script}`).toBeDefined();
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// Tauri and GNOME are peer desktop targets — the kitchen sink proves
|
|
526
|
-
// they co-select: one scaffold carries both the src-tauri webview
|
|
527
|
-
// shell and the GTK/GJS gnome target side by side.
|
|
528
|
-
it("keeps tauri and gnome as co-selected desktop targets", () => {
|
|
529
|
-
expect(existsSync(join(root, `apps/${name}/src-tauri/tauri.conf.json`))).toBe(true);
|
|
530
|
-
expect(existsSync(join(root, `apps/${name}/vite.config.gnome.ts`))).toBe(true);
|
|
531
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
532
|
-
expect(appPkg.scripts["tauri:dev"]).toBe("tauri dev");
|
|
533
|
-
expect(appPkg.devDependencies["@tauri-apps/cli"]).toBeDefined();
|
|
534
|
-
expect(appPkg.scripts.gnome).toBeDefined();
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// The GNOME target is the only piece that rewires module resolution
|
|
538
|
-
// rather than just adding files, and both halves of that rewiring
|
|
539
|
-
// fail silently if they drift. A shim that imports the bare barrel
|
|
540
|
-
// it is aliased FROM resolves back to itself — an infinite alias
|
|
541
|
-
// loop that surfaces as an out-of-memory bundler, not an import
|
|
542
|
-
// error. And the react-gnome packages are the runtime: without them
|
|
543
|
-
// the entry resolves nothing at all.
|
|
544
|
-
it("wires the gnome target without aliasing a shim back to itself", () => {
|
|
545
|
-
const aliased = ["components", "theme", "forms", "frappe-ui"] as const;
|
|
546
|
-
for (const barrel of aliased) {
|
|
547
|
-
const shim = readFileSync(join(root, `apps/${name}/gnome/shims/${barrel}.ts`), "utf-8");
|
|
548
|
-
expect(shim, `${barrel} shim must deep-import, never the bare barrel`).not.toMatch(
|
|
549
|
-
new RegExp(String.raw`from "@multiplatform\.one/${barrel}"`, "u"),
|
|
550
|
-
);
|
|
551
|
-
expect(shim, `${barrel} shim reaches its subgraph`).toMatch(
|
|
552
|
-
new RegExp(String.raw`@multiplatform\.one/${barrel}/src/`, "u"),
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
const appPkg = readJson(join(root, `apps/${name}/package.json`));
|
|
556
|
-
for (const dep of [
|
|
557
|
-
"@react-gnome/renderer",
|
|
558
|
-
"@react-gnome/react-native",
|
|
559
|
-
"@react-gnome/animation",
|
|
560
|
-
"@react-gnome/tamagui",
|
|
561
|
-
]) {
|
|
562
|
-
expect(appPkg.dependencies[dep], `runtime dep ${dep}`).toBeDefined();
|
|
563
|
-
}
|
|
564
|
-
expect(appPkg.devDependencies["@react-gnome/vite-plugin"]).toBeDefined();
|
|
565
|
-
expect(appPkg.devDependencies["@multiplatform.one/vite-plugin-gnome"]).toBeDefined();
|
|
566
|
-
// polyfills must be the FIRST import: the globals have to exist
|
|
567
|
-
// before any hoisted module initializer probes for them.
|
|
568
|
-
const entry = readFileSync(join(root, `apps/${name}/gnome/main.tsx`), "utf-8");
|
|
569
|
-
const firstImport = entry.split("\n").find((line) => line.startsWith("import "));
|
|
570
|
-
expect(firstImport).toBe(`import "./polyfills";`);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// The other half of that rewiring, and the half that fails most
|
|
574
|
-
// quietly. `@multiplatform.one/platform` ships its GNOME flags as
|
|
575
|
-
// `index.gnome.ts`, and the ONLY thing that selects that file is
|
|
576
|
-
// `.gnome.*` sitting ahead of react-gnome's `.native.*` in
|
|
577
|
-
// resolve.extensions. Drop the registration and a scaffolded project
|
|
578
|
-
// still builds, still runs, and reports `isGnome: false` forever.
|
|
579
|
-
it("registers .gnome.* ahead of .native.* so isGnome resolves", () => {
|
|
580
|
-
const config = readFileSync(join(root, `apps/${name}/vite.config.gnome.ts`), "utf-8");
|
|
581
|
-
expect(config, "gnomePlatformExtensions must be imported").toMatch(
|
|
582
|
-
/import\s*\{[^}]*\bgnomePlatformExtensions\b[^}]*\}\s*from\s*"@multiplatform\.one\/vite-plugin-gnome"/u,
|
|
583
|
-
);
|
|
584
|
-
expect(config, ".gnome.* must be registered ahead of .native.*").toMatch(
|
|
585
|
-
/extensions:\s*\[\s*\.\.\.gnomePlatformExtensions\s*,\s*\.\.\.gnomeExtensions\s*,?\s*\]/u,
|
|
586
|
-
);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// The @react-gnome aliases must not assume the scope sits in the
|
|
590
|
-
// app's OWN node_modules: the scaffold's .npmrc sets
|
|
591
|
-
// node-linker=hoisted, so every dependency hoists to the workspace
|
|
592
|
-
// root, the app-local dir never exists, and every alias points at a
|
|
593
|
-
// nonexistent path (measured — the build fails resolving the
|
|
594
|
-
// renderer). require.resolve cannot do the lookup either, because
|
|
595
|
-
// the @react-gnome export maps hide package.json; the config walks
|
|
596
|
-
// up to whichever node_modules holds the scope. And the registry
|
|
597
|
-
// tarballs ship dist ONLY (files: ["dist", …]), so the renderer
|
|
598
|
-
// alias needs a dist fallback when there is no src checkout.
|
|
599
|
-
it("finds @react-gnome by walking up, with a dist fallback for registry tarballs", () => {
|
|
600
|
-
const config = readFileSync(join(root, `apps/${name}/vite.config.gnome.ts`), "utf-8");
|
|
601
|
-
expect(config, "walk-up helper must exist").toContain("function reactGnomeScopeDir()");
|
|
602
|
-
expect(config, "aliases must use the walk-up, not the app-local dir").toMatch(
|
|
603
|
-
/gnomeLinkedAliases\(reactGnomeScopeDir\(\)\)/u,
|
|
604
|
-
);
|
|
605
|
-
expect(config, "renderer alias needs the dist fallback").toContain(
|
|
606
|
-
"renderer/dist/index.js",
|
|
607
|
-
);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it("leaves no template placeholders anywhere", () => {
|
|
611
|
-
const binary = new Set([".woff2", ".png", ".ico", ".icns"]);
|
|
612
|
-
const walk = (dir: string): string[] =>
|
|
613
|
-
readdirSync(dir).flatMap((entry) => {
|
|
614
|
-
const path = join(dir, entry);
|
|
615
|
-
if (statSync(path).isDirectory()) return walk(path);
|
|
616
|
-
return [path];
|
|
617
|
-
});
|
|
618
|
-
for (const file of walk(root)) {
|
|
619
|
-
if (binary.has(extname(file).toLowerCase())) continue;
|
|
620
|
-
expect(readFileSync(file, "utf-8"), `placeholder left in ${file}`).not.toMatch(
|
|
621
|
-
/__NAME(_PASCAL|_APPID)?__|__MPO_VERSION__|__YEAR__/u,
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
it("scaffolds deterministically (update baselines depend on it)", async () => {
|
|
627
|
-
// Same name in a different parent dir (paths and rendered content
|
|
628
|
-
// embed the project name); pieces passed in a different order.
|
|
629
|
-
const parent = mkdtempSync(join(tmpdir(), "mpo-again-"));
|
|
630
|
-
const cwd = process.cwd();
|
|
631
|
-
process.chdir(parent);
|
|
632
|
-
try {
|
|
633
|
-
await initApp(name, {
|
|
634
|
-
template: "universal",
|
|
635
|
-
skipInstall: true,
|
|
636
|
-
skipGit: true,
|
|
637
|
-
version: "6.1.0",
|
|
638
|
-
yes: true,
|
|
639
|
-
pieces: ["webext", "vscode", "tauri", "keycloak", "gnome", "frappe"],
|
|
640
|
-
});
|
|
641
|
-
} finally {
|
|
642
|
-
process.chdir(cwd);
|
|
643
|
-
}
|
|
644
|
-
const other = join(parent, name);
|
|
645
|
-
const rel = (dir: string, base: string): string[] =>
|
|
646
|
-
readdirSync(dir).flatMap((entry) => {
|
|
647
|
-
const path = join(dir, entry);
|
|
648
|
-
if (statSync(path).isDirectory()) return rel(path, base);
|
|
649
|
-
return [path.slice(base.length + 1)];
|
|
650
|
-
});
|
|
651
|
-
const ours = rel(root, root).sort();
|
|
652
|
-
expect(rel(other, other).sort()).toEqual(ours);
|
|
653
|
-
for (const file of ours) {
|
|
654
|
-
const a = readFileSync(join(root, file));
|
|
655
|
-
const b = readFileSync(join(other, file));
|
|
656
|
-
expect(b.equals(a), `content differs for ${file}`).toBe(true);
|
|
657
|
-
}
|
|
658
|
-
rmSync(parent, { recursive: true, force: true });
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it("rejects pieces on the web-only template", async () => {
|
|
663
|
-
await expect(
|
|
664
|
-
initApp("web-pieces", {
|
|
665
|
-
template: "app",
|
|
666
|
-
skipInstall: true,
|
|
667
|
-
skipGit: true,
|
|
668
|
-
yes: true,
|
|
669
|
-
pieces: ["webext"],
|
|
670
|
-
}),
|
|
671
|
-
).rejects.toThrow(/not available for the app template/u);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
it("rejects unknown pieces", async () => {
|
|
675
|
-
await expect(
|
|
676
|
-
initApp("bogus-pieces", {
|
|
677
|
-
template: "universal",
|
|
678
|
-
skipInstall: true,
|
|
679
|
-
skipGit: true,
|
|
680
|
-
yes: true,
|
|
681
|
-
pieces: ["solana" as never],
|
|
682
|
-
}),
|
|
683
|
-
).rejects.toThrow(/Unknown piece "solana"/u);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it("defaults provenance pieces to [] for pre-piece scaffolds", () => {
|
|
687
|
-
const legacy = mkdtempSync(join(tmpdir(), "mpo-legacy-prov-"));
|
|
688
|
-
writeFileSync(
|
|
689
|
-
join(legacy, ".mpo.json"),
|
|
690
|
-
JSON.stringify({
|
|
691
|
-
template: "universal",
|
|
692
|
-
cliVersion: "6.4.0",
|
|
693
|
-
name: "legacy",
|
|
694
|
-
mpoVersion: "^6.4.0",
|
|
695
|
-
}),
|
|
696
|
-
);
|
|
697
|
-
expect(readProvenance(legacy)?.pieces).toEqual([]);
|
|
698
|
-
rmSync(legacy, { recursive: true, force: true });
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
describe("mergeJsonValues", () => {
|
|
703
|
-
it("unions arrays preserving base order", () => {
|
|
704
|
-
expect(mergeJsonValues(["a", "b"], ["b", "c"])).toEqual(["a", "b", "c"]);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
it("merges nested objects with fragment scalars winning", () => {
|
|
708
|
-
expect(
|
|
709
|
-
mergeJsonValues(
|
|
710
|
-
{ scripts: { dev: "vite" }, private: true },
|
|
711
|
-
{ scripts: { build: "vite build", dev: "vite --host" } },
|
|
712
|
-
),
|
|
713
|
-
).toEqual({ scripts: { dev: "vite --host", build: "vite build" }, private: true });
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
it("replaces on type mismatch", () => {
|
|
717
|
-
expect(mergeJsonValues({ a: 1 }, ["x"])).toEqual(["x"]);
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
});
|