@escape-game-over/atlas 0.1.1

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 (63) hide show
  1. package/README.md +364 -0
  2. package/bin/use-project.mjs +131 -0
  3. package/docs/NOT-BUILT.md +329 -0
  4. package/docs/checks.md +139 -0
  5. package/docs/share-images.md +52 -0
  6. package/docs/toolchain.md +83 -0
  7. package/package.json +51 -0
  8. package/src/analytics/google.ts +351 -0
  9. package/src/analytics/index.ts +102 -0
  10. package/src/analytics/tags.ts +57 -0
  11. package/src/analytics/umami.ts +285 -0
  12. package/src/astro/MetaTags.astro +87 -0
  13. package/src/astro/consent.ts +165 -0
  14. package/src/astro/images.ts +315 -0
  15. package/src/astro/index.ts +44 -0
  16. package/src/astro/public-files.ts +129 -0
  17. package/src/astro/site-routes.ts +307 -0
  18. package/src/config.ts +218 -0
  19. package/src/contact.ts +233 -0
  20. package/src/file.ts +16 -0
  21. package/src/files.ts +39 -0
  22. package/src/hours.ts +312 -0
  23. package/src/i18n/define.ts +217 -0
  24. package/src/i18n/placeholders.ts +94 -0
  25. package/src/i18n/translate.ts +190 -0
  26. package/src/image.ts +29 -0
  27. package/src/index.ts +222 -0
  28. package/src/jsonld/article.ts +165 -0
  29. package/src/jsonld/breadcrumb.ts +34 -0
  30. package/src/jsonld/business.ts +196 -0
  31. package/src/jsonld/ids.ts +106 -0
  32. package/src/jsonld/index.ts +59 -0
  33. package/src/jsonld/node.ts +78 -0
  34. package/src/jsonld/organization.ts +154 -0
  35. package/src/jsonld/place.ts +96 -0
  36. package/src/jsonld/product.ts +172 -0
  37. package/src/jsonld/quantity.ts +55 -0
  38. package/src/jsonld/service.ts +237 -0
  39. package/src/jsonld/video.ts +239 -0
  40. package/src/jsonld/website.ts +58 -0
  41. package/src/llms.ts +160 -0
  42. package/src/meta/content.ts +190 -0
  43. package/src/meta/index.ts +432 -0
  44. package/src/meta/robots.ts +212 -0
  45. package/src/meta/share-image.ts +232 -0
  46. package/src/meta/tag.ts +133 -0
  47. package/src/meta/verification.ts +53 -0
  48. package/src/money.ts +237 -0
  49. package/src/project.ts +249 -0
  50. package/src/redirects.ts +266 -0
  51. package/src/robots.ts +80 -0
  52. package/src/routes/define.ts +412 -0
  53. package/src/routes/family.ts +251 -0
  54. package/src/routes/resolve.ts +266 -0
  55. package/src/site/api.ts +354 -0
  56. package/src/site/create.ts +660 -0
  57. package/src/site/index.ts +32 -0
  58. package/src/site/page.ts +148 -0
  59. package/src/sitemap.ts +257 -0
  60. package/src/types.ts +160 -0
  61. package/src/url.ts +144 -0
  62. package/src/warn.ts +88 -0
  63. package/src/xml.ts +103 -0
package/src/url.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * URL shapes checked at the point they are written.
3
+ *
4
+ * Shared rather than restated, because more than one thing here takes a URL from
5
+ * a consumer — the site's own origin, and the target of an external redirect —
6
+ * and they must agree about what an acceptable one looks like. Each rejects with
7
+ * a named error rather than a bare pattern mismatch, so the message says what is
8
+ * wrong instead of only that something is.
9
+ */
10
+
11
+ /**
12
+ * An absolute `https://` URL, as a shape.
13
+ *
14
+ * The plain type, for a URL that is *already* known good — what `createSite`
15
+ * hands back, and what a structured-data node quotes. The generic validators
16
+ * below are for a URL arriving from a consumer, where a named error is worth
17
+ * the type parameter; here there is nothing to explain, only a fact to keep.
18
+ *
19
+ * Sound because `createSite` throws on anything else before this is ever
20
+ * exposed. A type that outran its runtime check would be worse than none.
21
+ */
22
+ export type HttpsUrl = `https://${string}`;
23
+
24
+ /**
25
+ * The runtime half of `HttpsUrl`, written as a guard so the check that throws
26
+ * is also the check that narrows.
27
+ *
28
+ * A cast would assert the same thing without testing it, and would keep on
29
+ * asserting it after someone edited the condition above it.
30
+ */
31
+ export function isHttpsUrl(url: string): url is HttpsUrl {
32
+ return url.startsWith("https://");
33
+ }
34
+
35
+ /**
36
+ * A path from the root of some origin: `/logo.png`, `/it/sfide`.
37
+ *
38
+ * The other half of every URL this library builds. Typed so a caller cannot
39
+ * hand over `logo.png` and get `https://example.comlogo.png`, which is the
40
+ * failure that reads as a typo in the output rather than as a bug in the join.
41
+ */
42
+ export type UrlPath = `/${string}`;
43
+
44
+ /** The runtime half of `UrlPath`. See `isHttpsUrl`. */
45
+ export function isUrlPath(path: string): path is UrlPath {
46
+ return path.startsWith("/");
47
+ }
48
+
49
+ /**
50
+ * An origin without its trailing slash.
51
+ *
52
+ * Necessary because the *type* cannot rule one out — a template literal has no
53
+ * way to say "does not end in `/`" — so every origin arriving from outside is
54
+ * normalised rather than trusted. `https://x.example/` and `https://x.example`
55
+ * name the same origin, so this loses nothing and is not worth refusing over.
56
+ *
57
+ * It matters most where an origin becomes an identity: `https://x.example/`
58
+ * and `https://x.example` yield two different `@id` strings, and two
59
+ * deployments that wrote it differently would describe two separate brands.
60
+ */
61
+ export function normalizeOrigin(origin: HttpsUrl): HttpsUrl {
62
+ if (!origin.endsWith("/")) return origin;
63
+ const trimmed = origin.slice(0, -1);
64
+ // Survives unless the whole value was `https://`, which is a scheme rather
65
+ // than an origin. Re-checking beats a cast asserting what was not tested.
66
+ if (!isHttpsUrl(trimmed)) {
67
+ throw new Error(`"${origin}" is a scheme, not an origin.`);
68
+ }
69
+ return trimmed;
70
+ }
71
+
72
+ /**
73
+ * An origin and a path, with exactly one slash between them.
74
+ *
75
+ * Every absolute URL lib emits is a concatenation, and both ends can carry a
76
+ * slash. Doing it by hand in a dozen places is how one of them ends up with
77
+ * `//` — which is a different URL, served or redirected inconsistently, and
78
+ * duplicate content if a crawler reaches both.
79
+ */
80
+ export function joinUrl(origin: HttpsUrl, path: UrlPath): HttpsUrl {
81
+ return `${normalizeOrigin(origin)}${path}`;
82
+ }
83
+
84
+ export interface TrailingSlashNotAllowed<U extends string> {
85
+ readonly __URL_HAS_TRAILING_SLASH__: `url "${U}" must not end with a slash: every path lib builds already starts with one`;
86
+ }
87
+
88
+ export interface MustBeHttps<U extends string> {
89
+ readonly __URL_NOT_HTTPS__: `url "${U}" must start with https://: an http link is a downgrade, and browsers increasingly refuse it outright`;
90
+ }
91
+
92
+ /**
93
+ * An absolute `https://` URL. A path, query and trailing slash are all fine.
94
+ *
95
+ * For a URL that points *somewhere else* — the target of a redirect, say — where
96
+ * only the scheme is this library's business.
97
+ */
98
+ export type ValidHttpsUrl<U extends string> = U extends `https://${string}`
99
+ ? U
100
+ : MustBeHttps<U>;
101
+
102
+ /**
103
+ * Rejects an origin that is not a bare `https://` prefix without a trailing
104
+ * slash. A path is allowed, since deploying under a subdirectory is legitimate.
105
+ *
106
+ * Catches both at the point the URL is written, since a project states it as a
107
+ * literal. `createSite` still checks at build time, for the case where the URL
108
+ * comes from somewhere typed as plain `string` — an env var, say — and these
109
+ * checks pass vacuously.
110
+ */
111
+ export type ValidSiteUrl<U extends string> = U extends `${string}/`
112
+ ? TrailingSlashNotAllowed<U>
113
+ : ValidHttpsUrl<U>;
114
+
115
+ /**
116
+ * An asset's `src` as an absolute URL, whatever form it arrived in.
117
+ *
118
+ * Takes `string` because that is what a bundler declares `src` as, and turns it
119
+ * into one of the only two things it can legitimately be — an `https://` URL
120
+ * already, or a path on this origin.
121
+ *
122
+ * Neither rejection is pedantry. An `http://` asset is mixed content, dropped
123
+ * by anything that scrapes it. A relative `logo.png` would be joined as written
124
+ * and produce `https://example.comlogo.png`, which reads as a typo in the
125
+ * output long before anyone suspects the join.
126
+ *
127
+ * Throws rather than skipping, because the callers build *sets* — three crops
128
+ * of a photograph, one per aspect ratio — and quietly returning two of them is
129
+ * a set that looks complete and is not.
130
+ */
131
+ export function absoluteUrl(
132
+ origin: HttpsUrl,
133
+ src: string,
134
+ at: string
135
+ ): HttpsUrl {
136
+ if (isHttpsUrl(src)) return src;
137
+
138
+ if (!isUrlPath(src)) {
139
+ throw new Error(
140
+ `${at}: "${src}" is neither an https:// URL nor a path from the root.`
141
+ );
142
+ }
143
+ return joinUrl(origin, src);
144
+ }
package/src/warn.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The one place lib writes to a console.
3
+ *
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.
7
+ *
8
+ * Shared so the colouring and the `[lib]` prefix are decided once — a warning
9
+ * that looks different depending on which module raised it reads like two
10
+ * different tools.
11
+ */
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";
21
+
22
+ /**
23
+ * The escape character, named rather than embedded. 27 is its code point.
24
+ *
25
+ * The three constants below used to hold the raw control byte inside their
26
+ * string literals — invisible in every editor, and sitting in a folder whose
27
+ * whole purpose is to be copied between repositories, where a tool that
28
+ * normalises or re-encodes on the way through would take the colour with it
29
+ * silently and only in the copy.
30
+ *
31
+ * Built from the code point rather than from a unicode escape, which takes the
32
+ * same reasoning one step further: the source stays plain ASCII with nothing to
33
+ * mis-transcribe, and the constant says what the byte *is* rather than leaving
34
+ * a reader to decode it. Nothing in this file needs a character an editor
35
+ * cannot show.
36
+ */
37
+ const ESC = String.fromCharCode(27);
38
+
39
+ const YELLOW = `${ESC}[33m`;
40
+ const DIM = `${ESC}[2m`;
41
+ const RESET = `${ESC}[0m`;
42
+
43
+ /**
44
+ * Whether to emit ANSI colour.
45
+ *
46
+ * `NO_COLOR` is the convention every CLI honours, and the check reaches through
47
+ * `globalThis` rather than naming `process`: this file is part of the core,
48
+ * which is type-checked with `"types": []` and cannot see Node. A browser has no
49
+ * `process` and simply gets colour, which is what a browser console renders
50
+ * anyway.
51
+ */
52
+ const colour =
53
+ (
54
+ globalThis as {
55
+ process?: { env?: Readonly<Record<string, string | undefined>> };
56
+ }
57
+ ).process?.env?.NO_COLOR === undefined;
58
+
59
+ const paint = (code: string, text: string): string =>
60
+ colour ? `${code}${text}${RESET}` : text;
61
+
62
+ /**
63
+ * Warns about one page, or one file.
64
+ *
65
+ * `at` is whatever identifies the thing — a canonical URL, an asset path. It is
66
+ * always printed, because a warning nobody can locate is one nobody acts on.
67
+ */
68
+ export function warn(at: string, message: string): void {
69
+ console.warn(
70
+ `${paint(YELLOW, `[${PREFIX}] [WARN]`)} ${paint(DIM, shorten(at))}\n ${message}`
71
+ );
72
+ }
73
+
74
+ /**
75
+ * An absolute URL down to its path.
76
+ *
77
+ * Every warning in one build shares an origin, so printing it on each is the
78
+ * same forty characters repeated — and the path is what matches the page list
79
+ * the build is already printing beside it. Anything that is not a URL, such as
80
+ * a filename, is left as it is.
81
+ */
82
+ function shorten(at: string): string {
83
+ try {
84
+ return new URL(at).pathname;
85
+ } catch {
86
+ return at;
87
+ }
88
+ }
package/src/xml.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A minimal XML element builder.
3
+ *
4
+ * Generating XML by concatenating template literals means hand-managing
5
+ * indentation, stray newlines and — worse — remembering to escape at every
6
+ * interpolation. Building a tree instead makes the shape of the document read
7
+ * like the document, and escaping happens once, in the renderer.
8
+ */
9
+
10
+ const XML_ESCAPES: Readonly<Record<string, string>> = {
11
+ "&": "&amp;",
12
+ "<": "&lt;",
13
+ ">": "&gt;",
14
+ '"': "&quot;",
15
+ "'": "&apos;",
16
+ };
17
+
18
+ export function escapeXml(value: string): string {
19
+ return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
20
+ }
21
+
22
+ export type XmlAttributes = Readonly<Record<string, string | undefined>>;
23
+
24
+ export interface XmlElement {
25
+ readonly tag: string;
26
+ readonly attrs?: XmlAttributes;
27
+ readonly text?: string;
28
+ readonly children?: readonly XmlChild[];
29
+ }
30
+
31
+ /**
32
+ * `false` and `undefined` are allowed so conditional children read inline:
33
+ * `cond && el("priority", undefined, "1.0")`.
34
+ */
35
+ export type XmlChild = XmlElement | false | undefined;
36
+
37
+ export type XmlChildren = string | readonly XmlChild[];
38
+
39
+ /**
40
+ * Builds an element. Attributes are optional and may be skipped entirely:
41
+ *
42
+ * ```ts
43
+ * el("loc", "https://example.com") // text
44
+ * el("url", [el("loc", "…")]) // children
45
+ * el("xhtml:link", { rel: "alternate", href: "…" }) // attributes only
46
+ * el("urlset", { xmlns: "…" }, urls) // both
47
+ * ```
48
+ */
49
+ export function el(tag: string, children?: XmlChildren): XmlElement;
50
+ export function el(
51
+ tag: string,
52
+ attrs: XmlAttributes,
53
+ children?: XmlChildren
54
+ ): XmlElement;
55
+ export function el(
56
+ tag: string,
57
+ attrsOrChildren?: XmlAttributes | XmlChildren,
58
+ maybeChildren?: XmlChildren
59
+ ): XmlElement {
60
+ const isChildren =
61
+ typeof attrsOrChildren === "string" || Array.isArray(attrsOrChildren);
62
+ const attrs = isChildren ? undefined : (attrsOrChildren as XmlAttributes);
63
+ const children = isChildren
64
+ ? (attrsOrChildren as XmlChildren)
65
+ : maybeChildren;
66
+
67
+ return typeof children === "string"
68
+ ? { tag, attrs, text: children }
69
+ : { tag, attrs, children };
70
+ }
71
+
72
+ function renderAttrs(attrs: XmlAttributes | undefined): string {
73
+ if (attrs === undefined) return "";
74
+ return Object.entries(attrs)
75
+ .filter(([, value]) => value !== undefined)
76
+ .map(([name, value]) => ` ${name}="${escapeXml(value as string)}"`)
77
+ .join("");
78
+ }
79
+
80
+ /** Renders one element, indenting nested levels by two spaces. */
81
+ export function renderXml(node: XmlElement, depth = 0): string {
82
+ const pad = " ".repeat(depth);
83
+ const open = `${pad}<${node.tag}${renderAttrs(node.attrs)}`;
84
+
85
+ if (node.text !== undefined) {
86
+ return `${open}>${escapeXml(node.text)}</${node.tag}>`;
87
+ }
88
+
89
+ const children = (node.children ?? []).filter(
90
+ (child): child is XmlElement => child !== false && child !== undefined
91
+ );
92
+ if (children.length === 0) return `${open} />`;
93
+
94
+ const inner = children
95
+ .map((child) => renderXml(child, depth + 1))
96
+ .join("\n");
97
+ return `${open}>\n${inner}\n${pad}</${node.tag}>`;
98
+ }
99
+
100
+ /** Renders a whole document, declaration included, with a trailing newline. */
101
+ export function renderXmlDocument(root: XmlElement): string {
102
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${renderXml(root)}\n`;
103
+ }