utquidem
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
450 lines (398 loc) • 13.5 kB
text/typescript
import path from 'path';
import { AsyncSeriesHook, SyncHook } from 'tapable';
import { rollup } from 'rollup';
import { cloneDeep } from '@modern-js/utils/lodash';
import type {
Plugin as RollupPlugin,
RollupBuild,
RollupError,
RollupOutput,
} from 'rollup';
import rollupPluginReplace from '@rollup/plugin-replace';
import rollupPluginCommonjs, {
RollupCommonJSOptions,
} from '@rollup/plugin-commonjs';
import { wasm as rollupPluginWasm } from '@rollup/plugin-wasm';
import rollupPluginJson from '@rollup/plugin-json';
import type { RollupJsonOptions } from '@rollup/plugin-json';
import rollupPluginNodeResolve from '@rollup/plugin-node-resolve';
import type { RollupNodeResolveOptions } from '@rollup/plugin-node-resolve';
import rollupPluginSvgr from '@svgr/rollup';
import rollupPluginUrl from '@rollup/plugin-url';
import type {
EntryInput,
InputOptions,
OutputOptions,
} from './CompilerOptions';
import { Logger } from './Logger';
import { rollupPluginCatchFetch } from './rollup-plugins/rollup-plugin-catch-fetch';
import { rollupPluginStripSourceMapping } from './rollup-plugins/rollup-plugin-strip-source-mapping';
import {
rollupPluginCatchUnresolvedPluginName,
rollupPluginCatchUnresolved,
} from './rollup-plugins/rollup-plugin-catch-unresolved';
import { Compiler } from './Compiler';
import {
LessOptions,
rollupPluginLess,
} from './rollup-plugins/rollup-plugin-less';
import {
rollupPluginScss,
ScssOptions,
} from './rollup-plugins/rollup-plugin-scss';
import { rollupPluginCss } from './rollup-plugins/rollup-plugin-css';
import {
rollupPluginNodePolyfills,
NodePolyfillsOptions,
} from './rollup-plugins/rollup-plugin-node-polyfills';
import { getEntryFilename } from './utils/package';
import { getRollupReplaceKeys } from './utils/env';
import { EnvironmentVariables } from './Options';
import { rollupPluginCJSNamedExportDetect } from './rollup-plugins/rollup-plugin-cjs-named-export-detect';
export interface CompilationParams {
specifier: string;
importer?: string | null;
inlineDependency?: (id: string) => boolean;
/**
* user defined state
*/
state?: Record<string, any>;
}
export interface DependencyInfo {
specifier: string;
importer?: string;
}
interface CompilationHooks {
environment: SyncHook<EnvironmentVariables>;
initialize: AsyncSeriesHook<Compilation>;
inputOptions: AsyncSeriesHook<InputOptions>;
outputOptions: AsyncSeriesHook<OutputOptions>;
rollupPluginOptions: {
nodeResolve: SyncHook<RollupNodeResolveOptions>;
json: SyncHook<RollupJsonOptions>;
less: SyncHook<LessOptions>;
scss: SyncHook<ScssOptions>;
commonjs: SyncHook<RollupCommonJSOptions>;
nodePolyfills: SyncHook<NodePolyfillsOptions>;
};
rollupPlugins: AsyncSeriesHook<{
preRollupPlugins: RollupPlugin[];
rollupPlugins: RollupPlugin[];
postRollupPlugins: RollupPlugin[];
}>;
}
class Compilation {
public hooks: CompilationHooks = Object.freeze({
environment: new SyncHook<EnvironmentVariables>(['environment']),
initialize: new AsyncSeriesHook<Compilation>(['compilation']),
inputOptions: new AsyncSeriesHook<InputOptions>(['inputOptions']),
outputOptions: new AsyncSeriesHook<OutputOptions>(['outputOptions']),
rollupPluginOptions: {
nodeResolve: new SyncHook<RollupNodeResolveOptions>([
'nodeResolveOptions',
]),
json: new SyncHook<RollupJsonOptions>(['jsonOptions']),
less: new SyncHook<LessOptions>(['lessOptions']),
scss: new SyncHook<ScssOptions>(['scssOptions']),
commonjs: new SyncHook<RollupCommonJSOptions>(['commonjsOptions']),
nodePolyfills: new SyncHook<NodePolyfillsOptions>([
'nodePolyfillsOptions',
]),
},
rollupPlugins: new AsyncSeriesHook<{
preRollupPlugins: RollupPlugin[];
rollupPlugins: RollupPlugin[];
postRollupPlugins: RollupPlugin[];
}>(['rollupPlugins']),
});
public logger: Logger = this.getLogger('esmpack.Compilation');
public specifier: string;
public importer: string | null = null;
public inlineDependency: (id: string) => boolean;
/**
* Path in file system when specifier resolved
* It can be passed as importer for nested dependencies
* @default null
*/
public specifierFilePath: string | null = null;
public manifest: any | null = null;
public inputOptions: InputOptions = {};
public outputOptions: OutputOptions = {};
public rollupBuild: RollupBuild | null = null;
public rollupOutput: RollupOutput | null = null;
public dependencies: DependencyInfo[] = [];
private preRollupPlugins: RollupPlugin[] = [];
private readonly rollupPlugins: RollupPlugin[] = [];
private postRollupPlugins: RollupPlugin[] = [];
private inited: boolean = false;
private readonly env: EnvironmentVariables;
private state: Record<string, any>;
constructor(public compiler: Compiler, compilationParams: CompilationParams) {
this.specifier = compilationParams.specifier;
this.importer = compilationParams.importer || null;
this.state = compilationParams.state || {};
this.inlineDependency = compilationParams.inlineDependency || (() => false);
this.env = cloneDeep(compiler.options.env);
}
public async init() {
this.hooks.environment.call(this.env);
await this.hooks.initialize.promise(this);
await this.initInputOptions();
await this.initOutputOptions();
this.inited = true;
}
public setState(key: string, value: string) {
this.state[key] = value;
}
public getState(key: string) {
return this.state[key];
}
private async initInputOptions() {
this.inputOptions.onwarn = this.onwarn;
this.inputOptions.treeshake = false;
await this.initPlugins();
await this.hooks.inputOptions.promise(this.inputOptions);
}
private readonly onwarn: NonNullable<InputOptions['onwarn']> = (
warning,
warn,
) => {
// Warn about the first circular dependency, but then ignore the rest.
if (warning.code === 'CIRCULAR_DEPENDENCY') {
this.logger.silly(
`Warning: 1+ circular dependencies found via "${
warning.importer || ''
}".`,
);
return;
}
// TODO: sometime this is injected by helper function, in those cases, this should be safe and ignored
if (warning.code === 'THIS_IS_UNDEFINED') {
this.logger.silly(
`Warning: ${warning.message}${warning.id ? ` (${warning.id})` : ''}`,
);
return;
}
if (warning.code === 'EVAL') {
this.logger.silly(
`Warning: ${warning.message}${warning.id ? ` (${warning.id})` : ''}`,
);
return;
}
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
this.logger.silly(
`Warning: ${warning.message}${warning.id ? ` (${warning.id})` : ''}`,
);
return;
}
// Log "unresolved" import warnings as an error, causing Snowpack to fail at the end.
if (
warning.code === 'PLUGIN_WARNING' &&
warning.plugin === rollupPluginCatchUnresolvedPluginName
) {
// Display posix-style on all environments, mainly to help with CI :)
let msg = `${warning.message}`;
if (warning.id) {
msg = `${warning.id}: ${msg}`;
}
this.logger.error(msg);
throw new Error(msg);
}
warn(warning);
};
private async initOutputOptions() {
this.outputOptions = {
dir: this.compiler.options.outDir,
format: 'es',
sourcemap: false,
exports: 'named',
chunkFileNames: '/_chunk_/[name]-[hash].js',
entryFileNames: chunk => {
// output in true node_modules level
// avoid overwritten by different version
const { specifier, specifierFilePath } = this;
const specReg = new RegExp(`/node_modules/(.+)/${specifier}`);
const match = specifierFilePath?.match(specReg);
const prefix = match?.[1] || '';
const filename = getEntryFilename(chunk.name);
return path.join(prefix, filename);
},
};
await this.hooks.outputOptions.promise(this.outputOptions);
}
private getLogger(name: string) {
return new Logger(name);
}
async addInput(entryInput: EntryInput, manifest: any) {
if (!this.inited) {
const msg = 'init should be called';
this.logger.error(msg);
throw new Error(msg);
}
this.inputOptions.input = {
...this.inputOptions.input,
...entryInput,
};
this.manifest = manifest;
}
async addDependency(specifier: string, importer?: string) {
const ignoreThisAdd = this.dependencies.some(x => {
/**
* 1. specifier 必须相同
* 2. importer 相同,或者 importer 传入的是 undefined
*/
const ret =
x.specifier === specifier && (x.importer === importer || !importer);
return ret;
});
if (!ignoreThisAdd) {
this.dependencies.push({
specifier,
importer,
});
}
}
async build() {
try {
const lastCWD = process.cwd();
// rollup depends on process.cwd... some one fix this plz.
process.chdir(this.compiler.options.cwd);
this.rollupBuild = await rollup(this.inputOptions);
process.chdir(lastCWD);
} catch (_err: any) {
const err: RollupError = _err;
const errFilePath = err.loc?.file || err.id;
if (!errFilePath) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw err;
}
const suggestion = err.message;
// Display posix-style on all environments, mainly to help with CI :)
const fileName = errFilePath
.replace(this.compiler.options.cwd + path.sep, '')
.replace(/\\/g, '/');
let errMsg = `failed to load ${fileName}\n ${suggestion}`;
if (err.plugin) {
errMsg += `\n by ${err.plugin}`;
}
this.logger.error(errMsg);
throw new Error(errMsg);
}
}
async emit() {
if (!this.rollupBuild) {
const errMsg = 'Missing Rollup rollupBuild';
this.logger.error(errMsg);
throw new Error(errMsg);
}
try {
this.rollupOutput = await this.rollupBuild.write(this.outputOptions);
await this.rollupBuild.close();
} catch (e: any) {
this.logger.error(e);
throw e;
}
}
private initPrePlugins() {
this.preRollupPlugins = [
rollupPluginReplace(getRollupReplaceKeys(this.env)),
rollupPluginCatchFetch(),
rollupPluginCJSNamedExportDetect(),
];
}
private initPostPlugins() {
this.postRollupPlugins = [
rollupPluginCatchUnresolved(),
rollupPluginStripSourceMapping(),
];
}
private async initPlugins() {
const { cwd } = this.compiler.options;
this.initPrePlugins();
this.initPostPlugins();
/**
* Node Resolve
*/
const nodeResolveOptions = {
mainFields: ['exports', 'browser:module', 'browser', 'module', 'main'],
extensions: ['.mjs', '.cjs', '.js', '.json', '.jsx'], // Default: [ '.mjs', '.js', '.json', '.node' ]
// whether to prefer built-in modules (e.g. `fs`, `path`) or local ones with the same names
preferBuiltins: true, // Default: true
dedupe: [], // userDefinedRollup.dedupe,
};
this.hooks.rollupPluginOptions.nodeResolve.call(nodeResolveOptions);
this.rollupPlugins.push(rollupPluginNodeResolve(nodeResolveOptions));
/**
* JSON
*/
const jsonOptions = {
preferConst: true,
indent: ' ',
compact: false,
namedExports: true,
};
this.hooks.rollupPluginOptions.json.call(jsonOptions);
this.rollupPlugins.push(rollupPluginJson(jsonOptions));
/**
* Wasm
*/
this.rollupPlugins.push(rollupPluginWasm());
/**
* Less
*/
const lessOptions: LessOptions = {
paths: [path.resolve(cwd, 'node_modules')],
};
this.hooks.rollupPluginOptions.less.call(lessOptions);
this.rollupPlugins.push(rollupPluginLess(lessOptions));
/**
* SCSS
*/
const scssOptions: ScssOptions = {
includePaths: [path.resolve(cwd, 'node_modules')],
};
this.hooks.rollupPluginOptions.scss.call(scssOptions);
this.rollupPlugins.push(rollupPluginScss(scssOptions));
/**
* CSS
*/
this.rollupPlugins.push(rollupPluginCss(cwd));
/**
* SVG
*/
this.rollupPlugins.push(rollupPluginUrl());
this.rollupPlugins.push(rollupPluginSvgr() as any);
/**
* CommonJS
*/
const commonjsOptions: RollupCommonJSOptions = {
transformMixedEsModules: true,
extensions: ['.js', '.cjs'],
requireReturnsDefault: 'auto',
sourceMap: false,
};
this.hooks.rollupPluginOptions.commonjs.call(commonjsOptions);
this.rollupPlugins.push(rollupPluginCommonjs(commonjsOptions));
/**
* NodePolyfills
*/
const nodePolyfillsOptions: NodePolyfillsOptions = {
sourceMap: false,
};
this.hooks.rollupPluginOptions.nodePolyfills.call(nodePolyfillsOptions);
this.rollupPlugins.push(rollupPluginNodePolyfills(nodePolyfillsOptions));
await this.hooks.rollupPlugins.promise({
preRollupPlugins: this.preRollupPlugins,
rollupPlugins: this.rollupPlugins,
postRollupPlugins: this.postRollupPlugins,
});
const finalPlugins = [
...this.preRollupPlugins,
...this.rollupPlugins,
...this.postRollupPlugins,
];
this.inputOptions.plugins = finalPlugins;
this.outputOptions.plugins = finalPlugins;
}
}
export { Compilation };