Skip to content

Commit

Permalink
SSR reload detection using snowpack.onFileChange (#83)
Browse files Browse the repository at this point in the history
Co-authored-by: Andreas Ehrencrona <[email protected]>
  • Loading branch information
Rich-Harris and ehrencrona authored Nov 12, 2020
1 parent 8625876 commit b11e9fd
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 53 deletions.
2 changes: 1 addition & 1 deletion packages/app-utils/src/renderer/render/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ function try_serialize(data: any, fail?: (err: Error) => void) {
}

// Ensure we return something truthy so the client will not re-render the page over the error
function serialize_error(error?: Error) {
function serialize_error(error?: Error|null) {
if (!error) return null;
let serialized = try_serialize(error);
if (!serialized) {
Expand Down
15 changes: 9 additions & 6 deletions packages/kit/src/api/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import sirv from 'sirv';
import create_manifest_data from '../../core/create_manifest_data';
import { createServer, Server } from 'http';
import { create_app } from '../../core/create_app';
import snowpack, {SnowpackDevServer} from 'snowpack';
import snowpack, { SnowpackDevServer, SnowpackConfig } from 'snowpack';
import pkg from '../../../package.json';
import loader from './loader';
import { ManifestData, ReadyEvent } from '../../interfaces';
Expand All @@ -31,6 +31,7 @@ class Watcher extends EventEmitter {
cheapwatch: CheapWatch;

snowpack_port: number;
snowpack_config: SnowpackConfig;
snowpack: SnowpackDevServer;
server: Server;

Expand Down Expand Up @@ -84,12 +85,14 @@ class Watcher extends EventEmitter {

async init_snowpack() {
this.snowpack_port = await ports.find(this.opts.port + 1);
this.snowpack_config = snowpack.loadAndValidateConfig({
config: 'snowpack.config.js',
port: this.snowpack_port
}, pkg);

this.snowpack = await snowpack.startDevServer({
cwd: process.cwd(),
config: snowpack.loadAndValidateConfig({
config: 'snowpack.config.js',
port: this.snowpack_port
}, pkg),
config: this.snowpack_config,
lockfile: null,
pkgManifest: pkg
});
Expand All @@ -98,7 +101,7 @@ class Watcher extends EventEmitter {
async init_server() {
const { port } = this.opts;
const { snowpack_port } = this;
const load: Loader = loader(this.snowpack);
const load: Loader = loader(this.snowpack, this.snowpack_config);

const static_handler = sirv('static', {
dev: true
Expand Down
109 changes: 63 additions & 46 deletions packages/kit/src/api/dev/loader.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
import { URL } from 'url';
import { resolve, relative } from 'path';
import * as meriyah from 'meriyah';
import MagicString from 'magic-string';
import { extract_names } from 'periscopic';
import { Loader } from './types';
import { SnowpackDevServer } from 'snowpack';
import { SnowpackConfig, SnowpackDevServer } from 'snowpack';
import { walk } from 'estree-walker';

interface CachedExports {
hash: number;
exports: Promise<any>;
}

// This function makes it possible to load modules from the 'server'
// snowpack server, for the sake of SSR
export default function loader(snowpack: SnowpackDevServer): Loader {
const cache = new Map<string, CachedExports>();
export default function loader(snowpack: SnowpackDevServer, config: SnowpackConfig): Loader {
const cache = new Map<string, Promise<any>>();
const graph = new Map<string, Set<string>>();

const get_module = (importer: string, imported: string, url_stack: string[]) => {
if (imported[0] === '/' || imported[0] === '.') {
const { pathname } = new URL(imported, `http://localhost${importer}`);

if (!graph.has(pathname)) graph.set(pathname, new Set());
graph.get(pathname).add(importer);

return load(pathname, url_stack);
}

return Promise.resolve(load_node(imported));
};

const get_module = (importer: string, imported: string, url_stack: string[]) =>
imported[0] === '/' || imported[0] === '.'
? load(new URL(imported, `http://localhost${importer}`).pathname, url_stack)
: Promise.resolve(load_node(imported));
const invalidate_all = path => {
cache.delete(path);

const dependents = graph.get(path);
graph.delete(path);

if (dependents) dependents.forEach(invalidate_all);
};

const absolute_mount = map_keys(config.mount, resolve);

snowpack.onFileChange(callback => {
for (const abs_path in absolute_mount) {
if (callback.filePath.startsWith(abs_path)) {
const relative_path = relative(abs_path, callback.filePath);
const url = resolve(absolute_mount[abs_path].url, relative_path)

invalidate_all(url);
}
}
});

async function load(url: string, url_stack: string[]) {
// TODO: meriyah (JS parser) doesn't support `import.meta.hot = ...` used in HMR setup code.
Expand All @@ -32,32 +59,22 @@ export default function loader(snowpack: SnowpackDevServer): Loader {
return {}
}

let data: string;

try {
const result = await snowpack.loadUrl(url, {isSSR: true, encoding: 'utf8'});
data = result.contents;
} catch (err) {
throw new Error(`Failed to load ${url}: ${err.message}`);
}

let cached = cache.get(url);
const hash = get_hash(data);

if (!cached || cached.hash !== hash) {
cached = {
hash,
exports: initialize_module(url, data, url_stack.concat(url))
.catch(e => {
cache.delete(url);
throw e;
})
};

cache.set(url, cached);
}
if (cache.has(url)) return cache.get(url);

const exports = snowpack
.loadUrl(url, { isSSR: true, encoding: 'utf8' })
.catch(err => {
throw new Error(`Failed to load ${url}: ${err.message}`);
})
.then(result => initialize_module(url, result.contents, url_stack.concat(url)))
.catch(e => {
cache.delete(url);
console.error(e);
throw e;
});

return cached.exports;
cache.set(url, exports);
return exports;
}

async function initialize_module(url: string, data: string, url_stack: string[]) {
Expand Down Expand Up @@ -217,14 +234,6 @@ export default function loader(snowpack: SnowpackDevServer): Loader {
return url => load(url, []);
}

function get_hash(str: string) {
let hash = 5381;
let i = str.length;

while(i) hash = (hash * 33) ^ str.charCodeAt(--i);
return hash >>> 0;
}

function load_node(source: string) {
// mirror Rollup's interop by allowing both of these:
// import fs from 'fs';
Expand All @@ -235,4 +244,12 @@ function load_node(source: string) {
return mod[prop];
}
});
}
}

function map_keys<V>(object: Record<string, V>, map: (key: string) => string): Record<string, V> {
return Object.entries<V>(object).reduce((new_object, [k, v]) => {
new_object[map(k)] = v;

return new_object;
}, {} as Record<string, V>);
}

0 comments on commit b11e9fd

Please sign in to comment.