vite-plugin-solid
Version:
solid-js integration plugin for vite 3/4/5/6
253 lines (243 loc) • 8.91 kB
JavaScript
import * as babel from '@babel/core';
import solid from 'babel-preset-solid';
import { readFileSync } from 'fs';
import { mergeAndConcat } from 'merge-anything';
import { createRequire } from 'module';
import solidRefresh from 'solid-refresh/babel';
import { version, createFilter } from 'vite';
import { crawlFrameworkPkgs } from 'vitefu';
const require = createRequire(import.meta.url);
const runtimePublicPath = '/@solid-refresh';
const runtimeFilePath = require.resolve('solid-refresh/dist/solid-refresh.mjs');
const runtimeCode = readFileSync(runtimeFilePath, 'utf-8');
const isVite6 = +version.split('.')[0] >= 6;
/** Possible options for the extensions property */
/** Configuration options for vite-plugin-solid. */
function getExtension(filename) {
const index = filename.lastIndexOf('.');
return index < 0 ? '' : filename.substring(index).replace(/\?.+$/, '');
}
function containsSolidField(fields) {
const keys = Object.keys(fields);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key === 'solid') return true;
if (typeof fields[key] === 'object' && fields[key] != null && containsSolidField(fields[key])) return true;
}
return false;
}
function getJestDomExport(setupFiles) {
return setupFiles?.some(path => /jest-dom/.test(path)) ? undefined : ['@testing-library/jest-dom/vitest', '@testing-library/jest-dom/extend-expect'].find(path => {
try {
require.resolve(path);
return true;
} catch (e) {
return false;
}
});
}
function solidPlugin(options = {}) {
const filter = createFilter(options.include, options.exclude);
let needHmr = false;
let replaceDev = false;
let projectRoot = process.cwd();
let isTestMode = false;
return {
name: 'solid',
enforce: 'pre',
async config(userConfig, {
command
}) {
// We inject the dev mode only if the user explicitly wants it or if we are in dev (serve) mode
replaceDev = options.dev === true || options.dev !== false && command === 'serve';
projectRoot = userConfig.root;
isTestMode = userConfig.mode === 'test';
if (!userConfig.resolve) userConfig.resolve = {};
userConfig.resolve.alias = normalizeAliases(userConfig.resolve && userConfig.resolve.alias);
const solidPkgsConfig = await crawlFrameworkPkgs({
viteUserConfig: userConfig,
root: projectRoot || process.cwd(),
isBuild: command === 'build',
isFrameworkPkgByJson(pkgJson) {
return containsSolidField(pkgJson.exports || {});
}
});
// fix for bundling dev in production
const nestedDeps = replaceDev ? ['solid-js', 'solid-js/web', 'solid-js/store', 'solid-js/html', 'solid-js/h'] : [];
const userTest = userConfig.test ?? {};
const test = {};
if (userConfig.mode === 'test') {
// to simplify the processing of the config, we normalize the setupFiles to an array
const userSetupFiles = typeof userTest.setupFiles === 'string' ? [userTest.setupFiles] : userTest.setupFiles || [];
if (!userTest.environment && !options.ssr) {
test.environment = 'jsdom';
}
if (!userTest.server?.deps?.external?.find(item => /solid-js/.test(item.toString()))) {
test.server = {
deps: {
external: [/solid-js/]
}
};
}
if (!userTest.browser?.enabled) {
// vitest browser mode already has bundled jest-dom assertions
// https://main.vitest.dev/guide/browser/assertion-api.html#assertion-api
const jestDomImport = getJestDomExport(userSetupFiles);
if (jestDomImport) {
test.setupFiles = [jestDomImport];
}
}
}
return {
/**
* We only need esbuild on .ts or .js files.
* .tsx & .jsx files are handled by us
*/
// esbuild: { include: /\.ts$/ },
resolve: {
conditions: isVite6 ? undefined : ['solid', ...(replaceDev ? ['development'] : []), ...(userConfig.mode === 'test' && !options.ssr ? ['browser'] : [])],
dedupe: nestedDeps,
alias: [{
find: /^solid-refresh$/,
replacement: runtimePublicPath
}]
},
optimizeDeps: {
include: [...nestedDeps, ...solidPkgsConfig.optimizeDeps.include],
exclude: solidPkgsConfig.optimizeDeps.exclude
},
ssr: solidPkgsConfig.ssr,
...(test.server ? {
test
} : {})
};
},
// @ts-ignore This hook only works in Vite 6
async configEnvironment(name, config, opts) {
config.resolve ??= {};
// Emulate Vite default fallback for `resolve.conditions` if not set
if (config.resolve.conditions == null) {
// @ts-ignore These exports only exist in Vite 6
const {
defaultClientConditions,
defaultServerConditions
} = await import('vite');
if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) {
config.resolve.conditions = [...defaultClientConditions];
} else {
config.resolve.conditions = [...defaultServerConditions];
}
}
config.resolve.conditions = ['solid', ...(replaceDev ? ['development'] : []), ...(isTestMode && !opts.isSsrTargetWebworker ? ['browser'] : []), ...config.resolve.conditions];
},
configResolved(config) {
needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false;
},
resolveId(id) {
if (id === runtimePublicPath) return id;
},
load(id) {
if (id === runtimePublicPath) return runtimeCode;
},
async transform(source, id, transformOptions) {
const isSsr = transformOptions && transformOptions.ssr;
const currentFileExtension = getExtension(id);
const extensionsToWatch = options.extensions || [];
const allExtensions = extensionsToWatch.map(extension =>
// An extension can be a string or a tuple [extension, options]
typeof extension === 'string' ? extension : extension[0]);
if (!filter(id)) {
return null;
}
id = id.replace(/\?.*$/, '');
if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
return null;
}
const inNodeModules = /node_modules/.test(id);
let solidOptions;
if (options.ssr) {
if (isSsr) {
solidOptions = {
generate: 'ssr',
hydratable: true
};
} else {
solidOptions = {
generate: 'dom',
hydratable: true
};
}
} else {
solidOptions = {
generate: 'dom',
hydratable: false
};
}
// We need to know if the current file extension has a typescript options tied to it
const shouldBeProcessedWithTypescript = /\.[mc]?tsx$/i.test(id) || extensionsToWatch.some(extension => {
if (typeof extension === 'string') {
return extension.includes('tsx');
}
const [extensionName, extensionOptions] = extension;
if (extensionName !== currentFileExtension) return false;
return extensionOptions.typescript;
});
const plugins = ['jsx'];
if (shouldBeProcessedWithTypescript) {
plugins.push('typescript');
}
const opts = {
root: projectRoot,
filename: id,
sourceFileName: id,
presets: [[solid, {
...solidOptions,
...(options.solid || {})
}]],
plugins: needHmr && !isSsr && !inNodeModules ? [[solidRefresh, {
bundler: 'vite'
}]] : [],
ast: false,
sourceMaps: true,
configFile: false,
babelrc: false,
parserOpts: {
plugins
}
};
// Default value for babel user options
let babelUserOptions = {};
if (options.babel) {
if (typeof options.babel === 'function') {
const babelOptions = options.babel(source, id, isSsr);
babelUserOptions = babelOptions instanceof Promise ? await babelOptions : babelOptions;
} else {
babelUserOptions = options.babel;
}
}
const babelOptions = mergeAndConcat(babelUserOptions, opts);
const {
code,
map
} = await babel.transformAsync(source, babelOptions);
return {
code,
map
};
}
};
}
/**
* This basically normalize all aliases of the config into
* the array format of the alias.
*
* eg: alias: { '@': 'src/' } => [{ find: '@', replacement: 'src/' }]
*/
function normalizeAliases(alias = []) {
return Array.isArray(alias) ? alias : Object.entries(alias).map(([find, replacement]) => ({
find,
replacement
}));
}
export { solidPlugin as default };
//# sourceMappingURL=index.mjs.map