@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 +48 -7
- package/docs/NOT-BUILT.md +84 -0
- package/docs/toolchain.md +49 -22
- package/package.json +10 -6
- package/src/astro/MetaTags.astro +2 -4
- package/src/astro/carousel.ts +342 -0
- package/src/astro/dev-log.ts +128 -0
- package/src/astro/dom.ts +53 -0
- package/src/astro/element.ts +259 -0
- package/src/astro/site-routes.ts +111 -21
- package/src/astro/tsconfig.json +8 -0
- package/src/contact-form.ts +177 -0
- package/src/index.ts +7 -0
- package/src/routes/define.ts +3 -1
- package/src/routes/resolve.ts +28 -3
- package/src/site/api.ts +12 -0
- package/src/site/create.ts +132 -42
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,
|
|
325
|
-
# assertions in type-tests/ still reject
|
|
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`
|
|
332
|
-
`src/astro/` excluded
|
|
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
|
|
353
|
-
|
|
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
|
-
## `
|
|
19
|
+
## `.astro` runs on Biome's full HTML support, and needs both switches
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
```json
|
|
22
|
+
"html": {
|
|
23
|
+
"experimentalFullSupportEnabled": true,
|
|
24
|
+
"formatter": { "enabled": true }
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
50
|
-
the examples
|
|
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
|
-
##
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
49
|
+
"@biomejs/biome": "2.5.10",
|
|
46
50
|
"@types/node": "26.2.0",
|
|
47
|
-
"astro": "7.2.
|
|
51
|
+
"astro": "7.2.4",
|
|
48
52
|
"typescript": "6.0.3",
|
|
49
|
-
"vitest": "4.1.
|
|
53
|
+
"vitest": "4.1.11"
|
|
50
54
|
}
|
|
51
55
|
}
|
package/src/astro/MetaTags.astro
CHANGED
|
@@ -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
|
+
})}
|