mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
work in progress
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"rules": {
|
||||
"semi": [ 2, "always" ],
|
||||
"space-before-blocks": [ 2, "always" ],
|
||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
||||
"no-cond-assign": 0,
|
||||
"no-unused-vars": 2,
|
||||
"object-shorthand": [ 2, "always" ],
|
||||
"no-const-assign": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-var": 2,
|
||||
"no-unreachable": 2,
|
||||
"valid-typeof": 2,
|
||||
"quote-props": [ 2, "as-needed" ],
|
||||
"one-var": [ 2, "never" ],
|
||||
"prefer-arrow-callback": 2,
|
||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
||||
"arrow-spacing": 2,
|
||||
"no-inner-declarations": 0
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
7011
package-lock.json
generated
7011
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,8 @@
|
||||
"ts-node": "^4.1.0",
|
||||
"tslib": "^1.8.1",
|
||||
"typescript": "^2.6.2",
|
||||
"wait-on": "^2.0.2"
|
||||
"wait-on": "^2.0.2",
|
||||
"wait-port": "^0.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"cy:open": "cypress open",
|
||||
|
||||
@@ -25,7 +25,7 @@ const plugins = [
|
||||
|
||||
export default [
|
||||
{ name: 'cli', banner: true },
|
||||
{ name: 'core', banner: true },
|
||||
{ name: 'core' },
|
||||
{ name: 'middleware' },
|
||||
{ name: 'runtime', format: 'es' },
|
||||
{ name: 'webpack', file: 'webpack/config' }
|
||||
|
||||
@@ -2,14 +2,9 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { create_compilers, create_app, create_assets } from 'sapper/core.js';
|
||||
import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js';
|
||||
|
||||
export default function build({
|
||||
src,
|
||||
dest,
|
||||
dev,
|
||||
entry
|
||||
}: {
|
||||
export default async function build({ src, dest, dev, entry }: {
|
||||
src: string;
|
||||
dest: string;
|
||||
dev: boolean;
|
||||
@@ -18,11 +13,32 @@ export default function build({
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
// create main.js and server-routes.js
|
||||
create_app({ dev, entry, src });
|
||||
const routes = create_routes({ src });
|
||||
|
||||
// create app/manifest/client.js and app/manifest/server.js
|
||||
create_app({ routes, src, dev });
|
||||
|
||||
const { client, server, serviceworker } = create_compilers();
|
||||
|
||||
const client_stats = await compile(client);
|
||||
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_stats.toJson()));
|
||||
|
||||
await compile(server);
|
||||
|
||||
if (serviceworker) {
|
||||
create_serviceworker({
|
||||
routes,
|
||||
client_files: client_stats.toJson().assets.map((chunk: { name: string }) => `/client/${chunk.name}`),
|
||||
src
|
||||
});
|
||||
|
||||
await compile(serviceworker);
|
||||
}
|
||||
}
|
||||
|
||||
function compile(compiler: any) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
function handleErrors(err, stats) {
|
||||
compiler.run((err: Error, stats: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
process.exit(1);
|
||||
@@ -32,29 +48,10 @@ export default function build({
|
||||
console.error(stats.toString({ colors: true }));
|
||||
reject(new Error(`Encountered errors while building app`));
|
||||
}
|
||||
}
|
||||
|
||||
const { client, server } = create_compilers();
|
||||
|
||||
client.run((err, client_stats) => {
|
||||
handleErrors(err, client_stats);
|
||||
const client_info = client_stats.toJson();
|
||||
fs.writeFileSync(
|
||||
path.join(dest, 'stats.client.json'),
|
||||
JSON.stringify(client_info, null, ' ')
|
||||
);
|
||||
|
||||
server.run((err, server_stats) => {
|
||||
handleErrors(err, server_stats);
|
||||
const server_info = server_stats.toJson();
|
||||
fs.writeFileSync(
|
||||
path.join(dest, 'stats.server.json'),
|
||||
JSON.stringify(server_info, null, ' ')
|
||||
);
|
||||
|
||||
create_assets({ src, dest, dev, client_info, server_info });
|
||||
fulfil();
|
||||
});
|
||||
else {
|
||||
fulfil(stats);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as child_process from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as sander from 'sander';
|
||||
import express from 'express';
|
||||
@@ -6,30 +7,20 @@ import fetch from 'node-fetch';
|
||||
import URL from 'url-parse';
|
||||
import { create_assets } from 'sapper/core.js';
|
||||
|
||||
const { PORT = 3000, OUTPUT_DIR = 'dist' } = process.env;
|
||||
|
||||
const origin = `http://localhost:${PORT}`;
|
||||
const { OUTPUT_DIR = 'dist' } = process.env;
|
||||
|
||||
const app = express();
|
||||
|
||||
function read_json(file) {
|
||||
function read_json(file: string) {
|
||||
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
|
||||
}
|
||||
|
||||
export default function exporter({ src, dest }) { // TODO dest is a terrible name in this context
|
||||
export default async function exporter({ src, dest }) { // TODO dest is a terrible name in this context
|
||||
// Prep output directory
|
||||
sander.rimrafSync(OUTPUT_DIR);
|
||||
|
||||
const { service_worker } = create_assets({
|
||||
src, dest,
|
||||
dev: false,
|
||||
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||
});
|
||||
|
||||
sander.copydirSync('assets').to(OUTPUT_DIR);
|
||||
sander.copydirSync(dest, 'client').to(OUTPUT_DIR, 'client');
|
||||
sander.writeFileSync(OUTPUT_DIR, 'service-worker.js', service_worker);
|
||||
|
||||
// Intercept server route fetches
|
||||
function save(res) {
|
||||
@@ -48,9 +39,13 @@ export default function exporter({ src, dest }) { // TODO dest is a terrible nam
|
||||
});
|
||||
}
|
||||
|
||||
const port = await require('get-port')(3000);
|
||||
|
||||
const origin = `http://localhost:${port}`;
|
||||
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') {
|
||||
url = `http://localhost:${PORT}${url}`;
|
||||
url = `http://localhost:${port}${url}`;
|
||||
|
||||
return fetch(url, opts)
|
||||
.then(r => {
|
||||
@@ -62,9 +57,15 @@ export default function exporter({ src, dest }) { // TODO dest is a terrible nam
|
||||
return fetch(url, opts);
|
||||
};
|
||||
|
||||
const middleware = require('./middleware')({ dev: false }); // TODO this is filthy
|
||||
app.use(middleware);
|
||||
const server = app.listen(PORT);
|
||||
const proc = child_process.fork(path.resolve(`${dest}/server.js`), [], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
PORT: port,
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
});
|
||||
|
||||
await require('wait-port')({ port });
|
||||
|
||||
const seen = new Set();
|
||||
|
||||
@@ -99,5 +100,5 @@ export default function exporter({ src, dest }) { // TODO dest is a terrible nam
|
||||
}
|
||||
|
||||
return handle(new URL(origin)) // TODO all static routes
|
||||
.then(() => server.close());
|
||||
.then(() => proc.kill());
|
||||
}
|
||||
@@ -1,86 +1,73 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
import create_routes from './create_routes';
|
||||
import { fudge_mtime, posixify, write } from './utils';
|
||||
import { Route } from '../interfaces';
|
||||
|
||||
function posixify(file: string) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
|
||||
function create_app({ src, dev, entry }: {
|
||||
export default function create_app({ routes, src, dev }: {
|
||||
routes: Route[];
|
||||
src: string;
|
||||
dev: boolean;
|
||||
entry: { client: string; server: string };
|
||||
}) {
|
||||
const routes = create_routes({ src });
|
||||
mkdirp.sync('app/manifest');
|
||||
|
||||
function create_client_main() {
|
||||
const code = `[${routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => {
|
||||
const params =
|
||||
route.dynamic.length === 0
|
||||
write('app/manifest/client.js', generate_client(routes, src, dev));
|
||||
write('app/manifest/server.js', generate_server(routes, src));
|
||||
}
|
||||
|
||||
function generate_client(routes: Route[], src: string, dev: boolean) {
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!\nexport const routes = [
|
||||
${routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => {
|
||||
const params = route.dynamic.length === 0
|
||||
? '{}'
|
||||
: `{ ${route.dynamic
|
||||
.map((part, i) => `${part}: match[${i + 1}]`)
|
||||
.join(', ')} }`;
|
||||
: `{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
return `{ pattern: ${
|
||||
route.pattern
|
||||
}, params: match => (${params}), load: () => import(/* webpackChunkName: "${
|
||||
route.id
|
||||
}" */ '${file}') }`;
|
||||
})
|
||||
.join(', ')}]`;
|
||||
const file = posixify(`../../routes/${route.file}`);
|
||||
return `{ pattern: ${route.pattern}, params: ${route.dynamic.length > 0 ? `match` : `()`} => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`;
|
||||
})
|
||||
.join(',\n\t')}
|
||||
];`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
let main = fs
|
||||
.readFileSync('templates/main.js', 'utf-8')
|
||||
.replace(
|
||||
/__app__/g,
|
||||
posixify(path.resolve(__dirname, '../../runtime/app.js'))
|
||||
)
|
||||
.replace(/__routes__/g, code)
|
||||
.replace(/__dev__/g, String(dev));
|
||||
if (dev) {
|
||||
const hmr_client = posixify(
|
||||
require.resolve(`webpack-hot-middleware/client`)
|
||||
);
|
||||
|
||||
if (dev) {
|
||||
const hmr_client = posixify(
|
||||
require.resolve(`webpack-hot-middleware/client`)
|
||||
);
|
||||
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(entry.client, main);
|
||||
fudge_mtime(entry.client);
|
||||
code += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`;
|
||||
}
|
||||
|
||||
function create_server_routes() {
|
||||
const imports = routes
|
||||
return code;
|
||||
}
|
||||
|
||||
function generate_server(routes: Route[], src: string) {
|
||||
let code = `
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
${routes
|
||||
.map(route => {
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
const file = posixify(`../../routes/${route.file}`);
|
||||
return route.type === 'page'
|
||||
? `import ${route.id} from '${file}';`
|
||||
: `import * as ${route.id} from '${file}';`;
|
||||
})
|
||||
.join('\n');
|
||||
.join('\n')}
|
||||
|
||||
const exports = `export { ${routes.map(route => route.id)} };`;
|
||||
export const routes = [
|
||||
${routes
|
||||
.map(route => {
|
||||
const params = route.dynamic.length === 0
|
||||
? '{}'
|
||||
: `{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ')} }`;
|
||||
|
||||
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
|
||||
fudge_mtime(entry.server);
|
||||
}
|
||||
const file = posixify(`${src}/${route.file}`);
|
||||
return `{ id: '${route.id}', type: '${route.type}', pattern: ${route.pattern}, params: ${route.dynamic.length > 0 ? `match` : `()`} => (${params}), module: ${route.id} }`;
|
||||
})
|
||||
.join(',\n\t')
|
||||
}
|
||||
];`.replace(/^\t\t/gm, '').trim();
|
||||
|
||||
create_client_main();
|
||||
create_server_routes();
|
||||
}
|
||||
|
||||
export default create_app;
|
||||
return code;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import glob from 'glob';
|
||||
import { create_templates, render } from './templates';
|
||||
import create_routes from './create_routes';
|
||||
|
||||
function ensure_array(thing: any) {
|
||||
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
|
||||
}
|
||||
|
||||
type WebpackInfo = {
|
||||
assetsByChunkName: Record<string, string>;
|
||||
assets: Array<{ name: string }>
|
||||
}
|
||||
|
||||
export default function create_assets({ src, dest, dev, client_info, server_info }: {
|
||||
src: string;
|
||||
dest: string;
|
||||
dev: boolean;
|
||||
client_info: WebpackInfo;
|
||||
server_info: WebpackInfo;
|
||||
}) {
|
||||
create_templates(); // TODO refactor this...
|
||||
|
||||
const main_file = `/client/${ensure_array(client_info.assetsByChunkName.main)[0]}`;
|
||||
|
||||
const chunk_files = client_info.assets.map(chunk => `/client/${chunk.name}`);
|
||||
|
||||
const service_worker = generate_service_worker(chunk_files, src);
|
||||
const index = generate_index(main_file);
|
||||
|
||||
const routes = create_routes({ src });
|
||||
|
||||
if (dev) { // TODO move this into calling code
|
||||
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
|
||||
fs.writeFileSync(path.join(dest, 'index.html'), index);
|
||||
}
|
||||
|
||||
return {
|
||||
client: {
|
||||
main_file,
|
||||
chunk_files,
|
||||
|
||||
main: read(`${dest}${main_file}`),
|
||||
chunks: chunk_files.reduce((lookup: Record<string, string>, file) => {
|
||||
lookup[file] = read(`${dest}${file}`);
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
// TODO confusing that `routes` refers to an array *and* a lookup
|
||||
routes: routes.reduce((lookup: Record<string, string>, route) => {
|
||||
lookup[route.id] = `/client/${ensure_array(client_info.assetsByChunkName[route.id])[0]}`;
|
||||
return lookup;
|
||||
}, {}),
|
||||
|
||||
index,
|
||||
service_worker
|
||||
},
|
||||
|
||||
server: {
|
||||
entry: path.resolve(dest, 'server', server_info.assetsByChunkName.main)
|
||||
},
|
||||
|
||||
service_worker
|
||||
};
|
||||
}
|
||||
|
||||
function generate_service_worker(chunk_files: string[], src: string) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
const routes = create_routes({ src });
|
||||
|
||||
const route_code = `[${
|
||||
routes
|
||||
.filter(route => route.type === 'page')
|
||||
.map(route => `{ pattern: ${route.pattern} }`)
|
||||
.join(', ')
|
||||
}]`;
|
||||
|
||||
return read('templates/service-worker.js')
|
||||
.replace(/__timestamp__/g, String(Date.now()))
|
||||
.replace(/__assets__/g, JSON.stringify(assets))
|
||||
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
|
||||
.replace(/__routes__/g, route_code);
|
||||
}
|
||||
|
||||
function generate_index(main_file: string) {
|
||||
return render(200, {
|
||||
styles: '',
|
||||
head: '',
|
||||
html: '<noscript>Please enable JavaScript!</noscript>',
|
||||
main: main_file
|
||||
});
|
||||
}
|
||||
|
||||
function read(file: string) {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import relative from 'require-relative';
|
||||
export default function create_compilers() {
|
||||
const webpack = relative('webpack', process.cwd());
|
||||
|
||||
const serviceworker_config = try_require(path.resolve('webpack/service-worker.config.js'));
|
||||
|
||||
return {
|
||||
client: webpack(
|
||||
require(path.resolve('webpack/client.config.js'))
|
||||
@@ -13,13 +15,11 @@ export default function create_compilers() {
|
||||
require(path.resolve('webpack/server.config.js'))
|
||||
),
|
||||
|
||||
serviceWorker: webpack(
|
||||
tryRequire(path.resolve('webpack/server.config.js'))
|
||||
)
|
||||
serviceworker: serviceworker_config && webpack(serviceworker_config)
|
||||
};
|
||||
}
|
||||
|
||||
function tryRequire(specifier: string) {
|
||||
function try_require(specifier: string) {
|
||||
try {
|
||||
return require(specifier);
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import * as path from 'path';
|
||||
import glob from 'glob';
|
||||
|
||||
type Route = {
|
||||
id: string;
|
||||
type: 'page' | 'route';
|
||||
file: string;
|
||||
pattern: RegExp;
|
||||
test: (url: string) => boolean;
|
||||
exec: (url: string) => Record<string, string>;
|
||||
parts: string[];
|
||||
dynamic: string[];
|
||||
}
|
||||
import { Route } from '../interfaces';
|
||||
|
||||
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }: {
|
||||
src: string;
|
||||
|
||||
26
src/core/create_serviceworker.ts
Normal file
26
src/core/create_serviceworker.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import glob from 'glob';
|
||||
import create_routes from './create_routes';
|
||||
import { fudge_mtime, posixify, write } from './utils';
|
||||
import { Route } from '../interfaces';
|
||||
|
||||
export default function create_serviceworker({ routes, client_files, src }: {
|
||||
routes: Route[];
|
||||
client_files: string[];
|
||||
src: string;
|
||||
}) {
|
||||
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
|
||||
|
||||
let code = `
|
||||
export const timestamp = ${Date.now()};
|
||||
|
||||
export const assets = [\n\t${assets.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const shell = [\n\t${client_files.map((x: string) => `"${x}"`).join(',\n\t')}\n];
|
||||
|
||||
export const routes = [\n\t${routes.filter((r: Route) => r.type === 'page').map((r: Route) => `{ pattern: ${r.pattern} }`).join(',\n\t')}\n];
|
||||
`.replace(/^\t\t/gm, '');
|
||||
|
||||
write('app/manifest/service-worker.js', code);
|
||||
}
|
||||
74
src/core/create_template.ts
Normal file
74
src/core/create_template.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import framer from 'code-frame';
|
||||
import { locate } from 'locate-character';
|
||||
|
||||
function error(e: any) {
|
||||
if (e.title) console.error(chalk.bold.red(e.title));
|
||||
if (e.body) console.error(chalk.red(e.body));
|
||||
if (e.url) console.error(chalk.cyan(e.url));
|
||||
if (e.frame) console.error(chalk.grey(e.frame));
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default function create_templates() {
|
||||
const template = fs.readFileSync(`app/template.html`, 'utf-8');
|
||||
|
||||
const index = template.indexOf('%sapper.main%');
|
||||
if (index !== -1) {
|
||||
// TODO remove this in a future version
|
||||
const { line, column } = locate(template, index, { offsetLine: 1 });
|
||||
const frame = framer(template, line, column);
|
||||
|
||||
error({
|
||||
title: `app/template.html`,
|
||||
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
|
||||
url: 'https://github.com/sveltejs/sapper/issues/86',
|
||||
frame
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: (data: Record<string, string>) => {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
},
|
||||
stream: (res: any, data: Record<string, string | Promise<string>>) => {
|
||||
let i = 0;
|
||||
|
||||
function stream_inner(): Promise<void> {
|
||||
if (i >= template.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = template.indexOf('%sapper', i);
|
||||
|
||||
if (start === -1) {
|
||||
res.end(template.slice(i));
|
||||
return;
|
||||
}
|
||||
|
||||
res.write(template.slice(i, start));
|
||||
|
||||
const end = template.indexOf('%', start + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Bad template`); // TODO validate ahead of time
|
||||
}
|
||||
|
||||
const tag = template.slice(start + 1, end);
|
||||
const match = /sapper\.(\w+)/.exec(tag);
|
||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||
|
||||
return Promise.resolve(data[match[1]]).then(datamatch => {
|
||||
res.write(datamatch);
|
||||
i = end + 1;
|
||||
return stream_inner();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve().then(stream_inner);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { create_templates, render, stream } from './templates'; // TODO templates is an anomaly... fix post-#91
|
||||
|
||||
export { default as create_app } from './create_app';
|
||||
export { default as create_assets } from './create_assets';
|
||||
export { default as create_serviceworker } from './create_serviceworker';
|
||||
export { default as create_compilers } from './create_compilers';
|
||||
export { default as create_routes } from './create_routes';
|
||||
|
||||
export const templates = { create_templates, render, stream };
|
||||
export { default as create_template } from './create_template';
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import glob from 'glob';
|
||||
import chalk from 'chalk';
|
||||
import framer from 'code-frame';
|
||||
import { locate } from 'locate-character';
|
||||
|
||||
let templates;
|
||||
|
||||
function error(e) {
|
||||
if (e.title) console.error(chalk.bold.red(e.title));
|
||||
if (e.body) console.error(chalk.red(e.body));
|
||||
if (e.url) console.error(chalk.cyan(e.url));
|
||||
if (e.frame) console.error(chalk.grey(e.frame));
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function create_templates() {
|
||||
templates = glob.sync('*.html', { cwd: 'templates' })
|
||||
.map(file => {
|
||||
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
|
||||
const status = file.replace('.html', '').toLowerCase();
|
||||
|
||||
if (!/^[0-9x]{3}$/.test(status)) {
|
||||
error({
|
||||
title: `templates/${file}`,
|
||||
body: `Bad template — should be a valid status code like 404.html, or a wildcard like 2xx.html`
|
||||
});
|
||||
}
|
||||
|
||||
const index = template.indexOf('%sapper.main%');
|
||||
if (index !== -1) {
|
||||
// TODO remove this in a future version
|
||||
const { line, column } = locate(template, index, { offsetLine: 1 });
|
||||
const frame = framer(template, line, column);
|
||||
|
||||
error({
|
||||
title: `templates/${file}`,
|
||||
body: `<script src='%sapper.main%'> is unsupported — use %sapper.scripts% (without the <script> tag) instead`,
|
||||
url: 'https://github.com/sveltejs/sapper/issues/86',
|
||||
frame
|
||||
});
|
||||
}
|
||||
|
||||
const specificity = (
|
||||
(status[0] === 'x' ? 0 : 4) +
|
||||
(status[1] === 'x' ? 0 : 2) +
|
||||
(status[2] === 'x' ? 0 : 1)
|
||||
);
|
||||
|
||||
const pattern = new RegExp(`^${status.split('').map(d => d === 'x' ? '\\d' : d).join('')}$`);
|
||||
|
||||
return {
|
||||
test: status => pattern.test(status),
|
||||
specificity,
|
||||
render: data => {
|
||||
return template.replace(/%sapper\.(\w+)%/g, (match, key) => {
|
||||
return key in data ? data[key] : '';
|
||||
});
|
||||
},
|
||||
stream: (res, data) => {
|
||||
let i = 0;
|
||||
|
||||
function stream_inner() {
|
||||
if (i >= template.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = template.indexOf('%sapper', i);
|
||||
|
||||
if (start === -1) {
|
||||
res.end(template.slice(i));
|
||||
return;
|
||||
}
|
||||
|
||||
res.write(template.slice(i, start));
|
||||
|
||||
const end = template.indexOf('%', start + 1);
|
||||
if (end === -1) {
|
||||
throw new Error(`Bad template`); // TODO validate ahead of time
|
||||
}
|
||||
|
||||
const tag = template.slice(start + 1, end);
|
||||
const match = /sapper\.(\w+)/.exec(tag);
|
||||
if (!match || !(match[1] in data)) throw new Error(`Bad template`); // TODO ditto
|
||||
|
||||
return Promise.resolve(data[match[1]]).then(datamatch => {
|
||||
res.write(datamatch);
|
||||
i = end + 1;
|
||||
return stream_inner();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve().then(stream_inner);
|
||||
}
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.specificity - a.specificity);
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
export function render(status, data) {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.render(data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
}
|
||||
|
||||
export function stream(res, status, data) {
|
||||
const template = templates.find(template => template.test(status));
|
||||
if (template) return template.stream(res, data);
|
||||
|
||||
return `Missing template for status code ${status}`;
|
||||
}
|
||||
20
src/core/utils.ts
Normal file
20
src/core/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export function write(file: string, code: string) {
|
||||
fs.writeFileSync(file, code);
|
||||
fudge_mtime(file);
|
||||
}
|
||||
|
||||
export function posixify(file: string) {
|
||||
return file.replace(/[/\\]/g, '/');
|
||||
}
|
||||
|
||||
export function fudge_mtime(file: string) {
|
||||
// need to fudge the mtime so that webpack doesn't go doolally
|
||||
const { atime, mtime } = fs.statSync(file);
|
||||
fs.utimesSync(
|
||||
file,
|
||||
new Date(atime.getTime() - 999999),
|
||||
new Date(mtime.getTime() - 999999)
|
||||
);
|
||||
}
|
||||
15
src/interfaces.ts
Normal file
15
src/interfaces.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type Route = {
|
||||
id: string;
|
||||
type: 'page' | 'route';
|
||||
file: string;
|
||||
pattern: RegExp;
|
||||
test: (url: string) => boolean;
|
||||
exec: (url: string) => Record<string, string>;
|
||||
parts: string[];
|
||||
dynamic: string[];
|
||||
};
|
||||
|
||||
export type Template = {
|
||||
render: (data: Record<string, string>) => string;
|
||||
stream: (res, data: Record<string, string | Promise<string>>) => void;
|
||||
};
|
||||
@@ -1,11 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { create_app, create_assets, create_routes, templates } from 'sapper/core.js';
|
||||
import { create_app, create_serviceworker, create_routes, create_template } from 'sapper/core.js';
|
||||
import { dest } from '../config.js';
|
||||
|
||||
type Deferred = {
|
||||
promise?: Promise<any>;
|
||||
fulfil?: (value: any) => void;
|
||||
reject?: (err: Error) => void;
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
const d = {};
|
||||
const d: Deferred = {};
|
||||
|
||||
d.promise = new Promise((fulfil, reject) => {
|
||||
d.fulfil = fulfil;
|
||||
@@ -15,7 +21,7 @@ function deferred() {
|
||||
return d;
|
||||
}
|
||||
|
||||
export default function create_watcher({ compilers, dev, entry, src, onroutes }) {
|
||||
export default function create_watcher({ compilers, dev, entry, src, onroutes, ontemplate }) {
|
||||
const deferreds = {
|
||||
client: deferred(),
|
||||
server: deferred()
|
||||
@@ -31,27 +37,29 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
const server_info = server_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
|
||||
|
||||
return create_assets({
|
||||
src, dest, dev,
|
||||
client_info: client_stats.toJson(),
|
||||
server_info: server_stats.toJson()
|
||||
const client_files = client_info.assets.map((chunk: { name: string }) => `/client/${chunk.name}`);
|
||||
|
||||
return create_serviceworker({
|
||||
routes: create_routes({ src }),
|
||||
client_files,
|
||||
src
|
||||
});
|
||||
});
|
||||
|
||||
function watch_compiler(type) {
|
||||
function watch_compiler(type: 'client' | 'server') {
|
||||
const compiler = compilers[type];
|
||||
|
||||
compiler.plugin('invalid', filename => {
|
||||
compiler.plugin('invalid', (filename: string) => {
|
||||
console.log(chalk.cyan(`${type} bundle invalidated, file changed: ${chalk.bold(filename)}`));
|
||||
deferreds[type] = deferred();
|
||||
watcher.ready = invalidate();
|
||||
});
|
||||
|
||||
compiler.plugin('failed', err => {
|
||||
compiler.plugin('failed', (err: Error) => {
|
||||
deferreds[type].reject(err);
|
||||
});
|
||||
|
||||
return compiler.watch({}, (err, stats) => {
|
||||
return compiler.watch({}, (err: Error, stats: any) => {
|
||||
if (stats.hasErrors()) {
|
||||
deferreds[type].reject(stats.toJson().errors[0]);
|
||||
} else {
|
||||
@@ -62,7 +70,7 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
function watch_files(pattern, callback) {
|
||||
function watch_files(pattern: string, callback: () => void) {
|
||||
const watcher = chokidar.watch(pattern, {
|
||||
persistent: false
|
||||
});
|
||||
@@ -76,15 +84,13 @@ export default function create_watcher({ compilers, dev, entry, src, onroutes })
|
||||
const routes = create_routes({ src });
|
||||
onroutes(routes);
|
||||
|
||||
create_app({ dev, entry, src }); // TODO this calls `create_routes` again, we should pass `routes` to `create_app` instead
|
||||
create_app({ routes, src, dev });
|
||||
});
|
||||
|
||||
watch_files('templates/main.js', () => {
|
||||
create_app({ dev, entry, src });
|
||||
});
|
||||
watch_files('app/template.html', () => {
|
||||
const template = create_template();
|
||||
ontemplate(template);
|
||||
|
||||
watch_files('templates/**.html', () => {
|
||||
templates.create_templates();
|
||||
// TODO reload current page?
|
||||
});
|
||||
|
||||
@@ -4,117 +4,61 @@ import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import serialize from 'serialize-javascript';
|
||||
import escape_html from 'escape-html';
|
||||
import { create_routes, templates, create_compilers, create_assets } from 'sapper/core.js';
|
||||
import { create_routes, templates, create_compilers, create_template } from 'sapper/core.js';
|
||||
import { dest, entry, isDev, src } from '../config';
|
||||
import create_watcher from './create_watcher';
|
||||
import { Route, Template } from '../interfaces';
|
||||
|
||||
const dev = isDev();
|
||||
|
||||
function connect_dev() {
|
||||
mkdirp.sync(dest);
|
||||
rimraf.sync(path.join(dest, '**/*'));
|
||||
|
||||
const compilers = create_compilers();
|
||||
|
||||
let routes;
|
||||
|
||||
const watcher = create_watcher({
|
||||
dev, entry, src,
|
||||
compilers,
|
||||
onroutes: _ => {
|
||||
routes = _;
|
||||
}
|
||||
});
|
||||
|
||||
let asset_cache;
|
||||
|
||||
const middleware = compose_handlers([
|
||||
require('webpack-hot-middleware')(compilers.client, {
|
||||
reload: true,
|
||||
path: '/__webpack_hmr',
|
||||
heartbeat: 10 * 1000
|
||||
}),
|
||||
|
||||
(req, res, next) => {
|
||||
watcher.ready.then(cache => {
|
||||
asset_cache = cache;
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache, () => routes),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
]);
|
||||
|
||||
middleware.close = () => {
|
||||
watcher.close();
|
||||
// TODO shut down chokidar
|
||||
};
|
||||
|
||||
return middleware;
|
||||
type Assets = {
|
||||
index: string;
|
||||
service_worker: string;
|
||||
client: Record<string, string>;
|
||||
}
|
||||
|
||||
function connect_prod() {
|
||||
const asset_cache = create_assets({
|
||||
src, dest,
|
||||
dev: false,
|
||||
client_info: read_json(path.join(dest, 'stats.client.json')),
|
||||
server_info: read_json(path.join(dest, 'stats.server.json'))
|
||||
});
|
||||
export default function middleware({ routes }: {
|
||||
routes: Route[]
|
||||
}) {
|
||||
const client_info = JSON.parse(fs.readFileSync(path.join(dest, 'client_info.json'), 'utf-8'));
|
||||
|
||||
const routes = create_routes({ src }); // TODO rename update
|
||||
const assets: Assets = {
|
||||
index: try_read(path.join(dest, 'index.html')),
|
||||
service_worker: try_read(path.join(dest, 'service-worker.js')),
|
||||
client: fs.readdirSync(path.join(dest, 'client')).reduce((lookup: Record<string, string>, file: string) => {
|
||||
lookup[file] = try_read(path.join(dest, 'client', file));
|
||||
return lookup;
|
||||
}, {})
|
||||
};
|
||||
|
||||
const template = create_template();
|
||||
|
||||
const middleware = compose_handlers([
|
||||
set_req_pathname,
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/index.html',
|
||||
filter: (pathname: string) => pathname === '/index.html',
|
||||
type: 'text/html',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.index
|
||||
fn: () => assets.index
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname === '/service-worker.js',
|
||||
filter: (pathname: string) => pathname === '/service-worker.js',
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=600',
|
||||
fn: () => asset_cache.client.service_worker
|
||||
fn: () => assets.service_worker
|
||||
}),
|
||||
|
||||
get_asset_handler({
|
||||
filter: pathname => pathname.startsWith('/client/'),
|
||||
filter: (pathname: string) => pathname.startsWith('/client/'),
|
||||
type: 'application/javascript',
|
||||
cache: 'max-age=31536000',
|
||||
fn: pathname => asset_cache.client.chunks[pathname]
|
||||
fn: (pathname: string) => assets.client[pathname.replace('/client/', '')]
|
||||
}),
|
||||
|
||||
get_route_handler(() => asset_cache, () => routes),
|
||||
get_route_handler(client_info.assetsByChunkName, () => assets, () => routes, () => template),
|
||||
|
||||
get_not_found_handler(() => asset_cache)
|
||||
get_not_found_handler(client_info.assetsByChunkName, () => routes, () => template)
|
||||
]);
|
||||
|
||||
// here for API consistency between dev, and prod, but
|
||||
@@ -124,10 +68,6 @@ function connect_prod() {
|
||||
return middleware;
|
||||
}
|
||||
|
||||
export default function connect({ dev: _dev = dev } = {}) {
|
||||
return _dev ? connect_dev() : connect_prod();
|
||||
}
|
||||
|
||||
function set_req_pathname(req, res, next) {
|
||||
req.pathname = req.url.replace(/\?.*/, '');
|
||||
next();
|
||||
@@ -146,11 +86,11 @@ function get_asset_handler(opts) {
|
||||
|
||||
const resolved = Promise.resolve();
|
||||
|
||||
function get_route_handler(get_assets, get_routes) {
|
||||
function handle_route(route, req, res, next, { client, server }) {
|
||||
req.params = route.exec(req.pathname);
|
||||
function get_route_handler(chunks: Record<string, string>, get_assets: () => Assets, get_routes: () => Route[], get_template: () => Template) {
|
||||
function handle_route(route, req, res, next, { client }) {
|
||||
req.params = route.params(route.pattern.exec(req.pathname));
|
||||
|
||||
const mod = require(server.entry)[route.id];
|
||||
const mod = route.module;
|
||||
|
||||
if (route.type === 'page') {
|
||||
// for page routes, we're going to serve some HTML
|
||||
@@ -158,10 +98,12 @@ function get_route_handler(get_assets, get_routes) {
|
||||
|
||||
// preload main.js and current route
|
||||
// TODO detect other stuff we can preload? images, CSS, fonts?
|
||||
res.setHeader('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
|
||||
res.setHeader('Link', `</client/${chunks.main}>;rel="preload";as="script", </client/${chunks[route.id]}>;rel="preload";as="script"`);
|
||||
|
||||
const data = { params: req.params, query: req.query };
|
||||
|
||||
const template = get_template();
|
||||
|
||||
if (mod.preload) {
|
||||
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
|
||||
const serialized = try_serialize(preloaded);
|
||||
@@ -170,9 +112,9 @@ function get_route_handler(get_assets, get_routes) {
|
||||
return { rendered: mod.render(data), serialized };
|
||||
});
|
||||
|
||||
return templates.stream(res, 200, {
|
||||
return template.stream(res, {
|
||||
scripts: promise.then(({ serialized }) => {
|
||||
const main = `<script src='${client.main_file}'></script>`;
|
||||
const main = `<script src='/client/${chunks.main}'></script>`;
|
||||
|
||||
if (serialized) {
|
||||
return `<script>__SAPPER__ = { preloaded: ${serialized} };</script>${main}`;
|
||||
@@ -187,8 +129,8 @@ function get_route_handler(get_assets, get_routes) {
|
||||
} else {
|
||||
const { html, head, css } = mod.render(data);
|
||||
|
||||
const page = templates.render(200, {
|
||||
scripts: `<script src='${client.main_file}'></script>`,
|
||||
const page = template.render({
|
||||
scripts: `<script src='/client/${chunks.main}'></script>`,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
@@ -216,39 +158,55 @@ function get_route_handler(get_assets, get_routes) {
|
||||
return function find_route(req, res, next) {
|
||||
const url = req.pathname;
|
||||
|
||||
resolved
|
||||
.then(() => {
|
||||
const routes = get_routes();
|
||||
for (const route of routes) {
|
||||
if (route.test(url)) return handle_route(route, req, res, next, get_assets());
|
||||
}
|
||||
const routes = get_routes();
|
||||
|
||||
// no matching route — 404
|
||||
next();
|
||||
})
|
||||
.catch(err => {
|
||||
res.statusCode = 500;
|
||||
res.end(templates.render(500, {
|
||||
title: (err && err.name) || 'Internal server error',
|
||||
url,
|
||||
error: escape_html(err && (err.details || err.message || err) || 'Unknown error'),
|
||||
stack: err && err.stack.split('\n').slice(1).join('\n')
|
||||
}));
|
||||
});
|
||||
try {
|
||||
for (const route of routes) {
|
||||
if (route.pattern.test(url)) return handle_route(route, req, res, next, get_assets());
|
||||
}
|
||||
|
||||
// no matching route — 404
|
||||
next();
|
||||
} catch (error) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
const route = get_routes().find((route: Route) => route.pattern.test('/5xx'));
|
||||
const rendered = route ? route.module.render({
|
||||
status: 500,
|
||||
error
|
||||
}) : { head: '', css: '', html: 'Not found' };
|
||||
|
||||
const { head, css, html } = rendered;
|
||||
|
||||
res.end(get_template().render({
|
||||
scripts: `<script src='/client/${chunks.main}'></script>`,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get_not_found_handler(fn) {
|
||||
function get_not_found_handler(chunks: Record<string, string>, get_routes: () => Route[], get_template: () => Template) {
|
||||
return function handle_not_found(req, res) {
|
||||
const asset_cache = fn();
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end(templates.render(404, {
|
||||
title: 'Not found',
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
const route = get_routes().find((route: Route) => route.pattern.test('/4xx')); // TODO separate 4xx and 5xx out
|
||||
const rendered = route ? route.module.render({
|
||||
status: 404,
|
||||
method: req.method,
|
||||
scripts: `<script src='${asset_cache.client.main_file}'></script>`,
|
||||
url: req.url
|
||||
message: 'Not found'
|
||||
}) : { head: '', css: '', html: 'Not found' };
|
||||
|
||||
const { head, css, html } = rendered;
|
||||
|
||||
res.end(get_template().render({
|
||||
scripts: `<script src='/client/${chunks.main}'></script>`,
|
||||
html,
|
||||
head: `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`,
|
||||
styles: (css && css.code ? `<style>${css.code}</style>` : '')
|
||||
}));
|
||||
};
|
||||
}
|
||||
@@ -273,14 +231,22 @@ function compose_handlers(handlers) {
|
||||
};
|
||||
}
|
||||
|
||||
function read_json(file) {
|
||||
function read_json(file: string) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
}
|
||||
|
||||
function try_serialize(data) {
|
||||
function try_serialize(data: any) {
|
||||
try {
|
||||
return serialize(data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function try_read(file: string) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
entry: () => {
|
||||
return {
|
||||
main: [
|
||||
entry.client,
|
||||
'./app/client.js',
|
||||
// workaround for https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/456
|
||||
'style-loader/lib/addStyles',
|
||||
'css-loader/lib/css-base'
|
||||
@@ -28,14 +28,14 @@ export default {
|
||||
server: {
|
||||
entry: () => {
|
||||
return {
|
||||
main: entry.server
|
||||
server: './app/server.js'
|
||||
};
|
||||
},
|
||||
|
||||
output: () => {
|
||||
return {
|
||||
path: `${dest}/server`,
|
||||
filename: '[name].[hash].js',
|
||||
path: `${dest}`,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[id].[hash].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
};
|
||||
|
||||
6
test/app/app/client.js
Normal file
6
test/app/app/client.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { init } from '../../../runtime.js';
|
||||
import { routes } from './manifest/client.js';
|
||||
|
||||
window.init = () => {
|
||||
return init(document.querySelector('#sapper'), routes);
|
||||
};
|
||||
12
test/app/app/manifest/client.js
Normal file
12
test/app/app/manifest/client.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
export const routes = [
|
||||
{ pattern: /^\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_" */ '../../routes/index.html') },
|
||||
{ pattern: /^\/4xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_4xx" */ '../../routes/4xx.html') },
|
||||
{ pattern: /^\/about\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "about" */ '../../routes/about.html') },
|
||||
{ pattern: /^\/show-url\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "show_url" */ '../../routes/show-url.html') },
|
||||
{ pattern: /^\/slow-preload\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "slow_preload" */ '../../routes/slow-preload.html') },
|
||||
{ pattern: /^\/delete-test\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "delete_test" */ '../../routes/delete-test.html') },
|
||||
{ pattern: /^\/5xx\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "_5xx" */ '../../routes/5xx.html') },
|
||||
{ pattern: /^\/blog\/?$/, params: () => ({}), load: () => import(/* webpackChunkName: "blog" */ '../../routes/blog/index.html') },
|
||||
{ pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), load: () => import(/* webpackChunkName: "blog_$slug$" */ '../../routes/blog/[slug].html') }
|
||||
];
|
||||
28
test/app/app/manifest/server.js
Normal file
28
test/app/app/manifest/server.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// This file is generated by Sapper — do not edit it!
|
||||
import _ from '../../routes/index.html';
|
||||
import _4xx from '../../routes/4xx.html';
|
||||
import about from '../../routes/about.html';
|
||||
import show_url from '../../routes/show-url.html';
|
||||
import slow_preload from '../../routes/slow-preload.html';
|
||||
import delete_test from '../../routes/delete-test.html';
|
||||
import _5xx from '../../routes/5xx.html';
|
||||
import blog from '../../routes/blog/index.html';
|
||||
import * as api_blog_contents from '../../routes/api/blog/contents.js';
|
||||
import * as api_delete_$id$ from '../../routes/api/delete/[id].js';
|
||||
import * as api_blog_$slug$ from '../../routes/api/blog/[slug].js';
|
||||
import blog_$slug$ from '../../routes/blog/[slug].html';
|
||||
|
||||
export const routes = [
|
||||
{ id: '_', type: 'page', pattern: /^\/?$/, params: () => ({}), module: _ },
|
||||
{ id: '_4xx', type: 'page', pattern: /^\/4xx\/?$/, params: () => ({}), module: _4xx },
|
||||
{ id: 'about', type: 'page', pattern: /^\/about\/?$/, params: () => ({}), module: about },
|
||||
{ id: 'show_url', type: 'page', pattern: /^\/show-url\/?$/, params: () => ({}), module: show_url },
|
||||
{ id: 'slow_preload', type: 'page', pattern: /^\/slow-preload\/?$/, params: () => ({}), module: slow_preload },
|
||||
{ id: 'delete_test', type: 'page', pattern: /^\/delete-test\/?$/, params: () => ({}), module: delete_test },
|
||||
{ id: '_5xx', type: 'page', pattern: /^\/5xx\/?$/, params: () => ({}), module: _5xx },
|
||||
{ id: 'blog', type: 'page', pattern: /^\/blog\/?$/, params: () => ({}), module: blog },
|
||||
{ id: 'api_blog_contents', type: 'route', pattern: /^\/api\/blog\/contents\/?$/, params: () => ({}), module: api_blog_contents },
|
||||
{ id: 'api_delete_$id$', type: 'route', pattern: /^\/api\/delete(?:\/([^\/]+))?\/?$/, params: match => ({ id: match[1] }), module: api_delete_$id$ },
|
||||
{ id: 'api_blog_$slug$', type: 'route', pattern: /^\/api\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: api_blog_$slug$ },
|
||||
{ id: 'blog_$slug$', type: 'page', pattern: /^\/blog(?:\/([^\/]+))?\/?$/, params: match => ({ slug: match[1] }), module: blog_$slug$ }
|
||||
];
|
||||
37
test/app/app/manifest/service-worker.js
Normal file
37
test/app/app/manifest/service-worker.js
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
export const timestamp = 1518800295364;
|
||||
|
||||
export const assets = [
|
||||
"favicon.png",
|
||||
"global.css",
|
||||
"great-success.png",
|
||||
"manifest.json",
|
||||
"svelte-logo-192.png",
|
||||
"svelte-logo-512.png"
|
||||
];
|
||||
|
||||
export const shell = [
|
||||
"/client/_.0.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/blog.1.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/blog_$slug$.2.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/about.3.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/_5xx.4.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/_4xx.5.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/slow_preload.6.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/show_url.7.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/delete_test.8.3a37f8afa58c59f4bdf9.js",
|
||||
"/client/main.3a37f8afa58c59f4bdf9.js"
|
||||
];
|
||||
|
||||
export const routes = [
|
||||
{ pattern: /^\/?$/ },
|
||||
{ pattern: /^\/4xx\/?$/ },
|
||||
{ pattern: /^\/about\/?$/ },
|
||||
{ pattern: /^\/show-url\/?$/ },
|
||||
{ pattern: /^\/slow-preload\/?$/ },
|
||||
{ pattern: /^\/delete-test\/?$/ },
|
||||
{ pattern: /^\/5xx\/?$/ },
|
||||
{ pattern: /^\/blog\/?$/ },
|
||||
{ pattern: /^\/blog(?:\/([^\/]+))?\/?$/ }
|
||||
];
|
||||
|
||||
78
test/app/app/server.js
Normal file
78
test/app/app/server.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from 'fs';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import serve from 'serve-static';
|
||||
import sapper from '../../../middleware';
|
||||
import { routes } from './manifest/server.js';
|
||||
|
||||
let count;
|
||||
let ended;
|
||||
|
||||
process.on('message', message => {
|
||||
if (message.action === 'start') {
|
||||
count = 0;
|
||||
ended = false;
|
||||
process.send({ type: 'ready' });
|
||||
}
|
||||
|
||||
if (message.action === 'end') {
|
||||
ended = true;
|
||||
if (count === 0) process.send({ type: 'done' });
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use((req, res, next) => {
|
||||
count += 1;
|
||||
|
||||
const { write, end } = res;
|
||||
const chunks = [];
|
||||
|
||||
res.write = function(chunk) {
|
||||
chunks.push(new Buffer(chunk));
|
||||
write.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.end = function(chunk) {
|
||||
if (chunk) chunks.push(new Buffer(chunk));
|
||||
end.apply(res, arguments);
|
||||
|
||||
count -= 1;
|
||||
|
||||
process.send({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
headers: res._headers,
|
||||
body: Buffer.concat(chunks).toString()
|
||||
});
|
||||
|
||||
if (count === 0 && ended) {
|
||||
process.send({ type: 'done' });
|
||||
}
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const { PORT = 3000 } = process.env;
|
||||
|
||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
|
||||
return fetch(url, opts);
|
||||
};
|
||||
|
||||
app.use(compression({ threshold: 0 }));
|
||||
|
||||
app.use(serve('assets'));
|
||||
|
||||
app.use(sapper({
|
||||
routes
|
||||
}));
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening on port ${PORT}`);
|
||||
});
|
||||
@@ -1,14 +1,12 @@
|
||||
const ASSETS = `cache__timestamp__`;
|
||||
import { assets, shell, timestamp, routes } from './manifest/service-worker.js';
|
||||
|
||||
const ASSETS = `cachetimestamp`;
|
||||
|
||||
// `shell` is an array of all the files generated by webpack,
|
||||
// `assets` is an array of everything in the `assets` directory
|
||||
const to_cache = __shell__.concat(__assets__);
|
||||
const to_cache = shell.concat(assets);
|
||||
const cached = new Set(to_cache);
|
||||
|
||||
// `routes` is an array of `{ pattern: RegExp }` objects that
|
||||
// match the pages in your app
|
||||
const routes = __routes__;
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
18
test/app/routes/4xx.html
Normal file
18
test/app/routes/4xx.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<:Head>
|
||||
<title>{{status}}</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='home'>
|
||||
<h1>{{status}}</h1>
|
||||
<p>{{message}}</p>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
}
|
||||
};
|
||||
</script>
|
||||
18
test/app/routes/5xx.html
Normal file
18
test/app/routes/5xx.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<:Head>
|
||||
<title>{{status}}</title>
|
||||
</:Head>
|
||||
|
||||
<Layout page='home'>
|
||||
<h1>{{status}}</h1>
|
||||
<p>{{error.message}}</p>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const app = require('express')();
|
||||
const compression = require('compression');
|
||||
const sapper = require('sapper');
|
||||
const static = require('serve-static');
|
||||
|
||||
const { PORT = 3000 } = process.env;
|
||||
|
||||
// this allows us to do e.g. `fetch('/api/blog')` on the server
|
||||
const fetch = require('node-fetch');
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') url = `http://localhost:${PORT}${url}`;
|
||||
return fetch(url, opts);
|
||||
};
|
||||
|
||||
app.use(compression({ threshold: 0 }));
|
||||
|
||||
app.use(static('assets'));
|
||||
|
||||
app.use(sapper());
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening on port ${PORT}`);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
<!doctype>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<!-- %sapper.status% is the HTTP status code, e.g. 404 -->
|
||||
<title>%sapper.status%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: rgb(170,30,30);
|
||||
border-bottom: 1px solid #aaa;
|
||||
padding: 0 0 0.5em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%sapper.title%</h1>
|
||||
<p>Could not %sapper.method% %sapper.url%</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,47 +0,0 @@
|
||||
<!doctype>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<meta name='theme-color' content='#aa1e1e'>
|
||||
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<title>%sapper.status%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 800px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: rgb(170,30,30);
|
||||
border-bottom: 1px solid #aaa;
|
||||
padding: 0 0 0.5em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.stack {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%sapper.title%</h1>
|
||||
<pre>%sapper.error%</pre>
|
||||
<pre class='stack'>%sapper.stack%</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { init } from '../../../runtime.js';
|
||||
|
||||
window.init = () => {
|
||||
return init(document.querySelector('#sapper'), __routes__);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: config.client.entry(),
|
||||
entry: './app/client.js',
|
||||
output: config.client.output(),
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
const config = require('../../webpack/config.js');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const pkg = require('../package.json');
|
||||
const sapper_pkg = require('../../../package.json');
|
||||
|
||||
module.exports = {
|
||||
entry: config.server.entry(),
|
||||
entry: {
|
||||
'server': './app/server.js'
|
||||
},
|
||||
output: config.server.output(),
|
||||
target: 'node',
|
||||
resolve: {
|
||||
extensions: ['.js', '.html']
|
||||
},
|
||||
externals: Object.keys(pkg.dependencies).concat(Object.keys(sapper_pkg.dependencies)),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
||||
17
test/app/webpack/service-worker.config.js
Normal file
17
test/app/webpack/service-worker.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const path = require('path');
|
||||
const config = require('../../../webpack/config.js');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'service-worker': './app/service-worker.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(`.sapper`),
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].[id].[hash].js'
|
||||
},
|
||||
plugins: [
|
||||
!config.dev && new webpack.optimize.ModuleConcatenationPlugin()
|
||||
].filter(Boolean)
|
||||
};
|
||||
@@ -15,70 +15,25 @@ Nightmare.action('page', {
|
||||
}
|
||||
});
|
||||
|
||||
Nightmare.action('init', function(done) {
|
||||
this.evaluate_now(() => window.init(), done);
|
||||
});
|
||||
|
||||
function run(env) {
|
||||
describe(`env=${env}`, function () {
|
||||
this.timeout(20000);
|
||||
|
||||
let PORT;
|
||||
let server;
|
||||
let proc;
|
||||
let nightmare;
|
||||
let middleware;
|
||||
let capture;
|
||||
|
||||
let base;
|
||||
|
||||
function get(url) {
|
||||
return new Promise(fulfil => {
|
||||
const req = {
|
||||
url,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const result = {
|
||||
headers: {},
|
||||
body: ''
|
||||
};
|
||||
|
||||
const res = {
|
||||
setHeader(header, value) {
|
||||
result.headers[header] = value;
|
||||
},
|
||||
|
||||
set(headers, value) {
|
||||
if (typeof headers === 'string') {
|
||||
return res.set({ [headers]: value });
|
||||
}
|
||||
|
||||
Object.assign(result.headers, headers);
|
||||
},
|
||||
|
||||
status(code) {
|
||||
result.status = code;
|
||||
},
|
||||
|
||||
write(data) {
|
||||
result.body += data;
|
||||
},
|
||||
|
||||
end(data) {
|
||||
result.body += data;
|
||||
fulfil(result);
|
||||
}
|
||||
};
|
||||
|
||||
middleware(req, res, () => {
|
||||
fulfil(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
process.chdir(path.resolve(__dirname, '../app'));
|
||||
|
||||
process.env.NODE_ENV = env;
|
||||
|
||||
let exec_promise = Promise.resolve();
|
||||
let sapper;
|
||||
|
||||
if (env === 'production') {
|
||||
const cli = path.resolve(__dirname, '../../cli.js');
|
||||
@@ -90,81 +45,85 @@ function run(env) {
|
||||
delete require.cache[resolved];
|
||||
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
|
||||
|
||||
sapper = require(resolved);
|
||||
|
||||
return require('get-port')();
|
||||
}).then(port => {
|
||||
PORT = port;
|
||||
base = `http://localhost:${PORT}`;
|
||||
base = `http://localhost:${port}`;
|
||||
|
||||
Nightmare.action('init', function(done) {
|
||||
this.evaluate_now(() => window.init(), done);
|
||||
proc = require('child_process').fork('.sapper/server.js', {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
NODE_ENV: env,
|
||||
PORT: port
|
||||
}
|
||||
});
|
||||
|
||||
global.fetch = (url, opts) => {
|
||||
if (url[0] === '/') url = `${base}${url}`;
|
||||
return fetch(url, opts);
|
||||
};
|
||||
let handler;
|
||||
|
||||
proc.on('message', message => {
|
||||
if (handler) handler(message);
|
||||
});
|
||||
|
||||
let captured;
|
||||
capture = fn => {
|
||||
const result = captured = [];
|
||||
return fn().then(() => {
|
||||
captured = null;
|
||||
return result;
|
||||
return new Promise((fulfil, reject) => {
|
||||
const captured = [];
|
||||
|
||||
let start = Date.now();
|
||||
|
||||
handler = message => {
|
||||
if (message.type === 'ready') {
|
||||
fn().then(() => {
|
||||
proc.send({
|
||||
action: 'end'
|
||||
});
|
||||
}, reject);
|
||||
}
|
||||
|
||||
else if (message.type === 'done') {
|
||||
fulfil(captured);
|
||||
handler = null;
|
||||
}
|
||||
|
||||
else {
|
||||
captured.push(message);
|
||||
}
|
||||
};
|
||||
|
||||
proc.send({
|
||||
action: 'start'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(serve('assets'));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (captured) captured.push(req);
|
||||
next();
|
||||
});
|
||||
|
||||
middleware = sapper();
|
||||
app.use(middleware);
|
||||
|
||||
return new Promise((fulfil, reject) => {
|
||||
server = app.listen(PORT, err => {
|
||||
if (err) reject(err);
|
||||
else fulfil();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
middleware.close();
|
||||
proc.kill();
|
||||
|
||||
// give a chance to clean up
|
||||
return new Promise(fulfil => setTimeout(fulfil, 500));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nightmare = new Nightmare();
|
||||
|
||||
nightmare.on('console', (type, ...args) => {
|
||||
console[type](...args);
|
||||
});
|
||||
|
||||
nightmare.on('page', (type, ...args) => {
|
||||
if (type === 'error') {
|
||||
console.error(args[1]);
|
||||
} else {
|
||||
console.warn(type, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return nightmare.end();
|
||||
});
|
||||
|
||||
describe('basic functionality', () => {
|
||||
beforeEach(() => {
|
||||
nightmare = new Nightmare();
|
||||
|
||||
nightmare.on('console', (type, ...args) => {
|
||||
console[type](...args);
|
||||
});
|
||||
|
||||
nightmare.on('page', (type, ...args) => {
|
||||
if (type === 'error') {
|
||||
console.error(args[1]);
|
||||
} else {
|
||||
console.warn(type, args);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return nightmare.end();
|
||||
});
|
||||
|
||||
it('serves /', () => {
|
||||
return nightmare.goto(base).page.title().then(title => {
|
||||
assert.equal(title, 'Great success!');
|
||||
@@ -190,7 +149,7 @@ function run(env) {
|
||||
});
|
||||
|
||||
it('navigates to a new page without reloading', () => {
|
||||
return nightmare.goto(base).init().wait(100)
|
||||
return capture(() => nightmare.goto(base).init().wait(200))
|
||||
.then(() => {
|
||||
return capture(() => nightmare.click('a[href="/about"]'));
|
||||
})
|
||||
@@ -224,6 +183,7 @@ function run(env) {
|
||||
return nightmare
|
||||
.goto(`${base}/about`)
|
||||
.init()
|
||||
.wait(100)
|
||||
.then(() => {
|
||||
return capture(() => {
|
||||
return nightmare
|
||||
@@ -340,15 +300,17 @@ function run(env) {
|
||||
|
||||
describe('headers', () => {
|
||||
it('sets Content-Type and Link...preload headers', () => {
|
||||
return get('/').then(({ headers }) => {
|
||||
return capture(() => nightmare.goto(base).end()).then(requests => {
|
||||
const { headers } = requests[0];
|
||||
|
||||
assert.equal(
|
||||
headers['Content-Type'],
|
||||
headers['content-type'],
|
||||
'text/html'
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['Link']),
|
||||
headers['Link']
|
||||
/<\/client\/main.\w+\.js>;rel="preload";as="script", <\/client\/_.\d+.\w+.js>;rel="preload";as="script"/.test(headers['link']),
|
||||
headers['link']
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -402,9 +364,9 @@ function run(env) {
|
||||
const allPages = walkSync(dest);
|
||||
|
||||
expectedPages.forEach((expectedPage) => {
|
||||
assert.ok(allPages.includes(expectedPage),
|
||||
`Could not find page matching ${expectedPage}`);
|
||||
assert.ok(allPages.includes(expectedPage),`Could not find page matching ${expectedPage}`);
|
||||
});
|
||||
|
||||
expectedClientRegexes.forEach((expectedRegex) => {
|
||||
// Ensure each client page regular expression matches at least one
|
||||
// generated page.
|
||||
@@ -415,8 +377,7 @@ function run(env) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.ok(matched,
|
||||
`Could not find client page matching ${expectedRegex}`);
|
||||
assert.ok(matched, `Could not find client page matching ${expectedRegex}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user