mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-15 20:34:44 +00:00
work in progress
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user