@multiplatform.one/cli 6.1.0 → 6.4.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 +72 -0
- package/bin/createMultiplatformApp.mjs +77 -0
- package/lib/bin/multiplatformOne.mjs +35 -10
- package/lib/commands/e2e.mjs +3 -3
- package/lib/commands/init.mjs +16 -12
- package/lib/commands/initApp.mjs +175 -0
- package/lib/commands/updateApp.mjs +260 -0
- package/lib/generateVscode.mjs +27 -10
- package/package.json +23 -12
- package/src/bin/multiplatformOne.ts +83 -20
- package/src/commands/init.ts +7 -18
- package/src/commands/initApp.spec.ts +210 -0
- package/src/commands/initApp.ts +274 -0
- package/src/commands/updateApp.ts +277 -0
- package/src/generateVscode.ts +31 -9
- package/templates/app/AGENTS.md +142 -0
- package/templates/app/README.md +62 -0
- package/templates/app/apps/__NAME__/index.html +36 -0
- package/templates/app/apps/__NAME__/package.json +30 -0
- 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 +9 -0
- package/templates/app/apps/__NAME__/src/main.tsx +14 -0
- package/templates/app/apps/__NAME__/tsconfig.json +8 -0
- package/templates/app/apps/__NAME__/vite.config.ts +17 -0
- package/templates/app/features/__NAME__/package.json +34 -0
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +70 -0
- package/templates/app/features/__NAME__/src/index.ts +1 -0
- package/templates/app/features/__NAME__/tsconfig.json +5 -0
- package/templates/app/gitignore +8 -0
- package/templates/app/npmrc +2 -0
- package/templates/app/oxlintrc.json +39 -0
- package/templates/app/package.json +20 -0
- package/templates/app/packages/config/config.json +4 -0
- package/templates/app/packages/config/index.ts +5 -0
- package/templates/app/packages/config/package.json +17 -0
- package/templates/app/packages/config/tsconfig.json +5 -0
- package/templates/app/pnpm-workspace.yaml +4 -0
- package/templates/app/tsconfig.base.json +16 -0
- package/templates/universal/AGENTS.md +142 -0
- package/templates/universal/README.md +97 -0
- package/templates/universal/apps/__NAME__/app.config.js +31 -0
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +4 -0
- package/templates/universal/apps/__NAME__/package.json +61 -0
- package/templates/universal/apps/__NAME__/polyfills.native.ts +23 -0
- package/templates/universal/apps/__NAME__/polyfills.ts +3 -0
- 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 +22 -0
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +21 -0
- package/templates/universal/apps/__NAME__/routes/about.tsx +3 -0
- package/templates/universal/apps/__NAME__/routes/index.tsx +3 -0
- package/templates/universal/apps/__NAME__/src/app.ts +10 -0
- package/templates/universal/apps/__NAME__/tamagui.css +3 -0
- package/templates/universal/apps/__NAME__/tsconfig.json +8 -0
- package/templates/universal/apps/__NAME__/types.d.ts +9 -0
- package/templates/universal/apps/__NAME__/vite.config.ts +66 -0
- package/templates/universal/features/__NAME__/package.json +35 -0
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +27 -0
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +50 -0
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +22 -0
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +55 -0
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +40 -0
- package/templates/universal/features/__NAME__/src/index.ts +4 -0
- package/templates/universal/features/__NAME__/src/screens.spec.ts +12 -0
- package/templates/universal/features/__NAME__/tsconfig.json +5 -0
- package/templates/universal/features/__NAME__/vitest.config.mjs +7 -0
- package/templates/universal/gitignore +12 -0
- package/templates/universal/npmrc +3 -0
- package/templates/universal/oxlintrc.json +39 -0
- package/templates/universal/package.json +27 -0
- package/templates/universal/packages/config/config.json +4 -0
- package/templates/universal/packages/config/index.ts +5 -0
- package/templates/universal/packages/config/package.json +17 -0
- package/templates/universal/packages/config/tsconfig.json +5 -0
- package/templates/universal/packages/i18n/en/common.json +30 -0
- package/templates/universal/packages/i18n/es/common.json +30 -0
- package/templates/universal/packages/i18n/i18n.d.ts +16 -0
- package/templates/universal/packages/i18n/index.ts +26 -0
- package/templates/universal/packages/i18n/package.json +20 -0
- package/templates/universal/packages/i18n/resources.ts +13 -0
- package/templates/universal/packages/i18n/tsconfig.json +5 -0
- package/templates/universal/packages/themes/accent.ts +32 -0
- package/templates/universal/packages/themes/base.ts +50 -0
- package/templates/universal/packages/themes/fonts.ts +8 -0
- package/templates/universal/packages/themes/index.ts +33 -0
- package/templates/universal/packages/themes/package.json +23 -0
- package/templates/universal/packages/themes/tamagui.config.ts +14 -0
- package/templates/universal/packages/themes/tsconfig.json +5 -0
- package/templates/universal/pnpm-workspace.yaml +4 -0
- package/templates/universal/tsconfig.base.json +21 -0
- package/templates/universal/types/env.d.ts +9 -0
- package/types/bin/multiplatformOne.d.ts +11 -0
- package/types/bin/multiplatformOne.d.ts.map +1 -0
- package/types/commands/e2e.d.ts +14 -0
- package/types/commands/e2e.d.ts.map +1 -0
- package/types/commands/init.d.ts +16 -0
- package/types/commands/init.d.ts.map +1 -0
- package/types/commands/initApp.d.ts +41 -0
- package/types/commands/initApp.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +24 -0
- package/types/commands/updateApp.d.ts.map +1 -0
- package/types/generateVscode.d.ts +7 -0
- package/types/generateVscode.d.ts.map +1 -0
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -0
- package/types/types.d.ts +6 -0
- package/types/types.d.ts.map +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo update` for scaffolded consumer projects — the modern successor of
|
|
3
|
+
* the old cookiecutter flow (init a fresh project, swap your .git in, merge
|
|
4
|
+
* your changes on top), rebuilt with a TRUE three-way merge:
|
|
5
|
+
*
|
|
6
|
+
* base = the ORIGINAL scaffold, regenerated from the CLI version recorded
|
|
7
|
+
* in .mpo.json at init time (published CLIs ship their templates,
|
|
8
|
+
* so `pnpm dlx @multiplatform.one/cli@<old> init` reproduces it)
|
|
9
|
+
* ours = the project's HEAD (all your custom changes)
|
|
10
|
+
* theirs= a fresh scaffold from the CURRENT CLI version
|
|
11
|
+
*
|
|
12
|
+
* `git merge-tree --write-tree --merge-base=<base>` merges template
|
|
13
|
+
* evolution with your customizations; conflicts land in the worktree with
|
|
14
|
+
* normal conflict markers. An `.updateignore` file (one pathspec per line)
|
|
15
|
+
* pins matching paths to your HEAD version, exactly like the old script.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import spawn from "nano-spawn";
|
|
22
|
+
import { cliVersion, initApp, readProvenance, writeProvenance } from "./initApp";
|
|
23
|
+
|
|
24
|
+
export interface UpdateAppOptions {
|
|
25
|
+
/** Skip pnpm install after the merge. */
|
|
26
|
+
skipInstall?: boolean;
|
|
27
|
+
/** Override the semver range written for @multiplatform.one/* deps. */
|
|
28
|
+
version?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function git(projectDir: string, args: string[], env?: Record<string, string>) {
|
|
32
|
+
return spawn("git", args, {
|
|
33
|
+
cwd: projectDir,
|
|
34
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function gitOut(projectDir: string, args: string[], env?: Record<string, string>) {
|
|
39
|
+
const result = await git(projectDir, args, env);
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Import a directory tree into the project's object database and return a
|
|
44
|
+
* root-less commit for it (temp index; the worktree is never touched). */
|
|
45
|
+
async function commitTreeFromDir(
|
|
46
|
+
projectDir: string,
|
|
47
|
+
dir: string,
|
|
48
|
+
message: string,
|
|
49
|
+
parent?: string,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-index-")), "index");
|
|
52
|
+
const env = { GIT_INDEX_FILE: indexFile, GIT_WORK_TREE: dir };
|
|
53
|
+
await git(projectDir, ["add", "-A", "--force", "--", "."], env);
|
|
54
|
+
const tree = await gitOut(projectDir, ["write-tree"], env);
|
|
55
|
+
const commitArgs = parent
|
|
56
|
+
? ["commit-tree", tree, "-p", parent, "-m", message]
|
|
57
|
+
: ["commit-tree", tree, "-m", message];
|
|
58
|
+
const commit = await gitOut(projectDir, commitArgs);
|
|
59
|
+
rmSync(indexFile, { force: true });
|
|
60
|
+
return commit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Scaffold a project with a specific published CLI version into tmp and
|
|
64
|
+
* return the generated project dir. Falls back to the local generator when
|
|
65
|
+
* the requested version matches the running CLI (or dlx fails). */
|
|
66
|
+
async function scaffoldBaseline(
|
|
67
|
+
version: string,
|
|
68
|
+
name: string,
|
|
69
|
+
template: "universal" | "app",
|
|
70
|
+
mpoVersion: string,
|
|
71
|
+
): Promise<{ dir: string; cleanup: () => void }> {
|
|
72
|
+
const parent = mkdtempSync(join(tmpdir(), "mpo-update-"));
|
|
73
|
+
const cleanup = () => rmSync(parent, { recursive: true, force: true });
|
|
74
|
+
const current = cliVersion();
|
|
75
|
+
const templateFlag = template === "app" ? ["--web"] : ["--universal"];
|
|
76
|
+
if (version !== current) {
|
|
77
|
+
try {
|
|
78
|
+
await spawn(
|
|
79
|
+
"pnpm",
|
|
80
|
+
[
|
|
81
|
+
"--package",
|
|
82
|
+
`@multiplatform.one/cli@${version}`,
|
|
83
|
+
"dlx",
|
|
84
|
+
"mpo",
|
|
85
|
+
"init",
|
|
86
|
+
name,
|
|
87
|
+
"--yes",
|
|
88
|
+
"--skip-install",
|
|
89
|
+
"--mpo-version",
|
|
90
|
+
mpoVersion,
|
|
91
|
+
...templateFlag,
|
|
92
|
+
],
|
|
93
|
+
{ cwd: parent, stdio: "inherit" },
|
|
94
|
+
);
|
|
95
|
+
const dir = join(parent, name);
|
|
96
|
+
if (existsSync(join(dir, "package.json"))) return { dir, cleanup };
|
|
97
|
+
console.warn(`⚠️ dlx scaffold for @multiplatform.one/cli@${version} produced no project;`);
|
|
98
|
+
} catch {
|
|
99
|
+
console.warn(
|
|
100
|
+
`⚠️ could not scaffold baseline with @multiplatform.one/cli@${version} (network?);`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
console.warn(" falling back to the current CLI's template as the merge base.");
|
|
104
|
+
}
|
|
105
|
+
const previousCwd = process.cwd();
|
|
106
|
+
process.chdir(parent);
|
|
107
|
+
try {
|
|
108
|
+
await initApp(name, {
|
|
109
|
+
skipInstall: true,
|
|
110
|
+
yes: true,
|
|
111
|
+
template,
|
|
112
|
+
version: mpoVersion,
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
process.chdir(previousCwd);
|
|
116
|
+
}
|
|
117
|
+
return { dir: join(parent, name), cleanup };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function updateApp(options: UpdateAppOptions = {}): Promise<void> {
|
|
121
|
+
const projectDir = await gitOut(process.cwd(), ["rev-parse", "--show-toplevel"]).catch(() => {
|
|
122
|
+
throw new Error("mpo update must run inside a git repository");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Clean tree required — the merge lands in the worktree.
|
|
126
|
+
await git(projectDir, ["diff", "--quiet"]).catch(() => {
|
|
127
|
+
throw new Error("mpo update requires a clean working tree (unstaged changes present)");
|
|
128
|
+
});
|
|
129
|
+
await git(projectDir, ["diff", "--cached", "--quiet"]).catch(() => {
|
|
130
|
+
throw new Error("mpo update requires a clean working tree (staged changes present)");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const provenance = readProvenance(projectDir);
|
|
134
|
+
if (!provenance) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
".mpo.json not found — this project predates scaffold provenance. " +
|
|
137
|
+
"Create it with { template, cliVersion, name } matching how the project " +
|
|
138
|
+
"was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), " +
|
|
139
|
+
"then re-run `mpo update`.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const currentVersion = cliVersion() ?? "0.0.0";
|
|
144
|
+
const targetMpoRange = options.version
|
|
145
|
+
? options.version.startsWith("^") || options.version.startsWith("~")
|
|
146
|
+
? options.version
|
|
147
|
+
: `^${options.version}`
|
|
148
|
+
: `^${currentVersion}`;
|
|
149
|
+
|
|
150
|
+
console.log(`\nmpo update: ${provenance.cliVersion} → ${currentVersion}`);
|
|
151
|
+
console.log(` template: ${provenance.template}`);
|
|
152
|
+
console.log(` project: ${provenance.name}\n`);
|
|
153
|
+
|
|
154
|
+
// 1. Regenerate the ORIGINAL scaffold (merge base).
|
|
155
|
+
console.log(`Scaffolding merge base (cli@${provenance.cliVersion})...`);
|
|
156
|
+
const base = await scaffoldBaseline(
|
|
157
|
+
provenance.cliVersion,
|
|
158
|
+
provenance.name,
|
|
159
|
+
provenance.template,
|
|
160
|
+
provenance.mpoVersion,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// 2. Generate the CURRENT scaffold (theirs).
|
|
164
|
+
console.log(`Scaffolding update target (cli@${currentVersion})...`);
|
|
165
|
+
const next = await scaffoldBaseline(
|
|
166
|
+
currentVersion,
|
|
167
|
+
provenance.name,
|
|
168
|
+
provenance.template,
|
|
169
|
+
targetMpoRange,
|
|
170
|
+
);
|
|
171
|
+
writeProvenance(next.dir, {
|
|
172
|
+
template: provenance.template,
|
|
173
|
+
cliVersion: currentVersion,
|
|
174
|
+
name: provenance.name,
|
|
175
|
+
mpoVersion: targetMpoRange,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// 3. Three-way merge: base → HEAD (ours) vs base → next (theirs).
|
|
180
|
+
const head = await gitOut(projectDir, ["rev-parse", "HEAD"]);
|
|
181
|
+
const baseCommit = await commitTreeFromDir(
|
|
182
|
+
projectDir,
|
|
183
|
+
base.dir,
|
|
184
|
+
`mpo scaffold ${provenance.template}@${provenance.cliVersion}`,
|
|
185
|
+
);
|
|
186
|
+
const nextCommit = await commitTreeFromDir(
|
|
187
|
+
projectDir,
|
|
188
|
+
next.dir,
|
|
189
|
+
`mpo scaffold ${provenance.template}@${currentVersion}`,
|
|
190
|
+
baseCommit,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let mergedTree: string;
|
|
194
|
+
let conflicts: string[] = [];
|
|
195
|
+
try {
|
|
196
|
+
const out = await gitOut(projectDir, [
|
|
197
|
+
"merge-tree",
|
|
198
|
+
"--write-tree",
|
|
199
|
+
"--name-only",
|
|
200
|
+
`--merge-base=${baseCommit}`,
|
|
201
|
+
head,
|
|
202
|
+
nextCommit,
|
|
203
|
+
]);
|
|
204
|
+
mergedTree = out.split("\n")[0]!.trim();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Exit code 1 = conflicts; stdout still carries tree + conflict names.
|
|
207
|
+
const stdout = (err as { stdout?: string }).stdout ?? "";
|
|
208
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
209
|
+
if (!lines.length) throw err;
|
|
210
|
+
mergedTree = lines[0]!.trim();
|
|
211
|
+
conflicts = lines.slice(1).map((line) => line.trim());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The merged .mpo.json can interleave ours/theirs lines (add/add
|
|
215
|
+
// two-way merge) — the post-merge provenance is always written
|
|
216
|
+
// authoritatively below.
|
|
217
|
+
const finalProvenance = {
|
|
218
|
+
template: provenance.template,
|
|
219
|
+
cliVersion: currentVersion,
|
|
220
|
+
name: provenance.name,
|
|
221
|
+
mpoVersion: targetMpoRange,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (!conflicts.length) {
|
|
225
|
+
const message = `chore: mpo update ${provenance.cliVersion} → ${currentVersion}`;
|
|
226
|
+
const mergeCommit = await gitOut(projectDir, [
|
|
227
|
+
"commit-tree",
|
|
228
|
+
mergedTree,
|
|
229
|
+
"-p",
|
|
230
|
+
head,
|
|
231
|
+
"-p",
|
|
232
|
+
nextCommit,
|
|
233
|
+
"-m",
|
|
234
|
+
message,
|
|
235
|
+
]);
|
|
236
|
+
await git(projectDir, ["update-ref", "HEAD", mergeCommit]);
|
|
237
|
+
await git(projectDir, ["reset", "--hard", "HEAD"]);
|
|
238
|
+
writeProvenance(projectDir, finalProvenance);
|
|
239
|
+
await git(projectDir, ["add", "--", ".mpo.json"]);
|
|
240
|
+
await git(projectDir, ["commit", "--amend", "--no-edit", "--quiet"]);
|
|
241
|
+
const amended = await gitOut(projectDir, ["rev-parse", "--short", "HEAD"]);
|
|
242
|
+
console.log(`\n✅ merged cleanly → ${amended} ("${message}")`);
|
|
243
|
+
} else {
|
|
244
|
+
// Land the merged tree (with conflict markers) in index + worktree for
|
|
245
|
+
// manual resolution.
|
|
246
|
+
await git(projectDir, ["read-tree", "-u", "--reset", mergedTree]);
|
|
247
|
+
writeProvenance(projectDir, finalProvenance);
|
|
248
|
+
console.log("\n⚠️ merge conflicts — resolve the markers, then commit:");
|
|
249
|
+
for (const file of conflicts) console.log(` ${file}`);
|
|
250
|
+
console.log(
|
|
251
|
+
`\n git add -A && git commit -m "chore: mpo update ${provenance.cliVersion} → ${currentVersion}"`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 4. .updateignore: pin matching paths to the pre-update HEAD (always
|
|
256
|
+
// includes pnpm-lock.yaml — the install below regenerates it).
|
|
257
|
+
const pins = ["pnpm-lock.yaml"];
|
|
258
|
+
const updateignore = join(projectDir, ".updateignore");
|
|
259
|
+
if (existsSync(updateignore)) {
|
|
260
|
+
for (const line of readFileSync(updateignore, "utf-8").split("\n")) {
|
|
261
|
+
const pattern = line.trim();
|
|
262
|
+
if (pattern && !pattern.startsWith("#")) pins.push(pattern);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const pin of pins) {
|
|
266
|
+
await git(projectDir, ["checkout", head, "--", pin]).catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!options.skipInstall) {
|
|
270
|
+
console.log("\nInstalling dependencies...");
|
|
271
|
+
await spawn("pnpm", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
272
|
+
}
|
|
273
|
+
} finally {
|
|
274
|
+
base.cleanup();
|
|
275
|
+
next.cleanup();
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/generateVscode.ts
CHANGED
|
@@ -61,15 +61,36 @@ function launchConfigForApp(app: AppInfo): LaunchConfig[] {
|
|
|
61
61
|
const configs: LaunchConfig[] = [];
|
|
62
62
|
const wf = "${workspaceFolder}";
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// vscode + webext are delivery targets of the One platform app
|
|
65
|
+
// (apps/<name>/vscode, apps/<name>/webext) — their dev flows hang off the
|
|
66
|
+
// app's dev:vscode / dev:webext scripts, staging into dist-vscode /
|
|
67
|
+
// dist-webext.
|
|
68
|
+
if (scripts["dev:vscode"]) {
|
|
65
69
|
configs.push({
|
|
66
70
|
name: "vscode dev",
|
|
67
71
|
type: "extensionHost",
|
|
68
72
|
request: "launch",
|
|
69
|
-
args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}`],
|
|
73
|
+
args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}/dist-vscode`],
|
|
70
74
|
cwd: `${wf}/${relPath}`,
|
|
71
|
-
outFiles: [`${wf}/${relPath}/dist/extension/*.js`],
|
|
72
|
-
preLaunchTask: "npm: dev",
|
|
75
|
+
outFiles: [`${wf}/${relPath}/dist-vscode/extension/*.js`],
|
|
76
|
+
preLaunchTask: "npm: dev:vscode",
|
|
77
|
+
});
|
|
78
|
+
configs.push({
|
|
79
|
+
name: "vscode dev",
|
|
80
|
+
type: "node-terminal",
|
|
81
|
+
request: "launch",
|
|
82
|
+
cwd: `${wf}/${relPath}`,
|
|
83
|
+
command: "pnpm dev:vscode",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (scripts["dev:webext"]) {
|
|
88
|
+
configs.push({
|
|
89
|
+
name: "webext dev",
|
|
90
|
+
type: "node-terminal",
|
|
91
|
+
request: "launch",
|
|
92
|
+
cwd: `${wf}/${relPath}`,
|
|
93
|
+
command: "pnpm dev:webext",
|
|
73
94
|
});
|
|
74
95
|
}
|
|
75
96
|
|
|
@@ -159,8 +180,9 @@ interface TasksJson {
|
|
|
159
180
|
}
|
|
160
181
|
|
|
161
182
|
async function generateTasksJson(apps: AppInfo[]): Promise<TasksJson | null> {
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
// The vscode target lives inside the platform app (dev:vscode script).
|
|
184
|
+
const hostApp = apps.find((a) => a.scripts?.["dev:vscode"]);
|
|
185
|
+
if (!hostApp) {
|
|
164
186
|
return null;
|
|
165
187
|
}
|
|
166
188
|
|
|
@@ -169,7 +191,7 @@ async function generateTasksJson(apps: AppInfo[]): Promise<TasksJson | null> {
|
|
|
169
191
|
tasks: [
|
|
170
192
|
{
|
|
171
193
|
type: "npm",
|
|
172
|
-
script: "dev",
|
|
194
|
+
script: "dev:vscode",
|
|
173
195
|
problemMatcher: {
|
|
174
196
|
owner: "typescript",
|
|
175
197
|
fileLocation: "relative",
|
|
@@ -190,7 +212,7 @@ async function generateTasksJson(apps: AppInfo[]): Promise<TasksJson | null> {
|
|
|
190
212
|
isBackground: true,
|
|
191
213
|
presentation: { reveal: "never" },
|
|
192
214
|
group: { kind: "build", isDefault: true },
|
|
193
|
-
options: { cwd:
|
|
215
|
+
options: { cwd: `\${workspaceFolder}/${hostApp.path}` },
|
|
194
216
|
},
|
|
195
217
|
],
|
|
196
218
|
};
|
|
@@ -211,6 +233,6 @@ export async function generateVscodeConfig(root: string): Promise<void> {
|
|
|
211
233
|
await fs.writeFile(path.join(vsCodeDir, "tasks.json"), JSON.stringify(tasks, null, 2), "utf8");
|
|
212
234
|
console.log("Wrote .vscode/tasks.json");
|
|
213
235
|
} else {
|
|
214
|
-
console.log("Skipped .vscode/tasks.json (no
|
|
236
|
+
console.log("Skipped .vscode/tasks.json (no app with a dev:vscode script)");
|
|
215
237
|
}
|
|
216
238
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# AGENTS.md — multiplatform.one conventions
|
|
2
|
+
|
|
3
|
+
Machine-readable house rules for humans and coding agents. Prefer this file over
|
|
4
|
+
reconstructing conventions from meeting notes. Detailed standards live under
|
|
5
|
+
`agent-os/standards/`; the living UX rulebook is linked below.
|
|
6
|
+
|
|
7
|
+
## Workspace layout
|
|
8
|
+
|
|
9
|
+
| Path | Role |
|
|
10
|
+
| ------------------ | ------------------------------------------------------------------- |
|
|
11
|
+
| `apps/<name>/` | Private applications (not published) |
|
|
12
|
+
| `features/<name>/` | Feature modules for the matching app (screens, stories, playground) |
|
|
13
|
+
| `packages/` | Private app-integration packages (`@package/*`) |
|
|
14
|
+
| `public/` | Published `@multiplatform.one/*` packages |
|
|
15
|
+
|
|
16
|
+
**App ↔ features twin:** every product app `apps/<name>` MUST have `features/<name>`.
|
|
17
|
+
Platform/tooling shells (frappe, keycloak, storybook\*, vocs, vscode, webext, chain
|
|
18
|
+
shells) are exempt in the convention linter; consumer apps from `mpo init --app`
|
|
19
|
+
are not. Prefer feature code in `features/`, thin wiring in `apps/`.
|
|
20
|
+
|
|
21
|
+
**Internal deps:** monorepo packages use `workspace:*`. Consumer / app-only
|
|
22
|
+
templates use semver ranges from npm (`@multiplatform.one/*`).
|
|
23
|
+
|
|
24
|
+
## Package boundaries
|
|
25
|
+
|
|
26
|
+
| Package | Owns | Must not |
|
|
27
|
+
| ------------------------------- | ---------------------------------------------------- | -------------------------------------- |
|
|
28
|
+
| `@multiplatform.one/theme` | Knobs, presets, intents, recipes, `useResolvedKnobs` | App screens |
|
|
29
|
+
| `@multiplatform.one/components` | Catalog UI (layouts, surfaces, feedback, …) | Frappe/API assumptions |
|
|
30
|
+
| `@multiplatform.one/forms` | Fields, Form, InputParts, field contracts | Table chrome / Frappe fetch |
|
|
31
|
+
| `@multiplatform.one/table` | DataTable, cells, filters, URL table state | Frappe/API assumptions |
|
|
32
|
+
| `@multiplatform.one/frappe-ui` | Frappe adapters (`FrappeTable`, field registry) | Re-implementing table/forms primitives |
|
|
33
|
+
| `@multiplatform.one/frappe` | Frappe client / live query | UI |
|
|
34
|
+
|
|
35
|
+
Dependency direction: `frappe-ui` → `table` / `forms` / `components` / `theme`.
|
|
36
|
+
`table` and `forms` do not depend on `frappe-ui` or `frappe`.
|
|
37
|
+
|
|
38
|
+
### Import rules (enforced)
|
|
39
|
+
|
|
40
|
+
1. **No raw `tamagui` outside `public/`** — apps, features, and packages import
|
|
41
|
+
`@multiplatform.one/components`, `@multiplatform.one/forms`, or
|
|
42
|
+
`@multiplatform.one/theme` instead.
|
|
43
|
+
2. **Apps never relative-import `public/`** — use package names
|
|
44
|
+
(`@multiplatform.one/…`), never `../../../public/…`.
|
|
45
|
+
3. **No `.css` files in `apps/*`** (except ignored bench/vendor trees) — styling
|
|
46
|
+
goes through Tamagui + knobs.
|
|
47
|
+
4. **No hex color literals in `features/**`** — use theme tokens (`$color10`,
|
|
48
|
+
semantic ramps, knob recipes).
|
|
49
|
+
|
|
50
|
+
Run: `pnpm lint` (oxlint + `scripts/lint-conventions.mjs`).
|
|
51
|
+
|
|
52
|
+
## Knobs system (canonical)
|
|
53
|
+
|
|
54
|
+
Structural styling (radius, space, elevation, typography, animation) is independent
|
|
55
|
+
of Tamagui color themes. Full standard: `agent-os/standards/frontend/knobs-system.md`.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
const { knobProps, control, text, elevation } = useResolvedKnobs({
|
|
59
|
+
intent, // "error" | "accent" | "warning" | "success"
|
|
60
|
+
component: "Button",
|
|
61
|
+
compact,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
<View
|
|
65
|
+
{...knobProps.surface}
|
|
66
|
+
{...knobProps.borderRadius}
|
|
67
|
+
{...knobProps.panelPadding}
|
|
68
|
+
{...knobProps.gap}
|
|
69
|
+
{...knobProps.body}
|
|
70
|
+
hoverStyle={{ ...control.hoverKnobProps }}
|
|
71
|
+
pressStyle={{ ...control.pressKnobProps }}
|
|
72
|
+
focusVisibleStyle={{ ...control.focusVisibleKnobProps }}
|
|
73
|
+
/>;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
|
|
78
|
+
- Always spread complete `knobProps.*` fragments — never cherry-pick resolved tokens.
|
|
79
|
+
- `resolveKnobs()` is the only canonical mapping from abstract knobs → Tamagui props.
|
|
80
|
+
- Consumer `{...props}` come last (explicit eject), not internal defaults that fight recipes.
|
|
81
|
+
- Presets cascade (`<Preset preset="…" theme="…">`); nested `overrides` merge by key.
|
|
82
|
+
- One knob flip restyles the screen (LC-18 KNOB-TOTALITY), or the knob is broken.
|
|
83
|
+
|
|
84
|
+
## Forms contract
|
|
85
|
+
|
|
86
|
+
Standard refs: `agent-os/standards/frontend/field-architecture.md`,
|
|
87
|
+
`field-layout.md`, `form-colors.md`, `input-parts.md`.
|
|
88
|
+
|
|
89
|
+
- Initialize with `useFormField()` + `useResolvedValidators()`.
|
|
90
|
+
- Three-branch render: skeleton → standalone → form-integrated (`<Field>`).
|
|
91
|
+
- Canonical change prop is `onChange(value)` (LC-14). Deprecated aliases may fire
|
|
92
|
+
but must not be the only path.
|
|
93
|
+
- Field slot order: Label → Hint → Error → Control; error replaces helper in one slot.
|
|
94
|
+
- Use form color ramps from `shared/colorRamps.ts` — no hardcoded color tokens.
|
|
95
|
+
- Fields auto-detect `useIsInTableCell()` and render chromeless inside cells.
|
|
96
|
+
- Capability surfaces (camera, geo, …) MUST preflight before mount (LC-19).
|
|
97
|
+
|
|
98
|
+
## Table contract
|
|
99
|
+
|
|
100
|
+
Standard ref: `public/table/README.md` (package boundaries), cell/editor anatomy in
|
|
101
|
+
the living rulebook.
|
|
102
|
+
|
|
103
|
+
- `@multiplatform.one/table` is framework-agnostic display/filter/sort/paginate.
|
|
104
|
+
- Cell editors larger than the anchor use popover (web) / sheet (native) — never
|
|
105
|
+
grow the row (LC-06). Editor popups dismiss only on dismissal intent (LC-21).
|
|
106
|
+
- Frappe integration belongs in `@multiplatform.one/frappe-ui` (`FrappeTable`,
|
|
107
|
+
registry adapters), not in `table`.
|
|
108
|
+
- Child-table / TableInput follow the same field `onChange` contract as forms.
|
|
109
|
+
|
|
110
|
+
## Living rulebook
|
|
111
|
+
|
|
112
|
+
**Normative UX + composition rules (LC-\* and recipe taxonomy):**
|
|
113
|
+
|
|
114
|
+
→ [`docs/theme-propagation-spec.md`](./docs/theme-propagation-spec.md)
|
|
115
|
+
|
|
116
|
+
That document is the living rulebook: every owner-reported spacing/UX issue is
|
|
117
|
+
adjudicated there in-session as VIOLATION or GAP (new LC-\* rule + VERIFY). Agents
|
|
118
|
+
changing catalog components MUST read it first; a contradicting change updates the
|
|
119
|
+
spec in the same change. Companion docs:
|
|
120
|
+
|
|
121
|
+
- Catalog guidelines: `docs/design-guidelines.md` + `docs/design-guidelines-blueprint.md`
|
|
122
|
+
- Click-matrix / VERIFY arm: `docs/storybook-knob-edge-cases.md`
|
|
123
|
+
- Agent OS standards index: `agent-os/standards/index.yml`
|
|
124
|
+
|
|
125
|
+
## Tooling
|
|
126
|
+
|
|
127
|
+
| Command | Purpose |
|
|
128
|
+
| ----------------------- | -------------------------------------------------------------- |
|
|
129
|
+
| `pnpm lint` | oxlint (incl. convention rules) + structural convention checks |
|
|
130
|
+
| `pnpm lint:conventions` | Structural checks only (css twin, features twin) |
|
|
131
|
+
| `pnpm format` | oxfmt |
|
|
132
|
+
| `pnpm ci:local` | format + lint + typecheck + build + test + tree-clean |
|
|
133
|
+
|
|
134
|
+
Use **oxlint / oxfmt**, not ESLint / Prettier
|
|
135
|
+
(`agent-os/standards/global/linting-formatting.md`).
|
|
136
|
+
|
|
137
|
+
## Coding defaults
|
|
138
|
+
|
|
139
|
+
- Named exports only; cut dead/legacy aliases unless a task explicitly needs them.
|
|
140
|
+
- Cross-platform: shared code first; `.native.tsx` / `.web.tsx` only when required.
|
|
141
|
+
- Stories and tests live next to the component / feature they cover.
|
|
142
|
+
- Deeper style rules: `agent-os/standards/global/coding-style.md`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# **NAME**
|
|
2
|
+
|
|
3
|
+
App-only project scaffolded by [`@multiplatform.one/cli`](https://www.npmjs.com/package/@multiplatform.one/cli)
|
|
4
|
+
(`mpo init --app` / `create-multiplatform-app`).
|
|
5
|
+
|
|
6
|
+
This is **not** a fork of the multiplatform.one monorepo. Framework packages come from npm at
|
|
7
|
+
semver ranges (`@multiplatform.one/*`: `__MPO_VERSION__`), never `workspace:*`.
|
|
8
|
+
|
|
9
|
+
## Layout
|
|
10
|
+
|
|
11
|
+
| Path | Role |
|
|
12
|
+
| -------------------- | --------------------------------------------------------- |
|
|
13
|
+
| `apps/__NAME__/` | App shell (Vite + React web stub) |
|
|
14
|
+
| `features/__NAME__/` | Feature screens / UI — import from here, not from `apps/` |
|
|
15
|
+
| `packages/config/` | Shared app config (`@package/config`) |
|
|
16
|
+
|
|
17
|
+
Convention: every `apps/<name>` has a matching `features/<name>`.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm install
|
|
23
|
+
pnpm dev # start the web stub
|
|
24
|
+
pnpm build
|
|
25
|
+
pnpm lint
|
|
26
|
+
pnpm typecheck
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
Feature and app packages depend on published framework packages, for example:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"@multiplatform.one/components": "__MPO_VERSION__",
|
|
36
|
+
"@multiplatform.one/theme": "__MPO_VERSION__",
|
|
37
|
+
"@multiplatform.one/platform": "__MPO_VERSION__"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Upgrade with your usual package manager (`pnpm update @multiplatform.one/*`).
|
|
42
|
+
|
|
43
|
+
## Conventions
|
|
44
|
+
|
|
45
|
+
See [`AGENTS.md`](./AGENTS.md) for package boundaries, the apps ↔ features twin,
|
|
46
|
+
import rules (no raw `tamagui` outside framework packages, no relative `public/`
|
|
47
|
+
imports, no hex literals in features), and knobs/forms/table contracts.
|
|
48
|
+
|
|
49
|
+
## Next steps
|
|
50
|
+
|
|
51
|
+
1. Replace the stub `HomeScreen` in `features/__NAME__/src/HomeScreen.tsx`.
|
|
52
|
+
2. Add routes / screens under `features/__NAME__/`.
|
|
53
|
+
3. When you need native (Expo / one), extend `apps/__NAME__/` — keep business UI in `features/`.
|
|
54
|
+
4. Do **not** copy `public/` from the framework monorepo into this project.
|
|
55
|
+
|
|
56
|
+
## Create another app
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mpo init --app another-app
|
|
60
|
+
# or
|
|
61
|
+
npx --package=@multiplatform.one/cli create-multiplatform-app another-app
|
|
62
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__NAME_PASCAL__</title>
|
|
7
|
+
<!--
|
|
8
|
+
LC-33 FONT-STABLE-MOUNT: the shell owns Inter (the theme package does
|
|
9
|
+
not inject @font-face as a CSS side effect). Preload + a head face with
|
|
10
|
+
font-display: optional means first paint already renders Inter and the
|
|
11
|
+
face never swaps after mount. The binaries live in public/fonts/.
|
|
12
|
+
-->
|
|
13
|
+
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
14
|
+
<link rel="preload" href="/fonts/inter-700.woff2" as="font" type="font/woff2" crossorigin />
|
|
15
|
+
<style data-font-inter>
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: "Inter";
|
|
18
|
+
font-style: normal;
|
|
19
|
+
font-weight: 400;
|
|
20
|
+
font-display: optional;
|
|
21
|
+
src: url("/fonts/inter-400.woff2") format("woff2");
|
|
22
|
+
}
|
|
23
|
+
@font-face {
|
|
24
|
+
font-family: "Inter";
|
|
25
|
+
font-style: normal;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
font-display: optional;
|
|
28
|
+
src: url("/fonts/inter-700.woff2") format("woff2");
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div id="root"></div>
|
|
34
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/__NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@features/__NAME__": "workspace:*",
|
|
14
|
+
"@multiplatform.one/components": "__MPO_VERSION__",
|
|
15
|
+
"@multiplatform.one/platform": "__MPO_VERSION__",
|
|
16
|
+
"@multiplatform.one/theme": "__MPO_VERSION__",
|
|
17
|
+
"@package/config": "workspace:*",
|
|
18
|
+
"react": "^19.2.5",
|
|
19
|
+
"react-dom": "^19.2.5",
|
|
20
|
+
"react-native-web": "^0.21.2",
|
|
21
|
+
"tamagui": "2.0.0-rc.41"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "~19.2.14",
|
|
25
|
+
"@types/react-dom": "~19.2.3",
|
|
26
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
27
|
+
"typescript": "~5.9.3",
|
|
28
|
+
"vite": "^8.0.10"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App";
|
|
4
|
+
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (!root) {
|
|
7
|
+
throw new Error("#root element missing");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
createRoot(root).render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</StrictMode>,
|
|
14
|
+
);
|