@multiplatform.one/cli 6.1.0 → 6.3.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 +23 -9
- package/lib/commands/e2e.mjs +3 -3
- package/lib/commands/init.mjs +16 -12
- package/lib/commands/initApp.mjs +145 -0
- package/lib/generateVscode.mjs +27 -10
- package/package.json +23 -12
- package/src/bin/multiplatformOne.ts +54 -16
- package/src/commands/init.ts +7 -18
- package/src/commands/initApp.spec.ts +210 -0
- package/src/commands/initApp.ts +228 -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 +25 -0
- package/types/commands/initApp.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
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @multiplatform.one/cli
|
|
2
|
+
|
|
3
|
+
The multiplatform.one command line: scaffold new multiplatform projects
|
|
4
|
+
(`mpo init` / `create-multiplatform-app`), manage the monorepo dev
|
|
5
|
+
environment, and drive Frappe + E2E workflows.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -g @multiplatform.one/cli
|
|
11
|
+
# or run the scaffolder without installing:
|
|
12
|
+
npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two binaries: **`mpo`** (full CLI) and **`create-multiplatform-app`**
|
|
16
|
+
(scaffolder only).
|
|
17
|
+
|
|
18
|
+
## Scaffold a project (recommended)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
mpo init my-app # universal: web (SSR) + iOS + Android via one
|
|
22
|
+
cd my-app
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm dev # web + native dev server
|
|
25
|
+
pnpm ios # build & run the iOS dev client (pnpm dev in another terminal)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mpo init` creates a **consumer** project — not a monorepo fork. It prompts for
|
|
29
|
+
anything omitted (name; platforms, defaulting to universal); pass `--yes` for
|
|
30
|
+
CI. `@multiplatform.one/*` dependencies come from npm at semver ranges, never
|
|
31
|
+
`workspace:*`.
|
|
32
|
+
|
|
33
|
+
Universal layout: `apps/<name>` (routes + providers + vite/expo config),
|
|
34
|
+
`features/<name>` (starter Home/About screens, forms demo, vitest),
|
|
35
|
+
`packages/config`, `packages/i18n` (en/es), `packages/themes` (Tamagui theme).
|
|
36
|
+
|
|
37
|
+
Want web-only? `mpo init --web my-app` scaffolds the Vite + React variant
|
|
38
|
+
(`--app` is a back-compat alias; `create-multiplatform-app` without flags also
|
|
39
|
+
produces it — pass `--universal` there for the full project).
|
|
40
|
+
|
|
41
|
+
Options: `--universal`, `--web`/`--app`, `--skip-install`,
|
|
42
|
+
`--mpo-version <range>` (defaults to `^<cli version>`), `-y/--yes`.
|
|
43
|
+
|
|
44
|
+
## Other commands
|
|
45
|
+
|
|
46
|
+
| Command | Purpose |
|
|
47
|
+
| ---------------------------- | -------------------------------------------------------------------- |
|
|
48
|
+
| `mpo init --monorepo [name]` | DEPRECATED: clone/modify the full monorepo (framework dev forks) |
|
|
49
|
+
| `mpo update` | Merge upstream and re-apply workspace config |
|
|
50
|
+
| `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
|
|
51
|
+
| `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
|
|
52
|
+
| `mpo frappe dev` | Start the Frappe development server |
|
|
53
|
+
| `mpo frappe bench -- <args>` | Run a bench command |
|
|
54
|
+
| `mpo frappe clean` | Clean Frappe bench artifacts |
|
|
55
|
+
| `mpo build` | Run the build command |
|
|
56
|
+
| `mpo test` | Run tests (E2E flags: `--up-only`, `--down`, `--filter`, …) |
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
Templates live in `templates/universal/` and `templates/app/`. Generator unit
|
|
61
|
+
specs: `pnpm --filter @multiplatform.one/cli test`
|
|
62
|
+
(`src/commands/initApp.spec.ts` — tree + package.json shape for both templates).
|
|
63
|
+
|
|
64
|
+
## What it must not do
|
|
65
|
+
|
|
66
|
+
- The project templates never ship `workspace:*` dependencies on
|
|
67
|
+
`@multiplatform.one/*` or copies of the monorepo's `public/` packages —
|
|
68
|
+
consumers upgrade via npm (`pnpm update "@multiplatform.one/*"`)
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Apache-2.0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-multiplatform-app — npm create / npx entry for consumer scaffolds.
|
|
5
|
+
* Delegates to the same generator as `mpo init`.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to the web-only app template (back-compat). Pass --universal for
|
|
8
|
+
* the full web + iOS + Android project (same as `mpo init <name>`).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
12
|
+
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app --universal
|
|
13
|
+
* mpo init my-app # universal
|
|
14
|
+
* mpo init --web my-app # web-only
|
|
15
|
+
*/
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
function flagValue(argv, name) {
|
|
24
|
+
const idx = argv.indexOf(name);
|
|
25
|
+
if (idx < 0) return undefined;
|
|
26
|
+
return argv[idx + 1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
32
|
+
console.log(`create-multiplatform-app [name] [--universal] [--skip-install] [--mpo-version <range>] [--yes]
|
|
33
|
+
|
|
34
|
+
Scaffold a consumer project with @multiplatform.one/* at semver ranges
|
|
35
|
+
(not workspace:*):
|
|
36
|
+
|
|
37
|
+
(default) apps/<name> + features/<name> + packages/config (web-only)
|
|
38
|
+
--universal one-based web + iOS + Android app
|
|
39
|
+
(adds packages/i18n + packages/themes)
|
|
40
|
+
|
|
41
|
+
Same as: mpo init --web [name] / mpo init [name]
|
|
42
|
+
`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
46
|
+
console.log(require("../package.json").version);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const skipInstall = argv.includes("--skip-install");
|
|
51
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
52
|
+
const universal = argv.includes("--universal");
|
|
53
|
+
const version = flagValue(argv, "--mpo-version");
|
|
54
|
+
const name = argv.find((a) => !a.startsWith("-"));
|
|
55
|
+
|
|
56
|
+
const libPath = resolve(here, "../lib/commands/initApp.mjs");
|
|
57
|
+
let initApp;
|
|
58
|
+
try {
|
|
59
|
+
({ initApp } = await import(pathToFileURL(libPath).href));
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Could not load initApp. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await initApp(name, {
|
|
67
|
+
skipInstall,
|
|
68
|
+
version,
|
|
69
|
+
template: universal ? "universal" : "app",
|
|
70
|
+
yes,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch((err) => {
|
|
75
|
+
console.error(err instanceof Error ? err.message : err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
2
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
|
|
3
3
|
import { init, runModifyStep } from "../commands/init.mjs";
|
|
4
|
+
import { initApp } from "../commands/initApp.mjs";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
7
|
-
import spawn from "nano-spawn";
|
|
8
|
-
import yoctoSpinner from "yocto-spinner";
|
|
9
|
-
import YAML from "yaml";
|
|
10
8
|
import fsSync from "node:fs";
|
|
11
9
|
import { fileURLToPath } from "node:url";
|
|
12
10
|
import { formatServiceList, lookupProjectRoot, waitForFrappe, waitForKeycloak, waitForPostgres, waitServices } from "@multiplatform.one/utils/dev";
|
|
13
11
|
import { Command } from "commander";
|
|
12
|
+
import spawn from "nano-spawn";
|
|
13
|
+
import YAML from "yaml";
|
|
14
|
+
import yoctoSpinner from "yocto-spinner";
|
|
14
15
|
|
|
15
16
|
//#region \0rolldown/runtime.js
|
|
16
17
|
var __create = Object.create;
|
|
@@ -469,12 +470,25 @@ async function getFrappeApps(root) {
|
|
|
469
470
|
const program = new Command();
|
|
470
471
|
program.name("mpo");
|
|
471
472
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
472
|
-
program.command("init").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").option("-a, --apps <apps>", "comma-separated apps to include").option("-s, --services <services>", "comma-separated services to include").argument("[name]", "the name of the project").description("
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
473
|
+
program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--skip-install", "skip pnpm install after scaffolding").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("-y, --yes", "non-interactive: accept defaults instead of prompting").option("--monorepo", "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project").option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main").option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)").option("-s, --services <services>", "comma-separated services to include (--monorepo only)").argument("[name]", "the name of the project").description("scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)").action(async (name, options) => {
|
|
474
|
+
if (options.monorepo) {
|
|
475
|
+
console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
|
|
476
|
+
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
|
|
477
|
+
const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
|
|
478
|
+
await init(name, {
|
|
479
|
+
...options,
|
|
480
|
+
cloneScript
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
|
|
485
|
+
const webOnly = Boolean(options.web || options.app);
|
|
486
|
+
if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
|
|
487
|
+
await initApp(name, {
|
|
488
|
+
skipInstall: Boolean(options.skipInstall),
|
|
489
|
+
version: options.mpoVersion,
|
|
490
|
+
template: webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
491
|
+
yes: Boolean(options.yes)
|
|
478
492
|
});
|
|
479
493
|
});
|
|
480
494
|
program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main").option("-r, --remote <url>", "upstream remote URL", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").description("merge upstream and re-apply workspace config").action(async (options) => {
|
package/lib/commands/e2e.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import os from "node:os";
|
|
5
3
|
import spawn from "nano-spawn";
|
|
6
|
-
import yoctoSpinner from "yocto-spinner";
|
|
7
4
|
import YAML from "yaml";
|
|
5
|
+
import yoctoSpinner from "yocto-spinner";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import os from "node:os";
|
|
8
8
|
|
|
9
9
|
//#region src/commands/e2e.ts
|
|
10
10
|
function sessionFileName(portOffset) {
|
package/lib/commands/init.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/init.ts
|
|
@@ -20,8 +20,6 @@ const apps = [
|
|
|
20
20
|
"keycloak",
|
|
21
21
|
"storybook",
|
|
22
22
|
"storybook-expo",
|
|
23
|
-
"vscode",
|
|
24
|
-
"webext",
|
|
25
23
|
"vocs"
|
|
26
24
|
];
|
|
27
25
|
/** env variable prefixes to remove per service */
|
|
@@ -33,12 +31,14 @@ const serviceEnvPrefixes = {
|
|
|
33
31
|
};
|
|
34
32
|
/** env variable prefixes to remove per app */
|
|
35
33
|
const appEnvPrefixes = {
|
|
36
|
-
one: [
|
|
34
|
+
one: [
|
|
35
|
+
"ONE_",
|
|
36
|
+
"VSCODE_",
|
|
37
|
+
"WEBEXT_"
|
|
38
|
+
],
|
|
37
39
|
keycloak: ["KEYCLOAK_"],
|
|
38
40
|
storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
|
|
39
41
|
"storybook-expo": [],
|
|
40
|
-
vscode: ["VSCODE_"],
|
|
41
|
-
webext: ["WEBEXT_"],
|
|
42
42
|
vocs: ["VOCS_"]
|
|
43
43
|
};
|
|
44
44
|
/** env section headers (comments) to remove per app/service */
|
|
@@ -49,12 +49,14 @@ const serviceEnvSections = {
|
|
|
49
49
|
sui: []
|
|
50
50
|
};
|
|
51
51
|
const appEnvSections = {
|
|
52
|
-
one: [
|
|
52
|
+
one: [
|
|
53
|
+
"# one",
|
|
54
|
+
"# vscode",
|
|
55
|
+
"# webext"
|
|
56
|
+
],
|
|
53
57
|
keycloak: ["# keycloak"],
|
|
54
58
|
storybook: ["# storybook"],
|
|
55
59
|
"storybook-expo": [],
|
|
56
|
-
vscode: ["# vscode"],
|
|
57
|
-
webext: ["# webext"],
|
|
58
60
|
vocs: ["# vocs"]
|
|
59
61
|
};
|
|
60
62
|
/** launch.json configuration names per service/app */
|
|
@@ -65,12 +67,14 @@ const serviceLaunchNames = {
|
|
|
65
67
|
sui: ["sui dev"]
|
|
66
68
|
};
|
|
67
69
|
const appLaunchNames = {
|
|
68
|
-
one: [
|
|
70
|
+
one: [
|
|
71
|
+
"one dev",
|
|
72
|
+
"vscode dev",
|
|
73
|
+
"webext dev"
|
|
74
|
+
],
|
|
69
75
|
keycloak: ["keycloak dev", "keycloak storybook"],
|
|
70
76
|
storybook: ["storybook dev"],
|
|
71
77
|
"storybook-expo": ["storybook-expo dev"],
|
|
72
|
-
vscode: ["vscode dev"],
|
|
73
|
-
webext: ["webext dev"],
|
|
74
78
|
vocs: ["vocs dev"]
|
|
75
79
|
};
|
|
76
80
|
/**
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import spawn from "nano-spawn";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/initApp.ts
|
|
8
|
+
function toPascalCase(name) {
|
|
9
|
+
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
10
|
+
}
|
|
11
|
+
function validateName(name) {
|
|
12
|
+
const trimmed = name.trim();
|
|
13
|
+
if (!trimmed) throw new Error("Project name is required");
|
|
14
|
+
if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) throw new Error(`Invalid project name "${trimmed}". Use lowercase letters, numbers, and hyphens (e.g. my-app).`);
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
function resolveMpoVersion(explicit) {
|
|
18
|
+
if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
21
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
22
|
+
if (pkg.version) return `^${pkg.version}`;
|
|
23
|
+
} catch {}
|
|
24
|
+
return "^6.1.0";
|
|
25
|
+
}
|
|
26
|
+
function templatesRoot(template) {
|
|
27
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* npm always strips .gitignore / .npmrc from published tarballs, so the
|
|
31
|
+
* template stores them without the leading dot; restore it at scaffold time.
|
|
32
|
+
*/
|
|
33
|
+
const DOTFILE_NAMES = {
|
|
34
|
+
gitignore: ".gitignore",
|
|
35
|
+
npmrc: ".npmrc"
|
|
36
|
+
};
|
|
37
|
+
function render(content, vars) {
|
|
38
|
+
return content.replaceAll("__NAME_PASCAL__", vars.NAME_PASCAL).replaceAll("__NAME_APPID__", vars.NAME_APPID).replaceAll("__NAME__", vars.NAME).replaceAll("__MPO_VERSION__", vars.MPO_VERSION).replaceAll("__YEAR__", vars.YEAR);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Assets copied byte-for-byte — running them through the utf-8 render pass
|
|
42
|
+
* would corrupt them (fonts ship in the templates for LC-33 stable mount).
|
|
43
|
+
*/
|
|
44
|
+
const BINARY_EXTENSIONS = new Set([
|
|
45
|
+
".woff2",
|
|
46
|
+
".woff",
|
|
47
|
+
".ttf",
|
|
48
|
+
".otf",
|
|
49
|
+
".png",
|
|
50
|
+
".jpg",
|
|
51
|
+
".ico"
|
|
52
|
+
]);
|
|
53
|
+
function writeTree(srcDir, destDir, vars) {
|
|
54
|
+
if (!existsSync(srcDir)) throw new Error(`Project template missing at ${srcDir}`);
|
|
55
|
+
mkdirSync(destDir, { recursive: true });
|
|
56
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
57
|
+
const from = join(srcDir, entry.name);
|
|
58
|
+
const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
writeTree(from, to, vars);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
64
|
+
copyFileSync(from, to);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function resolveTemplate(options) {
|
|
71
|
+
if (options.template) return options.template;
|
|
72
|
+
if (options.yes || !process.stdout.isTTY) return "universal";
|
|
73
|
+
return (await inquirer.prompt([{
|
|
74
|
+
message: "Which platforms should this project target?",
|
|
75
|
+
name: "template",
|
|
76
|
+
type: "list",
|
|
77
|
+
choices: [{
|
|
78
|
+
name: "Universal — web + iOS + Android (one)",
|
|
79
|
+
value: "universal"
|
|
80
|
+
}, {
|
|
81
|
+
name: "Web only — Vite + React",
|
|
82
|
+
value: "app"
|
|
83
|
+
}],
|
|
84
|
+
default: "universal"
|
|
85
|
+
}])).template;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Scaffold a consumer project (universal by default, web-only with
|
|
89
|
+
* template: "app") that depends on @multiplatform.one/* at semver ranges
|
|
90
|
+
* (never workspace:*). This is NOT a monorepo fork.
|
|
91
|
+
*/
|
|
92
|
+
async function initApp(nameArg, options = {}) {
|
|
93
|
+
let projectName = nameArg;
|
|
94
|
+
if (!projectName) if (options.yes || !process.stdout.isTTY) projectName = "my-app";
|
|
95
|
+
else projectName = (await inquirer.prompt([{
|
|
96
|
+
message: "What is the app name?",
|
|
97
|
+
name: "name",
|
|
98
|
+
type: "input",
|
|
99
|
+
default: "my-app"
|
|
100
|
+
}])).name;
|
|
101
|
+
const name = validateName(projectName);
|
|
102
|
+
const template = await resolveTemplate(options);
|
|
103
|
+
const targetDir = resolve(name);
|
|
104
|
+
if (existsSync(targetDir)) {
|
|
105
|
+
if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
106
|
+
}
|
|
107
|
+
const vars = {
|
|
108
|
+
NAME: name,
|
|
109
|
+
NAME_PASCAL: toPascalCase(name),
|
|
110
|
+
NAME_APPID: name.replaceAll("-", ""),
|
|
111
|
+
MPO_VERSION: resolveMpoVersion(options.version),
|
|
112
|
+
YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
|
|
113
|
+
};
|
|
114
|
+
console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : "web-only"} project at ${targetDir}`);
|
|
115
|
+
console.log(` apps/${name}`);
|
|
116
|
+
console.log(` features/${name}`);
|
|
117
|
+
console.log(` packages/config`);
|
|
118
|
+
if (template === "universal") {
|
|
119
|
+
console.log(` packages/i18n`);
|
|
120
|
+
console.log(` packages/themes`);
|
|
121
|
+
}
|
|
122
|
+
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
123
|
+
writeTree(templatesRoot(template), targetDir, vars);
|
|
124
|
+
if (!options.skipInstall) {
|
|
125
|
+
console.log("Installing dependencies...");
|
|
126
|
+
await spawn("pnpm", ["install"], {
|
|
127
|
+
cwd: targetDir,
|
|
128
|
+
stdio: "inherit"
|
|
129
|
+
});
|
|
130
|
+
} else console.log("Skipped pnpm install (--skip-install).");
|
|
131
|
+
console.log(`\n✅ Project created at ${targetDir}`);
|
|
132
|
+
console.log("\nNext steps:");
|
|
133
|
+
console.log(` cd ${name}`);
|
|
134
|
+
if (options.skipInstall) console.log(" pnpm install");
|
|
135
|
+
console.log(" pnpm dev # web dev server");
|
|
136
|
+
if (template === "universal") {
|
|
137
|
+
console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
|
|
138
|
+
console.log(" pnpm android # Android");
|
|
139
|
+
}
|
|
140
|
+
console.log("\nThis project consumes published @multiplatform.one/* packages.");
|
|
141
|
+
console.log("It is not a fork of the multiplatform.one monorepo.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { initApp };
|
package/lib/generateVscode.mjs
CHANGED
|
@@ -41,14 +41,30 @@ function launchConfigForApp(app) {
|
|
|
41
41
|
const { name, path: relPath, scripts } = app;
|
|
42
42
|
const configs = [];
|
|
43
43
|
const wf = "${workspaceFolder}";
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
if (scripts["dev:vscode"]) {
|
|
45
|
+
configs.push({
|
|
46
|
+
name: "vscode dev",
|
|
47
|
+
type: "extensionHost",
|
|
48
|
+
request: "launch",
|
|
49
|
+
args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}/dist-vscode`],
|
|
50
|
+
cwd: `${wf}/${relPath}`,
|
|
51
|
+
outFiles: [`${wf}/${relPath}/dist-vscode/extension/*.js`],
|
|
52
|
+
preLaunchTask: "npm: dev:vscode"
|
|
53
|
+
});
|
|
54
|
+
configs.push({
|
|
55
|
+
name: "vscode dev",
|
|
56
|
+
type: "node-terminal",
|
|
57
|
+
request: "launch",
|
|
58
|
+
cwd: `${wf}/${relPath}`,
|
|
59
|
+
command: "pnpm dev:vscode"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (scripts["dev:webext"]) configs.push({
|
|
63
|
+
name: "webext dev",
|
|
64
|
+
type: "node-terminal",
|
|
47
65
|
request: "launch",
|
|
48
|
-
args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}`],
|
|
49
66
|
cwd: `${wf}/${relPath}`,
|
|
50
|
-
|
|
51
|
-
preLaunchTask: "npm: dev"
|
|
67
|
+
command: "pnpm dev:webext"
|
|
52
68
|
});
|
|
53
69
|
if (scripts.dev) configs.push({
|
|
54
70
|
name: `${name} dev`,
|
|
@@ -117,12 +133,13 @@ async function generateLaunchJson(root) {
|
|
|
117
133
|
};
|
|
118
134
|
}
|
|
119
135
|
async function generateTasksJson(apps) {
|
|
120
|
-
|
|
136
|
+
const hostApp = apps.find((a) => a.scripts?.["dev:vscode"]);
|
|
137
|
+
if (!hostApp) return null;
|
|
121
138
|
return {
|
|
122
139
|
version: "2.0.0",
|
|
123
140
|
tasks: [{
|
|
124
141
|
type: "npm",
|
|
125
|
-
script: "dev",
|
|
142
|
+
script: "dev:vscode",
|
|
126
143
|
problemMatcher: {
|
|
127
144
|
owner: "typescript",
|
|
128
145
|
fileLocation: "relative",
|
|
@@ -146,7 +163,7 @@ async function generateTasksJson(apps) {
|
|
|
146
163
|
kind: "build",
|
|
147
164
|
isDefault: true
|
|
148
165
|
},
|
|
149
|
-
options: { cwd:
|
|
166
|
+
options: { cwd: `\${workspaceFolder}/${hostApp.path}` }
|
|
150
167
|
}]
|
|
151
168
|
};
|
|
152
169
|
}
|
|
@@ -160,7 +177,7 @@ async function generateVscodeConfig(root) {
|
|
|
160
177
|
if (tasks) {
|
|
161
178
|
await fs.writeFile(path.join(vsCodeDir, "tasks.json"), JSON.stringify(tasks, null, 2), "utf8");
|
|
162
179
|
console.log("Wrote .vscode/tasks.json");
|
|
163
|
-
} else console.log("Skipped .vscode/tasks.json (no
|
|
180
|
+
} else console.log("Skipped .vscode/tasks.json (no app with a dev:vscode script)");
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/cli",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "multiplatform.one cli",
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "6.3.0",
|
|
4
|
+
"description": "multiplatform.one cli — mpo init / create-multiplatform-app",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"create-multiplatform-app",
|
|
7
|
+
"mpo",
|
|
8
|
+
"multiplatform.one",
|
|
9
|
+
"scaffold"
|
|
10
|
+
],
|
|
6
11
|
"bugs": {
|
|
7
12
|
"url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one/issues"
|
|
8
13
|
},
|
|
@@ -16,9 +21,11 @@
|
|
|
16
21
|
],
|
|
17
22
|
"repository": {
|
|
18
23
|
"type": "git",
|
|
19
|
-
"url": "https://gitlab.com/bitspur/
|
|
24
|
+
"url": "git+https://gitlab.com/bitspur/frappe/multiplatform.one.git",
|
|
25
|
+
"directory": "public/cli"
|
|
20
26
|
},
|
|
21
27
|
"bin": {
|
|
28
|
+
"create-multiplatform-app": "./bin/createMultiplatformApp.mjs",
|
|
22
29
|
"mpo": "./bin/multiplatformOne.mjs"
|
|
23
30
|
},
|
|
24
31
|
"source": "src/index.ts",
|
|
@@ -27,19 +34,21 @@
|
|
|
27
34
|
"lib",
|
|
28
35
|
"scripts",
|
|
29
36
|
"src",
|
|
30
|
-
"
|
|
37
|
+
"templates",
|
|
38
|
+
"types",
|
|
39
|
+
"!types/.tsbuildinfo"
|
|
31
40
|
],
|
|
32
41
|
"type": "module",
|
|
33
42
|
"module": "lib/index.mjs",
|
|
34
43
|
"types": "types/index.d.ts",
|
|
35
44
|
"exports": {
|
|
36
45
|
".": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
46
|
+
"types": "./types/index.d.ts",
|
|
47
|
+
"import": "./lib/index.mjs"
|
|
39
48
|
},
|
|
40
49
|
"./bin/multiplatformOne": {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
50
|
+
"types": "./types/bin/multiplatformOne.d.ts",
|
|
51
|
+
"import": "./lib/bin/multiplatformOne.mjs"
|
|
43
52
|
},
|
|
44
53
|
"./package.json": "./package.json"
|
|
45
54
|
},
|
|
@@ -52,18 +61,20 @@
|
|
|
52
61
|
"nano-spawn": "^2.1.0",
|
|
53
62
|
"yaml": "^2.8.3",
|
|
54
63
|
"yocto-spinner": "^1.1.0",
|
|
55
|
-
"@multiplatform.one/utils": "6.
|
|
64
|
+
"@multiplatform.one/utils": "6.3.0"
|
|
56
65
|
},
|
|
57
66
|
"devDependencies": {
|
|
58
67
|
"@types/inquirer": "^9.0.9",
|
|
59
68
|
"@types/node": "^25.6.0",
|
|
60
|
-
"typescript": "~5.9.3"
|
|
69
|
+
"typescript": "~5.9.3",
|
|
70
|
+
"vitest": "^4.1.5"
|
|
61
71
|
},
|
|
62
72
|
"engines": {
|
|
63
73
|
"node": ">= 16.0"
|
|
64
74
|
},
|
|
65
75
|
"scripts": {
|
|
66
|
-
"build": "rm -rf lib types 2>/dev/null && tsc -
|
|
76
|
+
"build": "rm -rf lib types 2>/dev/null && tsc -p tsconfig.build.json && tsdown",
|
|
77
|
+
"test": "vitest run",
|
|
67
78
|
"typecheck": "tsc --noEmit"
|
|
68
79
|
}
|
|
69
80
|
}
|
|
@@ -19,6 +19,7 @@ import YAML from "yaml";
|
|
|
19
19
|
import yoctoSpinner from "yocto-spinner";
|
|
20
20
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e";
|
|
21
21
|
import { init, runModifyStep } from "../commands/init";
|
|
22
|
+
import { initApp } from "../commands/initApp";
|
|
22
23
|
|
|
23
24
|
const projectRoot = lookupProjectRoot();
|
|
24
25
|
const availableServices = ["api", "frappe", "solana", "ethereum", "sui"];
|
|
@@ -216,25 +217,62 @@ program.version(
|
|
|
216
217
|
|
|
217
218
|
program
|
|
218
219
|
.command("init")
|
|
219
|
-
.option("
|
|
220
|
-
.option("
|
|
221
|
-
.option("
|
|
220
|
+
.option("--universal", "scaffold the universal template (web + iOS + Android; default)")
|
|
221
|
+
.option("--web", "scaffold the web-only template (Vite + React)")
|
|
222
|
+
.option("--app", "alias of --web (back-compat)")
|
|
223
|
+
.option("--skip-install", "skip pnpm install after scaffolding")
|
|
224
|
+
.option(
|
|
225
|
+
"--mpo-version <range>",
|
|
226
|
+
"semver range for @multiplatform.one/* (default: ^<cli version>)",
|
|
227
|
+
)
|
|
228
|
+
.option("-y, --yes", "non-interactive: accept defaults instead of prompting")
|
|
229
|
+
.option(
|
|
230
|
+
"--monorepo",
|
|
231
|
+
"DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project",
|
|
232
|
+
)
|
|
233
|
+
.option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main")
|
|
234
|
+
.option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)")
|
|
235
|
+
.option("-s, --services <services>", "comma-separated services to include (--monorepo only)")
|
|
222
236
|
.argument("[name]", "the name of the project")
|
|
223
|
-
.description(
|
|
237
|
+
.description(
|
|
238
|
+
"scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)",
|
|
239
|
+
)
|
|
224
240
|
.action(async (name: string | undefined, options) => {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
()
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
if (options.monorepo) {
|
|
242
|
+
console.warn(
|
|
243
|
+
"⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). " +
|
|
244
|
+
"New projects should use the default consumer scaffold instead.",
|
|
245
|
+
);
|
|
246
|
+
if (
|
|
247
|
+
await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
|
|
248
|
+
() => true,
|
|
249
|
+
() => false,
|
|
250
|
+
)
|
|
251
|
+
) {
|
|
252
|
+
throw new Error("mpo cannot be initialized inside a git repository");
|
|
253
|
+
}
|
|
254
|
+
const cloneScript = path.resolve(
|
|
255
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
256
|
+
"../../scripts/clone.sh",
|
|
257
|
+
);
|
|
258
|
+
await init(name, { ...options, cloneScript });
|
|
259
|
+
return;
|
|
232
260
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
if (options.apps || options.services) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const webOnly = Boolean(options.web || options.app);
|
|
267
|
+
if (webOnly && options.universal) {
|
|
268
|
+
throw new Error("Pass either --web/--app or --universal, not both");
|
|
269
|
+
}
|
|
270
|
+
await initApp(name, {
|
|
271
|
+
skipInstall: Boolean(options.skipInstall),
|
|
272
|
+
version: options.mpoVersion,
|
|
273
|
+
template: webOnly ? "app" : options.universal ? "universal" : undefined,
|
|
274
|
+
yes: Boolean(options.yes),
|
|
275
|
+
});
|
|
238
276
|
});
|
|
239
277
|
|
|
240
278
|
const defaultUpdateRemote = "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git";
|