@escape-game-over/atlas 0.1.23 → 0.1.25

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 (51) hide show
  1. package/README.md +29 -45
  2. package/bin/use-project.mjs +18 -13
  3. package/docs/NOT-BUILT.md +1 -1
  4. package/docs/client-scripts.md +73 -141
  5. package/docs/rich-text.md +25 -20
  6. package/package.json +5 -12
  7. package/src/analytics/google.ts +6 -6
  8. package/src/analytics/index.ts +4 -3
  9. package/src/analytics/tags.ts +14 -60
  10. package/src/analytics/umami.ts +8 -8
  11. package/src/astro/ConsentBanner.astro +25 -0
  12. package/src/astro/ConsentElement.astro +61 -0
  13. package/src/astro/Document.astro +44 -0
  14. package/src/astro/Image.astro +102 -0
  15. package/src/astro/MetaTags.astro +3 -26
  16. package/src/astro/RichText.astro +71 -0
  17. package/src/astro/Zoom.astro +61 -0
  18. package/src/astro/client.ts +19 -9
  19. package/src/astro/consent.ts +20 -0
  20. package/src/astro/dev-log.ts +8 -14
  21. package/src/astro/element.ts +111 -112
  22. package/src/astro/filters-view.ts +48 -64
  23. package/src/astro/filters.ts +46 -37
  24. package/src/astro/images.ts +27 -26
  25. package/src/astro/index.ts +2 -9
  26. package/src/astro/markup.ts +6 -6
  27. package/src/astro/site-routes.ts +10 -15
  28. package/src/config.ts +23 -36
  29. package/src/content/index.ts +2 -1
  30. package/src/content/marks.ts +13 -13
  31. package/src/content/rich.ts +58 -29
  32. package/src/hours.ts +48 -11
  33. package/src/i18n/define.ts +14 -74
  34. package/src/index.ts +41 -57
  35. package/src/jsonld/faq.ts +2 -1
  36. package/src/jsonld/node.ts +4 -14
  37. package/src/meta/index.ts +7 -13
  38. package/src/meta/share-image.ts +6 -19
  39. package/src/meta/tag.ts +1 -45
  40. package/src/money.ts +161 -6
  41. package/src/project.ts +84 -73
  42. package/src/routes/define.ts +8 -44
  43. package/src/routes/resolve.ts +1 -1
  44. package/src/site/api.ts +7 -33
  45. package/src/site/create.ts +6 -10
  46. package/src/site/define.ts +120 -0
  47. package/src/site/index.ts +2 -5
  48. package/src/site/page.ts +4 -2
  49. package/src/sitemap.ts +2 -35
  50. package/src/warn.ts +16 -17
  51. package/src/astro/dom.ts +0 -35
@@ -13,12 +13,12 @@ import type { GeneratedFile } from "../file.ts";
13
13
  import type { PublicFilePath } from "../files.ts";
14
14
  import { type BaseCatalog, mergeCatalog } from "../i18n/define.ts";
15
15
  import { createTranslateFactory } from "../i18n/translate.ts";
16
+ import type { ImageAsset } from "../image.ts";
16
17
  import { buildLlms, type LlmsItem, type LlmsSection } from "../llms.ts";
17
18
  import {
18
19
  buildMeta,
19
20
  buildNotFoundMeta,
20
21
  resolveShareImage,
21
- type SiteIcon,
22
22
  } from "../meta/index.ts";
23
23
  import type { ProjectInput } from "../project.ts";
24
24
  import {
@@ -171,9 +171,6 @@ export function createSite<
171
171
  | undefined,
172
172
  };
173
173
 
174
- // Declared config, not a call to `llms()`: the head is built page by page
175
- // and cannot know whether some other module generated the file, whereas
176
- // stating `llms` in the site config is a promise that one is published.
177
174
  const llmsName =
178
175
  (config_.llms === false ? undefined : config_.llms?.name) ?? "llms.txt";
179
176
  // Annotated, not inferred: a template expression widens to `string` unless
@@ -291,15 +288,16 @@ export function createSite<
291
288
  entries.map((entry) => entry.path)
292
289
  );
293
290
 
294
- function staticPaths(param: string): StaticPath<L, RouteId>[] {
291
+ function getStaticPaths(): StaticPath<L, RouteId>[] {
295
292
  const paths: StaticPath<L, RouteId>[] = entries.map((entry) => ({
296
293
  params: {
297
- [param]: entry.path === "/" ? undefined : entry.path.slice(1),
294
+ route: entry.path === "/" ? undefined : entry.path.slice(1),
298
295
  },
299
296
  props: {
300
297
  routeId: entry.routeId,
301
298
  locale: entry.locale,
302
299
  page: entry.page,
300
+ pages: resolved[entry.routeId]?.pages ?? 1,
303
301
  },
304
302
  }));
305
303
 
@@ -362,7 +360,7 @@ export function createSite<
362
360
  * a bad icon still fails the build, at the first page that renders.
363
361
  */
364
362
  let iconChecked = false;
365
- function checkedIcon(): SiteIcon {
363
+ function checkedIcon(): ImageAsset {
366
364
  if (iconChecked) return project.icon;
367
365
  if (project.icon.width !== project.icon.height) {
368
366
  throw new Error(
@@ -795,10 +793,8 @@ export function createSite<
795
793
  fileUrl,
796
794
  breadcrumbFor,
797
795
  orphanSegments,
798
- alternatesFor,
799
796
  localeLinksFor,
800
- entries,
801
- staticPaths,
797
+ getStaticPaths,
802
798
  sitemap,
803
799
  robots,
804
800
  llms,
@@ -0,0 +1,120 @@
1
+ import {
2
+ defineSiteConfig,
3
+ type LocalesOf,
4
+ type SiteConfigChecks,
5
+ type SiteConfigShape,
6
+ } from "../config.ts";
7
+ import {
8
+ type BaseCatalog,
9
+ defineMessages,
10
+ type NoExtraLocales,
11
+ type ValidateBase,
12
+ } from "../i18n/define.ts";
13
+ import {
14
+ defineProject,
15
+ type ProjectChecks,
16
+ type ProjectInput,
17
+ type ProjectOverrideChecks,
18
+ } from "../project.ts";
19
+ import {
20
+ defineRoutes,
21
+ type RouteRegistry,
22
+ type ValidateSlugDepth,
23
+ } from "../routes/define.ts";
24
+ import {
25
+ type FamilyMemberInput,
26
+ type FamilyOptions,
27
+ routeFamily,
28
+ } from "../routes/family.ts";
29
+ import { createSite } from "./create.ts";
30
+
31
+ /**
32
+ * A site: which languages exist and how URLs are shaped, and everything built
33
+ * on that, as methods — so no other file restates the config.
34
+ *
35
+ * ```ts
36
+ * // config/site.config.ts
37
+ * export const atlas = defineSite({ locales: {…}, defaultRouting: {…} });
38
+ *
39
+ * // config/routes.ts, messages.ts, projects/<name>/project.ts, site.ts
40
+ * const rooms = atlas.family("rooms", ROOMS, { enabled: true });
41
+ * export const defaultRoutes = atlas.routes({ ...rooms, home: {…} });
42
+ * export const defaultMessages = atlas.messages({…});
43
+ * export default atlas.project(defaultMessages, defaultRoutes, {…});
44
+ * export const site = atlas.site(defaultMessages, defaultRoutes, project);
45
+ * ```
46
+ *
47
+ * Every check each step makes is the same one it makes as a function: a typo'd
48
+ * key, a dropped placeholder or a slug at the wrong depth is an error on the
49
+ * line that is wrong.
50
+ */
51
+ export function defineSite<
52
+ const C extends SiteConfigShape & SiteConfigChecks<C>,
53
+ >(input: C) {
54
+ const config = defineSiteConfig(input as never) as C;
55
+ type L = LocalesOf<C>;
56
+
57
+ return {
58
+ config,
59
+
60
+ /** A route per member, under one prefix: `rooms/red-room` for `rooms`. */
61
+ family<
62
+ const Prefix extends string,
63
+ const Members extends readonly FamilyMemberInput<L>[],
64
+ const Options extends FamilyOptions<L> = Record<string, never>,
65
+ >(
66
+ prefix: Prefix,
67
+ members: Members,
68
+ options?: Options
69
+ ): ReturnType<typeof routeFamily<C, Prefix, Members, Options>> {
70
+ return routeFamily(config, prefix, members, options);
71
+ },
72
+
73
+ /** The route table every project starts from. */
74
+ routes<const T extends RouteRegistry<L>>(
75
+ routes: T & ValidateSlugDepth<T>
76
+ ): T {
77
+ return defineRoutes(config, routes as never) as T;
78
+ },
79
+
80
+ /** The copy every project starts from, in every locale. */
81
+ messages<const T extends BaseCatalog<L>>(
82
+ catalog: T & ValidateBase<T> & NoExtraLocales<T, L>
83
+ ): T {
84
+ return defineMessages(config, catalog as never) as T;
85
+ },
86
+
87
+ /** One deployment: what differs from the defaults. */
88
+ project<
89
+ const Catalog extends BaseCatalog<L>,
90
+ const Routes extends RouteRegistry<L>,
91
+ const T extends ProjectInput<L, Routes> &
92
+ ProjectChecks<L, Catalog, Routes, T>,
93
+ >(
94
+ messages: Catalog,
95
+ routes: Routes,
96
+ project: T & ProjectOverrideChecks<L, Catalog, Routes, T>
97
+ ): T {
98
+ return defineProject(
99
+ config,
100
+ messages,
101
+ routes,
102
+ project as never
103
+ ) as T;
104
+ },
105
+
106
+ /** The API every page reads, wired for one project. */
107
+ site<
108
+ const Catalog extends BaseCatalog<L>,
109
+ const Routes extends RouteRegistry<L>,
110
+ const Published extends L,
111
+ const P extends ProjectInput<L, Routes>,
112
+ >(
113
+ messages: Catalog,
114
+ routes: Routes,
115
+ project: P & { readonly enabledLocales: readonly Published[] }
116
+ ): ReturnType<typeof createSite<C, Catalog, Routes, Published, P>> {
117
+ return createSite(config, messages, routes, project);
118
+ },
119
+ };
120
+ }
package/src/site/index.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Three files, along the seam between what the API *is* and what builds it:
5
5
  *
6
- * - `api.ts` — the `Site` interface, and the types read back off it
7
- * (`RouteIdOf`, `PerRoute`, `WhenEnabled`). The half worth reading.
6
+ * - `api.ts` — the `Site` interface, and `PerRoute`, read back off it. The
7
+ * half worth reading.
8
8
  * - `page.ts` — one page's vocabulary: what a view is handed and what it
9
9
  * returns. Nothing here mentions a route registry.
10
10
  * - `create.ts` — `createSite`, the closure that satisfies the interface. Its
@@ -15,11 +15,8 @@
15
15
  export type {
16
16
  LlmsOptions,
17
17
  PerRoute,
18
- RouteIdOf,
19
18
  Site,
20
- WhenEnabled,
21
19
  } from "./api.ts";
22
- export { createSite } from "./create.ts";
23
20
  export type {
24
21
  Alternate,
25
22
  Crumb,
package/src/site/page.ts CHANGED
@@ -40,11 +40,13 @@ export type PageProps<L extends string, Id extends string> = {
40
40
  * it needs no guard.
41
41
  */
42
42
  readonly page: number;
43
+ /** How many pages this route's list runs to; `1` for a route that does not paginate. */
44
+ readonly pages: number;
43
45
  };
44
46
 
45
- /** One `{ params, props }` pair, shaped for a static-site router. */
47
+ /** One `{ params, props }` pair for `[...route].astro`. */
46
48
  export type StaticPath<L extends string, Id extends string> = {
47
- readonly params: Readonly<Record<string, string | undefined>>;
49
+ readonly params: { readonly route: string | undefined };
48
50
  readonly props: PageProps<L, Id>;
49
51
  };
50
52
 
package/src/sitemap.ts CHANGED
@@ -38,12 +38,6 @@ export interface SitemapConfig {
38
38
  readonly entryLimit?: number;
39
39
  }
40
40
 
41
- /** One `{ params, props }` pair for a sitemap file, shaped for a static router. */
42
- export type SitemapStaticPath = {
43
- readonly params: Readonly<Record<string, string>>;
44
- readonly props: { readonly file: GeneratedFile };
45
- };
46
-
47
41
  export interface Sitemap {
48
42
  /**
49
43
  * The file to submit and to reference from `robots.txt`.
@@ -55,33 +49,6 @@ export interface Sitemap {
55
49
  readonly entry: GeneratedFile;
56
50
  /** Every file that must be served, `entry` included. */
57
51
  readonly files: readonly GeneratedFile[];
58
-
59
- /**
60
- * Every file to generate, as `{ params, props }` for a dynamic route.
61
- *
62
- * `param` is the name of the parameter in the router's filename, e.g.
63
- * `"sitemap"` for `[sitemap].ts`. One route therefore serves the single
64
- * sitemap or the index plus its parts, without the caller knowing which
65
- * case it is in.
66
- *
67
- * Returns a fresh mutable array, because SSG routers typically demand one.
68
- */
69
- staticPaths(param: string): SitemapStaticPath[];
70
- }
71
-
72
- function toSitemap(
73
- entry: GeneratedFile,
74
- files: readonly GeneratedFile[]
75
- ): Sitemap {
76
- return {
77
- entry,
78
- files,
79
- staticPaths: (param) =>
80
- files.map((file) => ({
81
- params: { [param]: file.name },
82
- props: { file },
83
- })),
84
- };
85
52
  }
86
53
 
87
54
  export interface SitemapInput<L extends string, Id extends string> {
@@ -235,7 +202,7 @@ export function buildSitemap<L extends string, Id extends string>(
235
202
 
236
203
  if (chunks.length <= 1) {
237
204
  const only = file(input.siteUrl, input.name, urlset(included));
238
- return toSitemap(only, [only]);
205
+ return { entry: only, files: [only] };
239
206
  }
240
207
 
241
208
  const parts = chunks.map((entries, index) =>
@@ -253,5 +220,5 @@ export function buildSitemap<L extends string, Id extends string>(
253
220
  )
254
221
  );
255
222
 
256
- return toSitemap(index, [index, ...parts]);
223
+ return { entry: index, files: [index, ...parts] };
257
224
  }
package/src/warn.ts CHANGED
@@ -2,22 +2,16 @@
2
2
  * The one place lib writes to a console.
3
3
  *
4
4
  * Everything here is a *warning* by definition: a fact about a file or a string
5
- * that no type can see, worth saying out loud but never worth failing a build
6
- * over. Errors throw instead, and say so at the point they are wrong.
5
+ * that no type can see. Errors throw instead, and say so at the point they are
6
+ * wrong. `ATLAS_STRICT=1` makes every warning throw too, for CI.
7
7
  *
8
- * Shared so the colouring and the `[lib]` prefix are decided once — a warning
8
+ * Shared so the colouring and the `[atlas]` prefix are decided once — a warning
9
9
  * that looks different depending on which module raised it reads like two
10
10
  * different tools.
11
11
  */
12
12
 
13
- /**
14
- * What every line lib writes is labelled with.
15
- *
16
- * A constant rather than a literal at each call site: grep for it to find every
17
- * warning the library can raise, and change it once if this folder is ever
18
- * published under its real name.
19
- */
20
- const PREFIX = "lib";
13
+ /** What every line lib writes is labelled with. */
14
+ const PREFIX = "atlas";
21
15
 
22
16
  /**
23
17
  * The escape character, named rather than embedded. 27 is its code point.
@@ -49,12 +43,16 @@ const RESET = `${ESC}[0m`;
49
43
  * `process` and simply gets colour, which is what a browser console renders
50
44
  * anyway.
51
45
  */
52
- const colour =
53
- (
54
- globalThis as {
55
- process?: { env?: Readonly<Record<string, string | undefined>> };
56
- }
57
- ).process?.env?.NO_COLOR === undefined;
46
+ const env = (
47
+ globalThis as {
48
+ process?: { env?: Readonly<Record<string, string | undefined>> };
49
+ }
50
+ ).process?.env;
51
+
52
+ const colour = env?.NO_COLOR === undefined;
53
+
54
+ /** Read per call rather than once, so a test can switch it. */
55
+ const strict = (): boolean => env?.ATLAS_STRICT === "1";
58
56
 
59
57
  const paint = (code: string, text: string): string =>
60
58
  colour ? `${code}${text}${RESET}` : text;
@@ -66,6 +64,7 @@ const paint = (code: string, text: string): string =>
66
64
  * always printed, because a warning nobody can locate is one nobody acts on.
67
65
  */
68
66
  export function warn(at: string, message: string): void {
67
+ if (strict()) throw new Error(`${shorten(at)}: ${message}`);
69
68
  console.warn(
70
69
  `${paint(YELLOW, `[${PREFIX}] [WARN]`)} ${paint(DIM, shorten(at))}\n ${message}`
71
70
  );
package/src/astro/dom.ts DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Scoped element lookups, typed.
3
- *
4
- * ```ts
5
- * const { one, all } = within(root);
6
- * const dots = all<HTMLButtonElement>("[data-carousel-dot]");
7
- * ```
8
- *
9
- * The root is bound once, so a script cannot reach a second copy of itself
10
- * elsewhere on the page. `document` belongs in one place per script: finding
11
- * the roots.
12
- *
13
- * **A selector with a combinator still matches against the whole document.**
14
- * `within(form).one("form p")` can match a `<p>` under a different form; only
15
- * the final filter is scoped. Use `:scope` if a combinator is needed.
16
- */
17
-
18
- export interface Within {
19
- /** The first match inside the root, or `null`. */
20
- one<T extends Element = HTMLElement>(selector: string): T | null;
21
- /** Every match inside the root, as an array rather than a `NodeList`. */
22
- all<T extends Element = HTMLElement>(selector: string): T[];
23
- }
24
-
25
- // `HTMLElement` by default because `hidden`, `dataset` and `focus` live there;
26
- // the bound stays `Element` so inline `<svg>` lookups are not refused.
27
- export function within(root: ParentNode): Within {
28
- return {
29
- one: <T extends Element = HTMLElement>(selector: string) =>
30
- root.querySelector<T>(selector),
31
- all: <T extends Element = HTMLElement>(selector: string) => [
32
- ...root.querySelectorAll<T>(selector),
33
- ],
34
- };
35
- }