diff --git a/src/api/build.ts b/src/api/build.ts index fbfdf5a4b..6038dbc15 100644 --- a/src/api/build.ts +++ b/src/api/build.ts @@ -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({ diff --git a/src/api/dev.ts b/src/api/dev.ts index adfd73a92..c7e2575ce 100644 --- a/src/api/dev.ts +++ b/src/api/dev.ts @@ -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; unique_errors: Set; } + 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? diff --git a/src/cli.ts b/src/cli.ts index 5ebacc96f..72024f35d 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -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'); @@ -198,6 +203,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, @@ -210,11 +216,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)}`); } @@ -262,7 +269,8 @@ async function _build( src: string, routes: string, output: string, - dest: string + dest: string, + ext: string ) { const { build } = await import('./api/build'); @@ -273,7 +281,7 @@ async function _build( src, routes, dest, - + ext, oncompile: event => { let banner = `built ${event.type}`; let c = (txt: string) => colors.cyan(txt); diff --git a/src/core/create_manifest_data.ts b/src/core/create_manifest_data.ts index 6324733ed..4e4f2c6b2 100644 --- a/src/core/create_manifest_data.ts +++ b/src/core/create_manifest_data.ts @@ -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/`); diff --git a/test/apps/custom-extension/rollup.config.js b/test/apps/custom-extension/rollup.config.js new file mode 100644 index 000000000..7a8516673 --- /dev/null +++ b/test/apps/custom-extension/rollup.config.js @@ -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) + }) + ] + } +}; \ No newline at end of file diff --git a/test/apps/custom-extension/src/client.js b/test/apps/custom-extension/src/client.js new file mode 100644 index 000000000..6cce7e658 --- /dev/null +++ b/test/apps/custom-extension/src/client.js @@ -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); \ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/[slug].mdx b/test/apps/custom-extension/src/routes/[slug].mdx new file mode 100644 index 000000000..e31f6d9fd --- /dev/null +++ b/test/apps/custom-extension/src/routes/[slug].mdx @@ -0,0 +1,5 @@ + + +

{$page.params.slug.toUpperCase()}

\ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/_error.svelte b/test/apps/custom-extension/src/routes/_error.svelte new file mode 100644 index 000000000..4cd55d28d --- /dev/null +++ b/test/apps/custom-extension/src/routes/_error.svelte @@ -0,0 +1,3 @@ +

{status}

+ +

{error.message}

\ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/a.svelte b/test/apps/custom-extension/src/routes/a.svelte new file mode 100644 index 000000000..0e6757b2b --- /dev/null +++ b/test/apps/custom-extension/src/routes/a.svelte @@ -0,0 +1 @@ +

a

\ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/const.whokilledthemuffinman b/test/apps/custom-extension/src/routes/const.whokilledthemuffinman new file mode 100644 index 000000000..1dc9ef880 --- /dev/null +++ b/test/apps/custom-extension/src/routes/const.whokilledthemuffinman @@ -0,0 +1 @@ +

Tremendous!

\ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/index.jesuslivesineveryone b/test/apps/custom-extension/src/routes/index.jesuslivesineveryone new file mode 100644 index 000000000..b61b24443 --- /dev/null +++ b/test/apps/custom-extension/src/routes/index.jesuslivesineveryone @@ -0,0 +1,8 @@ +

Great success!

+ +a +ok +ok +ok + +
\ No newline at end of file diff --git a/test/apps/custom-extension/src/routes/unsafe-replacement.svelte b/test/apps/custom-extension/src/routes/unsafe-replacement.svelte new file mode 100644 index 000000000..7b0b2dd2f --- /dev/null +++ b/test/apps/custom-extension/src/routes/unsafe-replacement.svelte @@ -0,0 +1 @@ +

Bazooom!

\ No newline at end of file diff --git a/test/apps/custom-extension/src/server.js b/test/apps/custom-extension/src/server.js new file mode 100644 index 000000000..7f090b869 --- /dev/null +++ b/test/apps/custom-extension/src/server.js @@ -0,0 +1,8 @@ +import polka from 'polka'; +import * as sapper from '@sapper/server'; + +const { PORT } = process.env; + +polka() + .use(sapper.middleware()) + .listen(PORT); diff --git a/test/apps/custom-extension/src/service-worker.js b/test/apps/custom-extension/src/service-worker.js new file mode 100644 index 000000000..c80a8f4a4 --- /dev/null +++ b/test/apps/custom-extension/src/service-worker.js @@ -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; + } + }) + ); +}); diff --git a/test/apps/custom-extension/src/template.html b/test/apps/custom-extension/src/template.html new file mode 100644 index 000000000..0eb1f3ba4 --- /dev/null +++ b/test/apps/custom-extension/src/template.html @@ -0,0 +1,14 @@ + + + + + + %sapper.base% + %sapper.styles% + %sapper.head% + + +
%sapper.html%
+ %sapper.scripts% + + diff --git a/test/apps/custom-extension/test.ts b/test/apps/custom-extension/test.ts new file mode 100644 index 000000000..501b78eb5 --- /dev/null +++ b/test/apps/custom-extension/test.ts @@ -0,0 +1,75 @@ +import * as assert from 'assert'; +import * as puppeteer from 'puppeteer'; +import * as http from 'http'; +import { build } from '../../../api'; +import { AppRunner } from '../AppRunner'; +import { wait } from '../../utils'; + +declare let deleted: { id: number }; +declare let el: any; + +describe('custom extensions', function() { + this.timeout(10000); + + let runner: AppRunner; + let page: puppeteer.Page; + let base: string; + + // helpers + let start: () => Promise; + let prefetchRoutes: () => Promise; + let prefetch: (href: string) => Promise; + let goto: (href: string) => Promise; + let title: () => Promise; + + // hooks + before(async () => { + await build({ cwd: __dirname, bundler: 'rollup',ext: '.jesuslivesineveryone .whokilledthemuffinman .mdx .svelte' }); + + runner = new AppRunner(__dirname, '__sapper__/build/server/server.js'); + ({ base, page, start, prefetchRoutes, prefetch, goto, title } = await runner.start()); + }); + + after(() => runner.end()); + + it('works with arbitrary extensions', async () => { + await page.goto(base); + + assert.equal( + await title(), + 'Great success!' + ); + }); + + it('works with other arbitrary extensions', async () => { + await page.goto(`${base}/const`); + + assert.equal( + await title(), + 'Tremendous!' + ); + + await page.goto(`${base}/a`); + + assert.equal( + await title(), + 'a' + ); + + await page.goto(`${base}/test-slug`); + + assert.equal( + await title(), + 'TEST-SLUG' + ); + + await page.goto(`${base}/unsafe-replacement`); + + assert.equal( + await title(), + 'Bazooom!' + ); + }); + + +}); \ No newline at end of file diff --git a/test/unit/create_manifest_data/index.ts b/test/unit/create_manifest_data/index.ts index d21a537fa..cc5ec0ecf 100644 --- a/test/unit/create_manifest_data/index.ts +++ b/test/unit/create_manifest_data/index.ts @@ -165,4 +165,53 @@ 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: [ + null, + { component: blog, params: [] } + ] + }, + + { + pattern: /^\/blog\/([^\/]+?)\/?$/, + parts: [ + null, + { component: blog_$slug, params: ['slug'] } + ] + } + ]); + + }); }); \ No newline at end of file diff --git a/test/unit/create_manifest_data/samples/custom-extension/about.jazz b/test/unit/create_manifest_data/samples/custom-extension/about.jazz new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/blog/[slug].beebop b/test/unit/create_manifest_data/samples/custom-extension/blog/[slug].beebop new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/blog/[slug].json.js b/test/unit/create_manifest_data/samples/custom-extension/blog/[slug].json.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/blog/_default.html b/test/unit/create_manifest_data/samples/custom-extension/blog/_default.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/blog/index.html b/test/unit/create_manifest_data/samples/custom-extension/blog/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/blog/index.json.js b/test/unit/create_manifest_data/samples/custom-extension/blog/index.json.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/create_manifest_data/samples/custom-extension/index.funk b/test/unit/create_manifest_data/samples/custom-extension/index.funk new file mode 100644 index 000000000..e69de29bb