@farmfe/core
Version:
Farm is a extremely fast web build tool written in Rust. Farm can start a project in milliseconds and perform HMR within 10ms, making it much faster than similar tools like webpack and vite.
526 lines • 24.8 kB
JavaScript
import { FARM_CSS_MODULES_SUFFIX, VITE_PLUGIN_DEFAULT_MODULE_TYPE, convertEnforceToPriority, customParseQueryString, decodeStr, encodeStr, formatId, formatLoadModuleType, formatTransformModuleType, getContentValue, isObject, isStartsWithSlash, isString, normalizeAdapterVirtualModule, normalizePath, removeQuery, revertNormalizePath, transformFarmConfigToRollupNormalizedOutputOptions, transformResourceInfo2RollupRenderedChunk, transformRollupResource2FarmResource } from './utils.js';
import path from 'path';
import fse from 'fs-extra';
import { readFile } from 'fs/promises';
import { VIRTUAL_FARM_DYNAMIC_IMPORT_SUFFIX } from '../../compiler/index.js';
import merge from '../../utils/merge.js';
import { applyHtmlTransform } from './apply-html-transform.js';
import { farmUserConfigToViteConfig, proxyViteConfig, viteConfigToFarmConfig } from './farm-to-vite-config.js';
import { farmContextToViteContext } from './farm-to-vite-context.js';
import { transformFarmConfigToRollupNormalizedInputOptions, transformResourceInfo2RollupResource } from './utils.js';
import { ViteModuleGraphAdapter, createViteDevServerAdapter } from './vite-server-adapter.js';
/// turn a vite plugin to farm js plugin
export class VitePluginAdapter {
constructor(rawPlugin, farmConfig, filters, logger, mode) {
this.name = 'to-be-override';
this.priority = 0;
this.name = rawPlugin.name || `vite-plugin-adapted-${Date.now()}`;
if (!rawPlugin.name) {
throw new Error(`Vite plugin ${rawPlugin} is not compatible with Farm for now. Because plugin name is required in Farm.`);
}
this.priority = convertEnforceToPriority(rawPlugin.enforce);
this._rawPlugin = rawPlugin;
this._farmConfig = farmConfig;
this._viteConfig = farmUserConfigToViteConfig(farmConfig);
this._logger = logger;
this.filters = filters;
const hooksMap = {
buildStart: () => (this.buildStart = this.viteBuildStartToFarmBuildStart()),
resolveId: () => (this.resolve = this.viteResolveIdToFarmResolve()),
load: () => (this.load = this.viteLoadToFarmLoad()),
transform: () => (this.transform = this.viteTransformToFarmTransform()),
buildEnd: () => (this.buildEnd = this.viteBuildEndToFarmBuildEnd()),
// closeBundle: () => (this.finish = this.viteCloseBundleToFarmFinish()),
handleHotUpdate: () => (this.updateModules = this.viteHandleHotUpdateToFarmUpdateModules()),
renderChunk: () => (this.renderResourcePot =
this.viteHandleRenderChunkToFarmRenderResourcePot()),
renderStart: () => (this.renderStart = this.viteRenderStartToFarmRenderStart()),
augmentChunkHash: () => (this.augmentResourceHash =
this.viteAugmentChunkHashToFarmAugmentResourceHash()),
generateBundle: () => (this.finalizeResources =
this.viteGenerateBundleToFarmFinalizeResources()),
transformIndexHtml: () => (this.transformHtml = this.viteTransformIndexHtmlToFarmTransformHtml()),
'writeBundle|closeBundle': () => (this.writeResources = this.viteWriteCloseBundleToFarmWriteResources())
};
const alwaysExecutedHooks = ['buildStart'];
const productionOnlyHooks = [
'renderChunk',
'generateBundle',
'renderStart',
'closeBundle',
'writeBundle'
];
// convert hooks
for (const [hookNameGroup, fn] of Object.entries(hooksMap)) {
const hookNames = hookNameGroup.split('|');
for (const hookName of hookNames) {
if (rawPlugin[hookName] ||
alwaysExecutedHooks.includes(hookName)) {
if (mode !== 'production' && productionOnlyHooks.includes(hookName)) {
continue;
}
fn();
}
}
}
// if other unsupported vite plugins hooks are used, throw error
const unsupportedHooks = [
'moduleParsed',
'renderError',
'resolveDynamicImport',
'resolveFileUrl',
'resolveImportMeta',
'shouldTransformCachedModule',
'banner',
'footer'
];
for (const hookName of unsupportedHooks) {
if (this._rawPlugin[hookName]) {
throw new Error(`Vite plugin ${this.name} is not compatible with Farm for now. Because it uses hook "${hookName}" which is not supported by Farm.`);
}
}
}
get api() {
return this._rawPlugin.api;
}
// call both config and configResolved
async config(config) {
this._farmConfig = config;
this._viteConfig = farmUserConfigToViteConfig(this._farmConfig);
const configHook = this.wrapRawPluginHook('config', this._rawPlugin.config);
if (configHook) {
this._viteConfig = proxyViteConfig(merge(this._viteConfig, await configHook(proxyViteConfig(this._viteConfig, this.name, this._logger), this.getViteConfigEnv())), this.name, this._logger);
this._farmConfig = viteConfigToFarmConfig(this._viteConfig, this._farmConfig, this.name);
}
return this._farmConfig;
}
async configResolved(config) {
this._farmConfig = config;
this._viteConfig = proxyViteConfig(farmUserConfigToViteConfig(config), this.name, this._logger);
if (!this._rawPlugin.configResolved)
return;
const configResolvedHook = this.wrapRawPluginHook('configResolved', this._rawPlugin.configResolved);
if (configResolvedHook) {
await configResolvedHook(this._viteConfig);
}
}
async configureDevServer(devServer) {
const hook = this.wrapRawPluginHook('configureServer', this._rawPlugin.configureServer);
this._viteDevServer = createViteDevServerAdapter(this.name, this._viteConfig, devServer);
if (hook) {
await hook(this._viteDevServer);
this._viteDevServer.middlewareCallbacks.forEach((cb) => {
devServer.app().use((ctx, koaNext) => {
return new Promise((resolve, reject) => {
// koaNext is async, but vite's next is sync, we need a adapter here
const next = (err) => {
if (err)
reject(err);
koaNext().then(resolve);
};
return cb(ctx.req, ctx.res, next);
});
});
});
}
}
getViteConfigEnv() {
return {
isSsrBuild: this._farmConfig.compilation?.output?.targetEnv === 'node',
command: this._farmConfig.compilation?.mode === 'production' ? 'build' : 'serve',
mode: this._farmConfig.compilation?.mode
};
}
shouldExecutePlugin() {
const command = this._farmConfig.compilation?.mode === 'production' ? 'build' : 'serve';
if (typeof this._rawPlugin.apply === 'function') {
return this._rawPlugin.apply(this._viteConfig, {
mode: this._farmConfig.compilation.mode,
command,
isSsrBuild: this._farmConfig.compilation.output?.targetEnv === 'node'
});
}
else if (this._rawPlugin.apply === undefined) {
return true;
}
return this._rawPlugin.apply === command;
}
wrapExecutor(executor) {
return async (...args) => {
if (this.shouldExecutePlugin()) {
return await executor(...args);
}
};
}
wrapRawPluginHook(hookName, hook, farmContext, currentHandlingFile, hookContext) {
if (hook === undefined) {
return undefined;
}
if (typeof hook === 'object') {
if (!hook.handler) {
return undefined;
}
const logWarn = (name) => {
this._logger.warn(`Farm does not support '${name}' property of vite plugin ${this.name} hook ${hookName} for now. '${name}' property will be ignored.`);
};
const supportedHooks = ['transformIndexHtml'];
// TODO support order, if a hook has order, it should be split into two plugins
if (hook.order && !supportedHooks.includes(hookName)) {
logWarn('order');
}
if (hook.sequential) {
logWarn('sequential');
}
hook = hook.handler;
}
if (farmContext) {
const pluginContext = farmContextToViteContext(farmContext, currentHandlingFile, this.name, hookName, this._farmConfig, hookContext);
return hook.bind(pluginContext);
}
else {
return hook;
}
}
viteBuildStartToFarmBuildStart() {
return {
executor: this.wrapExecutor((_, context) => {
const hook = this.wrapRawPluginHook('buildStart', this._rawPlugin.buildStart, context);
if (this._viteDevServer) {
this._viteDevServer.moduleGraph.context = context;
}
return hook?.();
})
};
}
viteResolveIdToFarmResolve() {
return {
filters: { sources: ['.*'], importers: this.filters },
executor: this.wrapExecutor(async (params, context, hookContext) => {
if (VitePluginAdapter.isFarmInternalVirtualModule(params.source) ||
(params.importer &&
VitePluginAdapter.isFarmInternalVirtualModule(params.importer)) ||
hookContext?.caller === this.name + '.resolveId') {
return null;
}
const hook = this.wrapRawPluginHook('resolveId', this._rawPlugin.resolveId, context, undefined, hookContext);
const absImporterPath = normalizePath(path.resolve(process.cwd(), params.importer ?? ''));
let resolveIdResult = await hook?.(decodeStr(params.source), absImporterPath, { isEntry: params.kind === 'entry' });
if (isString(resolveIdResult)) {
resolveIdResult = normalizeAdapterVirtualModule(resolveIdResult);
return {
resolvedPath: removeQuery(encodeStr(resolveIdResult)),
query: customParseQueryString(resolveIdResult),
sideEffects: true,
external: false,
meta: {}
};
}
else if (isObject(resolveIdResult)) {
const resolveId = normalizeAdapterVirtualModule(resolveIdResult?.id);
return {
resolvedPath: removeQuery(encodeStr(resolveId)),
query: customParseQueryString(resolveId),
sideEffects: Boolean(resolveIdResult?.moduleSideEffects),
// TODO support relative and absolute external
external: Boolean(resolveIdResult?.external),
meta: resolveIdResult.meta ?? {}
};
}
// handles paths starting with / in the vite plugin,
// returning the correct path if the file exists in our root path
const rootAbsolutePath = path.join(this._farmConfig.root, params.source);
if (isStartsWithSlash(params.source) &&
fse.pathExistsSync(rootAbsolutePath)) {
return {
resolvedPath: removeQuery(encodeStr(rootAbsolutePath)),
query: customParseQueryString(rootAbsolutePath),
sideEffects: false,
external: false,
meta: {}
};
}
return null;
})
};
}
viteLoadToFarmLoad() {
return {
filters: {
resolvedPaths: this.filters
},
executor: this.wrapExecutor(async (params, context) => {
if (VitePluginAdapter.isFarmInternalVirtualModule(params.moduleId)) {
return null;
}
const hook = this.wrapRawPluginHook('load', this._rawPlugin.load, context, params.moduleId);
const isSSR = this._farmConfig.compilation.output?.targetEnv === 'node';
const resolvedPath = normalizePath(decodeStr(params.resolvedPath));
// append query
const id = formatId(resolvedPath, params.query);
const result = await hook?.(id, isSSR ? { ssr: true } : undefined);
if (result) {
let map = undefined;
if (typeof result === 'object' && result.map) {
if (typeof result.map === 'string') {
map = result.map;
}
else if (typeof result.map === 'object') {
map = JSON.stringify(result.map);
}
}
return {
content: getContentValue(result),
// only support css as first class citizen for vite plugins
moduleType: formatLoadModuleType(id),
sourceMap: map
// does not support meta and sideEffects
};
}
})
};
}
viteTransformToFarmTransform() {
// default module type and asset can be transformed by vite transform hook
const moduleTypesCouldTransform = [
VITE_PLUGIN_DEFAULT_MODULE_TYPE,
'asset',
'json'
];
return {
filters: {
resolvedPaths: this.filters
},
executor: this.wrapExecutor(async (params, context) => {
if (VitePluginAdapter.isFarmInternalVirtualModule(params.moduleId)) {
return null;
}
const hook = this.wrapRawPluginHook('transform', this._rawPlugin.transform, context, params.moduleId);
const isSSR = this._farmConfig.compilation.output?.targetEnv === 'node';
const resolvedPath = normalizePath(decodeStr(params.resolvedPath));
// append query
const id = formatId(resolvedPath, params.query);
const result = await hook?.(params.content, id, isSSR ? { ssr: true } : undefined);
if (result) {
const content = getContentValue(result);
// fix #1180, do not transform empty content
if (content) {
return {
content,
sourceMap: typeof result.map === 'object' && result.map !== null
? JSON.stringify(result.map)
: undefined,
moduleType: moduleTypesCouldTransform.includes(params.moduleType)
? formatTransformModuleType(id)
: params.moduleType
// TODO support meta and sideEffects
};
}
}
})
};
}
viteBuildEndToFarmBuildEnd() {
return {
executor: this.wrapExecutor((_, context) => {
const hook = this.wrapRawPluginHook('buildEnd', this._rawPlugin.buildEnd, context);
return hook?.();
})
};
}
viteHandleHotUpdateToFarmUpdateModules() {
return {
executor: this.wrapExecutor(async ({ paths }, ctx) => {
const hook = this.wrapRawPluginHook('handleHotUpdate', this._rawPlugin.handleHotUpdate, ctx);
let moduleGraph;
if (this._viteDevServer) {
moduleGraph = this._viteDevServer.moduleGraph;
}
else if (this._moduleGraph) {
moduleGraph = this._moduleGraph;
}
else {
moduleGraph = new ViteModuleGraphAdapter(this.name);
this._moduleGraph = moduleGraph;
}
moduleGraph.context = ctx;
const result = [];
for (const [file, _] of paths) {
const mods = moduleGraph.getModulesByFile(file);
const filename = normalizePath(file);
const ctx = {
file: filename,
timestamp: Date.now(),
modules: (mods ?? []).map((m) => ({
...m,
id: normalizePath(m.id),
file: normalizePath(m.file)
})),
read: function () {
return readFile(file, 'utf-8');
},
server: this._viteDevServer
};
const updateMods = await hook?.(ctx);
if (updateMods) {
result.push(...updateMods.map((mod) => mod.id));
}
else {
result.push(...mods.map((mod) => mod.id));
}
}
return [...new Set(result)].map((id) => revertNormalizePath(id));
})
};
}
viteHandleRenderChunkToFarmRenderResourcePot() {
return {
filters: {
moduleIds: this.filters
},
executor: this.wrapExecutor(async (param, ctx) => {
if (param.resourcePotInfo.resourcePotType !== 'js') {
return;
}
const hook = this.wrapRawPluginHook('renderChunk', this._rawPlugin.renderChunk, ctx);
const result = await hook(param.content, transformResourceInfo2RollupRenderedChunk(param.resourcePotInfo), {}, {
chunks: {}
});
if (result) {
if (typeof result === 'string') {
return { content: result };
}
else if (typeof result === 'object') {
return { content: result.code, sourceMap: result.map };
}
}
})
};
}
viteRenderStartToFarmRenderStart() {
return {
executor: this.wrapExecutor(async (param, ctx) => {
const hook = this.wrapRawPluginHook('renderStart', this._rawPlugin.renderStart, ctx);
await hook(transformFarmConfigToRollupNormalizedOutputOptions(param), transformFarmConfigToRollupNormalizedInputOptions(param));
})
};
}
viteAugmentChunkHashToFarmAugmentResourceHash() {
return {
filters: {
moduleIds: this.filters
},
executor: this.wrapExecutor(async (param, context) => {
if (param.resourcePotType !== 'js') {
return;
}
const hook = this.wrapRawPluginHook('augmentChunkHash', this._rawPlugin.augmentChunkHash, context);
const hash = await hook?.(transformResourceInfo2RollupRenderedChunk(param));
return hash;
})
};
}
viteGenerateBundleToFarmFinalizeResources() {
return {
executor: this.wrapExecutor(async (param, context) => {
// Fix resourcesMap deadlock called by emitFile.
// Cause Farm called resourcesMap.lock() before calling this hook, and this.emitFile would call resourcesMap.lock()
// this leads to deadlock when calling emitFile in finalize_resources hook.
// so we hack context.emitFile here to avoid deadlock
const emittedFiles = [];
context.emitFile = async (params) => {
emittedFiles.push(params);
};
const hook = this.wrapRawPluginHook('generateBundle', this._rawPlugin.generateBundle, context);
const bundles = Object.entries(param.resourcesMap).reduce((res, [key, val]) => {
res[key] = transformResourceInfo2RollupResource(val);
return res;
}, {});
await hook?.(transformFarmConfigToRollupNormalizedOutputOptions(param.config), bundles);
const emittedFilesMap = emittedFiles.reduce((res, item) => {
res[item.name] = {
name: item.name,
bytes: item.content,
emitted: false,
resourceType: 'asset',
origin: {
type: 'Module',
value: 'vite-plugin-adapter-generate-bundle-hook'
}
};
return res;
}, {});
const result = Object.entries(bundles).reduce((res, [key, val]) => {
res[key] = transformRollupResource2FarmResource(val, param.resourcesMap[key]);
return res;
}, emittedFilesMap);
return result;
})
};
}
viteTransformIndexHtmlToFarmTransformHtml() {
const rawTransformHtmlHook = this._rawPlugin.transformIndexHtml;
const order = rawTransformHtmlHook?.order ??
rawTransformHtmlHook?.enforce ??
this._rawPlugin.enforce ??
'normal';
const orderMap = {
pre: 0,
normal: 1,
post: 2
};
return {
order: orderMap[order] ?? 1,
executor: this.wrapExecutor(async (params, context) => {
const { htmlResource } = params;
const hook = this.wrapRawPluginHook('transformIndexHtml',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ignore type error
this._rawPlugin.transformIndexHtml, context);
const result = await this.callViteTransformIndexHtmlHook(htmlResource, hook);
if (result) {
htmlResource.bytes = [...Buffer.from(result)];
}
return htmlResource;
})
};
}
viteWriteCloseBundleToFarmWriteResources() {
return {
executor: this.wrapExecutor(async (param, context) => {
const hook = this.wrapRawPluginHook('writeBundle', this._rawPlugin.writeBundle, context);
if (hook) {
const bundles = Object.entries(param.resourcesMap).reduce((res, [key, val]) => {
res[key] = transformResourceInfo2RollupResource(val);
return res;
}, {});
await hook?.(transformFarmConfigToRollupNormalizedOutputOptions(param.config), bundles);
}
const closeBundle = this.wrapRawPluginHook('closeBundle', this._rawPlugin.closeBundle);
return closeBundle?.();
})
};
}
async callViteTransformIndexHtmlHook(resource, transformIndexHtmlHook, bundles) {
const html = Buffer.from(resource.bytes).toString();
const result = await transformIndexHtmlHook?.(html, {
path: resource.name,
filename: resource.name,
server: bundles === undefined ? this._viteDevServer : undefined,
bundle: bundles,
chunk: transformResourceInfo2RollupResource(resource)
});
if (result && typeof result !== 'string') {
return applyHtmlTransform(html, result);
}
else if (typeof result === 'string') {
return result;
}
}
// skip farm lazy compilation virtual module for vite plugin
static isFarmInternalVirtualModule(id) {
return (id.endsWith(VIRTUAL_FARM_DYNAMIC_IMPORT_SUFFIX) ||
// css has been handled before the virtual module is created
FARM_CSS_MODULES_SUFFIX.test(id));
}
}
//# sourceMappingURL=vite-plugin-adapter.js.map