vanilla-jet 1.4.3 → 1.5.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.
- package/CHANGELOG.md +61 -0
- package/bin.js +25 -0
- package/framework/dipper.js +31 -0
- package/framework/router.js +36 -1
- package/framework/server.js +19 -6
- package/framework/sw.template.js +82 -0
- package/gulpfile.js +19 -4
- package/master.md +450 -0
- package/package.json +2 -3
- package/scripts/compile_html.js +14 -6
- package/scripts/generate_sw.js +182 -0
- package/test/config.test.js +47 -0
- package/test/dipper.test.js +76 -0
- package/test/helpers.js +66 -0
- package/test/router.test.js +58 -0
- package/test/server.test.js +103 -0
- package/test/service-worker.test.js +118 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,67 @@ All notable project changes are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format follows a structure inspired by Keep a Changelog and semantic versioning.
|
|
6
6
|
|
|
7
|
+
## [1.5.1] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Service worker precache is now fully derived from `vanillaJet.package.json`.** `scripts/generate_sw.js`
|
|
12
|
+
reads the Dipper's full local registry (`coreDependencies` + `dependencies` + `styles` that resolve to
|
|
13
|
+
`/public/...`) instead of only the enqueued subset, so first-party assets like `coreLib/*` are precached
|
|
14
|
+
automatically. Consumers no longer need to hardcode a `service_worker.precache` list.
|
|
15
|
+
- Added `service_worker.precache_exclude` (opt-out by path) for declared assets you don't want cached.
|
|
16
|
+
`service_worker.precache` remains as an optional escape hatch for extras not declared in the package file.
|
|
17
|
+
|
|
18
|
+
### Compatibility notes
|
|
19
|
+
|
|
20
|
+
- Backward compatible: an explicit `service_worker.precache` still works (merged with the derived set).
|
|
21
|
+
|
|
22
|
+
## [1.5.0] - 2026-06-27
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Service worker support (opt-in)** — `settings.profile.enable_service_worker`.
|
|
27
|
+
- `framework/sw.template.js`: generic cache-first worker (precache + on-demand prefixes).
|
|
28
|
+
- `scripts/generate_sw.js` + Gulp task `generateServiceWorker`: generates `public/sw.js` at build time.
|
|
29
|
+
- Precache list = core bundles (`app.min.css`, `vanilla.min.js`, `core/vanillaJet.min.js`) + LOCAL resources enqueued by the Dipper + `service_worker.precache` extras (existing files only).
|
|
30
|
+
- Cache name is content-pinned (md5 of `path:size-mtime`), so any asset change rotates the cache; `activate()` purges stale caches.
|
|
31
|
+
- Matches use `{ ignoreSearch: true }` to stay compatible with fingerprinted (`?v=`) asset URLs.
|
|
32
|
+
- `framework/router.js`: serves `/sw.js` from root scope with `Service-Worker-Allowed: /` and `Cache-Control: no-cache` when enabled.
|
|
33
|
+
- `framework/dipper.js`: `includeServiceWorker()` inline registration helper (web-only; honors `window.__VJ_DISABLE_SW__` to opt out and tear down inside native WebViews).
|
|
34
|
+
- Config knobs: `service_worker.precache`, `service_worker.on_demand_prefixes`, `service_worker.cache_prefix`.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Build-time environment injection regression (backward compatibility):** `scripts/compile_html.js`
|
|
39
|
+
again resolves the build environment (passed by Gulp via `--env`, forwarded as argv) and reads
|
|
40
|
+
`settings[env]`, restoring correct `api_url`/`environment` injection (`includeEnvironment()`). The 1.4.x
|
|
41
|
+
rewrite read a literal `settings['profile']` and also rendered the page content as a template name,
|
|
42
|
+
producing `API_URL="undefined"` and `template not found` for 1.3.x-shaped configs. `gulpfile.js` now
|
|
43
|
+
forwards `--env` to both `compile_html.js` and `generate_sw.js`.
|
|
44
|
+
- **Broken dependency removed:** dropped `zlib@1.0.5` from `dependencies`. The framework uses Node's
|
|
45
|
+
built-in `zlib` (core modules take precedence), and the npm package has a native `node-waf` build step
|
|
46
|
+
that fails on modern Node — it broke `npm install`/`npm ci` for consumers. Pure dead weight.
|
|
47
|
+
- **CLI build commands regression (backward compatibility):** `bin.js` again handles
|
|
48
|
+
`build:qa`, `build:staging` and `build:prod` (they map to `gulp build --env <env>`). 1.3.x consumers
|
|
49
|
+
call `npx vanilla-jet build:<env>`; the 1.4.x CLI had dropped these, so the build silently did nothing.
|
|
50
|
+
- **Profile resolution regression (backward compatibility):** `framework/server.js` now resolves
|
|
51
|
+
`settings[options.profile] || settings['profile']`. Legacy consumers (1.3.x) that key settings by the
|
|
52
|
+
active profile name (e.g. `qa`, `production`) again receive their profile options instead of `{}`.
|
|
53
|
+
- **PaaS port binding:** server now honors `process.env.PORT` (Cloud Run / Heroku) before `settings.profile.port`.
|
|
54
|
+
- **Ephemeral port (`port: 0`) preserved:** port selection uses a nullish check instead of `|| 8080`, so a
|
|
55
|
+
deliberate `0` binds an ephemeral port instead of falling back to `8080`.
|
|
56
|
+
|
|
57
|
+
### Testing
|
|
58
|
+
|
|
59
|
+
- Added a real test harness (`test/`, `node --test`, no new deps) wired to `npm test`:
|
|
60
|
+
router, dipper, config-shape resolution, static serving (`200`/`304`/`404`), and service worker
|
|
61
|
+
generation + serving.
|
|
62
|
+
|
|
63
|
+
### Compatibility notes
|
|
64
|
+
|
|
65
|
+
- Service worker is **off by default**; existing apps are unaffected until they opt in.
|
|
66
|
+
- Profile-resolution fix is backward compatible with both the legacy and the nested config shapes.
|
|
67
|
+
|
|
7
68
|
## [1.4.3] - 2026-02-19
|
|
8
69
|
|
|
9
70
|
### Removed
|
package/bin.js
CHANGED
|
@@ -28,4 +28,29 @@ switch (args[0]) {
|
|
|
28
28
|
console.error('Error executing gulp:', error.message);
|
|
29
29
|
}
|
|
30
30
|
break;
|
|
31
|
+
|
|
32
|
+
// Environment-specific builds (restored for 1.3.x consumer compatibility).
|
|
33
|
+
case 'build:qa':
|
|
34
|
+
try {
|
|
35
|
+
execSync('npx gulp build --env qa', { stdio: 'inherit', cwd: __dirname });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error executing gulp:', error.message);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case 'build:staging':
|
|
42
|
+
try {
|
|
43
|
+
execSync('npx gulp build --env staging', { stdio: 'inherit', cwd: __dirname });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error executing gulp:', error.message);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'build:prod':
|
|
50
|
+
try {
|
|
51
|
+
execSync('npx gulp build --env production', { stdio: 'inherit', cwd: __dirname });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error executing gulp:', error.message);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
31
56
|
}
|
package/framework/dipper.js
CHANGED
|
@@ -385,6 +385,37 @@ Dipper.prototype.includeManifest = function() {
|
|
|
385
385
|
return tagString;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Inline registration for the VanillaJet service worker.
|
|
390
|
+
* Returns '' unless settings.profile.enable_service_worker is true.
|
|
391
|
+
* Web-only by design: a consumer running inside a native WebView can set
|
|
392
|
+
* `window.__VJ_DISABLE_SW__ = true` before this runs to opt out and tear down
|
|
393
|
+
* any previously installed worker (WebViews get no benefit and stuck SWs are
|
|
394
|
+
* hard to recover there).
|
|
395
|
+
*/
|
|
396
|
+
Dipper.prototype.includeServiceWorker = function() {
|
|
397
|
+
|
|
398
|
+
const obj = this;
|
|
399
|
+
if (!obj.options || !obj.options.enable_service_worker) {
|
|
400
|
+
return '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `
|
|
404
|
+
<script>
|
|
405
|
+
(function () {
|
|
406
|
+
if (!('serviceWorker' in navigator)) { return; }
|
|
407
|
+
if (window.__VJ_DISABLE_SW__) {
|
|
408
|
+
navigator.serviceWorker.getRegistration()
|
|
409
|
+
.then(function (registration) { if (registration) { registration.unregister(); } })
|
|
410
|
+
.catch(function (error) { console.error(error); });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
navigator.serviceWorker.register('/sw.js')
|
|
414
|
+
.catch(function (error) { console.error(error); });
|
|
415
|
+
})();
|
|
416
|
+
</script>`;
|
|
417
|
+
}
|
|
418
|
+
|
|
388
419
|
/**
|
|
389
420
|
* Setup Sentry for error reporting on production and qa environments.
|
|
390
421
|
*/
|
package/framework/router.js
CHANGED
|
@@ -41,6 +41,7 @@ class Router {
|
|
|
41
41
|
this.compressionMimes = [ 'css', 'js' ];
|
|
42
42
|
this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
|
|
43
43
|
this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
|
|
44
|
+
this.enableServiceWorker = Boolean(server?.options?.enable_service_worker);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
routeToRegExp(route) {
|
|
@@ -99,10 +100,15 @@ class Router {
|
|
|
99
100
|
}
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
// -- Service worker: served from root scope so it can control the whole origin
|
|
104
|
+
if (!handled && !isMatch && obj.enableServiceWorker && request.path === '/sw.js') {
|
|
105
|
+
return obj.serveServiceWorker(res);
|
|
106
|
+
}
|
|
107
|
+
|
|
102
108
|
// -- Check static files
|
|
103
109
|
if (!handled && !isMatch) {
|
|
104
110
|
|
|
105
|
-
let ext = path.extname(request.path).replace('.', ''),
|
|
111
|
+
let ext = path.extname(request.path).replace('.', ''),
|
|
106
112
|
extHandled = false,
|
|
107
113
|
extHeader = {};
|
|
108
114
|
|
|
@@ -369,6 +375,35 @@ class Router {
|
|
|
369
375
|
return this.defaultRoute;
|
|
370
376
|
}
|
|
371
377
|
|
|
378
|
+
serveServiceWorker(res) {
|
|
379
|
+
let obj = this;
|
|
380
|
+
let filename = path.join(obj.staticBasePath, 'public', 'sw.js');
|
|
381
|
+
fs.stat(filename, (err, stats) => {
|
|
382
|
+
if (err || !stats.isFile()) {
|
|
383
|
+
res.writeHead(404);
|
|
384
|
+
return res.end();
|
|
385
|
+
}
|
|
386
|
+
res.writeHead(200, {
|
|
387
|
+
'Content-Type': 'text/javascript; charset=utf-8',
|
|
388
|
+
// Allow the SW (served at /sw.js) to control the entire origin.
|
|
389
|
+
'Service-Worker-Allowed': '/',
|
|
390
|
+
// Keep the SW script itself fresh so updates roll out promptly.
|
|
391
|
+
'Cache-Control': 'no-cache'
|
|
392
|
+
});
|
|
393
|
+
let stream = fs.createReadStream(filename);
|
|
394
|
+
stream.on('error', () => {
|
|
395
|
+
res.writeHead(500);
|
|
396
|
+
res.end('Server Error');
|
|
397
|
+
});
|
|
398
|
+
res.on('close', () => {
|
|
399
|
+
if (!res.writableEnded) {
|
|
400
|
+
stream.destroy();
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
stream.pipe(res);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
372
407
|
onNotFound(response) {
|
|
373
408
|
response.setStatus(404);
|
|
374
409
|
response.respond();
|
package/framework/server.js
CHANGED
|
@@ -23,16 +23,20 @@ class Server {
|
|
|
23
23
|
|
|
24
24
|
init(options, endpoints) {
|
|
25
25
|
|
|
26
|
-
let obj = this,
|
|
27
|
-
settings = options.settings,
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
let obj = this,
|
|
27
|
+
settings = options.settings,
|
|
28
|
+
// Resolve the active profile. Legacy consumers key settings by the active
|
|
29
|
+
// profile name (settings[options.profile], e.g. 'qa'); newer ones expose a
|
|
30
|
+
// single nested 'profile' object. Support both for backward compatibility.
|
|
31
|
+
opts = settings[options.profile] || settings['profile'] || {},
|
|
32
|
+
shared = settings['shared'] || {},
|
|
30
33
|
security = settings['security'] || {};
|
|
31
34
|
|
|
32
35
|
_.defaults(opts, {
|
|
33
36
|
https_server: false,
|
|
34
37
|
wsServer: false,
|
|
35
38
|
enable_precompressed_negotiation: false,
|
|
39
|
+
enable_service_worker: false,
|
|
36
40
|
request_timeout_ms: 30000,
|
|
37
41
|
headers_timeout_ms: 35000,
|
|
38
42
|
keep_alive_timeout_ms: 5000
|
|
@@ -70,8 +74,11 @@ class Server {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
start() {
|
|
77
|
+
let boundPort = (this.httpx && this.httpx.address && this.httpx.address())
|
|
78
|
+
? this.httpx.address().port
|
|
79
|
+
: (process.env.PORT || this.options.port || 8080);
|
|
73
80
|
this.log('__________________ VanillaJet Server started ___________________');
|
|
74
|
-
this.log(` > Running on 0.0.0.0:${
|
|
81
|
+
this.log(` > Running on 0.0.0.0:${boundPort}/ < `);
|
|
75
82
|
this.log('------------------------------------------------------------------');
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -97,7 +104,13 @@ class Server {
|
|
|
97
104
|
obj.httpx.requestTimeout = Number(obj.options.request_timeout_ms) || 30000;
|
|
98
105
|
obj.httpx.headersTimeout = Number(obj.options.headers_timeout_ms) || 35000;
|
|
99
106
|
obj.httpx.keepAliveTimeout = Number(obj.options.keep_alive_timeout_ms) || 5000;
|
|
100
|
-
|
|
107
|
+
// Honor process.env.PORT first so PaaS runtimes (Cloud Run, Heroku, …) that
|
|
108
|
+
// inject the listening port at runtime work without config changes. Use a
|
|
109
|
+
// nullish check so a deliberate port 0 (ephemeral) is preserved.
|
|
110
|
+
let port = (process.env.PORT !== undefined && process.env.PORT !== '')
|
|
111
|
+
? process.env.PORT
|
|
112
|
+
: (obj.options.port !== undefined && obj.options.port !== null ? obj.options.port : 8080);
|
|
113
|
+
obj.httpx.listen(port);
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
log(value) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VanillaJet service worker template.
|
|
3
|
+
*
|
|
4
|
+
* This file is NOT used as-is. The build (scripts/generate_sw.js) replaces the
|
|
5
|
+
* __PLACEHOLDERS__ below with values derived from the compiled assets and the
|
|
6
|
+
* consumer config, and writes the result to public/sw.js.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: cache-first for a pinned set of local bundles/styles/plugins and an
|
|
9
|
+
* on-demand cache for prefixes (animations, images). The cache name is pinned to
|
|
10
|
+
* a content hash of the precached assets, so a rebuild that changes any asset
|
|
11
|
+
* produces a new cache and activate() purges the stale ones. Because VanillaJet
|
|
12
|
+
* fingerprints asset URLs (`?v=size-mtime`), matches use { ignoreSearch: true }
|
|
13
|
+
* so a cache entry keeps serving across version query changes within the cache.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CACHE_NAME = '__CACHE_NAME__';
|
|
17
|
+
const CACHE_PREFIX = '__CACHE_PREFIX__';
|
|
18
|
+
const PRECACHE_ASSETS = __PRECACHE_ASSETS__;
|
|
19
|
+
const ON_DEMAND_PREFIXES = __ON_DEMAND_PREFIXES__;
|
|
20
|
+
|
|
21
|
+
const MATCH_OPTIONS = { ignoreSearch: true };
|
|
22
|
+
|
|
23
|
+
function isCacheable(pathname) {
|
|
24
|
+
return (
|
|
25
|
+
PRECACHE_ASSETS.includes(pathname) ||
|
|
26
|
+
ON_DEMAND_PREFIXES.some((prefix) => pathname.startsWith(prefix))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
globalThis.addEventListener('install', (event) => {
|
|
31
|
+
event.waitUntil(
|
|
32
|
+
caches.open(CACHE_NAME).then((cache) =>
|
|
33
|
+
Promise.allSettled(PRECACHE_ASSETS.map((asset) => cache.add(asset))).then((results) => {
|
|
34
|
+
results.forEach((result, i) => {
|
|
35
|
+
if (result.status === 'rejected') {
|
|
36
|
+
console.error('SW precache failed: ' + PRECACHE_ASSETS[i], result.reason);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
event.waitUntil(globalThis.skipWaiting());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
globalThis.addEventListener('activate', (event) => {
|
|
46
|
+
event.waitUntil(
|
|
47
|
+
caches.keys().then((keys) =>
|
|
48
|
+
Promise.all(
|
|
49
|
+
keys
|
|
50
|
+
.filter((k) => k.startsWith(CACHE_PREFIX) && k !== CACHE_NAME)
|
|
51
|
+
.map((k) => caches.delete(k))
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
event.waitUntil(globalThis.clients.claim());
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
globalThis.addEventListener('fetch', (event) => {
|
|
59
|
+
const { request } = event;
|
|
60
|
+
if (request.method !== 'GET') return;
|
|
61
|
+
|
|
62
|
+
const url = new URL(request.url);
|
|
63
|
+
if (url.origin !== globalThis.location.origin || !isCacheable(url.pathname)) return;
|
|
64
|
+
|
|
65
|
+
// Cache-first: assets are immutable within a version (CACHE_NAME is content-pinned
|
|
66
|
+
// and activate() purges old caches on bump), so a cache hit never needs
|
|
67
|
+
// revalidation — skip the network entirely to save bandwidth on slow links.
|
|
68
|
+
event.respondWith(
|
|
69
|
+
caches.open(CACHE_NAME).then((cache) =>
|
|
70
|
+
cache.match(request, MATCH_OPTIONS).then((cached) => {
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
|
|
73
|
+
return fetch(request).then((response) => {
|
|
74
|
+
// waitUntil keeps the SW alive until the write finishes, so the asset
|
|
75
|
+
// is actually cached even if the SW is terminated right after.
|
|
76
|
+
if (response.ok) event.waitUntil(cache.put(request, response.clone()));
|
|
77
|
+
return response;
|
|
78
|
+
});
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
});
|
package/gulpfile.js
CHANGED
|
@@ -24,6 +24,11 @@ function getCwd() {
|
|
|
24
24
|
const base = getCwd();
|
|
25
25
|
const cssOrigin = `${getCwd()}/assets/styles/less/admin.less`;
|
|
26
26
|
|
|
27
|
+
// Build environment (passed via `gulp build --env <env>`). Forwarded to the
|
|
28
|
+
// HTML/SW generators so they resolve the matching profile (api_url, etc).
|
|
29
|
+
const argv = minimist(process.argv.slice(2));
|
|
30
|
+
const buildEnv = argv.env || 'development';
|
|
31
|
+
|
|
27
32
|
// Clean tasks
|
|
28
33
|
function cleanBuildJS() {
|
|
29
34
|
return del([`${getCwd()}/public/scripts/vanilla.min.js`], { force: true });
|
|
@@ -113,7 +118,13 @@ function compressCss() {
|
|
|
113
118
|
// Template compilation
|
|
114
119
|
function compileTemplates() {
|
|
115
120
|
return gulp.src('.')
|
|
116
|
-
.pipe(shell([`node scripts/compile_html.js`]));
|
|
121
|
+
.pipe(shell([`node scripts/compile_html.js ${buildEnv}`]));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Service worker generation (opt-in via settings.profile.enable_service_worker)
|
|
125
|
+
function generateServiceWorker() {
|
|
126
|
+
return gulp.src('.')
|
|
127
|
+
.pipe(shell([`node scripts/generate_sw.js ${buildEnv}`]));
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
// Watch task
|
|
@@ -124,7 +135,8 @@ function watchFiles(cb) {
|
|
|
124
135
|
watch([`${base}/assets/styles/less/**/*.less`], gulp.series(
|
|
125
136
|
buildLess,
|
|
126
137
|
compressCss,
|
|
127
|
-
compileTemplates
|
|
138
|
+
compileTemplates,
|
|
139
|
+
generateServiceWorker
|
|
128
140
|
));
|
|
129
141
|
|
|
130
142
|
// Watch HTML files
|
|
@@ -140,7 +152,8 @@ function watchFiles(cb) {
|
|
|
140
152
|
concatJs,
|
|
141
153
|
cleanMinified,
|
|
142
154
|
compressJs,
|
|
143
|
-
compileTemplates
|
|
155
|
+
compileTemplates,
|
|
156
|
+
generateServiceWorker
|
|
144
157
|
));
|
|
145
158
|
|
|
146
159
|
cb();
|
|
@@ -154,7 +167,8 @@ const build = gulp.series(
|
|
|
154
167
|
cleanMinified,
|
|
155
168
|
buildLess,
|
|
156
169
|
compileTemplates,
|
|
157
|
-
gulp.parallel(compressJs, compressCss)
|
|
170
|
+
gulp.parallel(compressJs, compressCss),
|
|
171
|
+
generateServiceWorker
|
|
158
172
|
);
|
|
159
173
|
|
|
160
174
|
const dev = gulp.series(
|
|
@@ -171,6 +185,7 @@ exports.concatJs = concatJs;
|
|
|
171
185
|
exports.compressJs = compressJs;
|
|
172
186
|
exports.compressCss = compressCss;
|
|
173
187
|
exports.compileTemplates = compileTemplates;
|
|
188
|
+
exports.generateServiceWorker = generateServiceWorker;
|
|
174
189
|
exports.build = build;
|
|
175
190
|
exports.dev = dev;
|
|
176
191
|
exports.default = dev;
|