unplugin-solid
Version:
Transform Solid.js code in various bundlers powered by unplugin
202 lines (199 loc) • 7.18 kB
JavaScript
import { createRequire } from "node:module";
import { readFileSync } from "node:fs";
import { transformAsync } from "@babel/core";
import { createFilter } from "@rollup/pluginutils";
import solid from "babel-preset-solid";
import { mergeAndConcat } from "merge-anything";
import solidRefresh from "solid-refresh/babel";
import { createUnplugin } from "unplugin";
import { version } from "vite";
import { crawlFrameworkPkgs } from "vitefu";
//#region src/core/utils.ts
const require = createRequire(import.meta.url);
function getExtension(filename) {
const index = filename.lastIndexOf(".");
return index === -1 ? "" : filename.slice(Math.max(0, index)).replace(/\?.+$/, "");
}
function containsSolidField(fields) {
const keys = Object.keys(fields);
for (const key of keys) {
if (key === "solid") return true;
if (typeof fields[key] === "object" && fields[key] != null && containsSolidField(fields[key])) return true;
}
return false;
}
const getJestDomExport = (setupFiles) => setupFiles?.some((path) => /jest-dom/.test(path)) ? void 0 : ["@testing-library/jest-dom/vitest", "@testing-library/jest-dom/extend-expect"].find((path) => {
try {
require.resolve(path);
return true;
} catch {
return false;
}
});
/**
* This basically normalize all aliases of the config into
* the array format of the alias.
*
* eg: alias: { '@': 'src/' } => [{ find: '@', replacement: 'src/' }]
*/
const normalizeAliases = (alias = []) => Array.isArray(alias) ? alias : Object.entries(alias).map(([find, replacement]) => ({
find,
replacement
}));
//#endregion
//#region src/core/index.ts
const runtimePublicPath = "/@solid-refresh";
const runtimeFilePath = require.resolve("solid-refresh/dist/solid-refresh.mjs");
const runtimeCode = readFileSync(runtimeFilePath, "utf-8");
const SOLID_EXTERNALS = [
"solid-js",
"solid-js/web",
"solid-js/store",
"solid-js/html",
"solid-js/h"
];
const plugin = createUnplugin((options = {}, meta) => {
const filter = createFilter(options.include, options.exclude);
let needHmr = false;
let replaceDev = false;
let projectRoot = process.cwd();
let isTestMode = false;
return {
name: "unplugin-solid",
enforce: "pre",
vite: {
async config(userConfig, { command }) {
replaceDev = options.dev === true || options.dev !== false && command === "serve";
projectRoot = userConfig.root;
isTestMode = userConfig.mode === "test";
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 ?? {});
}
});
const nestedDeps = replaceDev ? SOLID_EXTERNALS : [];
const userTest = userConfig.test ?? {};
const test = {};
if (userConfig.mode === "test") {
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) {
const jestDomImport = getJestDomExport(userSetupFiles);
if (jestDomImport) test.setupFiles = [jestDomImport];
}
}
const isViteGreaterThan6 = +version.split(".")[0] >= 6;
return {
resolve: {
conditions: isViteGreaterThan6 ? void 0 : [
"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 } : {}
};
},
async configEnvironment(name, config, opts) {
config.resolve ??= {};
if (config.resolve.conditions == null) {
const { defaultClientConditions, defaultServerConditions } = await import("vite");
config.resolve.conditions = config.consumer === "client" || name === "client" || opts.isSsrTargetWebworker ? [...defaultClientConditions] : [...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;
}
},
rolldown: { options(opts) {
opts.external ??= SOLID_EXTERNALS;
opts.transform ??= { jsx: "preserve" };
} },
async transform(source, id) {
const isSsr = !!options.ssr;
const currentFileExtension = getExtension(id);
const extensionsToWatch = options.extensions ?? [];
const allExtensions = extensionsToWatch.map((extension) => 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) solidOptions = isSsr ? {
generate: "ssr",
hydratable: true
} : {
generate: "dom",
hydratable: true
};
else solidOptions = {
generate: "dom",
hydratable: false
};
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: meta.framework }]] : [],
ast: false,
sourceMaps: true,
configFile: false,
babelrc: false,
parserOpts: { plugins }
};
let babelUserOptions = {};
if (options.babel) if (typeof options.babel === "function") {
const babelOptions$1 = options.babel(source, id, isSsr);
babelUserOptions = babelOptions$1 instanceof Promise ? await babelOptions$1 : babelOptions$1;
} else babelUserOptions = options.babel;
const babelOptions = mergeAndConcat(babelUserOptions, opts);
const { code, map } = await transformAsync(source, babelOptions);
return {
code,
map
};
}
};
});
//#endregion
export { plugin };