diff --git a/plugins/plugin-svelte/README.md b/plugins/plugin-svelte/README.md index a86b136e6b..75d3fcc0b8 100644 --- a/plugins/plugin-svelte/README.md +++ b/plugins/plugin-svelte/README.md @@ -10,44 +10,19 @@ npm install --save-dev @snowpack/plugin-svelte // snowpack.config.json { "plugins": [ - ["@snowpack/plugin-svelte", { /* see “Plugin Options” below */ }] + ["@snowpack/plugin-svelte", { /* see optional “Plugin Options” below */ }] ] } ``` ## Plugin Options -- `configFilePath: string` - relative URL to Svelte config, usually named `svelte.config.js`. Defaults to `svelte.config.js` in project root directory. +By default, this plugin will look for a `svelte.config.js` file in your project directory to load `preprocess` and `compilerOptions` configuration from. However, you can also customize Svelte directly via the plugin options below. -```js -// Example usage - ... - ["@snowpack/plugin-svelte", { configFilePath: './dir/svelte.config.js' }] - ... -``` - -This plugin also supports all Svelte compiler options. See [here](https://svelte.dev/docs#svelte_compile) for a list of supported options. +| Name | Type | Description | +| :---------------- | :--------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------ | +| `configFilePath` | `string` | Relative path to a Svelte config file. Defaults to load `svelte.config.js` from the current project root directory. | +| `preprocess` | [svelte.preprocess options](https://svelte.dev/docs#svelte_preprocess) | Configure the Svelte pre-processor. If this option is given, the config file `preprocess` option will be ignored. | +| `compilerOptions` | [svelte.compile options](https://svelte.dev/docs#svelte_compile) | Configure the Svelte compiler.If this option is given, the config file `preprocess` option will be ignored. | +| `hmrOptions` | [svelte-hmr options](https://github.com/rixo/svelte-hmr) | Configure HMR & "fast refresh" behavior for Svelte. | -### HMR Options - -You can pass Svelte HMR specific options through the `hot` option of the plugin. Here are the available options and their defaults: - -```js -{ - "plugins": [ - ["@snowpack/plugin-svelte", { - hot: { - // don't preserve local state - noPreserveState: false, - // escape hatch from preserve local state -- if this string appears anywhere - // in the component's code, then state won't be preserved for this update - noPreserveStateKey: '@!hmr', - // don't reload on fatal error - noReload: false, - // try to recover after runtime errors during component init - optimistic: false, - }, - }] - ] -} -``` diff --git a/plugins/plugin-svelte/plugin.js b/plugins/plugin-svelte/plugin.js index 08b1450cee..db521aa109 100644 --- a/plugins/plugin-svelte/plugin.js +++ b/plugins/plugin-svelte/plugin.js @@ -3,47 +3,58 @@ const svelteRollupPlugin = require('rollup-plugin-svelte'); const fs = require('fs'); const path = require('path'); const {createMakeHot} = require('svelte-hmr'); +const cwd = process.cwd(); let makeHot = (...args) => { makeHot = createMakeHot({walk: svelte.walk}); return makeHot(...args); }; -module.exports = function plugin(snowpackConfig, {hot: hotOptions, ...sveltePluginOptions} = {}) { +module.exports = function plugin(snowpackConfig, pluginOptions = {}) { const isDev = process.env.NODE_ENV !== 'production'; + const useSourceMaps = snowpackConfig.buildOptions.sourceMaps; // Support importing Svelte files when you install dependencies. snowpackConfig.installOptions.rollup.plugins.push( svelteRollupPlugin({include: '**/node_modules/**', dev: isDev}), ); - let {configFilePath = 'svelte.config.js', ...svelteOptions} = sveltePluginOptions || {}; - let userSvelteOptions; - let preprocessOptions; + if ( + pluginOptions.generate !== undefined || + pluginOptions.dev !== undefined || + pluginOptions.hydratable !== undefined || + pluginOptions.css !== undefined || + pluginOptions.preserveComments !== undefined || + pluginOptions.preserveWhitespace !== undefined || + pluginOptions.sveltePath !== undefined + ) { + throw new Error( + `[plugin-svelte] Svelte.compile options moved to new config value: {compilerOptions: {...}}`, + ); + } + + if (pluginOptions.compileOptions !== undefined) { + throw new Error( + `[plugin-svelte] Could not recognize "compileOptions". Did you mean "compilerOptions"?`, + ); + } - const userSvelteConfigLoc = path.resolve(process.cwd(), configFilePath); + let configFilePath = path.resolve(cwd, pluginOptions.configFilePath || 'svelte.config.js'); + let compilerOptions = pluginOptions.compilerOptions; + let preprocessOptions = pluginOptions.preprocess; + const hmrOptions = pluginOptions.hmrOptions; - if (fs.existsSync(userSvelteConfigLoc)) { - const userSvelteConfig = require(userSvelteConfigLoc); - const {preprocess, compilerOptions} = userSvelteConfig; - preprocessOptions = preprocess; - userSvelteOptions = compilerOptions; + if (fs.existsSync(configFilePath)) { + const configFileConfig = require(configFilePath); + preprocessOptions = preprocessOptions || configFileConfig.preprocess; + compilerOptions = compilerOptions || configFileConfig.compilerOptions; } else { //user svelte.config.js is optional and should not error if not configured - if (configFilePath !== 'svelte.config.js') - console.error( - `[plugin-svelte] failed to find Svelte config file: could not locate "${userSvelteConfigLoc}"`, - ); + if (pluginOptions.configFilePath) { + throw new Error(`[plugin-svelte] failed to find Svelte config file: "${configFilePath}"`); + } } - // Generate svelte options from user provided config (if given) - svelteOptions = { - dev: isDev, - css: false, - ...userSvelteOptions, - ...svelteOptions, - }; - return { name: '@snowpack/plugin-svelte', resolve: { @@ -66,44 +77,43 @@ module.exports = function plugin(snowpackConfig, {hot: hotOptions, ...sveltePlug ).code; } - const compileOptions = { + const finalCompileOptions = { generate: isSSR ? 'ssr' : 'dom', - ...svelteOptions, // Note(drew) should take precedence over generate above + css: false, + ...compilerOptions, // Note(drew) should take precedence over generate above + dev: isDev, outputFilename: filePath, filename: filePath, }; - const compiled = svelte.compile(codeToCompile, compileOptions); - + const compiled = svelte.compile(codeToCompile, finalCompileOptions); const {js, css} = compiled; - - const {sourceMaps} = snowpackConfig.buildOptions; const output = { '.js': { code: js.code, - map: sourceMaps ? js.map : undefined, + map: useSourceMaps ? js.map : undefined, }, }; if (isHmrEnabled && !isSSR) { output['.js'].code = makeHot({ id: filePath, - compiledCode: compiled.js.code, + compiledCode: js.code, hotOptions: { - ...hotOptions, + ...hmrOptions, absoluteImports: false, injectCss: true, }, compiled, originalCode: codeToCompile, - compileOptions, + compileOptions: finalCompileOptions, }); } - if (!svelteOptions.css && css && css.code) { + if (!finalCompileOptions.css && css && css.code) { output['.css'] = { code: css.code, - map: sourceMaps ? css.map : undefined, + map: useSourceMaps ? css.map : undefined, }; } return output; diff --git a/plugins/plugin-svelte/test/custom-config.js b/plugins/plugin-svelte/test/custom-config.js new file mode 100644 index 0000000000..e7b85b6f4a --- /dev/null +++ b/plugins/plugin-svelte/test/custom-config.js @@ -0,0 +1,8 @@ +module.exports = { + preprocess: { + __test: 'custom-config.js::preprocess' + }, + compilerOptions: { + __test: 'custom-config.js' + } +}; diff --git a/plugins/plugin-svelte/test/mocked.test.js b/plugins/plugin-svelte/test/mocked.test.js deleted file mode 100644 index 98a7963d5d..0000000000 --- a/plugins/plugin-svelte/test/mocked.test.js +++ /dev/null @@ -1,46 +0,0 @@ -const path = require('path'); - -const mockCompiler = jest.fn().mockImplementation((code) => ({js: {code}})); -const mockPreprocessor = jest.fn().mockImplementation((code) => code); -jest.mock('svelte/compiler', () => ({compile: mockCompiler, preprocess: mockPreprocessor})); // important: mock before import - -const plugin = require('../plugin'); - -const mockConfig = {buildOptions: {sourceMaps: false}, installOptions: {rollup: {plugins: []}}}; -const mockComponent = path.join(__dirname, 'Button.svelte'); - -describe('@snowpack/plugin-svelte (mocked)', () => { - afterEach(() => { - mockCompiler.mockClear(); - mockPreprocessor.mockClear(); - }); - - it('passes options to compiler', async () => { - const options = { - generate: 'ssr', - isDev: false, - }; - const optionsConfig = {configFilePath: './plugins/plugin-svelte/test/svelte.config.js'}; - - const sveltePlugin = plugin(mockConfig, {...options, ...optionsConfig}); - await sveltePlugin.load({filePath: mockComponent}); - const passedOptions = mockCompiler.mock.calls[0][1]; - - // this tests that all options passed above made it to the compiler - // objectContaining() allows additional options to be passed, but we only care that our options have been preserved - expect(passedOptions).toEqual(expect.objectContaining(options)); - // `configFilePath` option is expected not to be passed into Svelte - expect(passedOptions).toEqual(expect.not.objectContaining(optionsConfig)); - }); - - it('handles preprocessing', async () => { - const options = {configFilePath: './plugins/plugin-svelte/test/svelte.config.js'}; - - const sveltePlugin = plugin(mockConfig, options); - - await sveltePlugin.load({filePath: mockComponent}); - - // as long as this function has been called, we can assume success - expect(mockPreprocessor).toHaveBeenCalled(); - }); -}); diff --git a/plugins/plugin-svelte/test/plugin.test.js b/plugins/plugin-svelte/test/plugin.test.js new file mode 100644 index 0000000000..983e694fa8 --- /dev/null +++ b/plugins/plugin-svelte/test/plugin.test.js @@ -0,0 +1,96 @@ +const path = require('path'); + +const mockCompiler = jest.fn().mockImplementation((code) => ({js: {code}})); +const mockPreprocessor = jest.fn().mockImplementation((code) => code); +jest.mock('svelte/compiler', () => ({compile: mockCompiler, preprocess: mockPreprocessor})); // important: mock before import + +const plugin = require('../plugin'); + +const mockConfig = {buildOptions: {sourceMaps: false}, installOptions: {rollup: {plugins: []}}}; +const mockComponent = path.join(__dirname, 'Button.svelte'); + +describe('@snowpack/plugin-svelte (mocked)', () => { + afterEach(() => { + mockCompiler.mockClear(); + mockPreprocessor.mockClear(); + }); + + it('logs error if config options set but finds no file', async () => { + expect(() => { + plugin(mockConfig, { + configFilePath: './plugins/plugin-svelte/this-file-does-not-exist.js', + }); + }).toThrow(/failed to find Svelte config file/); + }); + + it('logs error if compileOptions is used instead of compilerOptions', async () => { + expect(() => { + plugin(mockConfig, { + compileOptions: {__test: 'ignore'}, + }); + }).toThrow( + `[plugin-svelte] Could not recognize "compileOptions". Did you mean "compilerOptions"?`, + ); + }); + + it('logs error if old style config format is used', async () => { + const badOptionCheck = /Svelte\.compile options moved to new config value/; + expect(() => + plugin(mockConfig, { + css: false, + }), + ).toThrow(badOptionCheck); + expect(() => + plugin(mockConfig, { + generate: 'dom', + }), + ).toThrow(badOptionCheck); + }); + + it('passes compilerOptions to compiler', async () => { + const compilerOptions = { + __test: 'compilerOptions', + }; + const sveltePlugin = plugin(mockConfig, {compilerOptions}); + await sveltePlugin.load({filePath: mockComponent}); + expect(mockCompiler.mock.calls[0][1]).toEqual({ + __test: 'compilerOptions', + css: false, + dev: true, + filename: mockComponent, + generate: 'dom', + outputFilename: mockComponent, + }); + }); + + it('passes preprocess options to compiler', async () => { + const preprocess = {__test: 'preprocess'}; + const sveltePlugin = plugin(mockConfig, {preprocess}); + await sveltePlugin.load({filePath: mockComponent}); + expect(mockPreprocessor.mock.calls[0][1]).toEqual(preprocess); + }); + + // For our users we load from the current working directory, but in jest that doesn't make sense + it.skip('load config from a default svelte config file', async () => { + const sveltePlugin = plugin(mockConfig, {}); + await sveltePlugin.load({filePath: mockComponent}); + expect(mockCompiler.mock.calls[0][1]).toEqual({__test: 'svelte.config.js'}); + expect(mockPreprocessor.mock.calls[0][1]).toEqual({__test: 'svelte.config.js::preprocess'}); + }); + + it('load config from a custom svelte config file', async () => { + const sveltePlugin = plugin(mockConfig, { + configFilePath: './plugins/plugin-svelte/test/custom-config.js', + }); + await sveltePlugin.load({filePath: mockComponent}); + expect(mockCompiler.mock.calls[0][1]).toEqual({ + __test: 'custom-config.js', + css: false, + dev: true, + filename: mockComponent, + generate: 'dom', + outputFilename: mockComponent, + }); + expect(mockPreprocessor.mock.calls[0][1]).toEqual({__test: 'custom-config.js::preprocess'}); + }); +}); diff --git a/plugins/plugin-svelte/test/svelte.config.js b/plugins/plugin-svelte/test/svelte.config.js index 95365a7e0e..d117eb2152 100644 --- a/plugins/plugin-svelte/test/svelte.config.js +++ b/plugins/plugin-svelte/test/svelte.config.js @@ -1,7 +1,8 @@ -const {sass} = require('svelte-preprocess-sass'); - module.exports = { preprocess: { - style: sass({}, {name: 'css'}), + __test: 'svelte.config.js::preprocess' }, + compilerOptions: { + __test: 'svelte.config.js' + } }; diff --git a/plugins/plugin-svelte/test/unmocked.test.js b/plugins/plugin-svelte/test/unmocked.test.js deleted file mode 100644 index 6e7b63b6d9..0000000000 --- a/plugins/plugin-svelte/test/unmocked.test.js +++ /dev/null @@ -1,25 +0,0 @@ -const path = require('path'); -const plugin = require('../plugin'); - -const mockConfig = {buildOptions: {sourceMaps: false}, installOptions: {rollup: {plugins: []}}}; -const mockComponent = path.join(__dirname, 'Button.svelte'); - -describe('@snowpack/plugin-svelte (unmocked)', () => { - it('generates code', async () => { - const options = {configFilePath: './plugins/plugin-svelte/test/svelte.config.js'}; - const sveltePlugin = plugin(mockConfig, options); - const result = await sveltePlugin.load({filePath: mockComponent}); - - // assume if some CSS & JS were returned, it transformed successfully - expect(result['.css'].code).toBeTruthy(); - expect(result['.js'].code).toBeTruthy(); - }); - it('logs error if config options set but finds no file', async () => { - const consoleSpy = await jest.spyOn(console, 'error').mockImplementation(() => {}); - const options = {configFilePath: './plugins/plugin-svelte/svelte.config.js'}; - const sveltePlugin = plugin(mockConfig, options); - const result = await sveltePlugin.load({filePath: mockComponent}); - - expect(consoleSpy).toHaveBeenCalled(); - }); -});