create-mithril-lynx 2.1.0 → 2.2.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.
Files changed (3) hide show
  1. package/README.md +24 -3
  2. package/package.json +1 -1
  3. package/src/index.js +114 -37
package/README.md CHANGED
@@ -49,13 +49,34 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
49
49
  | `--android` / `--target android` / `--target=android` / `target=android` | Also scaffold the sibling `<name>-android/` Gradle project. |
50
50
  | `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
51
51
  | `--app-name <name>` | Launcher label (default: the project name). |
52
- | `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()`. No native code: the import is inlined as a `data:` URI (`dataUriLimit: Infinity` in `lynx.config.ts`), which resolves the same way on every host — the real APK, LynxExplorer, or Lynx Go. Confirmed on device: no cold-start cost either (~730ms with the font vs. ~770ms without, on the same device). |
53
- | `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and a weight/style, download that one `.ttf`, and bundle it exactly like `--with-font`. Needs a real terminal (the picking is inherently interactive — use `--with-font <file.ttf>` in scripts/CI). Mutually exclusive with `--with-font`. |
54
- | `--font-family <name>` | Override the family name derived from the font's file name (or from Fontsource, with `--find-font`). Only meaningful with `--with-font`/`--find-font`. |
52
+ | `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()`. No native code: the import is inlined as a `data:` URI (`dataUriLimit: Infinity` in `lynx.config.ts`), which resolves the same way on every host — the real APK, LynxExplorer, or Lynx Go. Confirmed on device: no cold-start cost either (~730ms with the font vs. ~770ms without, on the same device). Comma-separate for more than one file. |
53
+ | `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and a weight/style, download that one `.ttf`, and bundle it exactly like `--with-font`. Needs a real terminal (the picking is inherently interactive — use `--with-font <file.ttf>` in scripts/CI). Mutually exclusive with `--with-font`. Comma-separate terms for more than one — quote the whole thing if any term has a space. |
54
+ | `--font-family <name>` | Override the family name derived from the font's file name (or from Fontsource, with `--find-font`). Only valid with exactly one font. |
55
55
 
56
56
  Anything not listed — in particular the four Android flags — requires `--android`
57
57
  (or one of its aliases); `--with-font`/`--find-font` imply it on their own.
58
58
 
59
+ ### More than one font
60
+
61
+ Both flags take a comma-separated list — useful for a body font plus a
62
+ monospace one for code, for example:
63
+
64
+ ```bash
65
+ npx create-mithril-lynx my-app --blank --android --find-font "Inter,JetBrains Mono"
66
+ ```
67
+
68
+ Each font gets its own `import`/`lynx.addFont()` call in `src/background.ts`.
69
+ Only the **first** one gets the automatic `text { font-family: ...; }` rule
70
+ in `src/style.css` (there's no way for the CLI to guess which elements
71
+ should use which font past that) — the rest are registered and ready to use,
72
+ but you assign them to your own classes by hand, e.g.:
73
+
74
+ ```css
75
+ .code {
76
+ font-family: "JetBrains Mono", monospace;
77
+ }
78
+ ```
79
+
59
80
  ### `--find-font`, in more detail
60
81
 
61
82
  Fontsource's own API (`api.fontsource.org`) has no free-text search — only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mithril-lynx",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Scaffolds a new mithril-lynx app (and, optionally, its Android host).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -135,6 +135,33 @@ function fontFamilyFor(filePath) {
135
135
  .join(" ");
136
136
  }
137
137
 
138
+ /** `--with-font`/`--find-font` accept a comma-separated list, for bundling
139
+ * more than one font (e.g. a body font and a monospace one for code). */
140
+ function splitList(value) {
141
+ return value
142
+ .split(",")
143
+ .map((v) => v.trim())
144
+ .filter(Boolean);
145
+ }
146
+
147
+ /** Renames `file` on any font past the first with the same basename, so
148
+ * e.g. two different "Inter" downloads (different weights) don't collide
149
+ * once copied into src/assets/fonts/. */
150
+ function dedupeFontFiles(fonts) {
151
+ const seen = new Set();
152
+ for (const font of fonts) {
153
+ let file = font.file;
154
+ let n = 2;
155
+ while (seen.has(file)) {
156
+ const ext = path.extname(font.file);
157
+ file = `${path.basename(font.file, ext)}-${n}${ext}`;
158
+ n += 1;
159
+ }
160
+ seen.add(file);
161
+ font.file = file;
162
+ }
163
+ }
164
+
138
165
  /** A valid JS identifier (camelCase, "Font" suffix) for a font-family string. */
139
166
  function jsIdentifierFor(family) {
140
167
  const words = family.split(/[^a-zA-Z0-9]+/).filter(Boolean);
@@ -371,40 +398,54 @@ async function resolveAndroidOptions(android, { rawName, projectName, canPrompt
371
398
  process.exit(1);
372
399
  }
373
400
 
374
- let font = null;
375
- if (android.findFontTerm != null) {
376
- const found = await findFontInteractively(android.findFontTerm);
401
+ // Both flags accept a comma-separated list, so more than one font (e.g.
402
+ // a body font and a monospace one for code) can be bundled in one run.
403
+ const fontPaths = android.fontPath != null ? splitList(android.fontPath) : [];
404
+ const findFontTerms = android.findFontTerm != null ? splitList(android.findFontTerm) : [];
405
+ const fontCount = fontPaths.length + findFontTerms.length;
406
+
407
+ if (android.fontFamily != null && fontCount > 1) {
408
+ cancel("--font-family only makes sense with exactly one font — omit it when bundling more than one, or edit src/style.css/background.ts by hand afterwards.");
409
+ process.exit(1);
410
+ }
411
+ if (android.fontFamily != null && fontCount === 0) {
412
+ cancel("--font-family only makes sense together with --with-font or --find-font.");
413
+ process.exit(1);
414
+ }
415
+
416
+ const fonts = [];
417
+ for (const term of findFontTerms) {
418
+ const found = await findFontInteractively(term);
377
419
  if (found == null) return null;
378
- font = {
420
+ fonts.push({
379
421
  sourcePath: found.sourcePath,
380
422
  file: path.basename(found.sourcePath),
381
423
  family: android.fontFamily ?? found.family,
382
- };
383
- } else if (android.fontPath != null) {
384
- const resolved = path.resolve(cwd, android.fontPath);
424
+ });
425
+ }
426
+ for (const rawPath of fontPaths) {
427
+ const resolved = path.resolve(cwd, rawPath);
385
428
  if (!fs.existsSync(resolved)) {
386
- cancel(`Font file not found: ${android.fontPath}`);
429
+ cancel(`Font file not found: ${rawPath}`);
387
430
  process.exit(1);
388
431
  }
389
432
  if (![".ttf", ".otf", ".ttc"].includes(path.extname(resolved).toLowerCase())) {
390
- cancel(`"${android.fontPath}" does not look like a font (.ttf/.otf/.ttc).`);
433
+ cancel(`"${rawPath}" does not look like a font (.ttf/.otf/.ttc).`);
391
434
  process.exit(1);
392
435
  }
393
- font = {
436
+ fonts.push({
394
437
  sourcePath: resolved,
395
438
  file: path.basename(resolved),
396
439
  family: android.fontFamily ?? fontFamilyFor(resolved),
397
- };
398
- } else if (android.fontFamily != null) {
399
- cancel("--font-family only makes sense together with --with-font or --find-font.");
400
- process.exit(1);
440
+ });
401
441
  }
442
+ dedupeFontFiles(fonts);
402
443
 
403
- return { androidId, appName, font };
444
+ return { androidId, appName, fonts };
404
445
  }
405
446
 
406
447
  function scaffoldAndroid({ targetDir, rawName, options }) {
407
- const { androidId, appName, font } = options;
448
+ const { androidId, appName, fonts } = options;
408
449
 
409
450
  const androidDirName = `${path.basename(rawName.replace(/^@[^/]+\//, ""))}-android`;
410
451
  const androidDir = path.join(path.dirname(targetDir), androidDirName);
@@ -469,7 +510,7 @@ function scaffoldAndroid({ targetDir, rawName, options }) {
469
510
  ["{{APP_NAME}}", appName.replace(/["\\]/g, "")],
470
511
  ]);
471
512
 
472
- return { androidDir, androidDirName, androidRelDir, androidId, appName, appClass, font, sdkDir };
513
+ return { androidDir, androidDirName, androidRelDir, androidId, appName, appClass, fonts, sdkDir };
473
514
  }
474
515
 
475
516
  function patchJsProject({ targetDir, android }) {
@@ -501,21 +542,51 @@ function patchJsProject({ targetDir, android }) {
501
542
  // measured 724-745ms with the font vs. 715-826ms without —
502
543
  // indistinguishable; the old +1-2s regression was specific to
503
544
  // @font-face's forced synchronous resolution, which doesn't apply here).
504
- if (android.font != null) {
505
- const { family, sourcePath, file } = android.font;
545
+ if (android.fonts.length > 0) {
506
546
  const fontsDir = path.join(targetDir, "src", "assets", "fonts");
507
547
  fs.mkdirSync(fontsDir, { recursive: true });
508
- fs.copyFileSync(sourcePath, path.join(fontsDir, file));
509
548
 
510
- const varName = jsIdentifierFor(family);
549
+ const usedVarNames = new Set();
550
+ const importLines = [];
551
+ const addFontLines = [];
552
+ for (const { family, sourcePath, file } of android.fonts) {
553
+ fs.copyFileSync(sourcePath, path.join(fontsDir, file));
554
+
555
+ let varName = jsIdentifierFor(family);
556
+ while (usedVarNames.has(varName)) varName = `${varName}2`;
557
+ usedVarNames.add(varName);
558
+
559
+ importLines.push(`import ${varName} from "./assets/fonts/${file}";`);
560
+ addFontLines.push(
561
+ `lynx.addFont({ "font-family": "${family}", src: \`url("\${${varName}}")\` }, () => {});`,
562
+ );
563
+ }
564
+
565
+ // Only the first font gets an automatic CSS rule (the app-wide
566
+ // default, matching --with-font's single-font behavior exactly) —
567
+ // with more than one, there's no way to guess which elements should
568
+ // use which, so the rest are just registered via lynx.addFont() and
569
+ // left for you to assign in your own CSS classes.
570
+ const defaultFamily = android.fonts[0].family;
571
+ const extraFamilies = android.fonts.slice(1).map((f) => f.family);
572
+
511
573
  const bgPath = path.join(targetDir, "src", "background.ts");
512
574
  const fontBlock = [
513
- `// Font: "${family}", bundled from src/assets/fonts/${file}. lynx.addFont()`,
514
- "// registers it directly dataUriLimit: Infinity (lynx.config.ts) inlines",
515
- "// the import as a data: URI, which resolves the same way on every host,",
575
+ android.fonts.length === 1
576
+ ? `// Font: "${defaultFamily}", bundled from src/assets/fonts/${android.fonts[0].file}. lynx.addFont()`
577
+ : `// Fonts: ${android.fonts.map((f) => `"${f.family}"`).join(", ")}, bundled from src/assets/fonts/. lynx.addFont()`,
578
+ "// registers each directly — dataUriLimit: Infinity (lynx.config.ts) inlines",
579
+ "// every import as a data: URI, which resolves the same way on every host,",
516
580
  "// no native code needed.",
517
- `import ${varName} from "./assets/fonts/${file}";`,
518
- `lynx.addFont({ "font-family": "${family}", src: \`url("\${${varName}}")\` }, () => {});`,
581
+ ...(extraFamilies.length > 0
582
+ ? [
583
+ `// Only "${defaultFamily}" got the automatic text { font-family: ... }`,
584
+ `// rule below — assign ${extraFamilies.map((f) => `"${f}"`).join(" / ")} to your own`,
585
+ "// classes in src/style.css, e.g. `.code { font-family: \"" + extraFamilies[0] + "\"; }`.",
586
+ ]
587
+ : []),
588
+ ...importLines,
589
+ ...addFontLines,
519
590
  "",
520
591
  "",
521
592
  ].join("\n");
@@ -523,7 +594,7 @@ function patchJsProject({ targetDir, android }) {
523
594
  fs.writeFileSync(bgPath, `${fontBlock}${existingBg}`);
524
595
 
525
596
  const cssPath = path.join(targetDir, "src", "style.css");
526
- const cssBlock = [`text {`, ` font-family: "${family}", sans-serif;`, `}`, ""].join("\n");
597
+ const cssBlock = [`text {`, ` font-family: "${defaultFamily}", sans-serif;`, `}`, ""].join("\n");
527
598
  const existingCss = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : "";
528
599
  fs.writeFileSync(cssPath, `${cssBlock}\n${existingCss}`);
529
600
  }
@@ -549,9 +620,10 @@ function patchJsProject({ targetDir, android }) {
549
620
  "",
550
621
  `- Application ID: \`${android.androidId}\``,
551
622
  `- Application class: \`${android.appClass}\` · Activity: \`MainActivity\``,
552
- ...(android.font != null
553
- ? [`- Font \`${android.font.family}\` loaded via \`lynx.addFont()\` from \`src/assets/fonts/${android.font.file}\` (bundled as a data: URI — no native code involved).`]
554
- : []),
623
+ ...android.fonts.map(
624
+ (f) =>
625
+ `- Font \`${f.family}\` loaded via \`lynx.addFont()\` from \`src/assets/fonts/${f.file}\` (bundled as a data: URI — no native code involved).`,
626
+ ),
555
627
  "",
556
628
  ].join("\n");
557
629
  fs.appendFileSync(readmePath, section);
@@ -577,11 +649,16 @@ Android host:
577
649
  --app-name <name> launcher label (default: the project name)
578
650
  --with-font <file.ttf> bundle the font into the JS project and register it
579
651
  with lynx.addFont() (works on every host, no native
580
- code — see README)
652
+ code — see README). Comma-separate for more than
653
+ one, e.g. --with-font a.ttf,b.ttf
581
654
  --find-font <term> search Fontsource (fontsource.org) for a font,
582
655
  pick a family and a weight/style interactively,
583
- and bundle it the same way as --with-font
584
- --font-family <name> override the family name derived from the file name
656
+ and bundle it the same way as --with-font.
657
+ Comma-separate terms for more than one quote
658
+ the whole thing if any term has a space, e.g.
659
+ --find-font "Inter,JetBrains Mono"
660
+ --font-family <name> override the family name derived from the file
661
+ name (only valid with exactly one font)
585
662
 
586
663
  Other:
587
664
  --no-install don't install dependencies
@@ -720,7 +797,7 @@ async function main() {
720
797
  ];
721
798
 
722
799
  if (androidResult != null) {
723
- const { androidDirName, sdkDir, font } = androidResult;
800
+ const { androidDirName, sdkDir, fonts } = androidResult;
724
801
  const notes = [
725
802
  `Android host generated in ${androidDirName}/ (Application ID ${androidOptions.androidId}).`,
726
803
  "",
@@ -732,9 +809,9 @@ async function main() {
732
809
  ? "⚠ Android SDK not found: export ANDROID_HOME and edit " +
733
810
  `${androidDirName}/local.properties (sdk.dir=...).`
734
811
  : `Android SDK found at ${sdkDir}.`,
735
- font != null
736
- ? `Font "${font.family}" bundled and registered via lynx.addFont() — works on every host.`
737
- : "No custom font — pass --with-font <file.ttf> to bundle one.",
812
+ fonts.length > 0
813
+ ? `Font${fonts.length > 1 ? "s" : ""} ${fonts.map((f) => `"${f.family}"`).join(", ")} bundled and registered via lynx.addFont() — works on every host.`
814
+ : "No custom font — pass --with-font <file.ttf> (or --find-font <term>) to bundle one.",
738
815
  ];
739
816
  outro(`Done! Next steps:\n\n ${steps.join("\n ")}\n\n${notes.join("\n")}`);
740
817
  return;