Merge pull request #632 from pngwn/master

Add support for custom route file extensions.
This commit is contained in:
Rich Harris
2019-05-20 22:33:56 -04:00
committed by GitHub
24 changed files with 346 additions and 16 deletions

View File

@@ -19,6 +19,7 @@ type Opts = {
static?: string;
legacy?: boolean;
bundler?: 'rollup' | 'webpack';
ext?: string;
oncompile?: ({ type, result }: { type: string, result: CompileResult }) => void;
};
@@ -32,6 +33,7 @@ export async function build({
bundler,
legacy = false,
ext,
oncompile = noop
}: Opts = {}) {
bundler = validate_bundler(bundler);
@@ -68,7 +70,7 @@ export async function build({
fs.writeFileSync(`${dest}/template.html`, minify_html(template));
const manifest_data = create_manifest_data(routes);
const manifest_data = create_manifest_data(routes, ext);
// create src/node_modules/@sapper/app.mjs and server.mjs
create_app({

View File

@@ -28,7 +28,8 @@ type Opts = {
hot?: boolean,
'devtools-port'?: number,
bundler?: 'rollup' | 'webpack',
port?: number
port?: number,
ext: string
};
export function dev(opts: Opts) {
@@ -47,7 +48,7 @@ class Watcher extends EventEmitter {
}
port: number;
closed: boolean;
dev_port: number;
live: boolean;
hot: boolean;
@@ -67,6 +68,7 @@ class Watcher extends EventEmitter {
unique_warnings: Set<string>;
unique_errors: Set<string>;
}
ext: string;
constructor({
cwd = '.',
@@ -80,7 +82,8 @@ class Watcher extends EventEmitter {
hot,
'devtools-port': devtools_port,
bundler,
port = +process.env.PORT
port = +process.env.PORT,
ext
}: Opts) {
super();
@@ -95,7 +98,7 @@ class Watcher extends EventEmitter {
output: path.resolve(cwd, output),
static: path.resolve(cwd, static_files)
};
this.ext = ext;
this.port = port;
this.closed = false;
@@ -161,7 +164,7 @@ class Watcher extends EventEmitter {
let manifest_data: ManifestData;
try {
manifest_data = create_manifest_data(routes);
manifest_data = create_manifest_data(routes, this.ext);
create_app({
bundler: this.bundler,
manifest_data,
@@ -189,7 +192,7 @@ class Watcher extends EventEmitter {
},
() => {
try {
const new_manifest_data = create_manifest_data(routes);
const new_manifest_data = create_manifest_data(routes, this.ext);
create_app({
bundler: this.bundler,
manifest_data, // TODO is this right? not new_manifest_data?

View File

@@ -31,6 +31,7 @@ prog.command('dev')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Development build directory', '__sapper__/dev')
.option('--ext', 'Custom Route Extension', '.svelte .html')
.action(async (opts: {
port: number,
open: boolean,
@@ -43,7 +44,8 @@ prog.command('dev')
routes: string,
static: string,
output: string,
'build-dir': string
'build-dir': string,
ext: string
}) => {
const { dev } = await import('./api/dev');
@@ -59,7 +61,8 @@ prog.command('dev')
'dev-port': opts['dev-port'],
live: opts.live,
hot: opts.hot,
bundler: opts.bundler
bundler: opts.bundler,
ext: opts.ext
});
let first = true;
@@ -151,6 +154,7 @@ prog.command('build [dest]')
.option('--src', 'Source directory', 'src')
.option('--routes', 'Routes directory', 'src/routes')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--ext', 'Custom Route Extension', '.svelte .html')
.example(`build custom-dir -p 4567`)
.action(async (dest = '__sapper__/build', opts: {
port: string,
@@ -159,12 +163,13 @@ prog.command('build [dest]')
cwd: string,
src: string,
routes: string,
output: string
output: string,
ext: string
}) => {
console.log(`> Building...`);
try {
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, dest, opts.ext);
const launcher = path.resolve(dest, 'index.js');
@@ -199,6 +204,7 @@ prog.command('export [dest]')
.option('--static', 'Static files directory', 'static')
.option('--output', 'Sapper output directory', 'src/node_modules/@sapper')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.option('--ext', 'Custom Route Extension', '.svelte .html')
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
legacy: boolean,
@@ -212,11 +218,12 @@ prog.command('export [dest]')
static: string,
output: string,
'build-dir': string,
ext: string
}) => {
try {
if (opts.build) {
console.log(`> Building...`);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir']);
await _build(opts.bundler, opts.legacy, opts.cwd, opts.src, opts.routes, opts.output, opts['build-dir'], opts.ext);
console.error(`\n> Built in ${elapsed(start)}`);
}
@@ -265,7 +272,8 @@ async function _build(
src: string,
routes: string,
output: string,
dest: string
dest: string,
ext: string
) {
const { build } = await import('./api/build');
@@ -276,7 +284,7 @@ async function _build(
src,
routes,
dest,
ext,
oncompile: event => {
let banner = `built ${event.type}`;
let c = (txt: string) => colors.cyan(txt);

View File

@@ -4,9 +4,10 @@ import svelte from 'svelte/compiler';
import { Page, PageComponent, ServerRoute, ManifestData } from '../interfaces';
import { posixify, reserved_words } from '../utils';
const component_extensions = ['.svelte', '.html']; // TODO make this configurable (to include e.g. .svelte.md?)
export default function create_manifest_data(cwd: string, extensions: string = '.svelte .html'): ManifestData {
const component_extensions = extensions.split(' ');
export default function create_manifest_data(cwd: string): ManifestData {
// TODO remove in a future version
if (!fs.existsSync(cwd)) {
throw new Error(`As of Sapper 0.21, the routes/ directory should become src/routes/`);

View File

@@ -0,0 +1,60 @@
import resolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
import svelte from 'rollup-plugin-svelte';
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const config = require('../../../config/rollup.js');
export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
extensions: ['.whokilledthemuffinman', '.jesuslivesineveryone', '.mdx', '.svelte', '.html'],
dev,
hydratable: true,
emitCss: true
}),
resolve()
]
},
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
extensions: ['.whokilledthemuffinman', '.jesuslivesineveryone', '.mdx', '.svelte', '.html'],
generate: 'ssr',
dev
}),
resolve({
preferBuiltins: true
})
],
external: ['sirv', 'polka']
},
serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
})
]
}
};

View File

@@ -0,0 +1,9 @@
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')
});
window.prefetchRoutes = () => sapper.prefetchRoutes();
window.prefetch = href => sapper.prefetch(href);
window.goto = href => sapper.goto(href);

View File

@@ -0,0 +1,6 @@
<script>
import { stores } from '@sapper/app';
const { page } = stores();
</script>
<h1>{$page.params.slug.toUpperCase()}</h1>

View File

@@ -0,0 +1,3 @@
<h1>hi</h1>
<p>hi</p>

View File

@@ -0,0 +1 @@
<h1>a</h1>

View File

@@ -0,0 +1 @@
<h1>Tremendous!</h1>

View File

@@ -0,0 +1,8 @@
<h1>Great success!</h1>
<a href="a">a</a>
<a href="ambiguous/ok.json">ok</a>
<a href="echo-query?message">ok</a>
<a href="echo-query?p=one&p=two">ok</a>
<div class='hydrate-test'></div>

View File

@@ -0,0 +1 @@
<h1>Bazooom!</h1>

View File

@@ -0,0 +1,9 @@
import polka from 'polka';
import * as sapper from '@sapper/server';
import { start } from '../../common.js';
const app = polka()
.use(sapper.middleware())
start(app);

View File

@@ -0,0 +1,81 @@
import * as sapper from '@sapper/service-worker';
const ASSETS = `cache${sapper.timestamp}`;
// `app.shell` is an array of all the files generated by webpack,
// `app.files` is an array of everything in the `static` directory
const to_cache = sapper.shell.concat(sapper.files);
const cached = new Set(to_cache);
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(ASSETS)
.then(cache => cache.addAll(to_cache))
.then(() => {
self.skipWaiting();
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(async keys => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key);
}
self.clients.claim();
})
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith('http')) return;
// ignore dev server requests
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
// always serve assets and webpack-generated files from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request));
return;
}
// for pages, you might want to serve a shell `index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
event.respondWith(caches.match('/index.html'));
return;
}
*/
if (event.request.cache === 'only-if-cached') return;
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches
.open(`offline${sapper.timestamp}`)
.then(async cache => {
try {
const response = await fetch(event.request);
cache.put(event.request, response.clone());
return response;
} catch(err) {
const response = await cache.match(event.request);
if (response) return response;
throw err;
}
})
);
});

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset='utf-8'>
%sapper.base%
%sapper.styles%
%sapper.head%
</head>
<body>
<div id='sapper'>%sapper.html%</div>
%sapper.scripts%
</body>
</html>

View File

@@ -0,0 +1,61 @@
import * as assert from 'assert';
import { build } from '../../../api';
import { AppRunner } from '../AppRunner';
describe('custom extensions', function() {
this.timeout(10000);
let r: AppRunner;
// hooks
before('build app', () => build({ cwd: __dirname , ext: '.jesuslivesineveryone .whokilledthemuffinman .mdx .svelte' }));
before('start runner', async () => {
r = await new AppRunner().start(__dirname);
});
after(() => r && r.end());
it('works with arbitrary extensions', async () => {
await r.load(`/`);
assert.equal(
await r.text('h1'),
'Great success!'
);
});
it('works with other arbitrary extensions', async () => {
await r.load(`/const`);
assert.equal(
await r.text('h1'),
'Tremendous!'
);
await r.load(`/a`);
assert.equal(
await r.text('h1'),
'a'
);
await r.load(`/test-slug`);
assert.equal(
await r.text('h1'),
'TEST-SLUG'
);
await r.load(`/unsafe-replacement`);
assert.equal(
await r.text('h1'),
'Bazooom!'
);
});
});

View File

@@ -177,4 +177,66 @@ describe('manifest_data', () => {
pattern: /^\/foo\/?$/
}]);
});
it('works with custom extensions' , () => {
const { components, pages, server_routes } = create_manifest_data(path.join(__dirname, 'samples/custom-extension'), '.jazz .beebop .funk .html');
const index = { name: 'index', file: 'index.funk', has_preload: false };
const about = { name: 'about', file: 'about.jazz', has_preload: false };
const blog = { name: 'blog', file: 'blog/index.html', has_preload: false };
const blog_$slug = { name: 'blog_$slug', file: 'blog/[slug].beebop', has_preload: false };
assert.deepEqual(components, [
index,
about,
blog,
blog_$slug
]);
assert.deepEqual(pages, [
{
pattern: /^\/$/,
parts: [
{ component: index, params: [] }
]
},
{
pattern: /^\/about\/?$/,
parts: [
{ component: about, params: [] }
]
},
{
pattern: /^\/blog\/?$/,
parts: [
{ component: blog, params: [] }
]
},
{
pattern: /^\/blog\/([^\/]+?)\/?$/,
parts: [
null,
{ component: blog_$slug, params: ['slug'] }
]
}
]);
assert.deepEqual(server_routes, [
{
name: 'route_blog_json',
pattern: /^\/blog.json$/,
file: 'blog/index.json.js',
params: []
},
{
name: 'route_blog_$slug_json',
pattern: /^\/blog\/([^\/]+?).json$/,
file: 'blog/[slug].json.js',
params: ['slug']
}
]);
});
});