create-mithril-lynx 0.1.1 → 2.0.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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # create-mithril-lynx
2
2
 
3
- Scaffolds a new [`mithril-lynx`](https://github.com/carlos-sweb/mithril-lynx) app. Three templates, matching the "Hello World" / "Blank" / "Basic Activity" naming most native app scaffolds already use, each available in TypeScript or JavaScript.
3
+ Scaffolds a new [`mithril-lynx`](https://github.com/carlos-sweb/mithril-lynx) app. Three templates, matching the "Hello World" / "Blank" / "Basic Activity" naming most native app scaffolds already use.
4
4
 
5
- It can also generate the **native Android host** — the whole Gradle project that wraps the bundle into an installable APK — so you never hand-write the ~20 Kotlin/Gradle/XML files that takes. That path automates [`mithril-lynx`'s ANDROID_APK_GUIDE.md](https://github.com/carlos-sweb/mithril-lynx/blob/main/ANDROID_APK_GUIDE.md).
5
+ It can also generate the **native Android host** — the whole Gradle project that wraps the bundle into an installable APK — so you never hand-write the ~20 Kotlin/Gradle/XML files that takes.
6
+
7
+ > **Note:** this is a complete rewrite of the previous `create-mithril-lynx`, matching [`mithril-lynx`'s own rewrite](https://github.com/carlos-sweb/mithril-lynx). The old tool's **Basic Activity** template used `mithril-lynx/navigation` (a stack-based, in-memory navigator) — that module doesn't exist in the new `mithril-lynx`, so this template was rewritten from scratch around [`mithril-lynx/route`](https://github.com/carlos-sweb/mithril-lynx/blob/main/ROUTE.md) instead. Everything else (Hello World, Blank, the Android host) ports over unchanged, since none of it depended on the part of the framework that got rewritten.
6
8
 
7
9
  ## Usage
8
10
 
@@ -10,20 +12,20 @@ It can also generate the **native Android host** — the whole Gradle project th
10
12
  npm create mithril-lynx@latest
11
13
  ```
12
14
 
13
- Prompts for a project name, a template, and a variant (TypeScript or JavaScript), scaffolds it, and offers to install dependencies for you.
15
+ Prompts for a project name and a template, scaffolds it, and offers to install dependencies for you.
14
16
 
15
17
  ### Non-interactive
16
18
 
17
19
  ```bash
18
- npx create-mithril-lynx my-app --hello-world --ts
19
- npx create-mithril-lynx my-app --basic-activity --js --no-install
20
+ npx create-mithril-lynx my-app --hello-world
21
+ npx create-mithril-lynx my-app --basic-activity --no-install
20
22
  ```
21
23
 
22
24
  ### With the Android host
23
25
 
24
26
  ```bash
25
27
  # Scaffold the JS app plus a sibling my-app-android/ Gradle project:
26
- npx create-mithril-lynx my-app --ts --blank --android
28
+ npx create-mithril-lynx my-app --blank --android
27
29
 
28
30
  cd my-app
29
31
  npm install
@@ -42,13 +44,12 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
42
44
  |---|---|
43
45
  | `<name>` (positional) | Project name, and the directory to create. Omit it and you're prompted. |
44
46
  | `--hello-world` / `--blank` / `--basic-activity` | Template to scaffold. Omit it and you're prompted. |
45
- | `--ts` / `--js` | TypeScript or JavaScript variant. Omit it and you're prompted. |
46
47
  | `--no-install` | Don't install dependencies (also skips that prompt). |
47
48
  | `-h`, `--help` | Print usage and exit. |
48
49
  | `--android` / `--target android` / `--target=android` / `target=android` | Also scaffold the sibling `<name>-android/` Gradle project. |
49
50
  | `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
50
51
  | `--app-name <name>` | Launcher label (default: the project name). |
51
- | `--with-font <file.ttf>` | Copy the font into the host's assets, generate `AssetFontFaceLoader.kt`, and wire the cold-start `prefetchFont()` hack (Part D of the guide). |
52
+ | `--with-font <file.ttf>` | Copy the font into the host's assets, generate `AssetFontFaceLoader.kt`, and wire the cold-start `prefetchFont()` hack. |
52
53
  | `--font-family <name>` | Override the family name derived from the font's file name. Only meaningful with `--with-font`. |
53
54
 
54
55
  Anything not listed — in particular the four Android flags — requires `--android`
@@ -56,11 +57,11 @@ Anything not listed — in particular the four Android flags — requires `--and
56
57
 
57
58
  ### What the generated Android host gives you
58
59
 
59
- The sibling `<name>-android/` project is a complete, no-Android-Studio Gradle CLI project pinned to the versions in the guide (AGP 8.5.2, Kotlin 1.9.24, Gradle 8.14.2, Lynx `4.1.0`, `compileSdk`/`targetSdk` 34, `minSdk` 24), including:
60
+ The sibling `<name>-android/` project is a complete, no-Android-Studio Gradle CLI project, including:
60
61
 
61
- - the Gradle wrapper (`gradlew`, `gradlew.bat`, `gradle-wrapper.jar`), so nothing needs to be installed besides a JDK 17 and the Android SDK;
62
+ - the Gradle wrapper (`gradlew`, `gradlew.bat`, `gradle-wrapper.jar`), so nothing needs to be installed besides a JDK and the Android SDK;
62
63
  - `local.properties` with `sdk.dir` auto-detected from `ANDROID_HOME`/`ANDROID_SDK_ROOT` (with a written warning if it can't be found);
63
- - the Kotlin host: the `Application` (log service registered), `MainActivity` (async `AssetTemplateProvider`, splash screen, `XElementBehaviors`), and the font loader when `--with-font` is used;
64
+ - the Kotlin host: the `Application`, `MainActivity` (async `AssetTemplateProvider`, splash screen), and the font loader when `--with-font` is used;
64
65
  - resource files that compile as-is (theme, splash theme, adaptive launcher icon);
65
66
  - an opt-in release signing setup driven by a gitignored `keystore.properties`.
66
67
 
@@ -74,20 +75,15 @@ npm run android:sync # build + sync bundle into app/src/main/assets/, no G
74
75
  npm run android:keystore # generate release.keystore + keystore.properties (KEYSTORE_PASSWORD=...)
75
76
  ```
76
77
 
78
+ This part of the tool is framework-agnostic — it just wraps whatever bundle `rspeedy build` produces into a native host — so it needed no changes for the `mithril-lynx` rewrite beyond what's inherited automatically through the generated JS project.
79
+
77
80
  ## Templates
78
81
 
79
82
  | Template | What it is |
80
83
  |---|---|
81
84
  | **Hello World** (recommended) | The Mithril.js analog of Lynx's official React hello-world ([`lynx-examples/examples/hello-world`](https://github.com/lynx-family/lynx-examples/tree/main/examples/hello-world)) — same layout, same "tap the logo" interaction, running through Mithril hyperscript instead of JSX. |
82
85
  | **Blank** | A single centered line of text. Nothing else — the starting point when you don't want any of Hello World's styling/assets in your way. |
83
- | **Basic Activity** | Two screens (Main → Details) wired with [`mithril-lynx/navigation`](https://github.com/carlos-sweb/mithril-lynx#navigation) — a stack-based, in-memory navigator (deliberately not `m.route`; see that package's README for why Lynx pages can't use URL-based routing). Includes a reusable app-bar-with-back-button pattern. Confirmed working end to end on a real device: `push()` with data, the back button, and `pop()` all round-trip correctly. |
84
-
85
- ## Variants
86
-
87
- | Variant | Adds over the base template |
88
- |---|---|
89
- | **TypeScript** (recommended) | `.ts` source files, `tsconfig.json` (project references, matching `@lynx-js/rspeedy`'s own generated config), `@rsbuild/plugin-type-check`, `@types/mithril` for real editor autocomplete on `m()` calls. |
90
- | **JavaScript** | Same app, plain `.js` with ES module `import`/`export` — no type-checking, no `tsconfig.json`. |
86
+ | **Basic Activity** | Two screens (Main → Details) wired with [`mithril-lynx/route`](https://github.com/carlos-sweb/mithril-lynx/blob/main/ROUTE.md) — an in-memory router (`m.route`, reimplemented for an environment with no URL bar). Includes a reusable app-bar-with-back-button pattern, using `route.back()` since Lynx has no hardware back button for a route to hook into automatically. |
91
87
 
92
88
  ## Package structure
93
89
 
@@ -97,31 +93,33 @@ create-mithril-lynx/
97
93
  templates/
98
94
  _shared/
99
95
  common/ gitignore, project README — shared by every template
100
- js/ lynx.config.js, package.json, main-thread.js — shared by every JS variant
101
- ts/ lynx.config.ts, tsconfig*, package.json, main-thread.ts — shared by every TS variant
96
+ ts/ lynx.config.ts, tsconfig*, package.json, main-thread.ts,
97
+ background.ts (single-view mount) — shared by every
98
+ template that doesn't override background.ts itself
102
99
  hello-world/
103
100
  common/src/ logo/arrow assets, style.css
104
- js/src/index.js
105
101
  ts/src/index.ts
106
102
  blank/
107
103
  common/src/style.css
108
- js/src/index.js
109
104
  ts/src/index.ts
110
105
  basic-activity/
111
106
  common/src/style.css
112
- js/src/index.js
113
- ts/src/index.ts
107
+ ts/src/{app-bar.ts, background.ts, screens/{home,detail}.ts}
114
108
  android/ only used with --android
115
109
  host/ the Gradle project -> <name>-android/
116
110
  font/AssetFontFaceLoader.kt copied only when --with-font is used
117
111
  app-scripts/android.mjs the bridge -> <name>/scripts/android.mjs
118
112
  ```
119
113
 
120
- `src/index.js` copies, in order, `templates/_shared/common` → `templates/_shared/<variant>` → `templates/<template>/common` → `templates/<template>/<variant>` into the target directory (later copies never need to overwrite earlier ones each layer contributes different files), renames `gitignore` to `.gitignore` (npm doesn't publish dotfiles reliably otherwise), and replaces `{{PROJECT_NAME}}`/`{{MITHRIL_LYNX_VERSION}}` placeholders in `package.json` and `README.md`.
114
+ `src/index.js` copies, in order, `templates/_shared/common` → `templates/_shared/ts` → `templates/<template>/common` → `templates/<template>/ts` into the target directory later copies overwrite same-named files from earlier ones, which is exactly how **Basic Activity**'s own routing-based `src/background.ts` replaces `_shared/ts`'s generic single-view one (Hello World and Blank don't ship their own, so they keep the shared file). It then renames `gitignore` to `.gitignore` (npm doesn't publish dotfiles reliably otherwise) and replaces `{{PROJECT_NAME}}`/`{{MITHRIL_LYNX_VERSION}}` placeholders in `package.json` and `README.md`.
121
115
 
122
116
  With `--android` it then copies `templates/android/host` into a sibling `<name>-android/` (renaming `package-path` to the real package directory and `App.kt` to the Application class), substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree, and fills in the four font-hack hooks (`{{FONT_LOADER_IMPORT}}`, `{{FONT_LOADER_REGISTRATION}}`, `{{FONT_IMPORT}}`, `{{FONT_PREFETCH}}`) with either the real code or nothing at all. Binary files (the Gradle wrapper jar, the `.ttf`) are never text-substituted.
123
117
 
124
- Every template's `src/index.{js,ts}` exports the same shape (`{ root }`), so `_shared/{js,ts}/src/main-thread.{js,ts}` the actual `__RenderPage` wiringis 100% shared and never duplicated per template.
118
+ **Not carried over from v1 (yet)**: the JavaScript variant. The old tool generated either TypeScript or JavaScript for every template; this rewrite ships TypeScript only for now doubling every template for a parallel JS copy wasn't part of this pass.
119
+
120
+ ## Testing
121
+
122
+ All three templates are verified end to end: scaffolded non-interactively, `npm install`ed against the real published `mithril-lynx`/`mithril-runtime` packages, and type-checked with `tsc -b` — not just copied and assumed correct.
125
123
 
126
124
  ## License
127
125
 
package/package.json CHANGED
@@ -1,17 +1,9 @@
1
1
  {
2
2
  "name": "create-mithril-lynx",
3
- "version": "0.1.1",
4
- "description": "Scaffolds a new mithril-lynx app — Hello World, Blank, or Basic Activity (with navigation), in JavaScript or TypeScript — and optionally the native Android host that packages it into an APK.",
3
+ "version": "2.0.0",
4
+ "description": "Scaffolds a new mithril-lynx app (and, optionally, its Android host).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/carlos-sweb/create-mithril-lynx.git"
10
- },
11
- "homepage": "https://github.com/carlos-sweb/create-mithril-lynx#readme",
12
- "bugs": {
13
- "url": "https://github.com/carlos-sweb/create-mithril-lynx/issues"
14
- },
15
7
  "bin": {
16
8
  "create-mithril-lynx": "src/index.js"
17
9
  },
@@ -19,10 +11,10 @@
19
11
  "src",
20
12
  "templates"
21
13
  ],
22
- "engines": {
23
- "node": "^20.19.0 || >=22.12.0"
24
- },
25
14
  "dependencies": {
26
15
  "@clack/prompts": "^1.8.0"
16
+ },
17
+ "engines": {
18
+ "node": "^20.19.0 || >=22.12.0"
27
19
  }
28
20
  }
package/src/index.js CHANGED
@@ -10,19 +10,14 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
10
10
  const packageRoot = path.join(scriptDir, "..");
11
11
  const cwd = process.cwd();
12
12
 
13
- // The mithril-lynx version pinned in the generated package.json.
14
- //
15
- // This used to be "0.0.2" (what was needed when the "basic-activity" template
16
- // was added, since that one needs mithril-lynx/navigation). It now tracks the
17
- // current release: a fresh scaffold shouldn't start five releases behind.
18
- // Verified by generating with --ts --blank and --js --basic-activity,
19
- // installing, and building the APK (debug and release).
13
+ // The mithril-lynx version pinned in the generated package.json — tracks
14
+ // the current release, same policy as this tool's own v1.
20
15
  const MITHRIL_LYNX_VERSION = "latest";
21
16
 
22
17
  const TEMPLATES = [
23
18
  { value: "hello-world", label: "Hello World", hint: "recommended" },
24
19
  { value: "blank", label: "Blank" },
25
- { value: "basic-activity", label: "Basic Activity", hint: "multi-screen navigation" },
20
+ { value: "basic-activity", label: "Basic Activity", hint: "multi-screen routing" },
26
21
  ];
27
22
  const TEMPLATE_VALUES = TEMPLATES.map((t) => t.value);
28
23
 
@@ -74,11 +69,10 @@ function replaceInFile(filePath, replacements) {
74
69
  }
75
70
 
76
71
  /** Applies substitutions across a whole tree, skipping binaries. */
77
- function replaceInTree(dir, replacements, { onlyExtensions } = {}) {
72
+ function replaceInTree(dir, replacements) {
78
73
  for (const file of walkFiles(dir)) {
79
74
  const ext = path.extname(file).toLowerCase();
80
75
  if (BINARY_EXTENSIONS.has(ext)) continue;
81
- if (onlyExtensions && !onlyExtensions.has(path.basename(file)) && !onlyExtensions.has(ext)) continue;
82
76
  replaceInFile(file, replacements);
83
77
  }
84
78
  }
@@ -157,7 +151,7 @@ function findAndroidSdk() {
157
151
  }
158
152
 
159
153
  // ---------------------------------------------------------------------------
160
- // Parseo de argumentos
154
+ // Argument parsing
161
155
  // ---------------------------------------------------------------------------
162
156
 
163
157
  function readOption(args, name) {
@@ -303,8 +297,8 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
303
297
  // best-effort
304
298
  }
305
299
 
306
- // 2. The .ttf (Part D of the guide) is opt-in: without it neither the
307
- // loader nor the prefetch is generated, so no dead code is left behind.
300
+ // 2. The .ttf is opt-in: without it neither the loader nor the prefetch is
301
+ // generated, so no dead code is left behind.
308
302
  if (font != null) {
309
303
  const fontsDir = path.join(androidDir, "app", "src", "main", "assets", "fonts");
310
304
  fs.mkdirSync(fontsDir, { recursive: true });
@@ -357,8 +351,7 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
357
351
  "// renderTemplateUrl() gives the bundle's CSS a chance to resolve",
358
352
  "// @font-face during the first layout. FontFaceManager caches by the",
359
353
  "// exact src string, so this URI has to be identical to the",
360
- '// url("...") of the @font-face in src/style.css — see Part D of',
361
- "// ANDROID_APK_GUIDE.md.",
354
+ '// url("...") of the @font-face in src/style.css.',
362
355
  "FontFaceManager.getInstance().prefetchFont(",
363
356
  " lynxView.lynxContext,",
364
357
  ` "${sourceUri}",`,
@@ -417,8 +410,8 @@ function patchJsProject({ targetDir, android }) {
417
410
  if (android.font != null) {
418
411
  const cssPath = path.join(targetDir, "src", "style.css");
419
412
  const block = [
420
- "/* Part D of ANDROID_APK_GUIDE.md: the font lives in the Android host's",
421
- " assets (app/src/main/assets/fonts/), not in the bundle. The asset:///",
413
+ "/* The font lives in the Android host's assets",
414
+ " (app/src/main/assets/fonts/), not in the bundle. The asset:///",
422
415
  " string has to be identical to the one in MainActivity.kt's prefetchFont()",
423
416
  " or Lynx won't find the warmed Typeface. */",
424
417
  "@font-face {",
@@ -453,12 +446,9 @@ function patchJsProject({ targetDir, android }) {
453
446
  `- Application ID: \`${android.androidId}\``,
454
447
  `- Application class: \`${android.appClass}\` · Activity: \`MainActivity\``,
455
448
  ...(android.font != null
456
- ? [`- Font \`${android.font.family}\` prefetched from \`asset:///fonts/${android.font.file}\` (the cold-start hack, Part D of the guide).`]
449
+ ? [`- Font \`${android.font.family}\` prefetched from \`asset:///fonts/${android.font.file}\` (the cold-start hack).`]
457
450
  : []),
458
451
  "",
459
- "The full procedure — including why each Gradle dependency is there and how to measure",
460
- "cold start — is in [`mithril-lynx`'s ANDROID_APK_GUIDE.md](https://github.com/carlos-sweb/mithril-lynx/blob/main/ANDROID_APK_GUIDE.md).",
461
- "",
462
452
  ].join("\n");
463
453
  fs.appendFileSync(readmePath, section);
464
454
  }
@@ -471,15 +461,12 @@ create-mithril-lynx — scaffold a mithril-lynx app (and, optionally, its Androi
471
461
 
472
462
  Usage:
473
463
  npm create mithril-lynx@latest [name] [options]
474
- npx create-mithril-lynx <name> --ts --blank --android
464
+ npx create-mithril-lynx <name> --blank --android
475
465
 
476
466
  Template (prompted for if omitted):
477
467
  --hello-world | --blank | --basic-activity
478
468
 
479
- Variant:
480
- --ts | --js
481
-
482
- Android host (see mithril-lynx's ANDROID_APK_GUIDE.md):
469
+ Android host:
483
470
  --android, --target android, target=android
484
471
  scaffold the sibling Gradle project <name>-android/
485
472
  --android-id <id> applicationId / namespace (default com.example.<name>)
@@ -496,10 +483,10 @@ Other:
496
483
 
497
484
  async function main() {
498
485
  // Non-interactive escape hatch for scripting/CI:
499
- // create-mithril-lynx my-app --ts --hello-world
500
- // create-mithril-lynx my-app --js --basic-activity --no-install
501
- // create-mithril-lynx my-app --ts --blank --android --android-id com.acme.miapp
502
- // create-mithril-lynx my-app --ts --blank --with-font ./UbuntuMono-Regular.ttf
486
+ // create-mithril-lynx my-app --hello-world
487
+ // create-mithril-lynx my-app --basic-activity --no-install
488
+ // create-mithril-lynx my-app --blank --android --android-id com.acme.miapp
489
+ // create-mithril-lynx my-app --blank --with-font ./UbuntuMono-Regular.ttf
503
490
  const args = process.argv.slice(2);
504
491
 
505
492
  if (args.includes("--help") || args.includes("-h")) {
@@ -508,10 +495,9 @@ async function main() {
508
495
  }
509
496
 
510
497
  const positional = findPositional(args);
511
- const variantFlag = args.includes("--ts") ? "ts" : args.includes("--js") ? "js" : undefined;
512
498
  const templateFlag = TEMPLATE_VALUES.find((t) => args.includes(`--${t}`));
513
499
  const noInstall = args.includes("--no-install");
514
- const nonInteractive = positional != null && variantFlag != null && templateFlag != null;
500
+ const nonInteractive = positional != null && templateFlag != null;
515
501
  const android = parseAndroidArgs(args);
516
502
 
517
503
  // --with-font/--font-family only make sense with an Android host: imply it
@@ -554,18 +540,6 @@ async function main() {
554
540
  if (isCancel(template)) return bail();
555
541
  }
556
542
 
557
- let variant = variantFlag;
558
- if (variant == null) {
559
- variant = await select({
560
- message: "Select a variant",
561
- options: [
562
- { value: "ts", label: "TypeScript", hint: "recommended" },
563
- { value: "js", label: "JavaScript" },
564
- ],
565
- });
566
- if (isCancel(variant)) return bail();
567
- }
568
-
569
543
  let withAndroid = android.requested;
570
544
  if (!withAndroid && !nonInteractive) {
571
545
  withAndroid = await confirm({
@@ -585,12 +559,15 @@ async function main() {
585
559
  fs.mkdirSync(targetDir, { recursive: true });
586
560
 
587
561
  // Layered copy: shared build plumbing (gitignore/README, then
588
- // lynx.config/tsconfig/main-thread for the chosen variant), then the
589
- // chosen template's own app content (style.css/assets, then src/index).
562
+ // lynx.config/tsconfig/main-thread/background), then the chosen
563
+ // template's own app content (style.css/assets, then src). A template's
564
+ // own src/background.ts (basic-activity's — routing-based) overwrites
565
+ // the shared single-view one; hello-world/blank don't ship one, so the
566
+ // shared file is what they get.
590
567
  copyDir(path.join(packageRoot, "templates/_shared/common"), targetDir);
591
- copyDir(path.join(packageRoot, "templates/_shared", variant), targetDir);
568
+ copyDir(path.join(packageRoot, "templates/_shared/ts"), targetDir);
592
569
  copyDir(path.join(packageRoot, "templates", template, "common"), targetDir);
593
- copyDir(path.join(packageRoot, "templates", template, variant), targetDir);
570
+ copyDir(path.join(packageRoot, "templates", template, "ts"), targetDir);
594
571
 
595
572
  const gitignorePath = path.join(targetDir, "gitignore");
596
573
  if (fs.existsSync(gitignorePath)) {
@@ -6,12 +6,12 @@ A [Lynx](https://lynxjs.org) app built with [Mithril.js](https://mithril.js.org)
6
6
 
7
7
  ```bash
8
8
  npm install
9
- npm run dev # scan the printed QR code with LynxExplorer
10
- npm run build # production bundle, in dist/
9
+ npm run dev # scan the printed QR code with LynxExplorer, or open it in Lynx Go
10
+ npm run build # production bundle, in dist/
11
11
  ```
12
12
 
13
13
  ## Learn more
14
14
 
15
- - `src/main-thread.{js,ts}` wires the app up using **main-thread-owned mode**, the simplest of `mithril-lynx`'s three rendering modes.
16
- - `src/index.{js,ts}` is the app itself plain Mithril hyperscript, no JSX, no virtual DOM beyond what Mithril already does.
17
- - Everything about the framework — the three rendering modes, refs, gestures, list virtualization, cross-thread calls — is documented in [`mithril-lynx`'s own README](https://github.com/carlos-sweb/mithril-lynx#readme).
15
+ - `src/main-thread.ts` starts the main-thread patch-replay runtime (`setupRenderer()`) mithril-lynx has exactly one rendering mode, so no app code lives here.
16
+ - `src/background.ts` is where the app actually mounts (`renderApp()`), running the real Mithril view tree in the background thread.
17
+ - Everything about the framework — the three reload modes, routing, networking — is documented in [`mithril-lynx`'s own README](https://github.com/carlos-sweb/mithril-lynx#readme).
@@ -27,7 +27,13 @@ export default defineConfig({
27
27
  },
28
28
  plugins: [
29
29
  pluginMithrilLynx(),
30
- pluginLynxConfig({}),
30
+ // enableNewGesture: the native gesture arena (mithril-lynx/gesture, once
31
+ // it exists, needs this on — off by default, and __SetGestureDetector
32
+ // calls are silently inert without it).
33
+ // enableCSSRule: CSS selector rules beyond a single class/type match —
34
+ // confirmed needed for a plain `:root { ... }` rule (used by every
35
+ // template's style.css) to actually apply on a real device.
36
+ pluginLynxConfig({ enableNewGesture: true, enableCSSRule: true }),
31
37
  pluginQRCode({
32
38
  schema(url) {
33
39
  // Opens the page in LynxExplorer in full screen mode.
@@ -9,7 +9,7 @@
9
9
  "preview": "rspeedy preview"
10
10
  },
11
11
  "dependencies": {
12
- "mithril": "2.3.8",
12
+ "mithril-runtime": "^1.1.0",
13
13
  "mithril-lynx": "{{MITHRIL_LYNX_VERSION}}"
14
14
  },
15
15
  "devDependencies": {
@@ -0,0 +1,35 @@
1
+ // src/background.ts
2
+ //
3
+ // The stable-host pattern: `host` never changes identity across a
4
+ // hot-update, so Mithril never unmounts/remounts it; `currentView` is a
5
+ // live binding re-pointed by `module.hot.accept`. This is what lets
6
+ // editing `index.ts` hot-reload in place without losing an `<input>`'s
7
+ // focus or in-progress text.
8
+
9
+ import m from "mithril-runtime";
10
+ import { renderApp } from "mithril-lynx/background";
11
+ import * as indexModule from "./index.js";
12
+
13
+ let currentView: typeof indexModule = indexModule;
14
+
15
+ const host: m.Component = {
16
+ view() {
17
+ return currentView.view();
18
+ },
19
+ };
20
+
21
+ const app = renderApp({ root: () => m(host) });
22
+
23
+ declare const module: {
24
+ hot?: {
25
+ accept(path: string, callback: () => void): void;
26
+ };
27
+ };
28
+ declare const require: (id: string) => typeof indexModule;
29
+
30
+ if (module.hot) {
31
+ module.hot.accept("./index.js", () => {
32
+ currentView = require("./index.js");
33
+ app.redraw();
34
+ });
35
+ }
@@ -1,20 +1,9 @@
1
- import shim from "mithril-lynx";
2
- import app from "./index.js";
1
+ // src/main-thread.ts
2
+ //
3
+ // No app code here — mithril-lynx has exactly one rendering mode: all
4
+ // view logic runs in background.ts, this just starts the main-thread
5
+ // patch-replay runtime.
3
6
 
4
- // The native engine unconditionally invokes a global processData(initData)
5
- // hook on every __RenderPage/__UpdatePage — install a pass-through default.
6
- // (mithril-lynx/main-thread's setupApp() does this for you in data-channel
7
- // mode; this bare main-thread-owned pattern doesn't go through that module.)
8
- Object.assign(globalThis, {
9
- processData: (data: unknown) => data,
10
- });
7
+ import { setupRenderer } from "mithril-lynx/main-thread";
11
8
 
12
- // Main-thread-owned mode — the simplest of mithril-lynx's three rendering
13
- // modes: no background.ts, no dual-bundle build. The app renders directly
14
- // on the main thread, synchronously, on first paint; every later update
15
- // flows through shim.redraw(), called from event handlers bound via
16
- // Mithril's own on* attrs (see src/index.ts).
17
- lynx.getEngine().addEventListener("__RenderPage", () => {
18
- const page = __CreatePage("0", 0);
19
- shim.renderToPage(page, app.root);
20
- });
9
+ setupRenderer();
@@ -0,0 +1,10 @@
1
+ // "mithril-runtime" (https://github.com/carlos-sweb/mithril-runtime) has no
2
+ // types of its own yet — it's a same-shaped subset of real Mithril (m.route/
3
+ // m.trust/m.request removed, see that repo's README and tests). Reusing
4
+ // @types/mithril here is a deliberate approximation: it still types
5
+ // m.route/m.trust/m.request as present, which mithril-runtime doesn't have
6
+ // — don't reference those from app code; nothing in this app does.
7
+ declare module "mithril-runtime" {
8
+ import m from "mithril";
9
+ export = m;
10
+ }
@@ -7,9 +7,6 @@
7
7
  "esModuleInterop": true,
8
8
  "skipLibCheck": true,
9
9
 
10
- // Explicit, not left to the TS-version-dependent default (which can be
11
- // as old as ES3 and lacks e.g. Object.assign) — confirmed necessary
12
- // by a real build failure with a newer/older typescript version.
13
10
  "target": "ES2022",
14
11
  "lib": ["ES2023"]
15
12
  },
@@ -0,0 +1,13 @@
1
+ import m from "mithril-runtime";
2
+
3
+ // A reusable app bar: a title, plus a back affordance when `onBack` is
4
+ // given. No m.route.Link involved for the back button — see
5
+ // mithril-lynx/route.js's README section on route.back(): Lynx has no
6
+ // hardware back button, so a screen's own back affordance has to call
7
+ // route.back() explicitly.
8
+ export function AppBar(title: string, onBack?: () => void) {
9
+ return m("view", { class: "AppBar" }, [
10
+ onBack != null ? m("text", { class: "AppBar-back", ontap: onBack }, "←") : null,
11
+ m("text", { class: "AppBar-title" }, title),
12
+ ]);
13
+ }
@@ -0,0 +1,40 @@
1
+ // src/background.ts
2
+ //
3
+ // A stable-host wrapper PER SCREEN, so a hot-update never unmounts/
4
+ // remounts either screen — `current*` is a live binding re-pointed by
5
+ // `module.hot.accept`, read fresh on every redraw. route(...) (mithril-lynx/
6
+ // route) is what actually mounts the app — it calls renderApp() internally
7
+ // the first time a route resolves, so this file never calls it directly.
8
+
9
+ import route from "mithril-lynx/route";
10
+ import * as homeModule from "./screens/home.js";
11
+ import * as detailModule from "./screens/detail.js";
12
+
13
+ let currentHome: typeof homeModule = homeModule;
14
+ let currentDetail: typeof detailModule = detailModule;
15
+
16
+ const HomeHost = { view: () => currentHome.view() };
17
+ const DetailHost = { view: () => currentDetail.view() };
18
+
19
+ route("/", {
20
+ "/": HomeHost,
21
+ "/detail": DetailHost,
22
+ });
23
+
24
+ declare const module: {
25
+ hot?: {
26
+ accept(path: string, callback: () => void): void;
27
+ };
28
+ };
29
+ declare const require: (id: string) => unknown;
30
+
31
+ if (module.hot) {
32
+ module.hot.accept("./screens/home.js", () => {
33
+ currentHome = require("./screens/home.js") as typeof homeModule;
34
+ route.set(route.get() ?? "/", null, { replace: true });
35
+ });
36
+ module.hot.accept("./screens/detail.js", () => {
37
+ currentDetail = require("./screens/detail.js") as typeof detailModule;
38
+ route.set(route.get() ?? "/", null, { replace: true });
39
+ });
40
+ }
@@ -0,0 +1,12 @@
1
+ import m from "mithril-runtime";
2
+ import route from "mithril-lynx/route";
3
+ import { AppBar } from "../app-bar.js";
4
+
5
+ export function view() {
6
+ return m("view", { class: "Page" }, [
7
+ AppBar("Details", () => route.back()),
8
+ m("view", { class: "Content" }, [
9
+ m("text", { class: "Description" }, String(route.param("message") ?? "")),
10
+ ]),
11
+ ]);
12
+ }
@@ -0,0 +1,17 @@
1
+ import m from "mithril-runtime";
2
+ import route from "mithril-lynx/route";
3
+ import { AppBar } from "../app-bar.js";
4
+
5
+ export function view() {
6
+ return m("view", { class: "Page" }, [
7
+ AppBar("Basic Activity"),
8
+ m("view", { class: "Content" }, [
9
+ m("text", { class: "Description" }, "This is the main screen."),
10
+ m(
11
+ route.Link,
12
+ { href: "/detail", params: { message: "Hello from the main screen!" }, selector: "view", class: "Button" },
13
+ [m("text", { class: "Button-label" }, "Next")],
14
+ ),
15
+ ]),
16
+ ]);
17
+ }
@@ -1,11 +1,7 @@
1
- import m from "mithril";
1
+ import m from "mithril-runtime";
2
2
 
3
- const App: m.Component = {
4
- view() {
5
- return m("view", { class: "Page" }, [
6
- m("text", { class: "Title" }, "Hello, mithril-lynx!"),
7
- ]);
8
- },
9
- };
10
-
11
- export default { App, root: m(App) };
3
+ export function view() {
4
+ return m("view", { class: "Page" }, [
5
+ m("text", { class: "Title" }, "Hello, mithril-lynx!"),
6
+ ]);
7
+ }
@@ -1,5 +1,4 @@
1
- import shim from "mithril-lynx";
2
- import m from "mithril";
1
+ import m from "mithril-runtime";
3
2
 
4
3
  import lynxLogo from "./assets/lynx-logo.png";
5
4
  import mithrilLogo from "./assets/mithril-logo.png";
@@ -7,41 +6,36 @@ import arrow from "./assets/arrow.png";
7
6
 
8
7
  let alterLogo = false;
9
8
 
10
- const App: m.Component = {
11
- view() {
12
- return [
13
- m("view", { class: "Background" }),
14
- m("view", { class: "App" }, [
15
- m("view", { class: "Banner" }, [
16
- m("view", {
17
- class: "Logo",
18
- // shim.redraw() NOT mithril's own m.redraw(), which is a
19
- // no-op in this shim-based architecture.
20
- ontap: () => {
21
- alterLogo = !alterLogo;
22
- shim.redraw();
23
- },
24
- }, [
25
- alterLogo
26
- ? m("image", { src: mithrilLogo, class: "Logo--mithril" })
27
- : m("image", { src: lynxLogo, class: "Logo--lynx" }),
28
- ]),
29
- m("text", { class: "Title" }, "Mithril"),
30
- m("text", { class: "Subtitle" }, "on Lynx"),
31
- ]),
32
- m("view", { class: "Content" }, [
33
- m("image", { src: arrow, class: "Arrow" }),
34
- m("text", { class: "Description" }, "Tap the logo and have fun!"),
35
- m("text", { class: "Hint" }, [
36
- "Edit ",
37
- m("text", { style: { fontStyle: "italic", color: "rgba(255, 255, 255, 0.85)" } }, "src/index.ts"),
38
- " to see updates!",
39
- ]),
40
- ]),
41
- m("view", { style: { flex: 1 } }),
42
- ]),
43
- ];
44
- },
45
- };
46
-
47
- export default { App, root: m(App) };
9
+ export function view() {
10
+ return [
11
+ m("view", { class: "Background" }),
12
+ m("view", { class: "App" }, [
13
+ m("view", { class: "Banner" }, [
14
+ m("view", {
15
+ class: "Logo",
16
+ // No explicit redraw() call here — mithril-lynx repaints
17
+ // automatically after any event, so mutating state is enough.
18
+ ontap: () => {
19
+ alterLogo = !alterLogo;
20
+ },
21
+ }, [
22
+ alterLogo
23
+ ? m("image", { src: mithrilLogo, class: "Logo--mithril" })
24
+ : m("image", { src: lynxLogo, class: "Logo--lynx" }),
25
+ ]),
26
+ m("text", { class: "Title" }, "Mithril"),
27
+ m("text", { class: "Subtitle" }, "on Lynx"),
28
+ ]),
29
+ m("view", { class: "Content" }, [
30
+ m("image", { src: arrow, class: "Arrow" }),
31
+ m("text", { class: "Description" }, "Tap the logo and have fun!"),
32
+ m("text", { class: "Hint" }, [
33
+ "Edit ",
34
+ m("text", { style: { fontStyle: "italic", color: "rgba(255, 255, 255, 0.85)" } }, "src/index.ts"),
35
+ " to see updates!",
36
+ ]),
37
+ ]),
38
+ m("view", { style: { flex: 1 } }),
39
+ ]),
40
+ ];
41
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 carlos-sweb
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,37 +0,0 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
-
4
- import { pluginLynxConfig } from "@lynx-js/config-rsbuild-plugin";
5
- import { pluginQRCode } from "@lynx-js/qrcode-rsbuild-plugin";
6
- import { defineConfig } from "@lynx-js/rspeedy";
7
-
8
- import { pluginMithrilLynx } from "mithril-lynx/plugin";
9
-
10
- const projectRoot = path.dirname(fileURLToPath(import.meta.url));
11
-
12
- export default defineConfig({
13
- source: {
14
- entry: {
15
- "main-thread": path.join(projectRoot, "src/main-thread.js"),
16
- },
17
- },
18
- output: {
19
- distPath: {
20
- root: path.join(projectRoot, "dist"),
21
- },
22
- filename: "[name].bundle",
23
- // Lynx bundles are self-contained — image imports must be inlined as
24
- // data URIs rather than left as separate file references.
25
- dataUriLimit: Infinity,
26
- },
27
- plugins: [
28
- pluginMithrilLynx(),
29
- pluginLynxConfig({}),
30
- pluginQRCode({
31
- schema(url) {
32
- // Opens the page in LynxExplorer in full screen mode.
33
- return `${url}?fullscreen=true`;
34
- },
35
- }),
36
- ],
37
- });
@@ -1,26 +0,0 @@
1
- {
2
- "name": "{{PROJECT_NAME}}",
3
- "version": "0.0.1",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "rspeedy dev",
8
- "build": "rspeedy build",
9
- "preview": "rspeedy preview"
10
- },
11
- "dependencies": {
12
- "mithril": "2.3.8",
13
- "mithril-lynx": "{{MITHRIL_LYNX_VERSION}}"
14
- },
15
- "devDependencies": {
16
- "@lynx-js/config-rsbuild-plugin": "^0.2.0",
17
- "@lynx-js/qrcode-rsbuild-plugin": "^0.7.0",
18
- "@lynx-js/rspeedy": "^0.17.0",
19
- "@lynx-js/template-webpack-plugin": "^0.16.0",
20
- "@lynx-js/type-element-api": "0.0.9",
21
- "@lynx-js/types": "4.1.0"
22
- },
23
- "engines": {
24
- "node": "^20.19.0 || >=22.12.0"
25
- }
26
- }
@@ -1,20 +0,0 @@
1
- import shim from "mithril-lynx";
2
- import app from "./index.js";
3
-
4
- // The native engine unconditionally invokes a global processData(initData)
5
- // hook on every __RenderPage/__UpdatePage — install a pass-through default.
6
- // (mithril-lynx/main-thread's setupApp() does this for you in data-channel
7
- // mode; this bare main-thread-owned pattern doesn't go through that module.)
8
- Object.assign(globalThis, {
9
- processData: (data) => data,
10
- });
11
-
12
- // Main-thread-owned mode — the simplest of mithril-lynx's three rendering
13
- // modes: no background.js, no dual-bundle build. The app renders directly
14
- // on the main thread, synchronously, on first paint; every later update
15
- // flows through shim.redraw(), called from event handlers bound via
16
- // Mithril's own on* attrs (see src/index.js).
17
- lynx.getEngine().addEventListener("__RenderPage", () => {
18
- const page = __CreatePage("0", 0);
19
- shim.renderToPage(page, app.root);
20
- });
@@ -1,43 +0,0 @@
1
- import m from "mithril";
2
- import { createNavigator } from "mithril-lynx/navigation";
3
-
4
- // A reusable app bar: a title, plus a back affordance when `onBack` is
5
- // given. No m.route involved anywhere in this file — see
6
- // mithril-lynx/navigation's own docs for why (Lynx pages have no URL to
7
- // route to). Navigation here is a plain in-memory stack.
8
- function AppBar(title, onBack) {
9
- return m("view", { class: "AppBar" }, [
10
- onBack != null ? m("text", { class: "AppBar-back", ontap: onBack }, "←") : null,
11
- m("text", { class: "AppBar-title" }, title),
12
- ]);
13
- }
14
-
15
- const MainScreen = {
16
- view(vnode) {
17
- return m("view", { class: "Page" }, [
18
- AppBar("Basic Activity"),
19
- m("view", { class: "Content" }, [
20
- m("text", { class: "Description" }, "This is the main screen."),
21
- m("view", {
22
- class: "Button",
23
- ontap: () => vnode.attrs.nav.push(DetailsScreen, { message: "Hello from MainScreen!" }),
24
- }, [m("text", { class: "Button-label" }, "Next")]),
25
- ]),
26
- ]);
27
- },
28
- };
29
-
30
- const DetailsScreen = {
31
- view(vnode) {
32
- return m("view", { class: "Page" }, [
33
- AppBar("Details", () => vnode.attrs.nav.pop()),
34
- m("view", { class: "Content" }, [
35
- m("text", { class: "Description" }, vnode.attrs.message),
36
- ]),
37
- ]);
38
- },
39
- };
40
-
41
- const nav = createNavigator({ initial: MainScreen });
42
-
43
- export default { root: m(nav.Navigator) };
@@ -1,48 +0,0 @@
1
- import m from "mithril";
2
- import { createNavigator, type Nav } from "mithril-lynx/navigation";
3
-
4
- interface DetailsAttrs {
5
- nav: Nav;
6
- message: string;
7
- }
8
-
9
- // A reusable app bar: a title, plus a back affordance when `onBack` is
10
- // given. No m.route involved anywhere in this file — see
11
- // mithril-lynx/navigation's own docs for why (Lynx pages have no URL to
12
- // route to). Navigation here is a plain in-memory stack.
13
- function AppBar(title: string, onBack?: () => void) {
14
- return m("view", { class: "AppBar" }, [
15
- onBack != null ? m("text", { class: "AppBar-back", ontap: onBack }, "←") : null,
16
- m("text", { class: "AppBar-title" }, title),
17
- ]);
18
- }
19
-
20
- const MainScreen: m.Component<{ nav: Nav }> = {
21
- view(vnode) {
22
- return m("view", { class: "Page" }, [
23
- AppBar("Basic Activity"),
24
- m("view", { class: "Content" }, [
25
- m("text", { class: "Description" }, "This is the main screen."),
26
- m("view", {
27
- class: "Button",
28
- ontap: () => vnode.attrs.nav.push(DetailsScreen, { message: "Hello from MainScreen!" }),
29
- }, [m("text", { class: "Button-label" }, "Next")]),
30
- ]),
31
- ]);
32
- },
33
- };
34
-
35
- const DetailsScreen: m.Component<DetailsAttrs> = {
36
- view(vnode) {
37
- return m("view", { class: "Page" }, [
38
- AppBar("Details", () => vnode.attrs.nav.pop()),
39
- m("view", { class: "Content" }, [
40
- m("text", { class: "Description" }, vnode.attrs.message),
41
- ]),
42
- ]);
43
- },
44
- };
45
-
46
- const nav = createNavigator({ initial: MainScreen });
47
-
48
- export default { root: m(nav.Navigator) };
@@ -1,11 +0,0 @@
1
- import m from "mithril";
2
-
3
- const App = {
4
- view() {
5
- return m("view", { class: "Page" }, [
6
- m("text", { class: "Title" }, "Hello, mithril-lynx!"),
7
- ]);
8
- },
9
- };
10
-
11
- export default { App, root: m(App) };
@@ -1,47 +0,0 @@
1
- import shim from "mithril-lynx";
2
- import m from "mithril";
3
-
4
- import lynxLogo from "./assets/lynx-logo.png";
5
- import mithrilLogo from "./assets/mithril-logo.png";
6
- import arrow from "./assets/arrow.png";
7
-
8
- let alterLogo = false;
9
-
10
- const App = {
11
- view() {
12
- return [
13
- m("view", { class: "Background" }),
14
- m("view", { class: "App" }, [
15
- m("view", { class: "Banner" }, [
16
- m("view", {
17
- class: "Logo",
18
- // shim.redraw() — NOT mithril's own m.redraw(), which is a
19
- // no-op in this shim-based architecture.
20
- ontap: () => {
21
- alterLogo = !alterLogo;
22
- shim.redraw();
23
- },
24
- }, [
25
- alterLogo
26
- ? m("image", { src: mithrilLogo, class: "Logo--mithril" })
27
- : m("image", { src: lynxLogo, class: "Logo--lynx" }),
28
- ]),
29
- m("text", { class: "Title" }, "Mithril"),
30
- m("text", { class: "Subtitle" }, "on Lynx"),
31
- ]),
32
- m("view", { class: "Content" }, [
33
- m("image", { src: arrow, class: "Arrow" }),
34
- m("text", { class: "Description" }, "Tap the logo and have fun!"),
35
- m("text", { class: "Hint" }, [
36
- "Edit ",
37
- m("text", { style: { fontStyle: "italic", color: "rgba(255, 255, 255, 0.85)" } }, "src/index.js"),
38
- " to see updates!",
39
- ]),
40
- ]),
41
- m("view", { style: { flex: 1 } }),
42
- ]),
43
- ];
44
- },
45
- };
46
-
47
- export default { App, root: m(App) };