@escape-game-over/atlas 0.1.1 → 0.1.2

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
@@ -305,6 +305,7 @@ src/ the package — see the export map above for what is reachable
305
305
  bin/ the `atlas` CLI
306
306
  tests/ runtime behaviour — what a merge resolves to, what a builder rejects
307
307
  type-tests/ what the types must reject, via @ts-expect-error
308
+ checks/astro/ the astro check root for src/astro — config only, nothing to read
308
309
  docs/ the long-form reasoning
309
310
  examples/ two complete consumers
310
311
  ```
@@ -321,16 +322,34 @@ npm test # everything else
321
322
  `npm test` runs three suites in order, and stops at the first failure:
322
323
 
323
324
  ```bash
324
- npm run test:types # tsc --noEmit — the package compiles, and 137 type
325
- # assertions in type-tests/ still reject what they must
325
+ npm run test:types # tsc --noEmit, then astro check — the package compiles,
326
+ # and 137 type assertions in type-tests/ still reject
327
+ # what they must
326
328
  npm run test:unit # vitest — 355 runtime tests
327
329
  npm run test:examples # every example, every deployment: astro check + astro build
328
330
  npm run test:watch # vitest, watching
329
331
  ```
330
332
 
331
- `test:types` is also the isolation contract: no consumer paths, no ambient types,
332
- `src/astro/` excluded. If it passes, the package is genuinely independent of
333
- anything consuming it.
333
+ `test:types` runs two passes, and the split is the isolation contract. The `tsc`
334
+ pass — no consumer paths, no ambient types, `src/astro/` excluded is what
335
+ proves the package is genuinely independent of anything consuming it.
336
+
337
+ The `astro check` pass covers `src/astro/`, the one directory that cannot meet
338
+ that bar. It needs `astro/client` for `astro:assets` and `ImageMetadata`, and
339
+ `MetaTags.astro` needs a checker that can parse `.astro` at all — `tsc` reads
340
+ zero `.astro` files no matter how they are globbed. Without this pass the one
341
+ component the package ships is checked by nothing: `astro check` in
342
+ `test:examples` only ever walks an example's own `src/`, so it never reaches it,
343
+ and a type error there passes the entire gate.
344
+
345
+ It runs from [`checks/astro`](checks/astro) rather than `src/astro` for one
346
+ practical reason: `astro check` writes a `.astro/` types directory and a
347
+ 1 MB Vite cache beside whatever it is rooted at, and `files: ["src"]` would
348
+ publish both. The `tsconfig.json` there adds nothing — it extends
349
+ `src/astro/tsconfig.json` and only widens the glob, so the rules stay in one
350
+ place. `src/astro/tsconfig.json` is what an editor finds; without it, opening
351
+ `MetaTags.astro` resolves against the root config, which supplies no ambient
352
+ types, and every prop degrades to `any`.
334
353
 
335
354
  `test:examples` **builds every deployment**, not just type-checks one, and both
336
355
  halves of that matter. `astro check` only ever sees whichever project
@@ -342,6 +361,28 @@ merged data or on a real file. Type-checking alone would pass all of it.
342
361
 
343
362
  The whole gate is about 15 seconds.
344
363
 
364
+ ### Open Biome issues
365
+
366
+ Three things the `.astro` setup works around rather than fixes. Each is open
367
+ upstream, and each explains a rule that would otherwise read as an oversight.
368
+
369
+ - [biomejs/biome#10321](https://github.com/biomejs/biome/issues/10321) —
370
+ *Astro: not formatting inside `{( … )}`* (`S-Bug-confirmed`). Biome formats
371
+ `.astro` markup but not the interior of an expression, so the 31 `.map()`
372
+ blocks across the templates keep their own indentation while the markup around
373
+ them follows `indentWidth`. Hand-aligned until this lands.
374
+ - [biomejs/biome#9944](https://github.com/biomejs/biome/issues/9944) —
375
+ *Astro: Parser error inside expression*. Biome parses the inside of `{ }` as
376
+ strict JSX, which rejects things Astro itself accepts. This is why every
377
+ comment in a template is `{/* … */}` and never `<!-- … -->`: an HTML comment
378
+ anywhere inside an expression is a parse error, and the JSX form is also
379
+ stripped from the built HTML rather than shipped to readers.
380
+ - [biomejs/biome#11275](https://github.com/biomejs/biome/issues/11275) —
381
+ *LSP formatting deletes Astro frontmatter/HTML in multi-project workspaces*
382
+ (`S-Bug-confirmed`). It names `html.experimentalFullSupportEnabled`, which this
383
+ repository sets, and a workspace of several projects, which this repository is.
384
+ The gate above is CLI-only and unaffected; editor format-on-save is the thing
385
+ to be careful with.
345
386
 
346
387
  ## Documentation
347
388
 
@@ -349,8 +390,8 @@ The whole gate is about 15 seconds.
349
390
  how the type tests and runtime tests divide the work.
350
391
  - [`docs/share-images.md`](docs/share-images.md) — what each social surface
351
392
  crops, and why one 1200×630 image serves all of them.
352
- - [`docs/toolchain.md`](docs/toolchain.md) — the pinned TypeScript, the Biome
353
- trade-off for `.astro`, and the traps.
393
+ - [`docs/toolchain.md`](docs/toolchain.md) — the pinned TypeScript, the two
394
+ switches `.astro` support needs from Biome, and the traps.
354
395
  - [`docs/NOT-BUILT.md`](docs/NOT-BUILT.md) — what we deliberately do not build,
355
396
  as questions and answers. Kept beside the code rather than in a backlog,
356
397
  because a backlog is a list of things still to do and every one of these is a
package/docs/NOT-BUILT.md CHANGED
@@ -281,6 +281,66 @@ wrote — the files it emits at build time, and the slash-free URLs it publishes
281
281
  as canonical — so a project that changed one would silently break the other. The
282
282
  integration sets all three and logs that it did, so nothing is hidden.
283
283
 
284
+ ### Should the generated files be routes, so a dev server keeps them current?
285
+
286
+ **No. They stay files written at `astro:build:done`, and in dev they are a
287
+ snapshot of the moment the config was evaluated.**
288
+
289
+ Start with the fact underneath, because it is not written down anywhere else and
290
+ it surprises people. `astro.config.ts` imports `site`, `redirects` and `llms`,
291
+ and Astro loads that config through a throwaway Vite server without recording
292
+ what it imported — `core/config/vite-load.js` returns the module and closes the
293
+ server. A dev server restarts only for the config file itself, `.astro/settings.json`,
294
+ or an exact path in `settings.watchFiles`, which holds `package.json` and the
295
+ tsconfig sources and nothing else. So editing `config/messages.ts` refreshes
296
+ every page that renders it, through Vite's module graph, and leaves the same
297
+ string in `llms.txt` untouched until you restart. **Builds are never affected**:
298
+ `astro build` evaluates the config in a fresh process. This is a dev-only
299
+ staleness, and `generate()` rebuilding per request does not cure it — it derives
300
+ the same answer from the same frozen inputs.
301
+
302
+ Routes look like the fix, and they genuinely are one. Measured on the b2b
303
+ example: an injected endpoint that reaches the site through the *page* graph
304
+ served the edited string immediately, side by side with the integration's
305
+ middleware still serving the old one.
306
+
307
+ **And `_redirects` can be one of those routes** — worth recording, because the
308
+ old comment in `site-routes.ts` said it could not be, and that was wrong. Astro's
309
+ page walker skips a *filename* beginning with `_`; an injected route names its
310
+ pattern and its entrypoint separately, so the underscore lands only on the URL.
311
+ It was built, served in dev, and written to `dist/_redirects` by the route alone.
312
+
313
+ What sinks it is reaching the caller's live `site` from an entrypoint compiled
314
+ into another module graph. Two ways, both built and both rejected:
315
+
316
+ - **Name the modules.** `siteRoutes({ modules: { site, redirects, llms } })`.
317
+ Dies on contact with a real project: `redirects` and `llms` are values computed
318
+ from files scattered wherever a project put them, and a list that misses one is
319
+ worse than no list — a control test confirmed the neighbours refresh while the
320
+ missed file stays stale, which teaches you the wrong rule about when dev can be
321
+ trusted.
322
+ - **Skip the names.** Each endpoint imports the consumer's `astro.config.ts` for
323
+ its side effect, which re-enters `siteRoutes()` in the page graph, and reads the
324
+ options back out of a `Symbol.for` slot on `globalThis`. This works — all four
325
+ files, fresh in dev, correct in a build, whole suite green — and it is the
326
+ version that was actually written. It was still thrown away: a global written by
327
+ one module graph and read by another, kept correct by importing a config file
328
+ for its side effects, is only safe while everyone remembers why. It also made
329
+ `site-routes.ts` longer, not shorter, and entangled the orphan warning with
330
+ codegen badly enough that an unrelated test needed a temp directory.
331
+
332
+ So the cost of the current design is one line of dev output being a snapshot, and
333
+ the cost of curing it is a mechanism nobody can infer from reading it. The first
334
+ is cheaper.
335
+
336
+ **What would flip it.** Astro tracking its config's dependency graph — then
337
+ `addWatchFile` over those paths would restart the server on any config edit and
338
+ the whole question dissolves, with no cleverness anywhere. Worth re-checking on a
339
+ major Astro release. Failing that, endpoint files written by each consuming
340
+ project (four small ones per repo, each importing its own `site`) buy the same
341
+ freshness with no mechanism at all — the trade is four files against a snapshot,
342
+ and today the snapshot wins.
343
+
284
344
  ### Should `/` serve a meta-refresh stub when every locale is prefixed?
285
345
 
286
346
  **No. A real 301 in `_redirects`.**
@@ -319,6 +379,30 @@ Reading order is the relationship a reader actually has with an article, so a
319
379
  post carries newer/older links instead. Browser back covers "the list I was
320
380
  looking at" better than any link can.
321
381
 
382
+ ### Should `/en-US/about-us` redirect when the default locale is unprefixed?
383
+
384
+ **No. Only `/en-US` itself does.**
385
+
386
+ With `prefixDefaultLocale: false` the default locale is served at `/about-us`
387
+ and nothing is built under `/en-US/`. Claiming the prefixed root is one rule and
388
+ answers the URL people actually type, having seen every other language wear a
389
+ prefix. Claiming the pages under it is a different proposition: a rule per page,
390
+ and slugs are translated per locale, so the URL a reader lands on by editing
391
+ `/el-GR/sxetika-me-emas` down to English keeps the *Greek* slug —
392
+ `/en-US/sxetika-me-emas`. Covering that means the cross product of the prefix
393
+ with every locale's spelling of every slug, which is a `_redirects` of hundreds
394
+ against the 2000 a host reads, all of it for URLs nothing has ever linked.
395
+
396
+ This was built first, in the one-rule-per-default-locale-page form, which is
397
+ worth naming because it looks complete and is not: it answers `/en-US/about-us`
398
+ and misses `/en-US/sxetika-me-emas`, so the case that motivated it is the case
399
+ it does not cover.
400
+
401
+ A wildcard would sidestep the count — `/en-US/* /:splat 301` is one line — but
402
+ not the translated slug, and it is Cloudflare's syntax rather than something
403
+ `ResolvedRedirect` can carry to another host. The pages get the 404 page, which
404
+ is what it is for.
405
+
322
406
  ---
323
407
 
324
408
  ## Adding to this
package/docs/toolchain.md CHANGED
@@ -16,19 +16,29 @@ means most of what this package guarantees stops being verified in the one place
16
16
  it matters. Track
17
17
  [withastro/roadmap#1321](https://github.com/withastro/roadmap/discussions/1321).
18
18
 
19
- ## `noUnusedVariables` / `noUnusedImports` are off for `.astro`
19
+ ## `.astro` runs on Biome's full HTML support, and needs both switches
20
20
 
21
- Biome's Astro support has two modes and neither does both jobs:
22
-
23
- - **Default mode** formats and lints frontmatter, but cannot see the template —
24
- so every const the markup uses is reported unused.
25
- - **`html.experimentalFullSupportEnabled`** lints accurately, but no longer
26
- formats frontmatter.
21
+ ```json
22
+ "html": {
23
+ "experimentalFullSupportEnabled": true,
24
+ "formatter": { "enabled": true }
25
+ }
26
+ ```
27
27
 
28
- This repo takes the default mode plus Biome's documented rule exclusions, and
29
- nothing is lost: `astro check` reports unused frontmatter variables and imports
30
- itself (`ts(6133)`) while correctly resolving template usage. The two tools cover
31
- each other.
28
+ One without the other is the trap. `experimentalFullSupportEnabled` moves
29
+ `.astro` onto the HTML-family path, where the formatter is off unless it is
30
+ asked for so setting only the first drops every `.astro` file from
31
+ `biome format` entirely. It reports them as *ignored* rather than as errors, so
32
+ `check-fmt` stays green while nothing is being formatted at all.
33
+
34
+ With both set, `noUnusedVariables` and `noUnusedImports` resolve template usage
35
+ correctly: a const the markup uses is not reported, a genuinely unused one is.
36
+ The `overrides` block that used to switch them off for `.astro` is gone. It
37
+ existed because the old default mode could not see the template and called every
38
+ const the markup uses unused; keeping it now would only suppress rules that
39
+ work. Removing it while still in default mode is the one combination to avoid —
40
+ the false positives come back as `FIXABLE`, and `lint:fix` will rename a const
41
+ the template still references.
32
42
 
33
43
  ## `biome.json` must not contain `//` comments
34
44
 
@@ -46,8 +56,9 @@ single catch-all page in each example, from the route registry.
46
56
  ## Formatting
47
57
 
48
58
  4-space indent, double quotes, `es5` trailing commas, LF. Biome formats `.astro`
49
- frontmatter but leaves the markup below it alone, which is why the templates in
50
- the examples are hand-aligned.
59
+ frontmatter *and* markup, but not the inside of a `{ }` expression so the
60
+ `.map()` blocks in the examples stay hand-aligned while the markup around them
61
+ follows `indentWidth`. See the open Biome issues in the README.
51
62
 
52
63
  ```bash
53
64
  npm run lint # check
@@ -71,13 +82,29 @@ The practical consequence: `/about-us` is `about-us.html`, not
71
82
  `about-us/index.html`. `dist/` is plain files — any static host works, and no
72
83
  server rules are required.
73
84
 
74
- ## Claiming `/` when every locale is prefixed
75
-
76
- When a project sets `prefixDefaultLocale: true`, nothing owns `/` and no page is
77
- generated for it. `site.redirects()` claims it with a real **301** in
78
- `_redirects` instead, pointing at the default locale's root a redirect rather
85
+ ## The URLs `site.redirects()` claims on its own
86
+
87
+ Two of this toolchain's decisions leave a URL unpublished that people still ask
88
+ for. Each is answered with a real **301** written into `_redirects` alongside
89
+ the rules a project states — derived from the route table, so a retranslated
90
+ slug or a switched-off page moves the rule with the page:
91
+
92
+ | Asked for | Answered with | Because |
93
+ | -------------- | ------------- | ---------------------------------------------- |
94
+ | `/` | `/en-US` | `prefixDefaultLocale: true` leaves `/` unowned |
95
+ | `/en-US` | `/` | `prefixDefaultLocale: false` leaves it unbuilt |
96
+ | `/news/page/1` | `/news` | page one is the bare slug, never `page/1` |
97
+
98
+ The first two are the same rule in its two directions: whichever root the
99
+ routing mode does not serve points at the one it does. Both together cost one
100
+ rule; the third costs one per paginated list per language. Nothing here scales
101
+ with the number of pages a site has — see [`NOT-BUILT.md`](NOT-BUILT.md) for why
102
+ the prefixed form of every *page* is left to the 404 instead. A project that
103
+ states its own rule for one of these paths keeps it: the inferred one steps
104
+ aside.
105
+
106
+ The root points at whichever route has an empty slug, so nothing has to name a
107
+ "home" page, and no rule is written if no route claims it. A redirect rather
79
108
  than a rendered stub, so a crawler is told the destination is canonical instead
80
- of following a refresh.
81
-
82
- The target is whichever route has an empty slug, so nothing has to name a "home"
83
- page. See [`NOT-BUILT.md`](NOT-BUILT.md) for why no meta-refresh stub is emitted.
109
+ of following a refresh — `NOT-BUILT.md` again for why no meta-refresh stub is
110
+ emitted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -15,7 +15,11 @@
15
15
  ".": "./src/index.ts",
16
16
  "./astro": "./src/astro/index.ts",
17
17
  "./astro/images": "./src/astro/images.ts",
18
+ "./astro/carousel": "./src/astro/carousel.ts",
18
19
  "./astro/consent": "./src/astro/consent.ts",
20
+ "./astro/dev-log": "./src/astro/dev-log.ts",
21
+ "./astro/dom": "./src/astro/dom.ts",
22
+ "./astro/element": "./src/astro/element.ts",
19
23
  "./astro/meta-tags": "./src/astro/MetaTags.astro"
20
24
  },
21
25
  "bin": {
@@ -29,23 +33,23 @@
29
33
  ],
30
34
  "scripts": {
31
35
  "test": "npm run test:types && npm run test:unit && npm run test:examples",
32
- "test:types": "tsc --noEmit",
36
+ "test:types": "tsc --noEmit && astro check --root checks/astro",
33
37
  "test:unit": "vitest run",
34
38
  "test:watch": "vitest",
35
39
  "test:examples": "npm run test --workspaces",
36
40
  "lint": "biome check .",
37
41
  "lint:fix": "biome check --write .",
38
- "format": "biome format --write .",
42
+ "fmt": "biome format --write .",
39
43
  "check-fmt": "biome check . --error-on-warnings"
40
44
  },
41
45
  "peerDependencies": {
42
46
  "astro": ">=7"
43
47
  },
44
48
  "devDependencies": {
45
- "@biomejs/biome": "2.5.8",
49
+ "@biomejs/biome": "2.5.10",
46
50
  "@types/node": "26.2.0",
47
- "astro": "7.2.2",
51
+ "astro": "7.2.4",
48
52
  "typescript": "6.0.3",
49
- "vitest": "4.1.10"
53
+ "vitest": "4.1.11"
50
54
  }
51
55
  }
@@ -39,8 +39,7 @@ interface Props {
39
39
  const { tags } = Astro.props;
40
40
  ---
41
41
 
42
- {
43
- tags.map((tag) => {
42
+ {tags.map((tag) => {
44
43
  if (tag.kind === "title") return <title>{tag.text}</title>;
45
44
  if (tag.kind === "link") return <link {...tag.attrs} />;
46
45
  // `set:html` writes the JSON raw. `serializeJsonLd` has already replaced
@@ -83,5 +82,4 @@ const { tags } = Astro.props;
83
82
  // copied into every project.
84
83
  const unhandled: never = tag;
85
84
  throw new Error(`Unhandled meta tag: ${JSON.stringify(unhandled)}`);
86
- })
87
- }
85
+ })}