diff --git a/.changeset/fuzzy-ghosts-call.md b/.changeset/fuzzy-ghosts-call.md new file mode 100644 index 000000000000..99dcafcd78df --- /dev/null +++ b/.changeset/fuzzy-ghosts-call.md @@ -0,0 +1,6 @@ +--- +'create-svelte': patch +'@sveltejs/kit': patch +--- + +Move options behind kit namespace, change paths -> kit.files diff --git a/examples/hn.svelte.dev/svelte.config.js b/examples/hn.svelte.dev/svelte.config.js index ba01daa31ca9..fc1b4b43a3ba 100644 --- a/examples/hn.svelte.dev/svelte.config.js +++ b/examples/hn.svelte.dev/svelte.config.js @@ -1,6 +1,8 @@ module.exports = { - // By default, `npm run build` will create a standard Node app. - // You can create optimized builds for different platforms by - // specifying a different adapter - adapter: '@sveltejs/adapter-netlify' -}; \ No newline at end of file + kit: { + // By default, `npm run build` will create a standard Node app. + // You can create optimized builds for different platforms by + // specifying a different adapter + adapter: '@sveltejs/adapter-netlify' + } +}; diff --git a/examples/realworld.svelte.dev/svelte.config.js b/examples/realworld.svelte.dev/svelte.config.js index c63e6d4a27a2..b36396c400ab 100644 --- a/examples/realworld.svelte.dev/svelte.config.js +++ b/examples/realworld.svelte.dev/svelte.config.js @@ -1,6 +1,8 @@ module.exports = { - // By default, `npm run build` will create a standard Node app. - // You can create optimized builds for different platforms by - // specifying a different adapter - adapter: '@sveltejs/adapter-node' + kit: { + // By default, `npm run build` will create a standard Node app. + // You can create optimized builds for different platforms by + // specifying a different adapter + adapter: '@sveltejs/adapter-node' + } }; \ No newline at end of file diff --git a/examples/sandbox/svelte.config.js b/examples/sandbox/svelte.config.js index c63e6d4a27a2..b36396c400ab 100644 --- a/examples/sandbox/svelte.config.js +++ b/examples/sandbox/svelte.config.js @@ -1,6 +1,8 @@ module.exports = { - // By default, `npm run build` will create a standard Node app. - // You can create optimized builds for different platforms by - // specifying a different adapter - adapter: '@sveltejs/adapter-node' + kit: { + // By default, `npm run build` will create a standard Node app. + // You can create optimized builds for different platforms by + // specifying a different adapter + adapter: '@sveltejs/adapter-node' + } }; \ No newline at end of file diff --git a/examples/svelte-kit-demo/svelte.config.js b/examples/svelte-kit-demo/svelte.config.js index c63e6d4a27a2..b36396c400ab 100644 --- a/examples/svelte-kit-demo/svelte.config.js +++ b/examples/svelte-kit-demo/svelte.config.js @@ -1,6 +1,8 @@ module.exports = { - // By default, `npm run build` will create a standard Node app. - // You can create optimized builds for different platforms by - // specifying a different adapter - adapter: '@sveltejs/adapter-node' + kit: { + // By default, `npm run build` will create a standard Node app. + // You can create optimized builds for different platforms by + // specifying a different adapter + adapter: '@sveltejs/adapter-node' + } }; \ No newline at end of file diff --git a/packages/create-svelte/template/svelte.config.js b/packages/create-svelte/template/svelte.config.js index 41f9bfa92743..07e32f5646e0 100644 --- a/packages/create-svelte/template/svelte.config.js +++ b/packages/create-svelte/template/svelte.config.js @@ -1,6 +1,8 @@ module.exports = { - // By default, `npm run build` will create a standard Node app. - // You can create optimized builds for different platforms by - // specifying a different adapter - adapter: '@sveltejs/adapter-node' + kit: { + // By default, `npm run build` will create a standard Node app. + // You can create optimized builds for different platforms by + // specifying a different adapter + adapter: '@sveltejs/adapter-node' + } }; diff --git a/packages/kit/src/api/adapt/index.js b/packages/kit/src/api/adapt/index.js index 14df60733596..c9deec1fa52d 100644 --- a/packages/kit/src/api/adapt/index.js +++ b/packages/kit/src/api/adapt/index.js @@ -6,18 +6,15 @@ import { logger } from '../utils'; import Builder from './Builder'; export async function adapt(config) { - if (!config.adapter) { - throw new Error('No adapter specified'); - } + const [adapter, options] = config.adapter; - if (typeof config.adapter !== 'string') { - // TODO - throw new Error('Adapter must be a string'); + if (!adapter) { + throw new Error('No adapter specified'); } const log = logger(); - console.log(colors.bold().cyan(`\n> Using ${config.adapter}`)); + console.log(colors.bold().cyan(`\n> Using ${adapter}`)); const manifest_file = resolve('.svelte/build/manifest.cjs'); @@ -29,13 +26,13 @@ export async function adapt(config) { const builder = new Builder({ generated_files: '.svelte/build/optimized', - static_files: config.paths.static, + static_files: config.files.static, manifest, log }); - const adapter = relative(config.adapter); - await adapter(builder); + const fn = relative(adapter); + await fn(builder, options); log.success('done'); } diff --git a/packages/kit/src/api/build/index.js b/packages/kit/src/api/build/index.js index c3e0ba54fb5c..d0963aef606d 100644 --- a/packages/kit/src/api/build/index.js +++ b/packages/kit/src/api/build/index.js @@ -36,7 +36,7 @@ const OPTIMIZED = `${DIR}/build/optimized`; const s = JSON.stringify; export async function build(config) { - const manifest = create_manifest_data(config.paths.routes); + const manifest = create_manifest_data(config.files.routes); mkdirp(ASSETS); await rimraf(UNOPTIMIZED); @@ -76,7 +76,7 @@ export async function build(config) { render(); - const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`; + const mount = `--mount.${config.files.routes}=/_app/routes --mount.${config.files.setup}=/_app/setup`; const promises = { transform_client: exec(`node ${snowpack_bin} build ${mount} --out=${UNOPTIMIZED}/client`).then( @@ -271,14 +271,14 @@ export async function build(config) { import * as setup from './_app/setup/index.js'; import manifest from '../../manifest.js'; - const template = ({ head, body }) => ${s(fs.readFileSync(config.paths.template, 'utf-8')) + const template = ({ head, body }) => ${s(fs.readFileSync(config.files.template, 'utf-8')) .replace('%svelte.head%', '" + head + "') .replace('%svelte.body%', '" + body + "')}; const client = ${s(client)}; export const paths = { - static: ${s(config.paths.static)} + static: ${s(config.files.static)} }; export function render(request, { only_prerender = false } = {}) { diff --git a/packages/kit/src/api/dev/index.js b/packages/kit/src/api/dev/index.js index bf2b11fa7228..4d2ba60e416b 100644 --- a/packages/kit/src/api/dev/index.js +++ b/packages/kit/src/api/dev/index.js @@ -51,7 +51,7 @@ class Watcher extends EventEmitter { async init_filewatcher() { this.cheapwatch = new CheapWatch({ - dir: this.config.paths.routes, + dir: this.config.files.routes, filter: ({ path }) => path.split('/').every((part) => !part.startsWith('_')) }); @@ -77,12 +77,12 @@ class Watcher extends EventEmitter { pkg ); - this.snowpack_config.mount[resolve(this.config.paths.routes)] = { + this.snowpack_config.mount[resolve(this.config.files.routes)] = { url: '/_app/routes', static: false, resolve: true }; - this.snowpack_config.mount[resolve(this.config.paths.setup)] = { + this.snowpack_config.mount[resolve(this.config.files.setup)] = { url: '/_app/setup', static: false, resolve: true @@ -99,7 +99,7 @@ class Watcher extends EventEmitter { async init_server() { const load = loader(this.snowpack, this.snowpack_config); - const static_handler = sirv(this.config.paths.static, { + const static_handler = sirv(this.config.files.static, { dev: true }); @@ -123,7 +123,7 @@ class Watcher extends EventEmitter { if (req.url === '/favicon.ico') return; - const template = readFileSync(this.config.paths.template, 'utf-8').replace( + const template = readFileSync(this.config.files.template, 'utf-8').replace( '', ` @@ -163,7 +163,7 @@ class Watcher extends EventEmitter { body }, { - static_dir: this.config.paths.static, + static_dir: this.config.files.static, template: ({ head, body }) => template.replace('%svelte.head%', () => head).replace('%svelte.body%', () => body), manifest: this.manifest, @@ -195,7 +195,7 @@ class Watcher extends EventEmitter { } update() { - this.manifest = create_manifest_data(this.config.paths.routes); + this.manifest = create_manifest_data(this.config.files.routes); create_app({ manifest_data: this.manifest, diff --git a/packages/kit/src/api/load_config/index.js b/packages/kit/src/api/load_config/index.js index 25f49b4e0521..1bced1073178 100644 --- a/packages/kit/src/api/load_config/index.js +++ b/packages/kit/src/api/load_config/index.js @@ -1,25 +1,62 @@ import relative from 'require-relative'; +import { bold, yellow } from 'kleur/colors'; +import options from './options'; -const default_config = { - target: null, - startGlobal: null, // used for testing - paths: { - static: 'static', - routes: 'src/routes', - setup: 'src/setup', - template: 'src/app.html' +function warn(msg) { + console.log(bold(yellow(msg))); +} + +function validate(definition, option, keypath) { + for (const key in option) { + if (!(key in definition)) { + throw new Error(`Unexpected option ${keypath}.${key}`); + } } -}; + + const merged = {}; + + for (const key in definition) { + const expected = definition[key]; + const actual = option[key]; + + const child_keypath = `${keypath}.${key}`; + const has_children = expected.default && typeof expected.default === 'object' && !Array.isArray(expected.default); + + if (key in option) { + if (has_children) { + if (actual && (typeof actual !== 'object' || Array.isArray(actual))) { + throw new Error(`${keypath}.${key} should be an object`); + } + + merged[key] = validate(expected.default, actual, child_keypath); + } else { + merged[key] = expected.validate(actual, child_keypath); + } + } else { + merged[key] = has_children + ? validate(expected.default, {}, child_keypath) + : expected.default; + } + } + + return merged; +} + +const expected = new Set(['compilerOptions', 'kit', 'preprocess']); export function load_config({ cwd = process.cwd() } = {}) { const config = relative('./svelte.config.js', cwd); + return validate_config(config); +} - return { - ...default_config, - ...config, - paths: { - ...default_config.paths, - ...config.paths +export function validate_config(config) { + for (const key in config) { + if (!expected.has(key)) { + warn(`Unexpected option ${key}${key in options ? ` (did you mean kit.${key}?)` : ''}`); } - }; + } + + const { kit = {} } = config; + + return validate(options, kit, 'kit'); } diff --git a/packages/kit/src/api/load_config/index.spec.js b/packages/kit/src/api/load_config/index.spec.js new file mode 100644 index 000000000000..3e7c928ef5d1 --- /dev/null +++ b/packages/kit/src/api/load_config/index.spec.js @@ -0,0 +1,65 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { validate_config } from './index'; + +test('fills in defaults', () => { + const validated = validate_config({}); + + assert.equal(validated, { + adapter: [null], + target: null, + startGlobal: null, + files: { + assets: 'static', + routes: 'src/routes', + setup: 'src/setup', + template: 'src/app.html' + } + }); +}); + +test('errors on invalid values', () => { + assert.throws(() => { + validate_config({ + kit: { + target: 42 + } + }); + }, /^kit\.target should be a string, if specified$/); +}); + +test('errors on invalid nested values', () => { + assert.throws(() => { + validate_config({ + kit: { + files: { + potato: 'blah' + } + } + }); + }, /^Unexpected option kit\.files\.potato$/); +}); + +test('fills in partial blanks', () => { + const validated = validate_config({ + kit: { + files: { + assets: 'public' + } + } + }); + + assert.equal(validated, { + adapter: [null], + target: null, + startGlobal: null, + files: { + assets: 'public', + routes: 'src/routes', + setup: 'src/setup', + template: 'src/app.html' + } + }); +}); + +test.run(); diff --git a/packages/kit/src/api/load_config/options.js b/packages/kit/src/api/load_config/options.js new file mode 100644 index 000000000000..6a3c415a2985 --- /dev/null +++ b/packages/kit/src/api/load_config/options.js @@ -0,0 +1,48 @@ +export default { + adapter: { + default: [null], + validate: (option, keypath) => { + // support both `adapter: 'foo'` and `adapter: ['foo', opts]` + if (!Array.isArray(option)) { + option = [option]; + } + + // TODO allow inline functions + assert_is_string(option[0], keypath); + + return option; + } + }, + + // TODO check that the selector is present in the provided template + target: expect_string(null), + + // used for testing + startGlobal: expect_string(null), + + files: { + default: { + // TODO check these files exist when the config is loaded? + assets: expect_string('static'), + routes: expect_string('src/routes'), + setup: expect_string('src/setup'), + template: expect_string('src/app.html') + } + } +}; + +function expect_string(string) { + return { + default: string, + validate: (option, keypath) => { + assert_is_string(option, keypath); + return option; + } + }; +} + +function assert_is_string(option, keypath) { + if (typeof option !== 'string') { + throw new Error(`${keypath} should be a string, if specified`); + } +} diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index fda60fb7a711..435f79fe4acd 100644 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -8,9 +8,15 @@ let config; try { config = load_config(); } catch (error) { - const adjective = error.code === 'ENOENT' ? 'Missing' : 'Malformed'; + let message = error.message; - console.error(colors.bold().red(`${adjective} svelte.config.js`)); + if (error.code === 'ENOENT') { + message = 'Missing svelte.config.js'; + } else if (error.name === 'SyntaxError') { + message = 'Malformed svelte.config.js'; + } + + console.error(colors.bold().red(message)); console.error(colors.grey(error.stack)); process.exit(1); } diff --git a/test/apps/basics/svelte.config.js b/test/apps/basics/svelte.config.js index cee5d066dd36..bbcc4e8c7632 100644 --- a/test/apps/basics/svelte.config.js +++ b/test/apps/basics/svelte.config.js @@ -1,9 +1,11 @@ module.exports = { - // TODO adapterless builds - adapter: '@sveltejs/adapter-node', + kit: { + // TODO adapterless builds + adapter: '@sveltejs/adapter-node', - // this creates `window.start` which starts the app, instead of - // it starting automatically — allows test runner to control - // when hydration occurs - startGlobal: 'start' + // this creates `window.start` which starts the app, instead of + // it starting automatically — allows test runner to control + // when hydration occurs + startGlobal: 'start' + } }; diff --git a/test/apps/options/svelte.config.js b/test/apps/options/svelte.config.js index 653048a21018..2757f58ddfcf 100644 --- a/test/apps/options/svelte.config.js +++ b/test/apps/options/svelte.config.js @@ -1,17 +1,19 @@ module.exports = { - // TODO adapterless builds - adapter: '@sveltejs/adapter-node', + kit: { + // TODO adapterless builds + adapter: '@sveltejs/adapter-node', - paths: { - static: 'public', - routes: 'source/pages', - template: 'source/template.html' - }, + files: { + assets: 'public', + routes: 'source/pages', + template: 'source/template.html' + }, - target: '#content-goes-here', + target: '#content-goes-here', - // this creates `window.start` which starts the app, instead of - // it starting automatically — allows test runner to control - // when hydration occurs - startGlobal: 'start' + // this creates `window.start` which starts the app, instead of + // it starting automatically — allows test runner to control + // when hydration occurs + startGlobal: 'start' + } };