@jxsuite/compiler 3.0.0 → 4.0.0
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/dist/cli.js +281 -276
- package/dist/compiler.js +130 -126
- package/package.json +10 -10
- package/src/cli.ts +10 -4
- package/src/shared.ts +514 -299
- package/src/site/base-path.ts +175 -0
- package/src/site/dev-command.ts +4 -2
- package/src/site/format-host.ts +54 -1
- package/src/site/headers-emitter.ts +20 -0
- package/src/site/i18n.ts +3 -2
- package/src/site/prototype-resolver.ts +2 -1
- package/src/site/service-worker.ts +22 -5
- package/src/site/site-build.ts +88 -49
- package/src/site/validate-command.ts +100 -3
- package/src/site/well-known.ts +14 -3
- package/src/targets/compile-client.ts +32 -6
- package/src/targets/compile-element.ts +39 -2
- package/src/targets/compile-server.ts +14 -7
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-path.ts — re-rooting a build's output onto the path the site is deployed under.
|
|
3
|
+
*
|
|
4
|
+
* A site at `https://example.pages.dev/m/my-site/` is served from `/m/my-site/`, and every
|
|
5
|
+
* absolute-path reference the build emits — `/assets/vue-reactivity.js`,
|
|
6
|
+
* `/components/site-counter.js`, `/images/hero.jpg` — resolves against `example.pages.dev/`
|
|
7
|
+
* instead, where nothing is. The page renders blank rather than merely wrong, because the runtime
|
|
8
|
+
* assets 404 first (issue 235).
|
|
9
|
+
*
|
|
10
|
+
* **The base is not a new option.** `project.json`'s `url` is the base URI every emitted reference
|
|
11
|
+
* is resolved against (RFC 3986 §5), and the compiler was resolving against its ORIGIN and
|
|
12
|
+
* discarding its path. `siteBasePath` in `@jxsuite/schema/asset-paths` reads the path back; a
|
|
13
|
+
* second `build.base` key would be a second writer on the one fact the canonical links and the
|
|
14
|
+
* sitemap are already built from, free to disagree with them.
|
|
15
|
+
*
|
|
16
|
+
* **The rewrite happens on finished HTML, and that is the point.** The alternative is to thread a
|
|
17
|
+
* prefix through the twelve emitters that can put a URL on a page — the import map and its
|
|
18
|
+
* modulepreloads, three page-template tiers that each write their own map, component scripts,
|
|
19
|
+
* island modules, sidecar and npm bundles, image `srcset`, `$head` entries, and whatever a page
|
|
20
|
+
* author wrote by hand. That list is exactly the kind that is complete until the next emitter is
|
|
21
|
+
* added, and #227 was already one emitter's map disagreeing with another's. One pass over the bytes
|
|
22
|
+
* that ship cannot drift from them.
|
|
23
|
+
*
|
|
24
|
+
* Outputs that are not HTML keep their own handling, because each has a syntax and none of them can
|
|
25
|
+
* be found by looking at a tag: `_headers`, `_redirects`, the service worker's precache list and
|
|
26
|
+
* scope, `manifest.webmanifest`, and the absolute URLs in `sitemap.xml` and `robots.txt`.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is idempotent (see `withBase`), so a value an emitter already prefixed survives
|
|
29
|
+
* this pass unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @docs framework/site/deployment
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { formatSrcset, parseSrcset, withBase } from "@jxsuite/schema/asset-paths";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Attributes whose value is a single URL.
|
|
38
|
+
*
|
|
39
|
+
* `href` and `src` are most of the traffic; the rest are the ones a Jx page can actually emit —
|
|
40
|
+
* `<form action>`, `<button formaction>`, `<video poster>`, `<object data>`. Anything not listed is
|
|
41
|
+
* left alone, which is the safe direction: a missed URL 404s visibly on a subpath deployment, while
|
|
42
|
+
* a rewritten non-URL corrupts a page that was working.
|
|
43
|
+
*/
|
|
44
|
+
const URL_ATTRS = new Set(["href", "src", "poster", "action", "formaction", "data"]);
|
|
45
|
+
|
|
46
|
+
/** Attributes whose value is a `srcset` candidate list rather than one URL. */
|
|
47
|
+
const SRCSET_ATTRS = new Set(["srcset", "imagesrcset"]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One HTML tag: the name, then attributes with quoted values consumed whole.
|
|
51
|
+
*
|
|
52
|
+
* Consuming quotes matters — a `>` inside an attribute value would otherwise end the tag early and
|
|
53
|
+
* leave the rest of its attributes as text this pass never looks at.
|
|
54
|
+
*/
|
|
55
|
+
const TAG_RE = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
|
|
56
|
+
|
|
57
|
+
/** One attribute inside a tag's attribute blob. */
|
|
58
|
+
const ATTR_RE = /([a-zA-Z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
59
|
+
|
|
60
|
+
/** `url(...)` inside a CSS value, capturing the optional quote so it can be put back. */
|
|
61
|
+
const CSS_URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
|
|
62
|
+
|
|
63
|
+
/** A JSON string literal holding an absolute-path reference — an import map's keys and values. */
|
|
64
|
+
const JSON_ABS_PATH_RE = /"(\/[^"/][^"]*)"/g;
|
|
65
|
+
|
|
66
|
+
/** `<script type="importmap">…</script>`, whose body is JSON rather than markup. */
|
|
67
|
+
const IMPORTMAP_RE = /(<script\b[^>]*\btype\s*=\s*["']importmap["'][^>]*>)([\s\S]*?)(<\/script>)/gi;
|
|
68
|
+
|
|
69
|
+
/** `<style>…</style>`, whose body is CSS rather than markup. */
|
|
70
|
+
const STYLE_BLOCK_RE = /(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `<meta>` names whose `content` is a URL.
|
|
74
|
+
*
|
|
75
|
+
* `content` is the one attribute here that usually holds prose, so it is rewritten only when the
|
|
76
|
+
* same tag says the value is a URL. OpenGraph and Twitter image/player metas are the cases a page
|
|
77
|
+
* actually produces from an author-written `/hero.jpg`.
|
|
78
|
+
*/
|
|
79
|
+
const URL_META_RE =
|
|
80
|
+
/\b(?:property|name)\s*=\s*["'](?:og:|twitter:)[\w:.-]*(?:image|url|video|audio|player)[\w:.-]*["']/i;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A quote around a `url()` target that survived attribute escaping.
|
|
84
|
+
*
|
|
85
|
+
* In a `style` ATTRIBUTE the quotes inside `url("…")` are escaped, so the target arrives wrapped in
|
|
86
|
+
* `"` rather than in a character {@link CSS_URL_RE} would have treated as a delimiter. Left
|
|
87
|
+
* alone the value no longer starts with `/` and is silently skipped — a background image that 404s
|
|
88
|
+
* on a subpath deployment with nothing to show for it.
|
|
89
|
+
*/
|
|
90
|
+
const ENTITY_QUOTE_RE = /^("|"|'|')([\s\S]*)\1$/;
|
|
91
|
+
|
|
92
|
+
/** Rewrite every `url(...)` in a CSS fragment, in a `<style>` block or a `style` attribute. */
|
|
93
|
+
function rewriteCssUrls(css: string, base: string): string {
|
|
94
|
+
return css.replaceAll(CSS_URL_RE, (match, quote: string, inner: string) => {
|
|
95
|
+
const raw = inner.trim();
|
|
96
|
+
const escaped = ENTITY_QUOTE_RE.exec(raw);
|
|
97
|
+
const target = escaped ? escaped[2]! : raw;
|
|
98
|
+
const out = withBase(base, target);
|
|
99
|
+
if (out === target) {
|
|
100
|
+
return match;
|
|
101
|
+
}
|
|
102
|
+
return `url(${quote}${escaped ? `${escaped[1]}${out}${escaped[1]}` : out}${quote})`;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Rewrite one attribute value, or return it unchanged when the attribute carries no URL. */
|
|
107
|
+
function rewriteAttrValue(tag: string, attrs: string, name: string, value: string, base: string) {
|
|
108
|
+
const lower = name.toLowerCase();
|
|
109
|
+
if (URL_ATTRS.has(lower)) {
|
|
110
|
+
return withBase(base, value);
|
|
111
|
+
}
|
|
112
|
+
if (SRCSET_ATTRS.has(lower)) {
|
|
113
|
+
const candidates = parseSrcset(value);
|
|
114
|
+
return candidates.length === 0
|
|
115
|
+
? value
|
|
116
|
+
: formatSrcset(
|
|
117
|
+
candidates.map((c) => ({ descriptor: c.descriptor, url: withBase(base, c.url) })),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (lower === "style") {
|
|
121
|
+
return rewriteCssUrls(value, base);
|
|
122
|
+
}
|
|
123
|
+
if (lower === "content" && tag === "meta" && URL_META_RE.test(attrs)) {
|
|
124
|
+
return withBase(base, value);
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Re-root every site-absolute URL in a page's HTML onto `base`.
|
|
131
|
+
*
|
|
132
|
+
* `base` of `""` is a site at an origin root, and the HTML is returned untouched — no allocation
|
|
133
|
+
* and no risk for the deployment Jx documents.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} html - A finished page
|
|
136
|
+
* @param {string} base - From `siteBasePath`
|
|
137
|
+
* @returns {string} The page as it should ship
|
|
138
|
+
*/
|
|
139
|
+
export function rewriteHtmlBase(html: string, base: string): string {
|
|
140
|
+
if (base === "") {
|
|
141
|
+
return html;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let out = html.replaceAll(IMPORTMAP_RE, (_match, open: string, body: string, close: string) => {
|
|
145
|
+
/* The import map's KEYS are addressable too: a map may name a module by URL rather than by bare
|
|
146
|
+
specifier, and a key left at the old root would resolve to a file the build did not write. */
|
|
147
|
+
const rewritten = body.replaceAll(
|
|
148
|
+
JSON_ABS_PATH_RE,
|
|
149
|
+
(_literal, path: string) => `"${withBase(base, path)}"`,
|
|
150
|
+
);
|
|
151
|
+
return open + rewritten + close;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
out = out.replaceAll(
|
|
155
|
+
STYLE_BLOCK_RE,
|
|
156
|
+
(_m, open: string, css: string, close: string) => open + rewriteCssUrls(css, base) + close,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return out.replaceAll(TAG_RE, (match, tagName: string, attrs: string) => {
|
|
160
|
+
if (attrs === "") {
|
|
161
|
+
return match;
|
|
162
|
+
}
|
|
163
|
+
const tag = tagName.toLowerCase();
|
|
164
|
+
const rewrittenAttrs = attrs.replaceAll(
|
|
165
|
+
ATTR_RE,
|
|
166
|
+
(whole, name: string, _quoted: string, dq: string | undefined, sq: string | undefined) => {
|
|
167
|
+
const quote = dq === undefined ? "'" : '"';
|
|
168
|
+
const value = dq ?? sq ?? "";
|
|
169
|
+
const next = rewriteAttrValue(tag, attrs, name, value, base);
|
|
170
|
+
return next === value ? whole : `${name}=${quote}${next}${quote}`;
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
return rewrittenAttrs === attrs ? match : `<${tagName}${rewrittenAttrs}>`;
|
|
174
|
+
});
|
|
175
|
+
}
|
package/src/site/dev-command.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Dev command — `jx dev` resolves the project's @jxsuite/server dev entry and spawns it under Bun.
|
|
3
3
|
*
|
|
4
4
|
* The compiler cannot depend on @jxsuite/server (the server already depends on the compiler), and
|
|
5
|
-
* the dev server is Bun-native while the jx bin runs under Node — so the command is pure
|
|
6
|
-
*
|
|
5
|
+
* the dev server is Bun-native while the jx bin runs under Node — so the command is pure
|
|
6
|
+
* resolution
|
|
7
|
+
*
|
|
8
|
+
* - Spawn: the entry module is resolved from the PROJECT's node_modules and executed with Bun.
|
|
7
9
|
*
|
|
8
10
|
* @module dev-command
|
|
9
11
|
*/
|
package/src/site/format-host.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
-
import { buildExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
13
|
+
import { buildExtensionRegistry, EXTENSION_MANIFEST } from "@jxsuite/schema/extension-registry";
|
|
14
14
|
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
15
15
|
import type { FormatHostIO, FormatRegistry, ProjectBlock } from "@jxsuite/schema/format-registry";
|
|
16
16
|
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
@@ -75,6 +75,59 @@ export function createNodeFormatIO(projectRoot: string): FormatHostIO {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Where an extension package's manifest resolves from — the two answers `createNodeFormatIO`
|
|
80
|
+
* merges.
|
|
81
|
+
*/
|
|
82
|
+
export interface ExtensionResolution {
|
|
83
|
+
/** Resolvable from the PROJECT's own node_modules. */
|
|
84
|
+
project: boolean;
|
|
85
|
+
/** Resolvable from the HOST's module graph — `createNodeFormatIO`'s fallback branch. */
|
|
86
|
+
host: boolean;
|
|
87
|
+
/** The manifest path from whichever probe answered; the project wins, matching resolution order. */
|
|
88
|
+
manifestPath?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Split `createNodeFormatIO`'s two-branch resolution into its two ANSWERS.
|
|
93
|
+
*
|
|
94
|
+
* The IO deliberately collapses them (project first, then host) because a caller loading a class
|
|
95
|
+
* only needs the file. A catalogue needs them apart: "the project installed this" and "this host
|
|
96
|
+
* ships it" are different affordances — install-then-enable versus enable — and a surface that
|
|
97
|
+
* cannot tell them apart offers the wrong one.
|
|
98
|
+
*
|
|
99
|
+
* Probes the MANIFEST rather than the package directory, because the manifest is what the registry
|
|
100
|
+
* resolves (see `loadExtension`): a package present in node_modules whose `exports` map omits
|
|
101
|
+
* `./jx-extension.json` is one whose enablement would throw, so it must not read as resolvable.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} projectRoot - The project whose node_modules is tried first
|
|
104
|
+
* @param {string} name - Bare package specifier, e.g. "@jxsuite/parser"
|
|
105
|
+
* @returns {ExtensionResolution}
|
|
106
|
+
*/
|
|
107
|
+
export function probeExtension(projectRoot: string, name: string): ExtensionResolution {
|
|
108
|
+
const ref = `${name}/${EXTENSION_MANIFEST}`;
|
|
109
|
+
let manifestPath: string | undefined;
|
|
110
|
+
|
|
111
|
+
let project = false;
|
|
112
|
+
try {
|
|
113
|
+
manifestPath = createRequire(resolve(projectRoot, "package.json")).resolve(ref);
|
|
114
|
+
project = true;
|
|
115
|
+
} catch {
|
|
116
|
+
// Not installed in the project. The host probe below decides whether it resolves at all.
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let host = false;
|
|
120
|
+
try {
|
|
121
|
+
const fromHost = createRequire(import.meta.url).resolve(ref);
|
|
122
|
+
host = true;
|
|
123
|
+
manifestPath ??= fromHost;
|
|
124
|
+
} catch {
|
|
125
|
+
// Not shipped by this host either.
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { host, project, ...(manifestPath === undefined ? {} : { manifestPath }) };
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
/** Build the extension registry for a project from its project.json `extensions` array. */
|
|
79
132
|
export async function buildProjectExtensionRegistry(
|
|
80
133
|
projectRoot: string,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @docs framework/site/deployment
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { withBase } from "@jxsuite/schema/asset-paths";
|
|
14
15
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
import { buildCspHeaders, normalizeCspConfig } from "./csp.ts";
|
|
@@ -181,6 +182,25 @@ export function contentTypeRules(outDir: string): HeaderRule[] {
|
|
|
181
182
|
return rules;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Re-root every rule's pattern onto the deployment base.
|
|
187
|
+
*
|
|
188
|
+
* A `_headers` pattern is matched against the REQUEST path, so on a site served from `/m/my-site/`
|
|
189
|
+
* a rule for `/assets/*` matches nothing at all — which is silent, because a missing cache header
|
|
190
|
+
* is a slow site rather than a broken one. The generated patterns and the author's `build.headers`
|
|
191
|
+
* rules go through together: both describe the same URL space.
|
|
192
|
+
*
|
|
193
|
+
* @param {readonly HeaderRule[]} rules
|
|
194
|
+
* @param {string} base - From `siteBasePath`; `""` returns the rules untouched
|
|
195
|
+
* @returns {HeaderRule[]}
|
|
196
|
+
*/
|
|
197
|
+
export function rebaseHeaderRules(rules: readonly HeaderRule[], base: string): HeaderRule[] {
|
|
198
|
+
if (base === "") {
|
|
199
|
+
return [...rules];
|
|
200
|
+
}
|
|
201
|
+
return rules.map((rule) => ({ headers: rule.headers, pattern: withBase(base, rule.pattern) }));
|
|
202
|
+
}
|
|
203
|
+
|
|
184
204
|
/** Render rules in the Netlify / Cloudflare Pages `_headers` format. */
|
|
185
205
|
export function renderHeaders(rules: readonly HeaderRule[]): string {
|
|
186
206
|
const blocks = rules.map((rule) => {
|
package/src/site/i18n.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @docs framework/site/i18n
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { siteAbsoluteUrl } from "@jxsuite/schema/asset-paths";
|
|
15
16
|
import {
|
|
16
17
|
canonicalizeLocale,
|
|
17
18
|
localeDirection,
|
|
@@ -298,13 +299,13 @@ export function localeAlternates(
|
|
|
298
299
|
let alternates = built.get(members);
|
|
299
300
|
if (alternates === undefined) {
|
|
300
301
|
alternates = members.map((m) => ({
|
|
301
|
-
href:
|
|
302
|
+
href: siteAbsoluteUrl(m.urlPattern, siteUrl),
|
|
302
303
|
hreflang: m.locale,
|
|
303
304
|
}));
|
|
304
305
|
const fallback = members.find((m) => m.locale === i18n.defaultLocale);
|
|
305
306
|
if (fallback !== undefined) {
|
|
306
307
|
alternates.push({
|
|
307
|
-
href:
|
|
308
|
+
href: siteAbsoluteUrl(fallback.urlPattern, siteUrl),
|
|
308
309
|
hreflang: "x-default",
|
|
309
310
|
});
|
|
310
311
|
}
|
|
@@ -56,8 +56,9 @@ const RESERVED_KEYS = new Set([
|
|
|
56
56
|
* to a .class.json path via doc.imports
|
|
57
57
|
*
|
|
58
58
|
* The resolver: - Reads the .class.json from disk - Follows $implementation to import the JS module
|
|
59
|
+
*
|
|
59
60
|
* - Instantiates the class with the config - Calls .resolve() and replaces the state entry with the
|
|
60
|
-
*
|
|
61
|
+
* result
|
|
61
62
|
*
|
|
62
63
|
* @param {JxMutableNode | JxDocument} doc - The page document (mutated in place)
|
|
63
64
|
* @param {{ sourcePath?: string; _pathParams?: Record<string, string> }} route - Route info
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @docs framework/site/deployment
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { withBase } from "@jxsuite/schema/asset-paths";
|
|
23
24
|
import { createHash } from "node:crypto";
|
|
24
25
|
import { existsSync } from "node:fs";
|
|
25
26
|
import { join } from "node:path";
|
|
@@ -100,6 +101,7 @@ function configId(config: ServiceWorkerConfig): string {
|
|
|
100
101
|
export function buildServiceWorker(
|
|
101
102
|
config: ServiceWorkerConfig,
|
|
102
103
|
outDir?: string,
|
|
104
|
+
base = "",
|
|
103
105
|
): ServiceWorkerOutput {
|
|
104
106
|
const warnings: string[] = [];
|
|
105
107
|
const errors: string[] = [];
|
|
@@ -140,15 +142,26 @@ export function buildServiceWorker(
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/*
|
|
146
|
+
* The worker runs in the browser and every URL in it is a REQUEST path, so on a site served from
|
|
147
|
+
* `/m/my-site/` the base belongs on all three: a precache entry without it fetches a 404, and
|
|
148
|
+
* `CACHE_FIRST` without it never matches, so the content-addressed images it exists to serve
|
|
149
|
+
* from cache go to the network on every view. `outputFileFor` above is deliberately checked
|
|
150
|
+
* BEFORE this — those URLs are looked up in `outDir`, which has no base.
|
|
151
|
+
*/
|
|
152
|
+
const cachedPrecache = precache.map((url) => withBase(base, url));
|
|
153
|
+
const cacheFirst = withBase(base, CACHE_FIRST_PREFIX);
|
|
154
|
+
const offlineFallback = fallback === undefined ? null : withBase(base, fallback);
|
|
155
|
+
|
|
143
156
|
const cache = `jx-${configId(config)}`;
|
|
144
157
|
const source = `/*
|
|
145
158
|
* Generated by @jxsuite/compiler — configure via \`serviceWorker\` in project.json.
|
|
146
159
|
* Set \`"serviceWorker": false\` to replace this with a worker that removes itself.
|
|
147
160
|
*/
|
|
148
161
|
const CACHE = ${JSON.stringify(cache)};
|
|
149
|
-
const PRECACHE = ${JSON.stringify(
|
|
150
|
-
const CACHE_FIRST = ${JSON.stringify(
|
|
151
|
-
const OFFLINE_FALLBACK = ${JSON.stringify(
|
|
162
|
+
const PRECACHE = ${JSON.stringify(cachedPrecache)};
|
|
163
|
+
const CACHE_FIRST = ${JSON.stringify(cacheFirst)};
|
|
164
|
+
const OFFLINE_FALLBACK = ${JSON.stringify(offlineFallback)};
|
|
152
165
|
|
|
153
166
|
self.addEventListener("install", (event) => {
|
|
154
167
|
// Take over as soon as possible: a half-updated pair of workers is harder to reason about than
|
|
@@ -285,10 +298,14 @@ self.addEventListener("activate", (event) => {
|
|
|
285
298
|
* @param {string} scope
|
|
286
299
|
* @returns {string}
|
|
287
300
|
*/
|
|
288
|
-
export function registrationScript(scope: string): string {
|
|
301
|
+
export function registrationScript(scope: string, base = ""): string {
|
|
302
|
+
/* Both are request paths. A worker's scope may not exceed its own directory, so a worker served
|
|
303
|
+
from `/m/my-site/sw.js` with a scope of `/` fails to register outright — the one failure here
|
|
304
|
+
that is loud rather than silent. */
|
|
305
|
+
const worker = withBase(base, `/${SERVICE_WORKER_PATH}`);
|
|
289
306
|
return (
|
|
290
307
|
`if('serviceWorker' in navigator){addEventListener('load',function(){` +
|
|
291
|
-
`navigator.serviceWorker.register('
|
|
308
|
+
`navigator.serviceWorker.register('${worker}',{scope:${JSON.stringify(withBase(base, scope))}})` +
|
|
292
309
|
`.catch(function(){})})}`
|
|
293
310
|
);
|
|
294
311
|
}
|