Skip to content

Commit 847f6fd

Browse files
authored
Improvement to service worker handling
Improvement to service worker handling
2 parents 40864d9 + be7531c commit 847f6fd

File tree

4 files changed

+179
-152
lines changed

4 files changed

+179
-152
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,3 @@ jobs:
117117
body,
118118
});
119119
}
120-
121-

test/browser-global-setup.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,11 @@ export default async function setup(project: TestProject) {
7878
}
7979
});
8080

81-
viteServer.stdout?.on('data', (data) =>
82-
logger.info(data.toString().trim()));
83-
viteServer.stderr?.on('data', (data) =>
84-
logger.error(data.toString().trim(), { error: new Error('Vite stderr') }));
81+
viteServer.stdout?.on('data', (data) => logger.info(data.toString().trim()));
82+
viteServer.stderr?.on('data', (data) => logger.error(data.toString().trim(), { error: new Error('Vite stderr') }));
83+
viteServer.on('error', (err) => logger.error('Vite server error', { error: err }));
8584

86-
viteServer.on('error', (err) =>
87-
logger.error('Vite server error', { error: err }));
88-
89-
if (!viteServer.pid) throw new Error('Vite server failed to launch');
85+
if (!viteServer.pid) { throw new Error('Vite server failed to launch'); }
9086
logger.info(`Server running (PID: ${viteServer.pid})`);
9187

9288
await waitOn({
@@ -99,7 +95,6 @@ export default async function setup(project: TestProject) {
9995
fs.writeFile(path.join(tempDir, 'cloudflare-port'), String(cloudflarePort))
10096
]);
10197

102-
// Type-safe context provision
10398
type ProvidedContext = {
10499
vitePort: number;
105100
cloudflarePort: number;
@@ -149,7 +144,6 @@ export default async function setup(project: TestProject) {
149144
}
150145
}
151146

152-
// Augment Vitest types for context provision
153147
declare module 'vitest' {
154148
export interface ProvidedContext {
155149
vitePort: number;

test/browser-tests.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ describe('Asset Hashing Tests', () => {
5656

5757
logger.info(`Starting tests with base URL: ${baseUrl}.`);
5858
browser = await chromium.launch();
59-
6059
logger.info('Browser launched successfully.');
60+
6161
} catch (error) {
6262
logger.error('Failed to set up test environment.', {
6363
error: error instanceof Error ? error : new Error(String(error))
@@ -67,10 +67,26 @@ describe('Asset Hashing Tests', () => {
6767
});
6868

6969
beforeEach(async () => {
70-
if (browser) {
71-
page = await browser.newPage();
72-
logger.info('New page created for test.');
73-
}
70+
if (browser) {
71+
page = await browser.newPage();
72+
await page.evaluate(async () => {
73+
if ('serviceWorker' in navigator) {
74+
const registrations = await navigator.serviceWorker.getRegistrations();
75+
for (const registration of registrations) {
76+
await registration.unregister();
77+
console.log('Unregistered service worker:', registration.scope);
78+
}
79+
return registrations.length;
80+
}
81+
return 0;
82+
}).then(count => {
83+
if (count > 0) {
84+
logger.info(`Unregistered ${count} service worker(s).`);
85+
}
86+
});
87+
88+
logger.info('New page created for test.');
89+
}
7490
});
7591

7692
afterEach(async () => {

vite.config.ts

Lines changed: 154 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,178 @@
11
import { type AssetConfig, PostBuildAssetsProcessorPlugin } from './post-build-assets-processor-plugin.ts';
2-
import { type UserConfig, defineConfig } from 'vite';
2+
import { type UserConfig, type Plugin, defineConfig } from 'vite';
33
import { relative, resolve } from 'node:path';
44
import { cloudflare } from '@cloudflare/vite-plugin';
55
import fs from 'node:fs';
66

7-
87
const projectRoot = resolve(__dirname);
98
const assetConfig: AssetConfig = {
10-
srcDir: 'src',
11-
outputDir: 'dist',
12-
13-
/** Directory where Vite stores assets in <outputDir>. */
14-
assetsSubdir: 'assets',
15-
16-
/** This is the expected site base URL against which
17-
* assets such as meta tags and JSON-LD is tested.
18-
*/
19-
siteBaseUrl: 'https://test.com'
9+
srcDir: 'src',
10+
outputDir: 'dist',
11+
assetsSubdir: 'assets',
12+
siteBaseUrl: 'https://test.com'
2013
};
2114

22-
2315
/**
2416
* Maps URL route paths to their corresponding HTML file metadata.
2517
*/
2618
interface HtmlFileMap {
27-
/**
28-
* @param routePath - The URL path pattern (e.g. "about" for "/about.html")
29-
* @returns Object containing file metadata
30-
*/
31-
[routePath: string]: {
32-
/**
33-
* Filesystem (non absolute) path to the HTML file.
34-
* @example "/project/src/about.html"
35-
*/
36-
filePath: string;
37-
38-
/**
39-
* Normalized route identifier matching the URL pattern.
40-
* @example
41-
* - "index" for "/index.html"
42-
* - "blog/post" for "/blog/post.html"
43-
*/
44-
routeKey: string;
45-
}
19+
[routePath: string]: {
20+
filePath: string;
21+
routeKey: string;
22+
}
4623
}
4724

48-
4925
/**
5026
* Recursively discovers all HTML files in a directory and maps them to route paths.
51-
*
52-
* @param dir - Directory to search (absolute path recommended)
53-
* @param root - Root directory for calculating relative routes
54-
* @returns Mapping of route paths to file information
55-
*
56-
* @example
57-
* const htmlFiles = getHtmlFiles('/project/src', '/project/src');
58-
* // Returns {
59-
* // "about": {
60-
* // filePath: "/project/src/about.html",
61-
* // routeKey: "about"
62-
* // }
63-
* // }
6427
*/
6528
const getHtmlFiles = (dir: string, root: string): HtmlFileMap => {
66-
const files: HtmlFileMap = {};
67-
68-
const traverse = (currentDir: string): void => {
69-
for (const file of fs.readdirSync(currentDir, { withFileTypes: true })) {
70-
const fullPath = resolve(currentDir, file.name);
71-
72-
if (file.isDirectory()) {
73-
traverse(fullPath);
74-
} else if (file.name.endsWith('.html')) {
75-
const relPath = relative(root, fullPath);
76-
const routeKey = relPath.replace(/\.html$/, '');
77-
78-
files[routeKey] = {
79-
filePath: fullPath,
80-
routeKey
81-
};
82-
}
83-
}
84-
};
85-
86-
traverse(dir);
87-
return files;
29+
const files: HtmlFileMap = {};
30+
31+
const traverse = (currentDir: string): void => {
32+
for (const file of fs.readdirSync(currentDir, { withFileTypes: true })) {
33+
const fullPath = resolve(currentDir, file.name);
34+
35+
if (file.isDirectory()) {
36+
traverse(fullPath);
37+
} else if (file.name.endsWith('.html')) {
38+
const relPath = relative(root, fullPath);
39+
const routeKey = relPath.replace(/\.html$/, '');
40+
41+
files[routeKey] = {
42+
filePath: fullPath,
43+
routeKey
44+
};
45+
}
46+
}
47+
};
48+
49+
traverse(dir);
50+
return files;
8851
};
8952

90-
53+
/**
54+
* Create a plugin to serve the service worker in development and test environments.
55+
*/
56+
const ServiceWorkerPlugin = (): Plugin => {
57+
return {
58+
name: 'vite-plugin-service-worker',
59+
apply: 'serve',
60+
configureServer(server) {
61+
const logger = server.config.logger;
62+
server.middlewares.use((req, res, next) => {
63+
if (req.url === '/service-worker.js') {
64+
try {
65+
const swPath = resolve(projectRoot, 'compiled-sw/service-worker.js');
66+
if (fs.existsSync(swPath)) {
67+
logger.info('Serving service-worker.js from compiled-sw directory');
68+
res.setHeader('Content-Type', 'application/javascript');
69+
res.end(fs.readFileSync(swPath, 'utf-8'));
70+
return;
71+
} else {
72+
logger.warn(`Service worker file not found at ${swPath}`);
73+
}
74+
} catch (error) {
75+
logger.error('Error serving service worker:', {
76+
error: error instanceof Error ? error : new Error(String(error))
77+
});
78+
}
79+
}
80+
next();
81+
});
82+
}
83+
};
84+
}
9185

9286
export default defineConfig(({ mode }) => {
93-
const DEFAULT_PORT = 9229;
94-
const NO_CACHE_MAX_AGE = 0;
95-
const isDev = mode === 'development';
96-
const isTest = mode === 'test';
97-
98-
const htmlFiles = getHtmlFiles('src', resolve(projectRoot, 'src'));
99-
100-
//Convert to Rollup input format.
101-
const rollupInput = Object.fromEntries(
102-
Object.entries(htmlFiles).map(([routeKey, { filePath }]) => [routeKey, filePath])
103-
);
104-
105-
106-
const config: UserConfig = {
107-
root: assetConfig.srcDir,
108-
publicDir: 'public',
109-
build: {
110-
assetsInlineLimit: 0,
111-
emptyOutDir: true,
112-
outDir: '../dist',
113-
minify: true,
114-
cssMinify: true,
115-
cssCodeSplit: true,
116-
rollupOptions: {
117-
input: {
118-
...rollupInput,
119-
'service-worker': resolve(projectRoot, 'compiled-sw/service-worker.js')
120-
},
121-
output: {
122-
entryFileNames: (chunkInfo) => {
123-
//While the service-worker.js is optimized, its filename
124-
//should be kept as-is.
125-
return chunkInfo.name === 'service-worker'
126-
? 'service-worker.js'
127-
: `${assetConfig.assetsSubdir}/[name]-[hash].js`;
128-
},
129-
chunkFileNames: `${assetConfig.assetsSubdir}/[name]-[hash].js`,
130-
assetFileNames: `${assetConfig.assetsSubdir}/[name]-[hash][extname]`
131-
}
132-
}
133-
},
134-
css: {
135-
devSourcemap: true
136-
},
137-
plugins: [
138-
cloudflare({
139-
configPath: resolve(__dirname, 'wrangler.jsonc'),
140-
inspectorPort: isTest
141-
? parseInt(process.env.CLOUDFLARE_INSPECTOR_PORT ?? '0')
142-
: DEFAULT_PORT
143-
}),
144-
PostBuildAssetsProcessorPlugin( { assetConfig, projectRoot })
145-
],
146-
...(isDev && {
147-
server: {
148-
fs: { strict: true },
149-
headers: {
150-
'Access-Control-Allow-Origin': '*',
151-
'Cache-Control': `no-store, max-age=${NO_CACHE_MAX_AGE}`
152-
}
153-
}
154-
}),
155-
logLevel: isDev ? 'info' : 'info'
156-
};
157-
158-
return config;
87+
const DEFAULT_PORT = 9229;
88+
const NO_CACHE_MAX_AGE = 0;
89+
const isDev = mode === 'development';
90+
const isTest = mode === 'test';
91+
92+
const htmlFiles = getHtmlFiles('src', resolve(projectRoot, 'src'));
93+
94+
//Convert to Rollup input format.
95+
const rollupInput = Object.fromEntries(
96+
Object.entries(htmlFiles).map(([routeKey, { filePath }]) => [routeKey, filePath])
97+
);
98+
99+
const config: UserConfig = {
100+
root: assetConfig.srcDir,
101+
publicDir: '../public',
102+
build: {
103+
assetsInlineLimit: 0,
104+
emptyOutDir: true,
105+
outDir: '../dist',
106+
minify: true,
107+
cssMinify: true,
108+
cssCodeSplit: true,
109+
rollupOptions: {
110+
input: {
111+
...rollupInput,
112+
'service-worker': resolve(projectRoot, 'compiled-sw/service-worker.js')
113+
},
114+
output: {
115+
entryFileNames: (chunkInfo) => {
116+
//While the service-worker.js is optimized, its filename
117+
//should be kept as-is.
118+
return chunkInfo.name === 'service-worker'
119+
? 'service-worker.js'
120+
: `${assetConfig.assetsSubdir}/[name]-[hash].js`;
121+
},
122+
chunkFileNames: `${assetConfig.assetsSubdir}/[name]-[hash].js`,
123+
assetFileNames: `${assetConfig.assetsSubdir}/[name]-[hash][extname]`
124+
}
125+
}
126+
},
127+
css: {
128+
devSourcemap: true
129+
},
130+
plugins: [
131+
...(isDev || isTest ? [ServiceWorkerPlugin()] : []),
132+
133+
cloudflare({
134+
configPath: resolve(__dirname, 'wrangler.jsonc'),
135+
inspectorPort: isTest
136+
? parseInt(process.env.CLOUDFLARE_INSPECTOR_PORT ?? '0')
137+
: DEFAULT_PORT
138+
}),
139+
PostBuildAssetsProcessorPlugin({ assetConfig, projectRoot })
140+
],
141+
...(isDev || isTest ? {
142+
server: {
143+
fs: { strict: true },
144+
headers: {
145+
'Access-Control-Allow-Origin': '*',
146+
'Cache-Control': `no-store, max-age=${NO_CACHE_MAX_AGE}`,
147+
'Content-Security-Policy': [
148+
"default-src 'none'",
149+
"script-src 'self'",
150+
"style-src 'self'",
151+
"img-src 'self' data:",
152+
"connect-src 'self' ws:",
153+
"require-trusted-types-for 'script'",
154+
"trusted-types default"
155+
].join('; '),
156+
'Cross-Origin-Opener-Policy': 'same-origin',
157+
'Cross-Origin-Embedder-Policy': 'require-corp',
158+
'Permissions-Policy': [
159+
'accelerometer=()',
160+
'camera=()',
161+
'geolocation=()',
162+
'gyroscope=()',
163+
'magnetometer=()',
164+
'microphone=()',
165+
'usb=()',
166+
'fullscreen=(self)'
167+
].join(', '),
168+
'X-Content-Type-Options': 'nosniff',
169+
'X-Frame-Options': 'DENY',
170+
'Referrer-Policy': 'strict-origin-when-cross-origin'
171+
}
172+
}
173+
} : {}),
174+
logLevel: isDev ? 'info' : 'info'
175+
};
176+
177+
return config;
159178
});

0 commit comments

Comments
 (0)