@baukit/pwa-web 0.1.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/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/cache-strategy.d.ts +33 -0
- package/dist/cache-strategy.d.ts.map +1 -0
- package/dist/cache-strategy.js +93 -0
- package/dist/cache-strategy.js.map +1 -0
- package/dist/cjs/cache-strategy.js +98 -0
- package/dist/cjs/fetch-handler.js +75 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/package.json +1 -0
- package/dist/fetch-handler.d.ts +19 -0
- package/dist/fetch-handler.d.ts.map +1 -0
- package/dist/fetch-handler.js +73 -0
- package/dist/fetch-handler.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Koss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @baukit/pwa-web
|
|
2
|
+
|
|
3
|
+
`@baukit/pwa-web` decides which caching strategy a PWA service worker should apply to a request.
|
|
4
|
+
The decision is a pure function over the request's method, URL, mode, and destination. Nothing in
|
|
5
|
+
this package reads `self`, `caches`, `clients`, or `location`, so every branch is unit-testable in
|
|
6
|
+
plain Node.
|
|
7
|
+
|
|
8
|
+
Products own the cache names, precache list, install and activate handlers, and offline fallback
|
|
9
|
+
copy. This package owns the classification and the strategy execution around whatever cache the
|
|
10
|
+
product supplies.
|
|
11
|
+
|
|
12
|
+
## Classifying a request
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { CACHE_STRATEGIES, decideCacheStrategy } from '@baukit/pwa-web';
|
|
16
|
+
|
|
17
|
+
decideCacheStrategy(
|
|
18
|
+
{ url: '/api/v1/sync/pull?since=12', method: 'GET' },
|
|
19
|
+
{ appOrigin: 'https://app.example.com', neverCachedPathPrefixes: ['/api/v1/sync'] },
|
|
20
|
+
);
|
|
21
|
+
// 'network-only'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The rules run in this order:
|
|
25
|
+
|
|
26
|
+
1. A path under any `neverCachedPathPrefixes` entry is `network-only`. The list is empty by
|
|
27
|
+
default, so no endpoint is special until a product names one. A sync endpoint belongs here:
|
|
28
|
+
replaying a stale delta corrupts the local database.
|
|
29
|
+
2. Any non-GET method is `network-only`.
|
|
30
|
+
3. A path under any `apiPathPrefixes` entry (default `['/api']`) gets `apiStrategy`, default
|
|
31
|
+
`network-first`.
|
|
32
|
+
4. A navigation (`mode === 'navigate'`) gets `defaultStrategy`, default `network-first`. When
|
|
33
|
+
`navigationFallback` is set, `createFetchHandler` looks up that app-shell path if the selected
|
|
34
|
+
strategy fails and the original request is not cached.
|
|
35
|
+
5. A same-origin request whose destination, file extension, or path prefix marks it static gets
|
|
36
|
+
`staticStrategy`, default `cache-first`.
|
|
37
|
+
6. Everything else gets `defaultStrategy`.
|
|
38
|
+
|
|
39
|
+
Prefix matching is segment-aware: `/api` matches `/api` and `/api/v1/me` but not `/apiary`.
|
|
40
|
+
Extension matching reads the last path segment only, so `/v1.2/plans` is not a static asset.
|
|
41
|
+
Cross-origin assets never reach `cache-first`, because a service worker should not fill its cache
|
|
42
|
+
with another origin's responses.
|
|
43
|
+
|
|
44
|
+
### Options
|
|
45
|
+
|
|
46
|
+
| Option | Default | Purpose |
|
|
47
|
+
| ------------------------- | ------------------------------------------------ | --------------------------------------------------------------- |
|
|
48
|
+
| `appOrigin` | required | Base for relative URLs and the same-origin test |
|
|
49
|
+
| `neverCachedPathPrefixes` | `[]` | Paths that must always hit the network, such as a sync endpoint |
|
|
50
|
+
| `apiPathPrefixes` | `['/api']` | Paths treated as API reads |
|
|
51
|
+
| `navigationFallback` | unset | Cached app-shell path used after an offline navigation miss |
|
|
52
|
+
| `staticPathPrefixes` | `[]` | Extensionless static paths, such as `/_expo/static/` |
|
|
53
|
+
| `staticDestinations` | audio, font, image, script, style, video, worker | `Request.destination` values that mean static |
|
|
54
|
+
| `staticExtensions` | common web asset extensions | File extensions that mean static |
|
|
55
|
+
| `apiStrategy` | `network-first` | Strategy for API reads |
|
|
56
|
+
| `staticStrategy` | `cache-first` | Strategy for static assets |
|
|
57
|
+
| `defaultStrategy` | `network-first` | Strategy for everything else |
|
|
58
|
+
|
|
59
|
+
`createCacheStrategyDecider(options)` binds the options once and returns a function of the
|
|
60
|
+
request, which is what an install-time asset scan wants.
|
|
61
|
+
|
|
62
|
+
`navigationFallback` applies only to same-origin GET requests with `mode: 'navigate'`. API routes
|
|
63
|
+
and paths in `neverCachedPathPrefixes` never use it, even if a caller supplies `mode: 'navigate'`.
|
|
64
|
+
The selected `defaultStrategy` still runs first. The handler checks the fallback path only after
|
|
65
|
+
that strategy cannot return a network response or a cached response for the requested URL. With no
|
|
66
|
+
fallback configured, navigation misses keep the previous behavior and reject with the network
|
|
67
|
+
error. The product must precache the fallback path.
|
|
68
|
+
|
|
69
|
+
## Running the strategy
|
|
70
|
+
|
|
71
|
+
`createFetchHandler` executes the chosen strategy through ports the product provides. It never
|
|
72
|
+
constructs a `Cache` or calls the global `fetch` itself, so the same handler runs under Vitest
|
|
73
|
+
with plain objects.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// public/sw.js
|
|
77
|
+
import { createFetchHandler } from '/pwa-web.js';
|
|
78
|
+
|
|
79
|
+
const STATIC_CACHE = 'app-static-v1';
|
|
80
|
+
const SHELL_CACHE = 'app-shell-v1';
|
|
81
|
+
|
|
82
|
+
const handleFetch = createFetchHandler({
|
|
83
|
+
appOrigin: self.location.origin,
|
|
84
|
+
neverCachedPathPrefixes: ['/api/v1/sync'],
|
|
85
|
+
navigationFallback: '/index.html',
|
|
86
|
+
staticPathPrefixes: ['/_expo/static/'],
|
|
87
|
+
ports: {
|
|
88
|
+
fetch: (request, init) => fetch(request, init),
|
|
89
|
+
matchCache: (request) => caches.match(request),
|
|
90
|
+
putCache: async (strategy, request, response) => {
|
|
91
|
+
const name = strategy === 'cache-first' ? STATIC_CACHE : SHELL_CACHE;
|
|
92
|
+
const cache = await caches.open(name);
|
|
93
|
+
await cache.put(request, response);
|
|
94
|
+
},
|
|
95
|
+
isCacheable: (response) => response.ok && response.type !== 'error',
|
|
96
|
+
cloneResponse: (response) => response.clone(),
|
|
97
|
+
},
|
|
98
|
+
onRevalidateError: () => {
|
|
99
|
+
// A background refresh failure must not surface to the page.
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
self.addEventListener('fetch', (event) => {
|
|
104
|
+
event.respondWith(handleFetch(event.request));
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Behavior per strategy:
|
|
109
|
+
|
|
110
|
+
- `network-only` fetches with `cache: 'no-store'` and never writes to the cache.
|
|
111
|
+
- `cache-first` returns a hit, otherwise fetches and stores a cacheable response.
|
|
112
|
+
- `network-first` fetches and stores a cacheable response, and falls back to the cache when the
|
|
113
|
+
fetch throws. If the request is an eligible navigation and its own cache entry is absent,
|
|
114
|
+
`navigationFallback` supplies one final cache lookup. With nothing cached it rethrows.
|
|
115
|
+
- `stale-while-revalidate` returns the cached response and refreshes in the background. On a
|
|
116
|
+
miss it awaits the network. A background failure goes to `onRevalidateError`.
|
|
117
|
+
|
|
118
|
+
`putCache` receives the strategy that produced the response, which is how the snippet above
|
|
119
|
+
routes static assets and shell responses into different caches.
|
|
120
|
+
|
|
121
|
+
## Build a service worker without a bundler
|
|
122
|
+
|
|
123
|
+
The package exports ESM for `import` and CommonJS for `require`. Build scripts can load either
|
|
124
|
+
format directly:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const { decideCacheStrategy } = require('@baukit/pwa-web');
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A service worker copied to `public/` cannot resolve `node_modules`. The following
|
|
131
|
+
`scripts/build-sw.mjs` reads the installed ESM files, removes their module syntax, and writes one
|
|
132
|
+
classic script. It exposes the package through `globalThis.BaukitPwa`, so a classic worker can use
|
|
133
|
+
`importScripts('/sw-strategy.js')`.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
137
|
+
import { dirname, join } from 'node:path';
|
|
138
|
+
import { fileURLToPath } from 'node:url';
|
|
139
|
+
|
|
140
|
+
const outputPath = process.argv[2] ?? 'public/sw-strategy.js';
|
|
141
|
+
const entryPath = fileURLToPath(import.meta.resolve('@baukit/pwa-web'));
|
|
142
|
+
const distDir = dirname(entryPath);
|
|
143
|
+
|
|
144
|
+
async function readDistFile(name) {
|
|
145
|
+
const source = await readFile(join(distDir, name), 'utf8');
|
|
146
|
+
return source
|
|
147
|
+
.replace(/import\s*\{[^}]+\}\s*from\s*['"]\.\/cache-strategy\.js['"];\s*/s, '')
|
|
148
|
+
.replace(/^export /gm, '')
|
|
149
|
+
.replace(/^\/\/# sourceMappingURL=.*$/gm, '')
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const source = `// Generated by scripts/build-sw.mjs. Do not edit.
|
|
154
|
+
${await readDistFile('cache-strategy.js')}
|
|
155
|
+
${await readDistFile('fetch-handler.js')}
|
|
156
|
+
globalThis.BaukitPwa = Object.freeze({
|
|
157
|
+
CACHE_STRATEGIES,
|
|
158
|
+
createCacheStrategyDecider,
|
|
159
|
+
createFetchHandler,
|
|
160
|
+
decideCacheStrategy,
|
|
161
|
+
});
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
await writeFile(outputPath, source);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Add generation and drift checks to the product's `package.json`:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"scripts": {
|
|
172
|
+
"build:sw": "node scripts/build-sw.mjs public/sw-strategy.js",
|
|
173
|
+
"build:sw:check": "sh -c 'tmp=$(mktemp); trap \"rm -f $tmp\" EXIT; node scripts/build-sw.mjs \"$tmp\" && diff -u public/sw-strategy.js \"$tmp\"'"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Run `build:sw` after updating `@baukit/pwa-web`. Commit the generated file and run
|
|
179
|
+
`build:sw:check` in CI. The diff fails when the installed package and committed worker differ.
|
|
180
|
+
|
|
181
|
+
## What this package does not do
|
|
182
|
+
|
|
183
|
+
It does not register the service worker, precache an app shell, version caches, prompt for
|
|
184
|
+
updates, or supply an offline page. Those depend on the product's build output and copy.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CACHE_STRATEGIES: {
|
|
2
|
+
readonly cacheFirst: "cache-first";
|
|
3
|
+
readonly networkFirst: "network-first";
|
|
4
|
+
readonly networkOnly: "network-only";
|
|
5
|
+
readonly staleWhileRevalidate: "stale-while-revalidate";
|
|
6
|
+
};
|
|
7
|
+
export type CacheStrategy = (typeof CACHE_STRATEGIES)[keyof typeof CACHE_STRATEGIES];
|
|
8
|
+
export interface CacheStrategyRequest {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly method?: string;
|
|
11
|
+
readonly mode?: string;
|
|
12
|
+
readonly destination?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CacheStrategyOptions {
|
|
15
|
+
readonly appOrigin: string;
|
|
16
|
+
readonly apiPathPrefixes?: readonly string[];
|
|
17
|
+
readonly neverCachedPathPrefixes?: readonly string[];
|
|
18
|
+
readonly navigationFallback?: string;
|
|
19
|
+
readonly staticPathPrefixes?: readonly string[];
|
|
20
|
+
readonly staticDestinations?: readonly string[];
|
|
21
|
+
readonly staticExtensions?: readonly string[];
|
|
22
|
+
readonly apiStrategy?: CacheStrategy;
|
|
23
|
+
readonly staticStrategy?: CacheStrategy;
|
|
24
|
+
readonly defaultStrategy?: CacheStrategy;
|
|
25
|
+
}
|
|
26
|
+
export interface CacheStrategyDecision {
|
|
27
|
+
readonly strategy: CacheStrategy;
|
|
28
|
+
readonly navigationFallback?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function decideCacheStrategyDecision(request: CacheStrategyRequest, options: CacheStrategyOptions): CacheStrategyDecision;
|
|
31
|
+
export declare function decideCacheStrategy(request: CacheStrategyRequest, options: CacheStrategyOptions): CacheStrategy;
|
|
32
|
+
export declare function createCacheStrategyDecider(options: CacheStrategyOptions): (request: CacheStrategyRequest) => CacheStrategy;
|
|
33
|
+
//# sourceMappingURL=cache-strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-strategy.d.ts","sourceRoot":"","sources":["../src/cache-strategy.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,OAAO,gBAAgB,CAAC,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AA+DD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,CA4CvB;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAEf;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,oBAAoB,GAC5B,CAAC,OAAO,EAAE,oBAAoB,KAAK,aAAa,CAElD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const CACHE_STRATEGIES = {
|
|
2
|
+
cacheFirst: 'cache-first',
|
|
3
|
+
networkFirst: 'network-first',
|
|
4
|
+
networkOnly: 'network-only',
|
|
5
|
+
staleWhileRevalidate: 'stale-while-revalidate',
|
|
6
|
+
};
|
|
7
|
+
const DEFAULT_API_PATH_PREFIXES = ['/api'];
|
|
8
|
+
const DEFAULT_STATIC_DESTINATIONS = [
|
|
9
|
+
'audio',
|
|
10
|
+
'font',
|
|
11
|
+
'image',
|
|
12
|
+
'script',
|
|
13
|
+
'style',
|
|
14
|
+
'video',
|
|
15
|
+
'worker',
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_STATIC_EXTENSIONS = [
|
|
18
|
+
'avif',
|
|
19
|
+
'css',
|
|
20
|
+
'gif',
|
|
21
|
+
'ico',
|
|
22
|
+
'jpeg',
|
|
23
|
+
'jpg',
|
|
24
|
+
'js',
|
|
25
|
+
'json',
|
|
26
|
+
'mjs',
|
|
27
|
+
'mp3',
|
|
28
|
+
'mp4',
|
|
29
|
+
'ogg',
|
|
30
|
+
'otf',
|
|
31
|
+
'png',
|
|
32
|
+
'svg',
|
|
33
|
+
'ttf',
|
|
34
|
+
'webmanifest',
|
|
35
|
+
'webp',
|
|
36
|
+
'woff',
|
|
37
|
+
'woff2',
|
|
38
|
+
];
|
|
39
|
+
function matchesPathPrefix(pathname, prefix) {
|
|
40
|
+
if (prefix.length === 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
44
|
+
return pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
45
|
+
}
|
|
46
|
+
function matchesAnyPathPrefix(pathname, prefixes) {
|
|
47
|
+
return prefixes.some((prefix) => matchesPathPrefix(pathname, prefix));
|
|
48
|
+
}
|
|
49
|
+
function hasStaticExtension(pathname, extensions) {
|
|
50
|
+
const lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);
|
|
51
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
52
|
+
if (dotIndex <= 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
|
|
56
|
+
return extensions.includes(extension);
|
|
57
|
+
}
|
|
58
|
+
function startsWithAny(pathname, prefixes) {
|
|
59
|
+
return prefixes.some((prefix) => prefix.length > 0 && pathname.startsWith(prefix));
|
|
60
|
+
}
|
|
61
|
+
export function decideCacheStrategyDecision(request, options) {
|
|
62
|
+
const { appOrigin, apiPathPrefixes = DEFAULT_API_PATH_PREFIXES, navigationFallback, neverCachedPathPrefixes = [], staticPathPrefixes = [], staticDestinations = DEFAULT_STATIC_DESTINATIONS, staticExtensions = DEFAULT_STATIC_EXTENSIONS, apiStrategy = CACHE_STRATEGIES.networkFirst, staticStrategy = CACHE_STRATEGIES.cacheFirst, defaultStrategy = CACHE_STRATEGIES.networkFirst, } = options;
|
|
63
|
+
const parsed = new URL(request.url, appOrigin);
|
|
64
|
+
const { pathname } = parsed;
|
|
65
|
+
const sameOrigin = parsed.origin === appOrigin;
|
|
66
|
+
if (matchesAnyPathPrefix(pathname, neverCachedPathPrefixes)) {
|
|
67
|
+
return { strategy: CACHE_STRATEGIES.networkOnly };
|
|
68
|
+
}
|
|
69
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
70
|
+
if (method !== 'GET') {
|
|
71
|
+
return { strategy: CACHE_STRATEGIES.networkOnly };
|
|
72
|
+
}
|
|
73
|
+
if (matchesAnyPathPrefix(pathname, apiPathPrefixes)) {
|
|
74
|
+
return { strategy: apiStrategy };
|
|
75
|
+
}
|
|
76
|
+
if (request.mode === 'navigate') {
|
|
77
|
+
if (navigationFallback !== undefined && sameOrigin) {
|
|
78
|
+
return { strategy: defaultStrategy, navigationFallback };
|
|
79
|
+
}
|
|
80
|
+
return { strategy: defaultStrategy };
|
|
81
|
+
}
|
|
82
|
+
const staticAsset = (request.destination !== undefined && staticDestinations.includes(request.destination)) ||
|
|
83
|
+
hasStaticExtension(pathname, staticExtensions) ||
|
|
84
|
+
startsWithAny(pathname, staticPathPrefixes);
|
|
85
|
+
return { strategy: sameOrigin && staticAsset ? staticStrategy : defaultStrategy };
|
|
86
|
+
}
|
|
87
|
+
export function decideCacheStrategy(request, options) {
|
|
88
|
+
return decideCacheStrategyDecision(request, options).strategy;
|
|
89
|
+
}
|
|
90
|
+
export function createCacheStrategyDecider(options) {
|
|
91
|
+
return (request) => decideCacheStrategy(request, options);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=cache-strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-strategy.js","sourceRoot":"","sources":["../src/cache-strategy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CACHE_STRATEGIES = void 0;
|
|
4
|
+
exports.decideCacheStrategyDecision = decideCacheStrategyDecision;
|
|
5
|
+
exports.decideCacheStrategy = decideCacheStrategy;
|
|
6
|
+
exports.createCacheStrategyDecider = createCacheStrategyDecider;
|
|
7
|
+
exports.CACHE_STRATEGIES = {
|
|
8
|
+
cacheFirst: 'cache-first',
|
|
9
|
+
networkFirst: 'network-first',
|
|
10
|
+
networkOnly: 'network-only',
|
|
11
|
+
staleWhileRevalidate: 'stale-while-revalidate',
|
|
12
|
+
};
|
|
13
|
+
const DEFAULT_API_PATH_PREFIXES = ['/api'];
|
|
14
|
+
const DEFAULT_STATIC_DESTINATIONS = [
|
|
15
|
+
'audio',
|
|
16
|
+
'font',
|
|
17
|
+
'image',
|
|
18
|
+
'script',
|
|
19
|
+
'style',
|
|
20
|
+
'video',
|
|
21
|
+
'worker',
|
|
22
|
+
];
|
|
23
|
+
const DEFAULT_STATIC_EXTENSIONS = [
|
|
24
|
+
'avif',
|
|
25
|
+
'css',
|
|
26
|
+
'gif',
|
|
27
|
+
'ico',
|
|
28
|
+
'jpeg',
|
|
29
|
+
'jpg',
|
|
30
|
+
'js',
|
|
31
|
+
'json',
|
|
32
|
+
'mjs',
|
|
33
|
+
'mp3',
|
|
34
|
+
'mp4',
|
|
35
|
+
'ogg',
|
|
36
|
+
'otf',
|
|
37
|
+
'png',
|
|
38
|
+
'svg',
|
|
39
|
+
'ttf',
|
|
40
|
+
'webmanifest',
|
|
41
|
+
'webp',
|
|
42
|
+
'woff',
|
|
43
|
+
'woff2',
|
|
44
|
+
];
|
|
45
|
+
function matchesPathPrefix(pathname, prefix) {
|
|
46
|
+
if (prefix.length === 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
50
|
+
return pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
51
|
+
}
|
|
52
|
+
function matchesAnyPathPrefix(pathname, prefixes) {
|
|
53
|
+
return prefixes.some((prefix) => matchesPathPrefix(pathname, prefix));
|
|
54
|
+
}
|
|
55
|
+
function hasStaticExtension(pathname, extensions) {
|
|
56
|
+
const lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);
|
|
57
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
58
|
+
if (dotIndex <= 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
|
|
62
|
+
return extensions.includes(extension);
|
|
63
|
+
}
|
|
64
|
+
function startsWithAny(pathname, prefixes) {
|
|
65
|
+
return prefixes.some((prefix) => prefix.length > 0 && pathname.startsWith(prefix));
|
|
66
|
+
}
|
|
67
|
+
function decideCacheStrategyDecision(request, options) {
|
|
68
|
+
const { appOrigin, apiPathPrefixes = DEFAULT_API_PATH_PREFIXES, navigationFallback, neverCachedPathPrefixes = [], staticPathPrefixes = [], staticDestinations = DEFAULT_STATIC_DESTINATIONS, staticExtensions = DEFAULT_STATIC_EXTENSIONS, apiStrategy = exports.CACHE_STRATEGIES.networkFirst, staticStrategy = exports.CACHE_STRATEGIES.cacheFirst, defaultStrategy = exports.CACHE_STRATEGIES.networkFirst, } = options;
|
|
69
|
+
const parsed = new URL(request.url, appOrigin);
|
|
70
|
+
const { pathname } = parsed;
|
|
71
|
+
const sameOrigin = parsed.origin === appOrigin;
|
|
72
|
+
if (matchesAnyPathPrefix(pathname, neverCachedPathPrefixes)) {
|
|
73
|
+
return { strategy: exports.CACHE_STRATEGIES.networkOnly };
|
|
74
|
+
}
|
|
75
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
76
|
+
if (method !== 'GET') {
|
|
77
|
+
return { strategy: exports.CACHE_STRATEGIES.networkOnly };
|
|
78
|
+
}
|
|
79
|
+
if (matchesAnyPathPrefix(pathname, apiPathPrefixes)) {
|
|
80
|
+
return { strategy: apiStrategy };
|
|
81
|
+
}
|
|
82
|
+
if (request.mode === 'navigate') {
|
|
83
|
+
if (navigationFallback !== undefined && sameOrigin) {
|
|
84
|
+
return { strategy: defaultStrategy, navigationFallback };
|
|
85
|
+
}
|
|
86
|
+
return { strategy: defaultStrategy };
|
|
87
|
+
}
|
|
88
|
+
const staticAsset = (request.destination !== undefined && staticDestinations.includes(request.destination)) ||
|
|
89
|
+
hasStaticExtension(pathname, staticExtensions) ||
|
|
90
|
+
startsWithAny(pathname, staticPathPrefixes);
|
|
91
|
+
return { strategy: sameOrigin && staticAsset ? staticStrategy : defaultStrategy };
|
|
92
|
+
}
|
|
93
|
+
function decideCacheStrategy(request, options) {
|
|
94
|
+
return decideCacheStrategyDecision(request, options).strategy;
|
|
95
|
+
}
|
|
96
|
+
function createCacheStrategyDecider(options) {
|
|
97
|
+
return (request) => decideCacheStrategy(request, options);
|
|
98
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFetchHandler = createFetchHandler;
|
|
4
|
+
const cache_strategy_js_1 = require("./cache-strategy.js");
|
|
5
|
+
async function cacheFirst(request, ports) {
|
|
6
|
+
const cached = await ports.matchCache(request);
|
|
7
|
+
if (cached !== undefined) {
|
|
8
|
+
return cached;
|
|
9
|
+
}
|
|
10
|
+
const response = await ports.fetch(request);
|
|
11
|
+
if (ports.isCacheable(response)) {
|
|
12
|
+
await ports.putCache(cache_strategy_js_1.CACHE_STRATEGIES.cacheFirst, request, ports.cloneResponse(response));
|
|
13
|
+
}
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
async function networkFirst(request, ports) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await ports.fetch(request);
|
|
19
|
+
if (ports.isCacheable(response)) {
|
|
20
|
+
await ports.putCache(cache_strategy_js_1.CACHE_STRATEGIES.networkFirst, request, ports.cloneResponse(response));
|
|
21
|
+
}
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const cached = await ports.matchCache(request);
|
|
26
|
+
if (cached !== undefined) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function staleWhileRevalidate(request, ports, onRevalidateError) {
|
|
33
|
+
const cached = await ports.matchCache(request);
|
|
34
|
+
const revalidate = (async () => {
|
|
35
|
+
const response = await ports.fetch(request);
|
|
36
|
+
if (ports.isCacheable(response)) {
|
|
37
|
+
await ports.putCache(cache_strategy_js_1.CACHE_STRATEGIES.staleWhileRevalidate, request, ports.cloneResponse(response));
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
})();
|
|
41
|
+
if (cached === undefined) {
|
|
42
|
+
return revalidate;
|
|
43
|
+
}
|
|
44
|
+
revalidate.catch((error) => {
|
|
45
|
+
onRevalidateError?.(error);
|
|
46
|
+
});
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
function createFetchHandler(options) {
|
|
50
|
+
const { ports, onRevalidateError, ...strategyOptions } = options;
|
|
51
|
+
return async (request) => {
|
|
52
|
+
const { strategy, navigationFallback } = (0, cache_strategy_js_1.decideCacheStrategyDecision)(request, strategyOptions);
|
|
53
|
+
try {
|
|
54
|
+
switch (strategy) {
|
|
55
|
+
case cache_strategy_js_1.CACHE_STRATEGIES.networkOnly:
|
|
56
|
+
return await ports.fetch(request, { cache: 'no-store' });
|
|
57
|
+
case cache_strategy_js_1.CACHE_STRATEGIES.cacheFirst:
|
|
58
|
+
return await cacheFirst(request, ports);
|
|
59
|
+
case cache_strategy_js_1.CACHE_STRATEGIES.staleWhileRevalidate:
|
|
60
|
+
return await staleWhileRevalidate(request, ports, onRevalidateError);
|
|
61
|
+
case cache_strategy_js_1.CACHE_STRATEGIES.networkFirst:
|
|
62
|
+
return await networkFirst(request, ports);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (navigationFallback !== undefined) {
|
|
67
|
+
const fallback = await ports.matchCache(navigationFallback);
|
|
68
|
+
if (fallback !== undefined) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFetchHandler = exports.decideCacheStrategy = exports.createCacheStrategyDecider = exports.CACHE_STRATEGIES = void 0;
|
|
4
|
+
var cache_strategy_js_1 = require("./cache-strategy.js");
|
|
5
|
+
Object.defineProperty(exports, "CACHE_STRATEGIES", { enumerable: true, get: function () { return cache_strategy_js_1.CACHE_STRATEGIES; } });
|
|
6
|
+
Object.defineProperty(exports, "createCacheStrategyDecider", { enumerable: true, get: function () { return cache_strategy_js_1.createCacheStrategyDecider; } });
|
|
7
|
+
Object.defineProperty(exports, "decideCacheStrategy", { enumerable: true, get: function () { return cache_strategy_js_1.decideCacheStrategy; } });
|
|
8
|
+
var fetch_handler_js_1 = require("./fetch-handler.js");
|
|
9
|
+
Object.defineProperty(exports, "createFetchHandler", { enumerable: true, get: function () { return fetch_handler_js_1.createFetchHandler; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CacheStrategy, type CacheStrategyOptions, type CacheStrategyRequest } from './cache-strategy.js';
|
|
2
|
+
export interface FetchLikeRequest extends CacheStrategyRequest {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
}
|
|
5
|
+
export interface FetchHandlerPorts<TRequest extends FetchLikeRequest, TResponse> {
|
|
6
|
+
readonly fetch: (request: TRequest, init?: {
|
|
7
|
+
readonly cache?: 'no-store';
|
|
8
|
+
}) => Promise<TResponse>;
|
|
9
|
+
readonly matchCache: (request: TRequest | string) => Promise<TResponse | undefined>;
|
|
10
|
+
readonly putCache: (strategy: CacheStrategy, request: TRequest, response: TResponse) => Promise<void>;
|
|
11
|
+
readonly isCacheable: (response: TResponse) => boolean;
|
|
12
|
+
readonly cloneResponse: (response: TResponse) => TResponse;
|
|
13
|
+
}
|
|
14
|
+
export interface FetchHandlerOptions<TRequest extends FetchLikeRequest, TResponse> extends CacheStrategyOptions {
|
|
15
|
+
readonly ports: FetchHandlerPorts<TRequest, TResponse>;
|
|
16
|
+
readonly onRevalidateError?: (error: unknown) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createFetchHandler<TRequest extends FetchLikeRequest, TResponse>(options: FetchHandlerOptions<TRequest, TResponse>): (request: TRequest) => Promise<TResponse>;
|
|
19
|
+
//# sourceMappingURL=fetch-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-handler.d.ts","sourceRoot":"","sources":["../src/fetch-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB;IAC5D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB,CAAC,QAAQ,SAAS,gBAAgB,EAAE,SAAS;IAC7E,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IAClG,QAAQ,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IACpF,QAAQ,CAAC,QAAQ,EAAE,CACjB,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,SAAS,KAChB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC;IACvD,QAAQ,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS,CAAC;CAC5D;AAED,MAAM,WAAW,mBAAmB,CAClC,QAAQ,SAAS,gBAAgB,EACjC,SAAS,CACT,SAAQ,oBAAoB;IAC5B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACvD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACvD;AAiED,wBAAgB,kBAAkB,CAAC,QAAQ,SAAS,gBAAgB,EAAE,SAAS,EAC7E,OAAO,EAAE,mBAAmB,CAAC,QAAQ,EAAE,SAAS,CAAC,GAChD,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,CAAC,CA0B3C"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CACHE_STRATEGIES, decideCacheStrategyDecision, } from './cache-strategy.js';
|
|
2
|
+
async function cacheFirst(request, ports) {
|
|
3
|
+
const cached = await ports.matchCache(request);
|
|
4
|
+
if (cached !== undefined) {
|
|
5
|
+
return cached;
|
|
6
|
+
}
|
|
7
|
+
const response = await ports.fetch(request);
|
|
8
|
+
if (ports.isCacheable(response)) {
|
|
9
|
+
await ports.putCache(CACHE_STRATEGIES.cacheFirst, request, ports.cloneResponse(response));
|
|
10
|
+
}
|
|
11
|
+
return response;
|
|
12
|
+
}
|
|
13
|
+
async function networkFirst(request, ports) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await ports.fetch(request);
|
|
16
|
+
if (ports.isCacheable(response)) {
|
|
17
|
+
await ports.putCache(CACHE_STRATEGIES.networkFirst, request, ports.cloneResponse(response));
|
|
18
|
+
}
|
|
19
|
+
return response;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const cached = await ports.matchCache(request);
|
|
23
|
+
if (cached !== undefined) {
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function staleWhileRevalidate(request, ports, onRevalidateError) {
|
|
30
|
+
const cached = await ports.matchCache(request);
|
|
31
|
+
const revalidate = (async () => {
|
|
32
|
+
const response = await ports.fetch(request);
|
|
33
|
+
if (ports.isCacheable(response)) {
|
|
34
|
+
await ports.putCache(CACHE_STRATEGIES.staleWhileRevalidate, request, ports.cloneResponse(response));
|
|
35
|
+
}
|
|
36
|
+
return response;
|
|
37
|
+
})();
|
|
38
|
+
if (cached === undefined) {
|
|
39
|
+
return revalidate;
|
|
40
|
+
}
|
|
41
|
+
revalidate.catch((error) => {
|
|
42
|
+
onRevalidateError?.(error);
|
|
43
|
+
});
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
export function createFetchHandler(options) {
|
|
47
|
+
const { ports, onRevalidateError, ...strategyOptions } = options;
|
|
48
|
+
return async (request) => {
|
|
49
|
+
const { strategy, navigationFallback } = decideCacheStrategyDecision(request, strategyOptions);
|
|
50
|
+
try {
|
|
51
|
+
switch (strategy) {
|
|
52
|
+
case CACHE_STRATEGIES.networkOnly:
|
|
53
|
+
return await ports.fetch(request, { cache: 'no-store' });
|
|
54
|
+
case CACHE_STRATEGIES.cacheFirst:
|
|
55
|
+
return await cacheFirst(request, ports);
|
|
56
|
+
case CACHE_STRATEGIES.staleWhileRevalidate:
|
|
57
|
+
return await staleWhileRevalidate(request, ports, onRevalidateError);
|
|
58
|
+
case CACHE_STRATEGIES.networkFirst:
|
|
59
|
+
return await networkFirst(request, ports);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (navigationFallback !== undefined) {
|
|
64
|
+
const fallback = await ports.matchCache(navigationFallback);
|
|
65
|
+
if (fallback !== undefined) {
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=fetch-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-handler.js","sourceRoot":"","sources":["../src/fetch-handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { CACHE_STRATEGIES, createCacheStrategyDecider, decideCacheStrategy, type CacheStrategy, type CacheStrategyOptions, type CacheStrategyRequest, } from './cache-strategy.js';
|
|
2
|
+
export { createFetchHandler, type FetchHandlerOptions, type FetchHandlerPorts, type FetchLikeRequest, } from './fetch-handler.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,0BAA0B,EAC1B,mBAAmB,GAIpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,GAInB,MAAM,oBAAoB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baukit/pwa-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pure-function caching-strategy decisions for PWA service workers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Patrick Koss",
|
|
7
|
+
"homepage": "https://github.com/PatrickKoss/baukit#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/PatrickKoss/baukit.git",
|
|
11
|
+
"directory": "typescript/packages/pwa-web"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/PatrickKoss/baukit/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24.0.0"
|
|
22
|
+
},
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"require": "./dist/cjs/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vitest": "4.1.10"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node --input-type=module -e \"import { writeFileSync } from 'node:fs'; writeFileSync('dist/cjs/package.json', '{\\\"type\\\":\\\"commonjs\\\"}\\\\n')\"",
|
|
42
|
+
"test": "vitest run --config ../../vitest.config.ts",
|
|
43
|
+
"lint": "eslint src --max-warnings=0",
|
|
44
|
+
"format:check": "prettier --check src package.json tsconfig.json tsconfig.cjs.json"
|
|
45
|
+
}
|
|
46
|
+
}
|