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.
- package/README.md +24 -3
- package/package.json +1 -1
- 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
|
|
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
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
420
|
+
fonts.push({
|
|
379
421
|
sourcePath: found.sourcePath,
|
|
380
422
|
file: path.basename(found.sourcePath),
|
|
381
423
|
family: android.fontFamily ?? found.family,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
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: ${
|
|
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(`"${
|
|
433
|
+
cancel(`"${rawPath}" does not look like a font (.ttf/.otf/.ttc).`);
|
|
391
434
|
process.exit(1);
|
|
392
435
|
}
|
|
393
|
-
|
|
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,
|
|
444
|
+
return { androidId, appName, fonts };
|
|
404
445
|
}
|
|
405
446
|
|
|
406
447
|
function scaffoldAndroid({ targetDir, rawName, options }) {
|
|
407
|
-
const { androidId, appName,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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: "${
|
|
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
|
-
...
|
|
553
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
736
|
-
? `Font "${
|
|
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;
|