Skip to content

Commit

Permalink
svelte plugin: refactor tests, document new format, clean up config l…
Browse files Browse the repository at this point in the history
…oading (#1304)

* svelte plugin: refactor tests, clean up config option loading

* Update README.md
  • Loading branch information
FredKSchott authored Oct 15, 2020
1 parent 48ca18b commit fe4bd1b
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 141 deletions.
41 changes: 8 additions & 33 deletions plugins/plugin-svelte/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}]
]
}
```
78 changes: 44 additions & 34 deletions plugins/plugin-svelte/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions plugins/plugin-svelte/test/custom-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preprocess: {
__test: 'custom-config.js::preprocess'
},
compilerOptions: {
__test: 'custom-config.js'
}
};
46 changes: 0 additions & 46 deletions plugins/plugin-svelte/test/mocked.test.js

This file was deleted.

96 changes: 96 additions & 0 deletions plugins/plugin-svelte/test/plugin.test.js
Original file line number Diff line number Diff line change
@@ -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'});
});
});
7 changes: 4 additions & 3 deletions plugins/plugin-svelte/test/svelte.config.js
Original file line number Diff line number Diff line change
@@ -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'
}
};
25 changes: 0 additions & 25 deletions plugins/plugin-svelte/test/unmocked.test.js

This file was deleted.

1 comment on commit fe4bd1b

@vercel
Copy link

@vercel vercel bot commented on fe4bd1b Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.