@farm.js/pwa 0.1.0-beta.2 → 0.1.0-beta.4

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 CHANGED
@@ -4,7 +4,7 @@ Installable, offline-aware Progressive Web Apps for Farm.js. The plugin generate
4
4
  service worker or copies a custom one during the production build, then registers it through Farm's
5
5
  browser plugin lifecycle.
6
6
 
7
- Farm.js is currently in beta.
7
+ This package is in beta. See [Stability and Support](https://farmjs.dev/docs/stability).
8
8
 
9
9
  ## Install
10
10
 
package/dist/build.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface PwaBuildInput {
5
5
  publicDir?: string;
6
6
  preset: string;
7
7
  basePath: string;
8
+ /** Prefix already present in emitted HTML paths (localized Farm SSG output). */
9
+ htmlBasePath?: string;
8
10
  options: ResolvedPwaOptions;
9
11
  }
10
12
  export interface PwaBuildResult {
@@ -18,6 +20,7 @@ export interface PwaBuildResult {
18
20
  export declare function writePwaBuildArtifacts(input: PwaBuildInput): Promise<PwaBuildResult>;
19
21
  export interface GenerateServiceWorkerOptions {
20
22
  basePath: string;
23
+ htmlBasePath?: string;
21
24
  cacheId: string;
22
25
  precacheUrls: string[];
23
26
  staticRoutes: Record<string, string>;
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAcD,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAyG1F;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,YAAY,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC1B,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC/C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CAyKnF;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7E;AAiDD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQpE"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAcD,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CA8G1F;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,YAAY,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC1B,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC/C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA0NnF;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK7E;AAiDD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGnE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKpE"}
package/dist/build.js CHANGED
@@ -15,6 +15,7 @@ const PRECACHE_EXTENSIONS = new Set([
15
15
  export async function writePwaBuildArtifacts(input) {
16
16
  const publicDir = input.publicDir ?? resolvePwaPublicDir(input.outputDir, input.preset);
17
17
  const basePath = normalizeBasePath(input.basePath);
18
+ const htmlBasePath = normalizeBasePath(input.htmlBasePath);
18
19
  const workerRelativePath = path.posix.join(basePath.replace(/^\//, ""), "sw.js");
19
20
  const workerPath = path.join(publicDir, ...workerRelativePath.split("/"));
20
21
  await assertWorkerPathInsidePublicDir(publicDir, workerPath);
@@ -39,14 +40,14 @@ export async function writePwaBuildArtifacts(input) {
39
40
  };
40
41
  }
41
42
  const files = await walkFiles(publicDir);
42
- const staticRouteFiles = resolveStaticRouteFiles(files);
43
+ const staticRouteFiles = resolveStaticRouteFiles(files, htmlBasePath);
43
44
  const selectedRoutes = selectStaticRoutes(staticRouteFiles, input.options.cache.staticRoutes, basePath);
44
45
  const configuredOfflineRoute = input.options.offline;
45
46
  const offlineRoute = configuredOfflineRoute
46
47
  ? withBasePath(configuredOfflineRoute, basePath)
47
48
  : false;
48
49
  if (offlineRoute && configuredOfflineRoute) {
49
- const emittedOfflineFile = staticRouteFiles[configuredOfflineRoute] ?? staticRouteFiles[offlineRoute];
50
+ const emittedOfflineFile = staticRouteFiles[configuredOfflineRoute];
50
51
  if (!emittedOfflineFile) {
51
52
  throw new Error(`[farm:pwa] Offline route ${JSON.stringify(input.options.offline)} was not emitted as a static page. ` +
52
53
  "Create that page as a static Farm route so it can be precached.");
@@ -56,7 +57,8 @@ export async function writePwaBuildArtifacts(input) {
56
57
  const assetFiles = files.filter((file) => toPublicUrl(file, basePath) !== withBasePath("/sw.js", basePath) &&
57
58
  PRECACHE_EXTENSIONS.has(path.extname(file).toLowerCase()));
58
59
  const precacheFiles = [...new Set([...assetFiles, ...Object.values(selectedRoutes)])].sort();
59
- const precacheUrls = precacheFiles.map((file) => toPublicUrl(file, basePath));
60
+ const htmlFiles = new Set(Object.values(selectedRoutes));
61
+ const precacheUrls = precacheFiles.map((file) => toPublicUrl(file, basePath, htmlFiles.has(file) ? htmlBasePath : basePath));
60
62
  const fileHashes = await Promise.all(precacheFiles.map(async (file) => {
61
63
  const content = await readFile(path.join(publicDir, file));
62
64
  return `${file}:${createHash("sha256").update(content).digest("hex")}`;
@@ -64,6 +66,7 @@ export async function writePwaBuildArtifacts(input) {
64
66
  const cacheId = createHash("sha256")
65
67
  .update(JSON.stringify({
66
68
  files: fileHashes,
69
+ urls: precacheUrls,
67
70
  routes: selectedRoutes,
68
71
  offlineRoute,
69
72
  update: input.options.update,
@@ -74,6 +77,7 @@ export async function writePwaBuildArtifacts(input) {
74
77
  await mkdir(path.dirname(workerPath), { recursive: true });
75
78
  await writeFile(workerPath, generateServiceWorker({
76
79
  basePath,
80
+ htmlBasePath,
77
81
  cacheId,
78
82
  precacheUrls,
79
83
  staticRoutes: selectedRoutes,
@@ -92,11 +96,11 @@ export async function writePwaBuildArtifacts(input) {
92
96
  }
93
97
  export function generateServiceWorker(options) {
94
98
  const routeFiles = Object.fromEntries(Object.entries(options.staticRoutes).map(([route, file]) => [
95
- encodePathname(route),
96
- toPublicUrl(file, options.basePath),
99
+ encodeRoutePathname(route),
100
+ toPublicUrl(file, options.basePath, options.htmlBasePath ?? "/"),
97
101
  ]));
98
102
  const offlineFile = options.offlineRoute
99
- ? (routeFiles[encodePathname(options.offlineRoute)] ?? null)
103
+ ? (routeFiles[encodeRoutePathname(options.offlineRoute)] ?? null)
100
104
  : null;
101
105
  const cacheScope = createHash("sha256")
102
106
  .update(normalizeBasePath(options.basePath))
@@ -187,8 +191,15 @@ function staleWhileRevalidateImage(request) {
187
191
  const response = (async () => {
188
192
  if (request.headers.has("authorization")) return fetch(request);
189
193
 
190
- const cache = await caches.open(IMAGE_CACHE);
191
- const cached = await cache.match(request);
194
+ let cache;
195
+ let cached;
196
+ try {
197
+ cache = await caches.open(IMAGE_CACHE);
198
+ cached = await cache.match(request);
199
+ } catch {
200
+ // Runtime caching is optional when browser storage is unavailable.
201
+ return fetch(request);
202
+ }
192
203
  const cachedAt = Number(cached?.headers.get("x-farm-pwa-cached-at") || 0);
193
204
  const fresh = cached && Date.now() - cachedAt <= IMAGE_OPTIONS.ttlMs;
194
205
  update = fetchAndCacheImage(request, cache);
@@ -218,22 +229,64 @@ async function fetchAndCacheImage(request, cache) {
218
229
  const response = await fetch(request);
219
230
  if (!isPublicCacheableImage(response)) return response;
220
231
 
221
- const headers = new Headers(response.headers);
222
- headers.set("x-farm-pwa-cached-at", String(Date.now()));
223
- const stored = new Response(response.clone().body, {
224
- status: response.status,
225
- statusText: response.statusText,
226
- headers,
227
- });
228
- await cache.put(request, stored);
229
- await trimImageCache(cache, IMAGE_OPTIONS.limit);
232
+ let stored;
233
+ try {
234
+ const headers = new Headers(response.headers);
235
+ headers.set("x-farm-pwa-cached-at", String(Date.now()));
236
+ stored = new Response(response.clone().body, {
237
+ status: response.status,
238
+ statusText: response.statusText,
239
+ headers,
240
+ });
241
+ await cache.put(request, stored);
242
+ await trimImageCache(cache, IMAGE_OPTIONS.limit);
243
+ } catch {
244
+ // Quota and storage failures must not discard a successful network response.
245
+ // Do not wait for tee cancellation, which can depend on the response consumer.
246
+ if (stored?.body) void stored.body.cancel().catch(() => undefined);
247
+ }
230
248
  return response;
231
249
  }
232
250
 
251
+ // Cache Storage is shared by every account that signs in on this browser, and a
252
+ // service worker cannot see the request's cookies to tell those accounts apart.
253
+ // So an image is kept only when the server marked it shareable or it looks like a
254
+ // static file, the same responses the browser's own HTTP cache would reuse.
233
255
  function isPublicCacheableImage(response) {
234
256
  if (!response.ok || response.type === "opaque") return false;
235
- const policy = (response.headers.get("cache-control") || "").toLowerCase();
236
- return !policy.includes("private") && !policy.includes("no-store") && !policy.includes("no-cache");
257
+ const directives = parseCacheControl(response.headers.get("cache-control"));
258
+ if (directives.has("private") || directives.has("no-store") || directives.has("no-cache")) {
259
+ return false;
260
+ }
261
+ const vary = (response.headers.get("vary") || "")
262
+ .toLowerCase()
263
+ .split(",")
264
+ .map((value) => value.trim());
265
+ // A response that varies on the caller's identity cannot be keyed here.
266
+ if (vary.includes("*") || vary.includes("cookie") || vary.includes("authorization")) {
267
+ return false;
268
+ }
269
+ if (directives.has("max-age")) return Number(directives.get("max-age")) > 0;
270
+ if (directives.has("public")) return true;
271
+ const expires = Date.parse(response.headers.get("expires") || "");
272
+ if (Number.isFinite(expires)) return expires > Date.now();
273
+ // Heuristic freshness (RFC 9111 section 4.2.2) needs a Last-Modified validator.
274
+ // Static files carry one. A dynamic response with no caching headers at all, such
275
+ // as an API route returning the signed-in account's avatar, does not, and keeping
276
+ // it would show that image to whoever uses this browser next.
277
+ return response.headers.has("last-modified");
278
+ }
279
+
280
+ function parseCacheControl(value) {
281
+ const directives = new Map();
282
+ for (const part of (value || "").toLowerCase().split(",")) {
283
+ const separator = part.indexOf("=");
284
+ const name = (separator === -1 ? part : part.slice(0, separator)).trim();
285
+ if (!name) continue;
286
+ const raw = separator === -1 ? "" : part.slice(separator + 1).trim();
287
+ directives.set(name, raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw);
288
+ }
289
+ return directives;
237
290
  }
238
291
 
239
292
  async function trimImageCache(cache, limit) {
@@ -310,9 +363,6 @@ export function withBasePath(route, basePath) {
310
363
  const normalizedRoute = route === "/" ? "/" : `/${route.replace(/^\/+|\/+$/g, "")}`;
311
364
  if (normalizedBase === "/")
312
365
  return normalizedRoute;
313
- if (normalizedRoute === normalizedBase || normalizedRoute.startsWith(`${normalizedBase}/`)) {
314
- return normalizedRoute;
315
- }
316
366
  return normalizedRoute === "/" ? normalizedBase : `${normalizedBase}${normalizedRoute}`;
317
367
  }
318
368
  async function walkFiles(root) {
@@ -326,7 +376,7 @@ async function walkFiles(root) {
326
376
  }));
327
377
  return files.flat().sort();
328
378
  }
329
- function resolveStaticRouteFiles(files) {
379
+ function resolveStaticRouteFiles(files, htmlBasePath) {
330
380
  const routes = {};
331
381
  for (const file of files) {
332
382
  if (path.extname(file).toLowerCase() !== ".html")
@@ -336,7 +386,7 @@ function resolveStaticRouteFiles(files) {
336
386
  const route = withoutIndex
337
387
  ? `/${withoutIndex.replace(/\.html$/, "").replace(/^\/+|\/+$/g, "")}`
338
388
  : "/";
339
- routes[route] = normalized;
389
+ routes[stripBasePath(route, htmlBasePath)] = normalized;
340
390
  }
341
391
  return routes;
342
392
  }
@@ -349,7 +399,7 @@ function selectStaticRoutes(emitted, configured, basePath) {
349
399
  const selected = {};
350
400
  for (const requested of configured) {
351
401
  const route = withBasePath(requested, basePath);
352
- const emittedFile = emitted[requested] ?? emitted[route];
402
+ const emittedFile = emitted[requested];
353
403
  if (!emittedFile) {
354
404
  throw new Error(`[farm:pwa] Static route ${JSON.stringify(requested)} was not found in the production output.`);
355
405
  }
@@ -357,17 +407,39 @@ function selectStaticRoutes(emitted, configured, basePath) {
357
407
  }
358
408
  return selected;
359
409
  }
360
- function toPublicUrl(file, basePath) {
361
- const url = `/${file
362
- .replace(/\\/g, "/")
363
- .split("/")
364
- .map((segment) => encodeURIComponent(segment))
365
- .join("/")}`;
366
- return withBasePath(url, basePath);
410
+ function stripBasePath(route, basePath) {
411
+ const base = normalizeBasePath(basePath);
412
+ if (base === "/")
413
+ return route;
414
+ if (route === base)
415
+ return "/";
416
+ return route.startsWith(`${base}/`) ? route.slice(base.length) : route;
417
+ }
418
+ function toPublicUrl(file, basePath, fileBasePath = basePath) {
419
+ // Generated assets can already live under basePath. HTML uses the explicit
420
+ // SSG output prefix instead: a folder named like basePath can be a real route.
421
+ const route = stripBasePath(`/${file.replace(/\\/g, "/")}`, fileBasePath);
422
+ return withBasePath(encodePathname(route), basePath);
423
+ }
424
+ // Encode a route path the way browsers encode `location.pathname` so the
425
+ // STATIC_ROUTES keys match the pathname a live navigation produces at runtime.
426
+ // encodeURIComponent (used by toPublicUrl for asset URLs) is wrong for the keys:
427
+ // it percent-encodes ordinary path characters such as @ , = + ; $ that browsers
428
+ // leave untouched, so a route like /@alice would be keyed as /%40alice and never
429
+ // match a navigation to /@alice, silently falling through to the offline page.
430
+ function encodeRoutePathname(pathname) {
431
+ return new URL(pathname, "http://farm.localhost").pathname;
367
432
  }
368
433
  function encodePathname(pathname) {
434
+ // Match how a browser encodes location.pathname. encodeURIComponent is safe
435
+ // for every character but over-encodes the path sub-delimiters and ":"/"@"
436
+ // that browsers leave literal, so a precached asset like /logo@2x.css would
437
+ // be stored (and its PRECACHE_PATHS key derived) as /logo%402x.css and never
438
+ // match the live request for /logo@2x.css. Restore that path-safe set while
439
+ // leaving "?", "#", spaces and non-ASCII percent-encoded, the same divergence
440
+ // encodeRoutePathname documents for static-route keys.
369
441
  return pathname
370
442
  .split("/")
371
- .map((segment) => encodeURIComponent(segment))
443
+ .map((segment) => encodeURIComponent(segment).replace(/%(24|26|2B|2C|3A|3B|3D|40)/gi, (match) => decodeURIComponent(match)))
372
444
  .join("/");
373
445
  }
package/dist/index.d.ts CHANGED
@@ -26,5 +26,5 @@ export declare function pwa(options?: PwaPluginOptions): import("@farm.js/core/p
26
26
  scope: string;
27
27
  update: "prompt" | "auto";
28
28
  workerType: "classic" | "module";
29
- }, unknown, false>;
29
+ }, unknown, false, import("@farm.js/core/plugin").PluginRoutes>;
30
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,GACxB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,yBAAyB,CAAC;IACxC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,yBAAyB,CAAC;IACxC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,mDAAmD;QACnD,YAAY,CAAC,EAAE,qBAAqB,CAAC;KACtC;CACF;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,OAAO,GAAE,gBAAqB;;;;;;;mBA0IjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,GACxB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,yBAAyB,CAAC;IACxC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,yBAAyB,CAAC;IACxC,WAAW,IAAI,IAAI,CAAC;CACrB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,mDAAmD;QACnD,YAAY,CAAC,EAAE,qBAAqB,CAAC;KACtC;CACF;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,OAAO,GAAE,gBAAqB;;;;;;;gEAgJjD"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export function pwa(options = {}) {
10
10
  const resolved = resolvePwaOptions(options);
11
11
  let configuredRoot = ".";
12
12
  let configuredBasePath = "/";
13
+ let htmlBasePath = "/";
13
14
  let configuredOutputDir = ".farm/.output";
14
15
  const publicConfig = {
15
16
  workerUrl: "/sw.js",
@@ -23,6 +24,10 @@ export function pwa(options = {}) {
23
24
  configuredRoot = config.root ?? ".";
24
25
  const basePath = normalizeBasePath(config.basePath);
25
26
  configuredBasePath = basePath;
27
+ // Only localized SSG output already includes the application's basePath.
28
+ const i18n = config.i18n;
29
+ htmlBasePath =
30
+ i18n && typeof i18n === "object" && "enabled" in i18n && i18n.enabled ? basePath : "/";
26
31
  configuredOutputDir = `${config.root ?? "."}/.farm/.output`;
27
32
  publicConfig.workerUrl = withBasePath("/sw.js", basePath);
28
33
  publicConfig.scope = basePath === "/" ? "/" : `${basePath}/`;
@@ -42,6 +47,7 @@ export function pwa(options = {}) {
42
47
  publicDir: nitroConfig.output?.publicDir,
43
48
  preset: nitroConfig.preset ?? "node-server",
44
49
  basePath: configuredBasePath,
50
+ htmlBasePath,
45
51
  options: resolved,
46
52
  });
47
53
  generated = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/pwa",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Installable, offline-aware Progressive Web Apps for Farm.js",
5
5
  "keywords": [
6
6
  "farm.js",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@farm.js/core": "0.1.0-beta.93"
35
+ "@farm.js/core": "0.1.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.10.5",