Skip to content

Commit 19bb41c

Browse files
serve _wmr.js from index.html (#523)
* serve _wmr.js from index.html * Update package.json * fix 200 fallback * Update start.js * undo pkg.json change to demo script * gracefully recover * move try catch * move try up * try with connect in the info * Ensure client does a reload once it reconnects * remove additional log Co-authored-by: Marvin Hagemeister <hello@marvinh.dev>
1 parent c4728db commit 19bb41c

File tree

6 files changed

+177
-97
lines changed

6 files changed

+177
-97
lines changed

.changeset/green-ducks-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'wmr': patch
3+
---
4+
5+
Eagerly load "\_wmr.js" in the "index.html" file to provide graceful error-fallbacks

packages/wmr/src/lib/transform-html.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,22 @@ export async function transformHtml(html, { transformUrl }) {
6060
const result = await transformer.process(html);
6161
return result.html;
6262
}
63+
64+
const transformInjectWmr = async tree => {
65+
tree.walk(node => {
66+
if (node.tag === 'head') {
67+
node.content.unshift('\n\t\t', {
68+
tag: 'script',
69+
attrs: { type: 'module' },
70+
content: ["\n\t\t\timport '/_wmr.js';\n\t\t"]
71+
});
72+
}
73+
return node;
74+
});
75+
};
76+
77+
export async function injectWmr(html) {
78+
const transformer = posthtml([transformInjectWmr]);
79+
const result = await transformer.process(html);
80+
return result.html;
81+
}

packages/wmr/src/plugins/wmr/client.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,24 @@ const strip = url => url.replace(/\?t=\d+/g, '');
77
const resolve = url => new URL(url, location.origin).href;
88
let ws;
99

10-
function connect() {
10+
/**
11+
* @param {boolean} [needsReload] Force page to reload once it's connected
12+
* to the server.
13+
*/
14+
function connect(needsReload) {
1115
ws = new WebSocket(location.origin.replace('http', 'ws') + '/_hmr', 'hmr');
1216
function sendSocketMessage(msg) {
1317
ws.send(JSON.stringify(msg));
1418
}
1519

1620
ws.addEventListener('open', () => {
17-
queue.forEach(sendSocketMessage);
18-
queue = [];
21+
log(`Connected to server.`);
22+
if (needsReload) {
23+
window.location.reload();
24+
} else {
25+
queue.forEach(sendSocketMessage);
26+
queue = [];
27+
}
1928
});
2029

2130
ws.addEventListener('message', handleMessage);
@@ -79,8 +88,7 @@ function handleMessage(e) {
7988
if (data.kind === 'restart') {
8089
let timeout = setTimeout(() => {
8190
try {
82-
connect();
83-
log(`Connected to server.`);
91+
connect(true);
8492
clearTimeout(timeout);
8593
} catch (err) {}
8694
}, 1000);

packages/wmr/src/start.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import * as kl from 'kolorist';
2+
import { promises as fs } from 'fs';
3+
import { posix, resolve } from 'path';
24
import server from './server.js';
35
import wmrMiddleware from './wmr-middleware.js';
46
import { getServerAddresses, supportsSearchParams } from './lib/net-utils.js';
57
import { normalizeOptions } from './lib/normalize-options.js';
68
import { setCwd } from './plugins/npm-plugin/registry.js';
79
import { formatBootMessage, debug } from './lib/output-utils.js';
810
import { watch } from './lib/fs-watcher.js';
11+
import { injectWmr } from './lib/transform-html.js';
912

1013
/**
1114
* @typedef OtherOptions
@@ -82,7 +85,8 @@ async function bootServer(options, configWatchFiles) {
8285
...options,
8386
onError: sendError,
8487
onChange: sendChanges
85-
})
88+
}),
89+
injectWmrMiddleware(options)
8690
);
8791

8892
// eslint-disable-next-line
@@ -138,6 +142,31 @@ async function bootServer(options, configWatchFiles) {
138142
};
139143
}
140144

145+
const injectWmrMiddleware = ({ cwd }) => {
146+
return async (req, res, next) => {
147+
try {
148+
// If we haven't intercepted the request it's safe to assume we need to inject wmr.
149+
const path = posix.normalize(req.path);
150+
if (!/\.[a-z]+$/gi.test(path) && !path.startsWith('/@npm')) {
151+
const start = Date.now();
152+
const index = resolve(cwd, 'index.html');
153+
const html = await fs.readFile(index, 'utf-8');
154+
const result = await injectWmr(html);
155+
const time = Date.now() - start;
156+
res.writeHead(200, {
157+
'Content-Type': 'text/html;charset=utf-8',
158+
'Content-Length': Buffer.byteLength(result, 'utf-8'),
159+
'Server-Timing': `index.html;dur=${time}`
160+
});
161+
res.end(result);
162+
}
163+
} catch (e) {
164+
next();
165+
}
166+
next();
167+
};
168+
};
169+
141170
/**
142171
* Close all open connections to a server. Adapted from
143172
* https://github.com/vitejs/vite/blob/352cd397d8c9d2849690e3af0e84b00c6016b987/packages/vite/src/node/server/index.ts#L628

packages/wmr/src/wmr-middleware.js

Lines changed: 105 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export default function wmrMiddleware(options) {
6969
const mod = moduleGraph.get(filename);
7070
if (!mod) return false;
7171

72+
if (mod.hasErrored) {
73+
mod.hasErrored = false;
74+
return false;
75+
}
76+
7277
if (mod.acceptingUpdates) {
7378
mod.stale = true;
7479
pendingChanges.add(filename);
@@ -103,6 +108,7 @@ export default function wmrMiddleware(options) {
103108
pendingChanges.add('/' + filename);
104109
} else if (/\.(mjs|[tj]sx?)$/.test(filename)) {
105110
if (!moduleGraph.has(filename)) {
111+
onChange({ reload: true });
106112
clearTimeout(timeout);
107113
return;
108114
}
@@ -265,117 +271,125 @@ export const TRANSFORMS = {
265271
// Handle individual JavaScript modules
266272
async js({ id, file, prefix, res, cwd, out, NonRollup, req }) {
267273
let code;
268-
res.setHeader('Content-Type', 'application/javascript;charset=utf-8');
274+
try {
275+
res.setHeader('Content-Type', 'application/javascript;charset=utf-8');
269276

270-
if (WRITE_CACHE.has(id)) {
271-
logJsTransform(`<-- ${kl.cyan(formatPath(id))} [cached]`);
272-
return WRITE_CACHE.get(id);
273-
}
277+
if (WRITE_CACHE.has(id)) {
278+
logJsTransform(`<-- ${kl.cyan(formatPath(id))} [cached]`);
279+
return WRITE_CACHE.get(id);
280+
}
274281

275-
const resolved = await NonRollup.resolveId(id);
276-
const resolvedId = typeof resolved == 'object' ? resolved && resolved.id : resolved;
277-
let result = resolvedId && (await NonRollup.load(resolvedId));
282+
const resolved = await NonRollup.resolveId(id);
283+
const resolvedId = typeof resolved == 'object' ? resolved && resolved.id : resolved;
284+
let result = resolvedId && (await NonRollup.load(resolvedId));
278285

279-
code = typeof result == 'object' ? result && result.code : result;
286+
code = typeof result == 'object' ? result && result.code : result;
280287

281-
if (code == null || code === false) {
282-
if (prefix) file = file.replace(prefix, '');
283-
code = await fs.readFile(resolve(cwd, file), 'utf-8');
284-
}
288+
if (code == null || code === false) {
289+
if (prefix) file = file.replace(prefix, '');
290+
code = await fs.readFile(resolve(cwd, file), 'utf-8');
291+
}
285292

286-
code = await NonRollup.transform(code, id);
293+
code = await NonRollup.transform(code, id);
287294

288-
code = await transformImports(code, id, {
289-
resolveImportMeta(property) {
290-
return NonRollup.resolveImportMeta(property);
291-
},
292-
async resolveId(spec, importer) {
293-
if (spec === 'wmr') return '/_wmr.js';
294-
if (/^(data:|https?:|\/\/)/.test(spec)) {
295-
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
296-
return spec;
297-
}
298-
299-
let graphId = importer.startsWith('/') ? importer.slice(1) : importer;
300-
if (!moduleGraph.has(graphId)) {
301-
moduleGraph.set(graphId, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
302-
}
303-
const mod = moduleGraph.get(graphId);
304-
305-
// const resolved = await NonRollup.resolveId(spec, importer);
306-
let originalSpec = spec;
307-
const resolved = await NonRollup.resolveId(spec, file);
308-
if (resolved) {
309-
spec = typeof resolved == 'object' ? resolved.id : resolved;
310-
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
311-
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
312-
if (!/^\.?\.?\//.test(spec)) {
313-
spec = './' + spec;
314-
}
295+
code = await transformImports(code, id, {
296+
resolveImportMeta(property) {
297+
return NonRollup.resolveImportMeta(property);
298+
},
299+
async resolveId(spec, importer) {
300+
if (spec === 'wmr') return '/_wmr.js';
301+
if (/^(data:|https?:|\/\/)/.test(spec)) {
302+
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
303+
return spec;
304+
}
305+
let graphId = importer.startsWith('/') ? importer.slice(1) : importer;
306+
if (!moduleGraph.has(graphId)) {
307+
moduleGraph.set(graphId, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
315308
}
316-
if (typeof resolved == 'object' && resolved.external) {
317-
if (/^(data|https?):/.test(spec)) {
318-
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
309+
const mod = moduleGraph.get(graphId);
310+
if (mod.hasErrored) mod.hasErrored = false;
311+
312+
// const resolved = await NonRollup.resolveId(spec, importer);
313+
let originalSpec = spec;
314+
const resolved = await NonRollup.resolveId(spec, file);
315+
if (resolved) {
316+
spec = typeof resolved == 'object' ? resolved.id : resolved;
317+
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
318+
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
319+
if (!/^\.?\.?\//.test(spec)) {
320+
spec = './' + spec;
321+
}
322+
}
323+
if (typeof resolved == 'object' && resolved.external) {
324+
if (/^(data|https?):/.test(spec)) {
325+
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
326+
return spec;
327+
}
328+
329+
spec = relative(cwd, spec).split(sep).join(posix.sep);
330+
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
319331
return spec;
320332
}
321-
322-
spec = relative(cwd, spec).split(sep).join(posix.sep);
323-
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
324-
return spec;
325333
}
326-
}
327334

328-
// \0abc:foo --> /@abcF/foo
329-
spec = spec.replace(/^\0?([a-z-]+):(.+)$/, (s, prefix, spec) => {
330-
// \0abc:/abs/disk/path --> /@abc/cwd-relative-path
331-
if (spec[0] === '/' || spec[0] === sep) {
332-
spec = relative(cwd, spec).split(sep).join(posix.sep);
335+
// \0abc:foo --> /@abcF/foo
336+
spec = spec.replace(/^\0?([a-z-]+):(.+)$/, (s, prefix, spec) => {
337+
// \0abc:/abs/disk/path --> /@abc/cwd-relative-path
338+
if (spec[0] === '/' || spec[0] === sep) {
339+
spec = relative(cwd, spec).split(sep).join(posix.sep);
340+
}
341+
return '/@' + prefix + '/' + spec;
342+
});
343+
344+
// foo.css --> foo.css.js (import of CSS Modules proxy module)
345+
if (spec.match(/\.(css|s[ac]ss)$/)) spec += '.js';
346+
347+
// Bare specifiers are npm packages:
348+
if (!/^\0?\.?\.?[/\\]/.test(spec)) {
349+
const meta = normalizeSpecifier(spec);
350+
351+
// // Option 1: resolve all package verions (note: adds non-trivial delay to imports)
352+
// await resolvePackageVersion(meta);
353+
// // Option 2: omit package versions that resolve to the root
354+
// // if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) {
355+
// // meta.version = '';
356+
// // }
357+
// spec = `/@npm/${meta.module}${meta.version ? '@' + meta.version : ''}${meta.path ? '/' + meta.path : ''}`;
358+
359+
// Option 3: omit root package versions
360+
spec = `/@npm/${meta.module}${meta.path ? '/' + meta.path : ''}`;
333361
}
334-
return '/@' + prefix + '/' + spec;
335-
});
336-
337-
// foo.css --> foo.css.js (import of CSS Modules proxy module)
338-
if (spec.match(/\.(css|s[ac]ss)$/)) spec += '.js';
339-
340-
// Bare specifiers are npm packages:
341-
if (!/^\0?\.?\.?[/\\]/.test(spec)) {
342-
const meta = normalizeSpecifier(spec);
343362

344-
// // Option 1: resolve all package verions (note: adds non-trivial delay to imports)
345-
// await resolvePackageVersion(meta);
346-
// // Option 2: omit package versions that resolve to the root
347-
// // if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) {
348-
// // meta.version = '';
349-
// // }
350-
// spec = `/@npm/${meta.module}${meta.version ? '@' + meta.version : ''}${meta.path ? '/' + meta.path : ''}`;
363+
const modSpec = spec.startsWith('../') ? spec.replace(/..\/g/, '') : spec.replace('./', '');
364+
mod.dependencies.add(modSpec);
365+
if (!moduleGraph.has(modSpec)) {
366+
moduleGraph.set(modSpec, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
367+
}
351368

352-
// Option 3: omit root package versions
353-
spec = `/@npm/${meta.module}${meta.path ? '/' + meta.path : ''}`;
354-
}
369+
const specModule = moduleGraph.get(modSpec);
370+
specModule.dependents.add(graphId);
371+
if (specModule.stale) {
372+
return spec + `?t=${Date.now()}`;
373+
}
355374

356-
const modSpec = spec.startsWith('../') ? spec.replace(/..\/g/, '') : spec.replace('./', '');
357-
mod.dependencies.add(modSpec);
358-
if (!moduleGraph.has(modSpec)) {
359-
moduleGraph.set(modSpec, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
360-
}
375+
if (originalSpec !== spec) {
376+
logJsTransform(`${kl.cyan(formatPath(originalSpec))} -> ${kl.dim(formatPath(spec))}`);
377+
}
361378

362-
const specModule = moduleGraph.get(modSpec);
363-
specModule.dependents.add(graphId);
364-
if (specModule.stale) {
365-
return spec + `?t=${Date.now()}`;
379+
return spec;
366380
}
381+
});
367382

368-
if (originalSpec !== spec) {
369-
logJsTransform(`${kl.cyan(formatPath(originalSpec))} -> ${kl.dim(formatPath(spec))}`);
370-
}
383+
writeCacheFile(out, id, code);
371384

372-
return spec;
385+
return code;
386+
} catch (e) {
387+
const mod = moduleGraph.get(id);
388+
if (mod) {
389+
mod.hasErrored = true;
373390
}
374-
});
375-
376-
writeCacheFile(out, id, code);
377-
378-
return code;
391+
throw e;
392+
}
379393
},
380394
// Handles "CSS Modules" proxy modules (style.module.css.js)
381395
async cssModule({ id, file, cwd, out, res }) {

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,11 @@
12341234
semver "^7.3.2"
12351235
tsutils "^3.17.1"
12361236

1237+
"@wmr-plugins/directory-import@*":
1238+
version "0.1.2"
1239+
resolved "https://registry.yarnpkg.com/@wmr-plugins/directory-import/-/directory-import-0.1.2.tgz#492603b7d31b501f4bcb040b3b8aa33b3aef69fd"
1240+
integrity sha512-/7Ox60OLG4tuG5qbHVLgDm7XD/lJ+mNVZAODGXfJ10vlR+FMxGbM17jNd64BSePQuDoU+iGZim2IPXUem7BOGQ==
1241+
12371242
JSV@^4.0.2:
12381243
version "4.0.2"
12391244
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"

0 commit comments

Comments
 (0)