work in progress

This commit is contained in:
Rich Harris
2018-02-16 12:01:55 -05:00
parent 9a760c570f
commit f9828f9fd2
36 changed files with 667 additions and 7791 deletions

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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) {

View File

@@ -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;

View 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);
}

View 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);
}
};
}

View File

@@ -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';

View File

@@ -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
View 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)
);
}