Skip to content

Commit

Permalink
serve _wmr.js from index.html (#523)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
JoviDeCroock and marvinhagemeister authored Apr 2, 2021
1 parent c4728db commit 19bb41c
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 97 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-ducks-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'wmr': patch
---

Eagerly load "\_wmr.js" in the "index.html" file to provide graceful error-fallbacks
19 changes: 19 additions & 0 deletions packages/wmr/src/lib/transform-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,22 @@ export async function transformHtml(html, { transformUrl }) {
const result = await transformer.process(html);
return result.html;
}

const transformInjectWmr = async tree => {
tree.walk(node => {
if (node.tag === 'head') {
node.content.unshift('\n\t\t', {
tag: 'script',
attrs: { type: 'module' },
content: ["\n\t\t\timport '/_wmr.js';\n\t\t"]
});
}
return node;
});
};

export async function injectWmr(html) {
const transformer = posthtml([transformInjectWmr]);
const result = await transformer.process(html);
return result.html;
}
18 changes: 13 additions & 5 deletions packages/wmr/src/plugins/wmr/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ const strip = url => url.replace(/\?t=\d+/g, '');
const resolve = url => new URL(url, location.origin).href;
let ws;

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

ws.addEventListener('open', () => {
queue.forEach(sendSocketMessage);
queue = [];
log(`Connected to server.`);
if (needsReload) {
window.location.reload();
} else {
queue.forEach(sendSocketMessage);
queue = [];
}
});

ws.addEventListener('message', handleMessage);
Expand Down Expand Up @@ -79,8 +88,7 @@ function handleMessage(e) {
if (data.kind === 'restart') {
let timeout = setTimeout(() => {
try {
connect();
log(`Connected to server.`);
connect(true);
clearTimeout(timeout);
} catch (err) {}
}, 1000);
Expand Down
31 changes: 30 additions & 1 deletion packages/wmr/src/start.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as kl from 'kolorist';
import { promises as fs } from 'fs';
import { posix, resolve } from 'path';
import server from './server.js';
import wmrMiddleware from './wmr-middleware.js';
import { getServerAddresses, supportsSearchParams } from './lib/net-utils.js';
import { normalizeOptions } from './lib/normalize-options.js';
import { setCwd } from './plugins/npm-plugin/registry.js';
import { formatBootMessage, debug } from './lib/output-utils.js';
import { watch } from './lib/fs-watcher.js';
import { injectWmr } from './lib/transform-html.js';

/**
* @typedef OtherOptions
Expand Down Expand Up @@ -82,7 +85,8 @@ async function bootServer(options, configWatchFiles) {
...options,
onError: sendError,
onChange: sendChanges
})
}),
injectWmrMiddleware(options)
);

// eslint-disable-next-line
Expand Down Expand Up @@ -138,6 +142,31 @@ async function bootServer(options, configWatchFiles) {
};
}

const injectWmrMiddleware = ({ cwd }) => {
return async (req, res, next) => {
try {
// If we haven't intercepted the request it's safe to assume we need to inject wmr.
const path = posix.normalize(req.path);
if (!/\.[a-z]+$/gi.test(path) && !path.startsWith('/@npm')) {
const start = Date.now();
const index = resolve(cwd, 'index.html');
const html = await fs.readFile(index, 'utf-8');
const result = await injectWmr(html);
const time = Date.now() - start;
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf-8',
'Content-Length': Buffer.byteLength(result, 'utf-8'),
'Server-Timing': `index.html;dur=${time}`
});
res.end(result);
}
} catch (e) {
next();
}
next();
};
};

/**
* Close all open connections to a server. Adapted from
* https://github.com/vitejs/vite/blob/352cd397d8c9d2849690e3af0e84b00c6016b987/packages/vite/src/node/server/index.ts#L628
Expand Down
196 changes: 105 additions & 91 deletions packages/wmr/src/wmr-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export default function wmrMiddleware(options) {
const mod = moduleGraph.get(filename);
if (!mod) return false;

if (mod.hasErrored) {
mod.hasErrored = false;
return false;
}

if (mod.acceptingUpdates) {
mod.stale = true;
pendingChanges.add(filename);
Expand Down Expand Up @@ -103,6 +108,7 @@ export default function wmrMiddleware(options) {
pendingChanges.add('/' + filename);
} else if (/\.(mjs|[tj]sx?)$/.test(filename)) {
if (!moduleGraph.has(filename)) {
onChange({ reload: true });
clearTimeout(timeout);
return;
}
Expand Down Expand Up @@ -265,117 +271,125 @@ export const TRANSFORMS = {
// Handle individual JavaScript modules
async js({ id, file, prefix, res, cwd, out, NonRollup, req }) {
let code;
res.setHeader('Content-Type', 'application/javascript;charset=utf-8');
try {
res.setHeader('Content-Type', 'application/javascript;charset=utf-8');

if (WRITE_CACHE.has(id)) {
logJsTransform(`<-- ${kl.cyan(formatPath(id))} [cached]`);
return WRITE_CACHE.get(id);
}
if (WRITE_CACHE.has(id)) {
logJsTransform(`<-- ${kl.cyan(formatPath(id))} [cached]`);
return WRITE_CACHE.get(id);
}

const resolved = await NonRollup.resolveId(id);
const resolvedId = typeof resolved == 'object' ? resolved && resolved.id : resolved;
let result = resolvedId && (await NonRollup.load(resolvedId));
const resolved = await NonRollup.resolveId(id);
const resolvedId = typeof resolved == 'object' ? resolved && resolved.id : resolved;
let result = resolvedId && (await NonRollup.load(resolvedId));

code = typeof result == 'object' ? result && result.code : result;
code = typeof result == 'object' ? result && result.code : result;

if (code == null || code === false) {
if (prefix) file = file.replace(prefix, '');
code = await fs.readFile(resolve(cwd, file), 'utf-8');
}
if (code == null || code === false) {
if (prefix) file = file.replace(prefix, '');
code = await fs.readFile(resolve(cwd, file), 'utf-8');
}

code = await NonRollup.transform(code, id);
code = await NonRollup.transform(code, id);

code = await transformImports(code, id, {
resolveImportMeta(property) {
return NonRollup.resolveImportMeta(property);
},
async resolveId(spec, importer) {
if (spec === 'wmr') return '/_wmr.js';
if (/^(data:|https?:|\/\/)/.test(spec)) {
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
return spec;
}

let graphId = importer.startsWith('/') ? importer.slice(1) : importer;
if (!moduleGraph.has(graphId)) {
moduleGraph.set(graphId, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
}
const mod = moduleGraph.get(graphId);

// const resolved = await NonRollup.resolveId(spec, importer);
let originalSpec = spec;
const resolved = await NonRollup.resolveId(spec, file);
if (resolved) {
spec = typeof resolved == 'object' ? resolved.id : resolved;
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
if (!/^\.?\.?\//.test(spec)) {
spec = './' + spec;
}
code = await transformImports(code, id, {
resolveImportMeta(property) {
return NonRollup.resolveImportMeta(property);
},
async resolveId(spec, importer) {
if (spec === 'wmr') return '/_wmr.js';
if (/^(data:|https?:|\/\/)/.test(spec)) {
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
return spec;
}
let graphId = importer.startsWith('/') ? importer.slice(1) : importer;
if (!moduleGraph.has(graphId)) {
moduleGraph.set(graphId, { dependencies: new Set(), dependents: new Set(), acceptingUpdates: false });
}
if (typeof resolved == 'object' && resolved.external) {
if (/^(data|https?):/.test(spec)) {
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
const mod = moduleGraph.get(graphId);
if (mod.hasErrored) mod.hasErrored = false;

// const resolved = await NonRollup.resolveId(spec, importer);
let originalSpec = spec;
const resolved = await NonRollup.resolveId(spec, file);
if (resolved) {
spec = typeof resolved == 'object' ? resolved.id : resolved;
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
if (!/^\.?\.?\//.test(spec)) {
spec = './' + spec;
}
}
if (typeof resolved == 'object' && resolved.external) {
if (/^(data|https?):/.test(spec)) {
logJsTransform(`${kl.cyan(formatPath(spec))} [external]`);
return spec;
}

spec = relative(cwd, spec).split(sep).join(posix.sep);
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
return spec;
}

spec = relative(cwd, spec).split(sep).join(posix.sep);
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
return spec;
}
}

// \0abc:foo --> /@abcF/foo
spec = spec.replace(/^\0?([a-z-]+):(.+)$/, (s, prefix, spec) => {
// \0abc:/abs/disk/path --> /@abc/cwd-relative-path
if (spec[0] === '/' || spec[0] === sep) {
spec = relative(cwd, spec).split(sep).join(posix.sep);
// \0abc:foo --> /@abcF/foo
spec = spec.replace(/^\0?([a-z-]+):(.+)$/, (s, prefix, spec) => {
// \0abc:/abs/disk/path --> /@abc/cwd-relative-path
if (spec[0] === '/' || spec[0] === sep) {
spec = relative(cwd, spec).split(sep).join(posix.sep);
}
return '/@' + prefix + '/' + spec;
});

// foo.css --> foo.css.js (import of CSS Modules proxy module)
if (spec.match(/\.(css|s[ac]ss)$/)) spec += '.js';

// Bare specifiers are npm packages:
if (!/^\0?\.?\.?[/\\]/.test(spec)) {
const meta = normalizeSpecifier(spec);

// // Option 1: resolve all package verions (note: adds non-trivial delay to imports)
// await resolvePackageVersion(meta);
// // Option 2: omit package versions that resolve to the root
// // if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) {
// // meta.version = '';
// // }
// spec = `/@npm/${meta.module}${meta.version ? '@' + meta.version : ''}${meta.path ? '/' + meta.path : ''}`;

// Option 3: omit root package versions
spec = `/@npm/${meta.module}${meta.path ? '/' + meta.path : ''}`;
}
return '/@' + prefix + '/' + spec;
});

// foo.css --> foo.css.js (import of CSS Modules proxy module)
if (spec.match(/\.(css|s[ac]ss)$/)) spec += '.js';

// Bare specifiers are npm packages:
if (!/^\0?\.?\.?[/\\]/.test(spec)) {
const meta = normalizeSpecifier(spec);

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

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

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

const specModule = moduleGraph.get(modSpec);
specModule.dependents.add(graphId);
if (specModule.stale) {
return spec + `?t=${Date.now()}`;
return spec;
}
});

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

return spec;
return code;
} catch (e) {
const mod = moduleGraph.get(id);
if (mod) {
mod.hasErrored = true;
}
});

writeCacheFile(out, id, code);

return code;
throw e;
}
},
// Handles "CSS Modules" proxy modules (style.module.css.js)
async cssModule({ id, file, cwd, out, res }) {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,11 @@
semver "^7.3.2"
tsutils "^3.17.1"

"@wmr-plugins/directory-import@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@wmr-plugins/directory-import/-/directory-import-0.1.2.tgz#492603b7d31b501f4bcb040b3b8aa33b3aef69fd"
integrity sha512-/7Ox60OLG4tuG5qbHVLgDm7XD/lJ+mNVZAODGXfJ10vlR+FMxGbM17jNd64BSePQuDoU+iGZim2IPXUem7BOGQ==

JSV@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
Expand Down

0 comments on commit 19bb41c

Please sign in to comment.