@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.
- package/README.md +29 -45
- package/bin/use-project.mjs +18 -13
- package/docs/NOT-BUILT.md +1 -1
- package/docs/client-scripts.md +73 -141
- package/docs/rich-text.md +25 -20
- package/package.json +5 -12
- package/src/analytics/google.ts +6 -6
- package/src/analytics/index.ts +4 -3
- package/src/analytics/tags.ts +14 -60
- package/src/analytics/umami.ts +8 -8
- package/src/astro/ConsentBanner.astro +25 -0
- package/src/astro/ConsentElement.astro +61 -0
- package/src/astro/Document.astro +44 -0
- package/src/astro/Image.astro +102 -0
- package/src/astro/MetaTags.astro +3 -26
- package/src/astro/RichText.astro +71 -0
- package/src/astro/Zoom.astro +61 -0
- package/src/astro/client.ts +19 -9
- package/src/astro/consent.ts +20 -0
- package/src/astro/dev-log.ts +8 -14
- package/src/astro/element.ts +111 -112
- package/src/astro/filters-view.ts +48 -64
- package/src/astro/filters.ts +46 -37
- package/src/astro/images.ts +27 -26
- package/src/astro/index.ts +2 -9
- package/src/astro/markup.ts +6 -6
- package/src/astro/site-routes.ts +10 -15
- package/src/config.ts +23 -36
- package/src/content/index.ts +2 -1
- package/src/content/marks.ts +13 -13
- package/src/content/rich.ts +58 -29
- package/src/hours.ts +48 -11
- package/src/i18n/define.ts +14 -74
- package/src/index.ts +41 -57
- package/src/jsonld/faq.ts +2 -1
- package/src/jsonld/node.ts +4 -14
- package/src/meta/index.ts +7 -13
- package/src/meta/share-image.ts +6 -19
- package/src/meta/tag.ts +1 -45
- package/src/money.ts +161 -6
- package/src/project.ts +84 -73
- package/src/routes/define.ts +8 -44
- package/src/routes/resolve.ts +1 -1
- package/src/site/api.ts +7 -33
- package/src/site/create.ts +6 -10
- package/src/site/define.ts +120 -0
- package/src/site/index.ts +2 -5
- package/src/site/page.ts +4 -2
- package/src/sitemap.ts +2 -35
- package/src/warn.ts +16 -17
- package/src/astro/dom.ts +0 -35
package/src/site/create.ts
CHANGED
|
@@ -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
|
|
291
|
+
function getStaticPaths(): StaticPath<L, RouteId>[] {
|
|
295
292
|
const paths: StaticPath<L, RouteId>[] = entries.map((entry) => ({
|
|
296
293
|
params: {
|
|
297
|
-
|
|
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():
|
|
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
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
|
47
|
+
/** One `{ params, props }` pair for `[...route].astro`. */
|
|
46
48
|
export type StaticPath<L extends string, Id extends string> = {
|
|
47
|
-
readonly params:
|
|
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
|
|
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
|
|
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
|
|
6
|
-
*
|
|
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 `[
|
|
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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|