rollup 打包结果其中一个文件数据结构:
{
"assertions": {},
"ast": {
// 当前模块 ast
},
"code": "import _createClass from \"@babel/runtime/helpers/createClass\";\nimport _classCallCheck from \"@babel/runtime/helpers/classCallCheck\";\nimport _defineProperty from \"@babel/runtime/helpers/defineProperty\";\nimport \"core-js/modules/es7.array.includes.js\";\nimport a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nvar b = require(\"path\");\nvar fn = function fn() {\n console.log(\"index fn243\", a);\n console.log(packageJosn.version);\n console.log(b);\n};\nfn();\nvar A = /*#__PURE__*/_createClass(function A() {\n _classCallCheck(this, A);\n _defineProperty(this, \"a\", void 0);\n this.a = \"ww2\";\n});\nvar aa = new A();\nvar res = [];\nconsole.log(res.includes(\"ff\"));\nconsole.log(foo);\nexport default fn;",
"customTransformCache": false,
"dependencies": [
"/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/createClass.js",
"/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/classCallCheck.js",
"/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/defineProperty.js",
"core-js/modules/es7.array.includes.js",
"a-test",
"/Users/xxx/Desktop/demo/foo.js",
"/Users/xxx/Desktop/demo/package.json"
],
"id": "/Users/xxx/Desktop/demo/index.js",
"meta": {
"commonjs": {
"hasDefaultExport": true,
"isCommonJS": false
}
},
"moduleSideEffects": true,
"originalCode": "import a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nconst b = require(\"path\");\n\nconst fn = () => {\n console.log(\"index fn243\", a);\n console.log(packageJosn.version);\n console.log(b);\n};\nfn();\n\nclass A {\n a;\n constructor() {\n this.a = \"ww2\";\n }\n}\n\nconst aa = new A();\n\nlet res = [];\nconsole.log(res.includes(\"ff\"));\n\nconsole.log(foo);\n\nexport default fn;\n",
"originalSourcemap": null,
"resolvedIds": {
"./package.json": {
"assertions": {},
"external": false,
"id": "/Users/xxx/Desktop/demo/package.json",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "node-resolve",
"syntheticNamedExports": false
},
"core-js/modules/es7.array.includes.js": {
"assertions": {},
"external": true,
"id": "core-js/modules/es7.array.includes.js",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "rollup",
"syntheticNamedExports": false
},
"./foo.js": {
"assertions": {},
"external": false,
"id": "/Users/xxx/Desktop/demo/foo.js",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "node-resolve",
"syntheticNamedExports": false
},
"a-test": {
"assertions": {},
"external": true,
"id": "a-test",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "rollup",
"syntheticNamedExports": false
},
"@babel/runtime/helpers/defineProperty": {
"assertions": {},
"external": false,
"id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/defineProperty.js",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "node-resolve",
"syntheticNamedExports": false
},
"@babel/runtime/helpers/createClass": {
"assertions": {},
"external": false,
"id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/createClass.js",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "node-resolve",
"syntheticNamedExports": false
},
"@babel/runtime/helpers/classCallCheck": {
"assertions": {},
"external": false,
"id": "/Users/xxx/Desktop/demo/node_modules/@babel/runtime/helpers/esm/classCallCheck.js",
"meta": {},
"moduleSideEffects": true,
"resolvedBy": "node-resolve",
"syntheticNamedExports": false
}
},
"sourcemapChain": [
{
"version": 3,
"names": [
"a",
"foo",
"packageJosn",
"b",
"require",
"fn",
"console",
"log",
"version",
"A",
"_createClass",
"_classCallCheck",
"_defineProperty",
"aa",
"res",
"includes"
],
"sources": [
"index.js"
],
"sourcesContent": [
"import a from \"a-test\";\nimport foo from \"./foo.js\";\nimport packageJosn from \"./package.json\";\nconst b = require(\"path\");\n\nconst fn = () => {\n console.log(\"index fn243\", a);\n console.log(packageJosn.version);\n console.log(b);\n};\nfn();\n\nclass A {\n a;\n constructor() {\n this.a = \"ww2\";\n }\n}\n\nconst aa = new A();\n\nlet res = [];\nconsole.log(res.includes(\"ff\"));\n\nconsole.log(foo);\n\nexport default fn;\n"
],
"mappings": [
// ...
],
"syntheticNamedExports": false,
"transformDependencies": [
"./index2.ts"
],
"transformFiles": [
{
"type": "asset",
"source": "export default what"
}
]
}
export default function watch(configs: RollupOptions[] | RollupOptions): RollupWatcher {
const emitter = new WatchEmitter() as RollupWatcher; // 对外暴露给用户注册 watch 过程相关的事件订阅器
// 真正内部实现监听的方法
watchInternal(configs, emitter).catch(error => {
handleError(error);
});
return emitter; // 对外暴露给用户注册的和 watch 相关的 emitter 事件 ,注册的事件会在 watchInternal 中运行各个阶段触发
}
import type { AwaitedEventListener, AwaitingEventEmitter } from '../rollup/types';
export class WatchEmitter<T extends { [event: string]: (...parameters: any) => any }>
implements AwaitingEventEmitter<T>
{
private currentHandlers: {
[K in keyof T]?: AwaitedEventListener<T, K>[];
} = Object.create(null);
private persistentHandlers: {
[K in keyof T]?: AwaitedEventListener<T, K>[];
} = Object.create(null);
// Will be overwritten by Rollup
async close(): Promise<void> {}
emit<K extends keyof T>(event: K, ...parameters: Parameters<T[K]>): Promise<unknown> {
return Promise.all(
[...this.getCurrentHandlers(event), ...this.getPersistentHandlers(event)].map(handler =>
handler(...parameters)
)
);
}
off<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
const listeners = this.persistentHandlers[event];
if (listeners) {
// A hack stolen from "mitt": ">>> 0" does not change numbers >= 0, but -1
// (which would remove the last array element if used unchanged) is turned
// into max_int, which is outside the array and does not change anything.
listeners.splice(listeners.indexOf(listener) >>> 0, 1);
}
return this;
}
on<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
this.getPersistentHandlers(event).push(listener);
return this;
}
onCurrentRun<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
this.getCurrentHandlers(event).push(listener);
return this;
}
once<K extends keyof T>(event: K, listener: AwaitedEventListener<T, K>): this {
const selfRemovingListener: AwaitedEventListener<T, K> = (...parameters) => {
this.off(event, selfRemovingListener);
return listener(...parameters);
};
this.on(event, selfRemovingListener);
return this;
}
removeAllListeners(): this {
this.removeListenersForCurrentRun();
this.persistentHandlers = Object.create(null);
return this;
}
removeListenersForCurrentRun(): this {
this.currentHandlers = Object.create(null);
return this;
}
private getCurrentHandlers<K extends keyof T>(event: K): AwaitedEventListener<T, K>[] {
return this.currentHandlers[event] || (this.currentHandlers[event] = []);
}
private getPersistentHandlers<K extends keyof T>(event: K): AwaitedEventListener<T, K>[] {
return this.persistentHandlers[event] || (this.persistentHandlers[event] = []);
}
}
const optionsList = await Promise.all(ensureArray(configs).map(config => mergeOptions(config))); // 和内置 options 合并
const watchOptionsList = optionsList.filter(config => config.watch !== false); // 只拿到配置了 watch 的 options 配置
if (watchOptionsList.length === 0) {
return error(
errorInvalidOption(
'watch',
URL_WATCH,
'there must be at least one config where "watch" is not set to "false"'
)
);
}
await loadFsEvents(); // 加载 mac 下的监听文件变化的第三方库:fsevents
const { Watcher } = await import('./watch'); // 真正内部实现监听的 watcher
new Watcher(watchOptionsList, emitter);
export class Watcher {
readonly emitter: RollupWatcher;
private buildDelay = 0;
private buildTimeout: NodeJS.Timer | null = null;
private closed = false;
private readonly invalidatedIds = new Map<string, ChangeEvent>(); // 收集 watch 过程每一次变化的文件 id 及其对应的事件
private rerun = false;
private running = true;
private readonly tasks: Task[];
// optionsList 所有配置了 watch 的 option; emitter 暴露给用户注册 watch 过程相关事件的订阅器
constructor(optionsList: readonly MergedRollupOptions[], emitter: RollupWatcher) {
this.emitter = emitter;
emitter.close = this.close.bind(this);
this.tasks = optionsList.map(options => new Task(this, options)); // 每一个配置了 watch 的 option 都对应一个 Task,Task 内通过 FileWatcher 实现了通过 chokidar 进行监听
for (const { watch } of optionsList) {
// 每次重新运行的防抖时间
if (watch && typeof watch.buildDelay === 'number') {
this.buildDelay = Math.max(this.buildDelay, watch.buildDelay!);
}
}
// 初始执行
process.nextTick(() => this.run());
}
async close(): Promise<void> {
if (this.closed) return;
this.closed = true;
if (this.buildTimeout) clearTimeout(this.buildTimeout);
for (const task of this.tasks) {
task.close();
}
await this.emitter.emit('close');
this.emitter.removeAllListeners();
}
// 文件变化后调用此函数重新打包
invalidate(file?: { event: ChangeEvent; id: string }): void {
if (file) {
const previousEvent = this.invalidatedIds.get(file.id);
const event = previousEvent ? eventsRewrites[previousEvent][file.event] : file.event;
// 为每一个变换了的文件 id 设置对应的事件 "delete" | "add" ...
if (event === 'buggy') {
//TODO: throws or warn? Currently just ignore, uses new event
this.invalidatedIds.set(file.id, file.event);
} else if (event === null) {
this.invalidatedIds.delete(file.id);
} else {
this.invalidatedIds.set(file.id, event);
}
}
if (this.running) {
this.rerun = true;
return;
}
if (this.buildTimeout) clearTimeout(this.buildTimeout);
// 定时器防抖触发 rollup 重新 run
this.buildTimeout = setTimeout(async () => {
this.buildTimeout = null;
try {
// 文件变化后触发对应emitter
await Promise.all(
[...this.invalidatedIds].map(([id, event]) => this.emitter.emit('change', id, { event }))
);
this.invalidatedIds.clear();
await this.emitter.emit('restart');
this.emitter.removeListenersForCurrentRun(); // 取消当前过程注册的所有事件
// 重新打包
this.run();
} catch (error: any) {
this.invalidatedIds.clear();
await this.emitter.emit('event', {
code: 'ERROR',
error,
result: null
});
await this.emitter.emit('event', {
code: 'END'
});
}
}, this.buildDelay);
}
private async run(): Promise<void> {
this.running = true;
await this.emitter.emit('event', {
code: 'START'
});
// 根据配置项重新运行 rollup
for (const task of this.tasks) {
await task.run();
}
this.running = false;
await this.emitter.emit('event', {
code: 'END'
});
if (this.rerun) {
this.rerun = false;
this.invalidate();
}
}
}
export class Task {
cache: RollupCache = { modules: [] };
watchFiles: string[] = [];
private closed = false;
private readonly fileWatcher: FileWatcher; // 真正去实现监听的 watcher
private filter: (id: string) => boolean; // 过滤监听
private invalidated = true;
private readonly options: MergedRollupOptions;
private readonly outputFiles: string[];
private readonly outputs: OutputOptions[];
private skipWrite: boolean;
private watched = new Set<string>(); // 收集每次监听的文件 id
private readonly watcher: Watcher; // watcher 为管理 Task 的 Watcher
constructor(watcher: Watcher, options: MergedRollupOptions) {
this.watcher = watcher; // watcher 为管理 Task 的Watcher
this.options = options;
// 是否跳过输出文件
this.skipWrite = Boolean(options.watch && options.watch.skipWrite);
this.outputs = this.options.output;
// 解析输出文件地址的路径
this.outputFiles = this.outputs.map(output => {
if (output.file || output.dir) return resolve(output.file || output.dir!);
return undefined as never;
});
const watchOptions: WatcherOptions = this.options.watch || {};
// 创建 watch 中配置了 exclude、include 相关的过滤方法
this.filter = createFilter(watchOptions.include, watchOptions.exclude);
this.fileWatcher = new FileWatcher(this, {
...watchOptions.chokidar,
disableGlobbing: true,
ignoreInitial: true
});
}
close(): void {
this.closed = true;
this.fileWatcher.close();
}
// chokidar 监听到文件变化后触发 ,id 变化的文件 id; event 对应的文件变化类型; isTransformDependency 当前模块在插件中额外依赖的 id
invalidate(id: string, details: { event: ChangeEvent; isTransformDependency?: boolean }): void {
this.invalidated = true;
// 当前模块在 rollup 打包时插件中添加的依赖模块
// 当依赖模块更新后,该模块缓存失效清空
if (details.isTransformDependency) {
for (const module of this.cache.modules) {
// 插件里调用 this.addWatchFile 时,watch的文件会放进该模块的 transformDependencies 属性里
// 插件里调用 this.emitFile 生成的文件会放进该模块的 transformFiles 属性里
if (!module.transformDependencies.includes(id)) continue;
// effective invalidation
module.originalCode = null as never; // 清除缓存的内容,后续重新生成
}
}
this.watcher.invalidate({ event: details.event, id });
}
async run(): Promise<void> {
if (!this.invalidated) return; // 是否在运行中
this.invalidated = false;
const options = {
...this.options,
cache: this.cache
};
const start = Date.now();
await this.watcher.emitter.emit('event', {
code: 'BUNDLE_START',
input: this.options.input,
output: this.outputFiles
});
let result: RollupBuild | null = null;
try {
result = await rollupInternal(options, this.watcher.emitter); // 到此和非 watch 模式下一致
if (this.closed) {
return;
}
// 打包完成,获取到所有需要监听变化的文件 id
this.updateWatchedFiles(result);
this.skipWrite || (await Promise.all(this.outputs.map(output => result!.write(output))));
await this.watcher.emitter.emit('event', {
code: 'BUNDLE_END',
duration: Date.now() - start,
input: this.options.input,
output: this.outputFiles,
result
});
} catch (error: any) {
if (!this.closed) {
if (Array.isArray(error.watchFiles)) {
for (const id of error.watchFiles) {
this.watchFile(id);
}
}
if (error.id) {
this.cache.modules = this.cache.modules.filter(module => module.id !== error.id);
}
}
await this.watcher.emitter.emit('event', {
code: 'ERROR',
error,
result
});
}
}
// 添加打包过程需要的所有监听文件、清除不需要的监听文件
private updateWatchedFiles(result: RollupBuild) {
const previouslyWatched = this.watched;
this.watched = new Set();
// 返回本次打包运行监听的文件 id
this.watchFiles = result.watchFiles;
// 缓存打包结果
this.cache = result.cache!;
for (const id of this.watchFiles) {
this.watchFile(id);
}
// 获取缓存文件的 id
for (const module of this.cache.modules) {
// 在插件中添加的需要观测的依赖
for (const depId of module.transformDependencies) {
this.watchFile(depId, true);
}
}
// 取消监听上一轮不需要监听的文件 id
for (const id of previouslyWatched) {
if (!this.watched.has(id)) {
this.fileWatcher.unwatch(id);
}
}
}
private watchFile(id: string, isTransformDependency = false) {
if (!this.filter(id)) return;
this.watched.add(id); // 收集每次监听的文件 id
if (this.outputFiles.includes(id)) {
throw new Error('Cannot import the generated bundle');
}
// this is necessary to ensure that any 'renamed' files
// continue to be watched following an error
this.fileWatcher.watch(id, isTransformDependency);
}
}
import { platform } from 'node:os';
import chokidar, { type FSWatcher } from 'chokidar';
import type { ChangeEvent, ChokidarOptions } from '../rollup/types';
import type { Task } from './watch';
export class FileWatcher {
private readonly chokidarOptions: ChokidarOptions;
private readonly task: Task;
private readonly transformWatchers = new Map<string, FSWatcher>(); // 收集所有监听插件中添加的依赖文件的 chokdir watcher
private readonly watcher: FSWatcher; // 正常的 Task 对应的 chokidar watcher
constructor(task: Task, chokidarOptions: ChokidarOptions) {
this.chokidarOptions = chokidarOptions; // 用户传递的 chokidar 配置
this.task = task; // 每一个带有 watch 的 option 对应的 Task
this.watcher = this.createWatcher(null); // 通过 chokidar 监听的实例
}
close(): void {
this.watcher.close();
for (const watcher of this.transformWatchers.values()) {
watcher.close();
}
}
unwatch(id: string): void {
this.watcher.unwatch(id);
const transformWatcher = this.transformWatchers.get(id);
if (transformWatcher) {
this.transformWatchers.delete(id);
transformWatcher.close();
}
}
watch(id: string, isTransformDependency: boolean): void {
// 如果是插件中添加的依赖文件,单独生成一个 chokidar watcher
if (isTransformDependency) {
const watcher = this.transformWatchers.get(id) ?? this.createWatcher(id);
watcher.add(id);
this.transformWatchers.set(id, watcher);
} else {
this.watcher.add(id);
}
}
// transformWatcherId 区分是否是插件中添加的依赖发生了变化
private createWatcher(transformWatcherId: string | null): FSWatcher {
const task = this.task;
const isLinux = platform() === 'linux';
const isTransformDependency = transformWatcherId !== null;
// chokidar 监听到文件变化时触发的回调
const handleChange = (id: string, event: ChangeEvent) => {
const changedId = transformWatcherId || id;
if (isLinux) {
// unwatching and watching fixes an issue with chokidar where on certain systems,
// a file that was unlinked and immediately recreated would create a change event
// but then no longer any further events
watcher.unwatch(changedId);
watcher.add(changedId);
}
// 重新运行打包
task.invalidate(changedId, { event, isTransformDependency });
};
// 通过 chokidar 进行监听
const watcher = chokidar
.watch([], this.chokidarOptions)
.on('add', id => handleChange(id, 'create'))
.on('change', id => handleChange(id, 'update'))
.on('unlink', id => handleChange(id, 'delete'));
return watcher;
}
}