create-mithril-lynx 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,7 +50,7 @@ Every flag, in one table — `npx create-mithril-lynx --help` prints the same li
50
50
  | `--android-id <id>` | `applicationId` / `namespace` (default `com.example.<name>`). |
51
51
  | `--app-name <name>` | Launcher label (default: the project name). |
52
52
  | `--with-font <file.ttf>` | Bundle the font into `src/assets/fonts/` and register it with `lynx.addFont()` in a loop. **DEV** (`npm run dev` / Lynx Go): `require()` inlines a `data:` URI so the font shows up in Explorer. **PROD** (`npm run build` / APK): `asset:///fonts/<file>` resolved by `AssetFontFaceLoader` on the host (keeps the bundle small — embedding `data:` URIs in the APK re-introduces the cold-start cost tracked in [lynx#9431](https://github.com/lynx-family/lynx/issues/9431)). 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. |
53
+ | `--find-font <term>` | Search [Fontsource](https://fontsource.org)'s catalog for `<term>`, prompt you to pick a family and one or more weights/styles, download each `.ttf`, and bundle them 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
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`
@@ -67,18 +67,23 @@ npx create-mithril-lynx my-app --blank --android --find-font "Inter,JetBrains Mo
67
67
 
68
68
  Each font is one entry in a `FONTS` array in `src/background.ts`, registered
69
69
  with a single `for…of` + `lynx.addFont()` loop (DEV `require` / PROD
70
- `asset:///` — see the `--with-font` row above). Only the **first** one gets
71
- the automatic `text { font-family: ...; }` rule in `src/style.css` (there's
72
- no way for the CLI to guess which elements should use which font past that)
73
- — the rest are registered and ready to use, but you assign them to your own
74
- classes by hand, e.g.:
70
+ `asset:///` — see the `--with-font` row above).
71
+
72
+ The CSS side lives in a separate, **generated** file, `src/fonts.css`, which
73
+ `src/style.css` pulls in with a single `@import "fonts.css";` line at the top.
74
+ `fonts.css` holds `text { font-family: ...; }` for the **first** font and, when
75
+ there is more than one font, one class per family/variant:
75
76
 
76
77
  ```css
77
- .code {
78
- font-family: "JetBrains Mono", monospace;
79
- }
78
+ .Ubuntu_Regular { font-family: "Ubuntu Regular", sans-serif; }
79
+ .Ubuntu_Bold { font-family: "Ubuntu Bold", sans-serif; }
80
+ .Inter_Regular { font-family: "Inter Regular", sans-serif; }
80
81
  ```
81
82
 
83
+ `fonts.css` is owned by the tooling and rewritten from the full font list on
84
+ every run (scaffold or `add-font`) — don't edit it by hand; put your own rules
85
+ in `style.css`. (Unused classes can be pruned by a later PostCSS pass.)
86
+
82
87
  ### Adding fonts after the project exists
83
88
 
84
89
  ```bash
@@ -88,11 +93,12 @@ npx create-mithril-lynx add-font --find-font "Roboto Mono"
88
93
  ```
89
94
 
90
95
  Copying the `.ttf` into `src/assets/fonts/` (and the Android host's
91
- `assets/fonts/` when present) is always safe. Wiring `background.ts` /
92
- `style.css` is automatic **only while those files still match the stock
93
- template** (including a previously generated `FONTS` block on top of it). If
94
- you've edited them, the command prints the exact block to paste by hand and
95
- leaves your files untouched.
96
+ `assets/fonts/` when present) is always safe. `src/fonts.css` is always
97
+ regenerated, and `style.css` only ever gets the one `@import "fonts.css";`
98
+ line (added if missing). Wiring `background.ts` is automatic **only while it
99
+ still matches the stock template** (including a previously generated `FONTS`
100
+ block on top of it); if you've edited it, the command prints the exact block
101
+ to paste by hand and leaves your file untouched.
96
102
 
97
103
  ### `--find-font`, in more detail
98
104
 
@@ -101,11 +107,13 @@ exact `id`/`family` filters — so `--find-font` downloads the whole font list
101
107
  once (~2100 fonts, ~540KB as of 2026-09), caches it for 24h in the OS temp
102
108
  directory, and filters client-side on `family`/`id` substrings. Picking a
103
109
  family fetches that font's own detail (its real weight/style/subset `.ttf`
104
- URLs), lets you pick one variant (defaulting to 400/normal if available),
105
- downloads it to a temp file, and hands it to the same code path
110
+ URLs), lets you pick one or more variants (space to toggle; defaulting to 400/normal if available),
111
+ downloads each to a temp file, and hands them to the same code path
106
112
  `--with-font <file>` uses — there's no separate font-loading mechanism to
107
113
  maintain.
108
114
 
115
+ Every picked variant is registered as `<Family> <Variant>` (`Inter Regular`, `Inter Bold`, `Inter Light Italic`…), with a matching class in `src/fonts.css` (`.Inter_Bold`). The first one (400/normal if picked) is the default `text` font.
116
+
109
117
  ```bash
110
118
  npx create-mithril-lynx my-app --blank --android --find-font Inter
111
119
  ```
@@ -172,7 +180,7 @@ create-mithril-lynx/
172
180
 
173
181
  `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`.
174
182
 
175
- 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) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`, each font is copied into the JS project (`src/assets/fonts/`) *and* seeded into the Android host's `assets/fonts/`, wired up with a `FONTS` loop + `lynx.addFont()` in `src/background.ts` plus a `text { font-family: ... }` rule in `src/style.css` for the first font. `scripts/android.mjs` keeps `assets/fonts/` in sync on every `npm run android` / `android:sync`.
183
+ 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) and substitutes `{{PACKAGE_NAME}}`, `{{APP_CLASS}}`, `{{APP_NAME}}`, `{{ANDROID_DIR}}` and `{{SDK_DIR}}` across the tree. Binary files (the Gradle wrapper jar, a `--with-font` `.ttf`) are never text-substituted. With `--with-font`, each font is copied into the JS project (`src/assets/fonts/`) *and* seeded into the Android host's `assets/fonts/`, wired up with a `FONTS` loop + `lynx.addFont()` in `src/background.ts` plus a generated `src/fonts.css` (`text { font-family }` for the first font, one class per font when there are several) imported from `src/style.css`. `scripts/android.mjs` keeps `assets/fonts/` in sync on every `npm run android` / `android:sync`.
176
184
 
177
185
  **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.
178
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mithril-lynx",
3
- "version": "2.3.0",
3
+ "version": "2.4.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/fonts-wire.js CHANGED
@@ -7,7 +7,10 @@
7
7
  * host's assets/fonts/ when present) — that part is always safe.
8
8
  * 2. If src/background.ts still matches a stock template (optionally with
9
9
  * our generated FONTS block on top), rewrite the font block in place.
10
- * 3. Same idea for src/style.css's automatic `text { font-family }` rule.
10
+ * 3. Always (re)generate src/fonts.css — a system-owned file holding the
11
+ * `text { font-family }` default and one class per family/variant — and
12
+ * make sure src/style.css has `@import "fonts.css";`. Users never edit
13
+ * fonts.css by hand; every add-font run rewrites it from the full list.
11
14
  * 4. Otherwise print the exact lines the user must paste by hand.
12
15
  */
13
16
  import fs from "node:fs";
@@ -33,7 +36,6 @@ export function normalizeSource(text) {
33
36
 
34
37
  export function buildFontBlock(fonts) {
35
38
  const defaultFamily = fonts[0].family;
36
- const extraFamilies = fonts.slice(1).map((f) => f.family);
37
39
  const requireEntries = fonts
38
40
  .map(
39
41
  ({ family, file }) =>
@@ -55,13 +57,8 @@ export function buildFontBlock(fonts) {
55
57
  "// • DEV (Lynx Go / `npm run dev`): require() → data: URI inline",
56
58
  "// • PROD (Android APK / `npm run build`): asset:///fonts/<file>",
57
59
  "// resolved by AssetFontFaceLoader (lynx#9431 workaround).",
58
- ...(extraFamilies.length > 0
59
- ? [
60
- `// Only "${defaultFamily}" got the automatic text { font-family: ... }`,
61
- `// rule below — assign ${extraFamilies.map((f) => `"${f}"`).join(" / ")} to your own`,
62
- '// classes in src/style.css, e.g. `.code { font-family: "' + extraFamilies[0] + '"; }`.',
63
- ]
64
- : []),
60
+ "// The default `text { font-family }` and one .Family_Variant class per font",
61
+ "// live in src/fonts.css, which is generated — don't edit it by hand.",
65
62
  "type FontEntry = { family: string; file: string; url: string };",
66
63
  "const FONTS: FontEntry[] = import.meta.env.DEV",
67
64
  "\t? [",
@@ -78,8 +75,35 @@ export function buildFontBlock(fonts) {
78
75
  ].join("\n");
79
76
  }
80
77
 
81
- export function buildCssFontBlock(family) {
82
- return [`text {`, ` font-family: "${family}", sans-serif;`, `}`, "", ""].join("\n");
78
+ /** `Ubuntu Light Italic` → `Ubuntu_Light_Italic` (usable as a CSS class name). */
79
+ export function cssClassName(family) {
80
+ return family.trim().replace(/[^\w-]+/g, "_");
81
+ }
82
+
83
+ export const FONTS_CSS_FILE = "fonts.css";
84
+ const FONTS_CSS_IMPORT = `@import "${FONTS_CSS_FILE}";`;
85
+
86
+ /**
87
+ * Contents of src/fonts.css. With one font: just the `text` default. With
88
+ * more than one: also a `.Family_Variant { font-family }` class per font, so
89
+ * a later (postcss) pass can drop the classes the app never uses.
90
+ */
91
+ export function buildFontsCss(fonts) {
92
+ const lines = [
93
+ "/* GENERATED by create-mithril-lynx (--with-font / --find-font / add-font).",
94
+ " * Owned by the tooling: it is rewritten from the full font list on every",
95
+ " * add-font run, so do not edit it by hand — put your own rules in style.css. */",
96
+ "text {",
97
+ ` font-family: "${fonts[0].family}", sans-serif;`,
98
+ "}",
99
+ "",
100
+ ];
101
+ if (fonts.length > 1) {
102
+ for (const { family } of fonts) {
103
+ lines.push(`.${cssClassName(family)} {`, ` font-family: "${family}", sans-serif;`, "}", "");
104
+ }
105
+ }
106
+ return lines.join("\n");
83
107
  }
84
108
 
85
109
  /** Match our generated font block at the top of background.ts (if any). */
@@ -132,31 +156,14 @@ function loadBaseBackgrounds() {
132
156
  ];
133
157
  }
134
158
 
135
- function loadBaseStyles() {
136
- const out = [];
137
- for (const rel of [
138
- "templates/blank/common/src/style.css",
139
- "templates/hello-world/common/src/style.css",
140
- "templates/basic-activity/common/src/style.css",
141
- ]) {
142
- out.push(normalizeSource(fs.readFileSync(path.join(packageRoot, rel), "utf8")));
143
- }
144
- return out;
145
- }
146
-
147
159
  export function isStockBackground(restSource) {
148
160
  const normalized = normalizeSource(restSource);
149
161
  return loadBaseBackgrounds().some((base) => base === normalized);
150
162
  }
151
163
 
152
- export function isStockCss(restSource) {
153
- const normalized = normalizeSource(restSource);
154
- return loadBaseStyles().some((base) => base === normalized);
155
- }
156
-
157
164
  /**
158
165
  * Copy font files into the JS project (and Android host assets when found).
159
- * Always safe — does not touch background.ts / style.css.
166
+ * Always safe — does not touch background.ts / style.css / fonts.css.
160
167
  */
161
168
  export function copyFontFiles({ projectRoot, fonts, androidDir = null }) {
162
169
  const jsFontsDir = path.join(projectRoot, "src", "assets", "fonts");
@@ -231,36 +238,29 @@ export function applyBackgroundFonts(projectRoot, fonts) {
231
238
  }
232
239
 
233
240
  /**
234
- * Try to ensure the automatic text { font-family } rule for `defaultFamily`.
235
- * Extra fonts are never written into CSS (same policy as scaffold).
236
- * Returns { ok, skipped?, reason?, manualBlock? }.
241
+ * Write src/fonts.css from the full font list and make sure src/style.css
242
+ * imports it. style.css is only ever touched to add the one `@import` line
243
+ * (and to drop the `text { font-family }` block older versions put there).
244
+ * Returns { ok, reason?, manualBlock? } — ok=false only when style.css
245
+ * couldn't be found to add the import.
237
246
  */
238
- export function applyCssDefaultFont(projectRoot, defaultFamily, { hadExistingFonts }) {
239
- const cssPath = path.join(projectRoot, "src", "style.css");
247
+ export function applyFontsCss(projectRoot, fonts) {
248
+ const srcDir = path.join(projectRoot, "src");
249
+ fs.mkdirSync(srcDir, { recursive: true });
250
+ fs.writeFileSync(path.join(srcDir, FONTS_CSS_FILE), buildFontsCss(fonts));
251
+
252
+ const cssPath = path.join(srcDir, "style.css");
240
253
  if (!fs.existsSync(cssPath)) {
241
- return {
242
- ok: false,
243
- reason: "src/style.css not found",
244
- manualBlock: buildCssFontBlock(defaultFamily),
245
- };
254
+ return { ok: false, reason: "src/style.css not found", manualBlock: `${FONTS_CSS_IMPORT}\n` };
246
255
  }
247
256
  const raw = fs.readFileSync(cssPath, "utf8");
248
- const { rest, family: existingFamily } = stripCssFontBlock(raw);
249
-
250
- // Already has our rule and we were only appending more fonts — leave it.
251
- if (hadExistingFonts && existingFamily != null) {
257
+ const { block, rest } = stripCssFontBlock(raw);
258
+ if (/@import\s+(?:url\()?["']?(?:\.\/)?fonts\.css["']?\)?\s*;/.test(rest)) {
259
+ if (block != null) fs.writeFileSync(cssPath, rest);
252
260
  return { ok: true, skipped: true };
253
261
  }
254
-
255
- if (!isStockCss(rest)) {
256
- return {
257
- ok: false,
258
- reason: "src/style.css no longer matches the stock template",
259
- manualBlock: buildCssFontBlock(defaultFamily),
260
- };
261
- }
262
-
263
- fs.writeFileSync(cssPath, `${buildCssFontBlock(defaultFamily)}${rest}`);
262
+ // @import must come before any other rule, so it goes at the very top.
263
+ fs.writeFileSync(cssPath, `${FONTS_CSS_IMPORT}\n\n${rest}`);
264
264
  return { ok: true };
265
265
  }
266
266
 
package/src/index.js CHANGED
@@ -5,14 +5,14 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { execSync } from "node:child_process";
7
7
 
8
- import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
8
+ import { cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, text } from "@clack/prompts";
9
9
 
10
10
  import { fetchFontDetail, fetchFontList, listVariants, searchFonts } from "./fontsource.js";
11
11
  import {
12
12
  applyBackgroundFonts,
13
- applyCssDefaultFont,
14
- buildCssFontBlock,
13
+ applyFontsCss,
15
14
  buildFontBlock,
15
+ cssClassName,
16
16
  copyFontFiles,
17
17
  findAndroidDir,
18
18
  findProjectRoot,
@@ -255,6 +255,12 @@ const WEIGHT_NAMES = {
255
255
  900: "Black",
256
256
  };
257
257
 
258
+ /** "Bold", "Light Italic" — the part appended to the family name. */
259
+ function variantName(variant) {
260
+ const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
261
+ return `${weightName}${variant.style === "italic" ? " Italic" : ""}`;
262
+ }
263
+
258
264
  function variantLabel(variant) {
259
265
  const weightName = WEIGHT_NAMES[variant.weight] ?? String(variant.weight);
260
266
  const style = variant.style === "italic" ? " Italic" : "";
@@ -264,8 +270,8 @@ function variantLabel(variant) {
264
270
  /**
265
271
  * Interactive search-download flow for `--find-font <term>`: queries
266
272
  * Fontsource's catalog locally (see src/fontsource.js — the API itself has
267
- * no free-text search), lets the user pick a family and one weight/style,
268
- * downloads that .ttf to a temp file, and returns it in the same shape
273
+ * no free-text search), lets the user pick a family and one or more weights/styles,
274
+ * downloads each .ttf to a temp file, and returns them in the same shape
269
275
  * `--with-font <file>` expects, so the rest of the pipeline (entirely
270
276
  * JS-side — see patchJsProject()) doesn't need to know which path was used.
271
277
  */
@@ -322,33 +328,45 @@ async function findFontInteractively(term) {
322
328
  s2.stop(`${variants.length || allVariants.length} variant(s) available.`);
323
329
 
324
330
  const pickFrom = variants.length > 0 ? variants : allVariants;
325
- const chosenVariant = await select({
326
- message: "Which weight/style?",
327
- initialValue: pickFrom.find((v) => v.weight === 400 && v.style === "normal") ?? pickFrom[0],
331
+ const defaultVariant = pickFrom.find((v) => v.weight === 400 && v.style === "normal") ?? pickFrom[0];
332
+ const chosenVariants = await multiselect({
333
+ message: "Which weights/styles? (space to toggle, enter to confirm)",
334
+ initialValues: [defaultVariant],
335
+ required: true,
328
336
  options: pickFrom.map((v) => ({ value: v, label: variantLabel(v) })),
329
337
  });
330
- if (isCancel(chosenVariant)) return null;
331
-
332
- const s3 = spinner();
333
- s3.start(`Downloading ${detail.family} ${variantLabel(chosenVariant)}…`);
334
- let bytes;
335
- try {
336
- const response = await fetch(chosenVariant.url);
337
- if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
338
- bytes = Buffer.from(await response.arrayBuffer());
339
- } catch (error) {
340
- s3.stop("Download failed.");
341
- cancel(error instanceof Error ? error.message : String(error));
342
- process.exit(1);
343
- }
344
- const tempFile = path.join(
345
- os.tmpdir(),
346
- `${chosenId}-${chosenVariant.weight}-${chosenVariant.style}-${chosenVariant.subset}.ttf`,
338
+ if (isCancel(chosenVariants)) return null;
339
+ // Regular first (it becomes the family's plain name), then by weight/style.
340
+ chosenVariants.sort(
341
+ (a, b) =>
342
+ (b === defaultVariant) - (a === defaultVariant) || a.weight - b.weight || a.style.localeCompare(b.style),
347
343
  );
348
- fs.writeFileSync(tempFile, bytes);
349
- s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
350
344
 
351
- return { sourcePath: tempFile, family: detail.family };
345
+ const results = [];
346
+ for (const [index, variant] of chosenVariants.entries()) {
347
+ const s3 = spinner();
348
+ s3.start(`Downloading ${detail.family} ${variantLabel(variant)}…`);
349
+ let bytes;
350
+ try {
351
+ const response = await fetch(variant.url);
352
+ if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`);
353
+ bytes = Buffer.from(await response.arrayBuffer());
354
+ } catch (error) {
355
+ s3.stop("Download failed.");
356
+ cancel(error instanceof Error ? error.message : String(error));
357
+ process.exit(1);
358
+ }
359
+ const tempFile = path.join(
360
+ os.tmpdir(),
361
+ `${chosenId}-${variant.weight}-${variant.style}-${variant.subset}.ttf`,
362
+ );
363
+ fs.writeFileSync(tempFile, bytes);
364
+ s3.stop(`Downloaded ${(bytes.length / 1024).toFixed(1)} kB.`);
365
+ // Each file is registered under its own family name (addFont has no
366
+ // weight/style axis here): "Ubuntu Regular", "Ubuntu Bold Italic"…
367
+ results.push({ sourcePath: tempFile, base: detail.family, family: `${detail.family} ${variantName(variant)}` });
368
+ }
369
+ return results;
352
370
  }
353
371
 
354
372
  // ---------------------------------------------------------------------------
@@ -418,7 +436,7 @@ async function resolveFontsFromFlags(android, { baseDir, requireFonts }) {
418
436
 
419
437
  if (android.fontFamily != null && fontCount > 1) {
420
438
  cancel(
421
- "--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.",
439
+ "--font-family only makes sense with exactly one font — omit it when bundling more than one, or use the classes in src/fonts.css.",
422
440
  );
423
441
  process.exit(1);
424
442
  }
@@ -438,11 +456,20 @@ async function resolveFontsFromFlags(android, { baseDir, requireFonts }) {
438
456
  for (const term of findFontTerms) {
439
457
  const found = await findFontInteractively(term);
440
458
  if (found == null) return null;
441
- fonts.push({
442
- sourcePath: found.sourcePath,
443
- file: path.basename(found.sourcePath),
444
- family: android.fontFamily ?? found.family,
445
- });
459
+ for (const variant of found) {
460
+ fonts.push({
461
+ sourcePath: variant.sourcePath,
462
+ file: path.basename(variant.sourcePath),
463
+ // --font-family replaces the family part; the variant suffix stays
464
+ // unless there is only one variant.
465
+ family:
466
+ android.fontFamily == null
467
+ ? variant.family
468
+ : found.length === 1
469
+ ? android.fontFamily
470
+ : variant.family.replace(variant.base, android.fontFamily),
471
+ });
472
+ }
446
473
  }
447
474
  for (const rawPath of fontPaths) {
448
475
  const resolved = path.resolve(baseDir, rawPath);
@@ -505,7 +532,6 @@ async function runAddFont(args) {
505
532
  const { fonts: existingInBg } = stripFontBlock(
506
533
  fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "",
507
534
  );
508
- const hadExistingFonts = existingInBg.length > 0;
509
535
  const merged = mergeFontLists(existingInBg, incoming, dedupeFontFiles);
510
536
 
511
537
  const androidDir = findAndroidDir(projectRoot);
@@ -535,30 +561,19 @@ async function runAddFont(args) {
535
561
  printManualBlock("src/background.ts", bgResult.manualBlock);
536
562
  }
537
563
 
538
- const cssResult = applyCssDefaultFont(projectRoot, merged[0].family, { hadExistingFonts });
539
- if (cssResult.ok && cssResult.skipped) {
540
- console.log(
541
- ` · src/style.css already has text { font-family: … } — assign extra fonts in your own classes.`,
542
- );
543
- } else if (cssResult.ok) {
544
- console.log(" ✔ Updated src/style.css");
545
- } else {
546
- console.log(` ✖ Could not auto-edit src/style.css (${cssResult.reason}).`);
547
- console.log(" Paste this at the top of src/style.css (or assign the family on your own classes):");
564
+ const cssResult = applyFontsCss(projectRoot, merged);
565
+ console.log(" ✔ Regenerated src/fonts.css (generated — don't edit it by hand)");
566
+ if (cssResult.ok && !cssResult.skipped) {
567
+ console.log(' ✔ Added @import "fonts.css"; to src/style.css');
568
+ } else if (!cssResult.ok) {
569
+ console.log(` ✖ Could not add the import to src/style.css (${cssResult.reason}).`);
570
+ console.log(" Add this line at the very top of your stylesheet:");
548
571
  printManualBlock("src/style.css", cssResult.manualBlock);
549
572
  }
550
573
 
551
- const extras = merged.slice(hadExistingFonts ? existingInBg.length : 1).map((f) => f.family);
552
- // When we had no prior fonts, slice(1) are extras beyond the CSS default.
553
- // When we had prior fonts, newly added ones after existingInBg.length need a note.
554
- const newlyAddedFamilies = incoming.map((f) => f.family);
555
- if (hadExistingFonts && newlyAddedFamilies.length > 0) {
556
- console.log(
557
- ` · New font${newlyAddedFamilies.length > 1 ? "s" : ""} ${newlyAddedFamilies.map((f) => `"${f}"`).join(", ")} — set font-family on your own CSS classes.`,
558
- );
559
- } else if (!hadExistingFonts && extras.length > 0) {
574
+ if (merged.length > 1) {
560
575
  console.log(
561
- ` · Extra font${extras.length > 1 ? "s" : ""} ${extras.map((f) => `"${f}"`).join(", ")} — set font-family on your own CSS classes.`,
576
+ ` · Use the classes in src/fonts.css: ${merged.map((f) => "." + cssClassName(f.family)).join(", ")}`,
562
577
  );
563
578
  }
564
579
 
@@ -670,9 +685,7 @@ function patchJsProject({ targetDir, android }) {
670
685
  const existingBg = fs.existsSync(bgPath) ? fs.readFileSync(bgPath, "utf8") : "";
671
686
  fs.writeFileSync(bgPath, `${buildFontBlock(android.fonts)}${existingBg}`);
672
687
 
673
- const cssPath = path.join(targetDir, "src", "style.css");
674
- const existingCss = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : "";
675
- fs.writeFileSync(cssPath, `${buildCssFontBlock(android.fonts[0].family)}${existingCss}`);
688
+ applyFontsCss(targetDir, android.fonts);
676
689
  }
677
690
 
678
691
  // README: how the two halves are used together.