Hi, I’m Xiaoyu Xiaoyu, dedicated to sharing interesting and practical technical articles. Content is divided into translation and original, if you have any questions, feel free to comment or private letter, I hope to progress with you. Everyone’s support is my motivation to create.

plan

The Rollup series is intended to be chapter by chapter, more concise, more focused and easier to understand

It is currently intended to be divided into the following chapters:

  • rollup.rollup
  • rollup.generate + rollup.write
  • Rollup. watch <==== current article
  • tree shaking
  • plugins

TL; DR

A picture is worth a thousand words!

Pay attention to the point

All the notes are here and can be read by yourself

!!!!!!!!! Tip => labeled TODO for specific implementation details, will be analyzed as the case may be.

!!!!!!!!! Note that => each subtitle is an internal implementation of the parent title (function)

!!!!!!!!! We can return the id of the file that we want to return (i.e., the address, relative path, and decision path) for rollup to load

Rollup is a core that only does the most basic things, such as providing a default loading mechanism for modules (files), such as packaging different styles of content, our plug-in provides a path for loading files, parsing file contents (handling TS, Sass, etc.), and so on. It is a pluggable design. Similar to Webpack, plug and pull is a very flexible and long-term iterative design, which is also the core of a large framework, there is power in numbers

Main generic modules and their meanings

  1. Graph: a globally unique Graph containing entries and dependencies, operations, caches, etc. Is the heart of rollup
  2. PathTracker: The side-effect-free module relies on path tracking
  3. PluginDriver: Plug-in driver, call plug-in, provide plug-in environment context, etc
  4. FileEmitter: Resource operator
  5. GlobalScope: GlobalScope, as opposed to local scope
  6. ModuleLoader: ModuleLoader
  7. NodeBase: The construction base class of the AST syntax (ArrayExpression, AwaitExpression, etc.)

Code parsing

  • Two methods and three classes

That’s right, there are five points, and each point is doing its job

First there is the main class: Watcher, which gets the configuration passed by the user, then creates the Task instance, and then invokes the Run method of the Watcher instance on the next event poll to initiate the rollup build. Watcher returns an Emitter object that allows users to add hook functions and close Watcher.

class Watcher {
	constructor(configs: GenericConfigObject[] | GenericConfigObject) {
		this.emitter = new (class extends EventEmitter {
			close: (a)= > void;
			constructor(close: () => void) {
				super(a);// For the user to turn off enable
				this.close = close;
				/ / no warning
				// Allows more than 10 bundles to be watched without
				// showing the `MaxListenersExceededWarning` to the user.
				this.setMaxListeners(Infinity);
			}
		})(this.close.bind(this)) as RollupWatcher;

		this.tasks = (Array.isArray(configs) ? configs : configs ? [configs] : []).map(
			config= > new Task(this, config) // One configuration entry one task, executed sequentially
		);
		this.running = true;
		process.nextTick((a)= > this.run());
	}
    
    private run() {
		this.running = true;

		// When emit 'event' events, it is passed to cli for use. It is similar to the hook function by code. We can also use the added listening event to do what we want to do
		this.emit('event', {
			code: 'START'
		});

        // Initialize promise
		let taskPromise = Promise.resolve();
        // Execute tasks sequentially
		for (const task of this.tasks) taskPromise = taskPromise.then((a)= > task.run());

		return taskPromise
			.then((a)= > {
				this.running = false;

				this.emit('event', {
					code: 'END'
				});
			})
			.catch(error= > {
				this.running = false;
				this.emit('event', {
					code: 'ERROR',
					error
				});
			})
			.then((a)= > {
				if (this.rerun) {
					this.rerun = false;
					this.invalidate(); }}); }}Copy the code

Then there are tasks, the Task class, which performs rollup build tasks with a single function. When we create a new Task, the Task constructor initializes the configuration for rollup construction, which includes the input configuration, output configuration, Chokidar configuration, and user-filtered files. A rollup build occurs when task.run() is executed, and each task is cached with the build result for rebuilding when the file changes or deleting the task when the listener is closed.

class Task {
	constructor(watcher: Watcher, config: GenericConfigObject) {
		// Get the Watch instance
		this.watcher = watcher;

		this.closed = false;
		this.watched = new Set(a);const { inputOptions, outputOptions } = mergeOptions({
			config
		});
		this.inputOptions = inputOptions;

		this.outputs = outputOptions;
		this.outputFiles = this.outputs.map(output= > {
			if (output.file || output.dir) returnpath.resolve(output.file || output.dir!) ;return undefined as any;
		});

		const watchOptions: WatcherOptions = inputOptions.watch || {};
		if ('useChokidar' in watchOptions)
			(watchOptions as any).chokidar = (watchOptions as any).useChokidar;

		let chokidarOptions = 'chokidar' inwatchOptions ? watchOptions.chokidar : !! chokidar;if(chokidarOptions) { chokidarOptions = { ... (chokidarOptions ===true ? {} : chokidarOptions),
				disableGlobbing: true.ignoreInitial: true
			};
		}

		if(chokidarOptions && ! chokidar) {throw new Error(
				`watch.chokidar was provided, but chokidar could not be found. Have you installed it? `
			);
		}

		this.chokidarOptions = chokidarOptions as WatchOptions;
		this.chokidarOptionsHash = JSON.stringify(chokidarOptions);

		this.filter = createFilter(watchOptions.include, watchOptions.exclude);
	}

    // Close: clear task
	close() {
		this.closed = true;
		for (const id of this.watched) {
			deleteTask(id, this.this.chokidarOptionsHash);
		}
	}

	invalidate(id: string, isTransformDependency: boolean) {
		this.invalidated = true;
		if (isTransformDependency) {
			for (const module of this.cache.modules) {
				if (module.transformDependencies.indexOf(id) === - 1) continue;
				// effective invalidation
				module.originalCode = null asany; }}// Call invalidate on watcher
		this.watcher.invalidate(id);
	}

	run() {
        / / throttling
		if (!this.invalidated) return;
		this.invalidated = false;

		constoptions = { ... this.inputOptions,cache: this.cache
		};

		const start = Date.now();
			
        / / hooks
		this.watcher.emit('event', {
			code: 'BUNDLE_START'.input: this.inputOptions.input,
			output: this.outputFiles
		});
		
        // Pass the watcher instance for the rollup method to listen for change and restart to trigger the watchChange hook
		setWatcher(this.watcher.emitter);
		return rollup(options)
			.then(result= > {
				if (this.closed) return undefined as any;
				this.updateWatchedFiles(result);
				return Promise.all(this.outputs.map(output= > result.write(output))).then((a)= > result);
			})
			.then((result: RollupBuild) = > {
				this.watcher.emit('event', {
					code: 'BUNDLE_END'.duration: Date.now() - start,
					input: this.inputOptions.input,
					output: this.outputFiles,
					result
				});
			})
			.catch((error: RollupError) = > {
				if (this.closed) return;

				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); }throw error;
			});
	}

	private updateWatchedFiles(result: RollupBuild) {
		// The last listening set
		const previouslyWatched = this.watched;
		// Create a listener set
		this.watched = new Set(a);// Assign watchFiles to the listening files obtained at build time
		this.watchFiles = result.watchFiles;
		this.cache = result.cache;
		// Add the listening file to the listening set
		for (const id of this.watchFiles) {
			this.watchFile(id);
		}
		for (const module of this.cache.modules) {
			for (const depId of module.transformDependencies) {
				this.watchFile(depId, true); }}// Delete the file that was monitored last time
		for (const id of previouslyWatched) {
			if (!this.watched.has(id)) deleteTask(id, this.this.chokidarOptionsHash);
		}
	}

	private watchFile(id: string, isTransformDependency = false) {
		if (!this.filter(id)) return;
		this.watched.add(id);

		if (this.outputFiles.some(file= > file === id)) {
			throw new Error('Cannot import the generated bundle');
		}

		// Add tasks
		// this is necessary to ensure that any 'renamed' files
		// continue to be watched following an error
		addTask(id, this.this.chokidarOptions, this.chokidarOptionsHash, isTransformDependency); }}Copy the code

So far, we know what happens when we execute rollup.watch, but how does rollup listen for changes to rebuild when we modify the file?

This involves the two methods mentioned in the title, one is addTask, one is deleteTask, the two methods are very simple, is to add and delete tasks, here do not explain, browse by yourself. Add creates a new task that calls the last unmentioned class FileWatcher, which, yes, is used to listen for changes.

FileWatcher initializes the listening task and listens to files using either Chokidar or Node’s built-in fs.watch fault tolerance, depending on whether chokidarOptions are passed.

// addTask
const watcher = group.get(id) || new FileWatcher(id, chokidarOptions, group);
Copy the code

The invalidate method is triggered when a file changes

invalidate(id: string, isTransformDependency: boolean) {
    this.invalidated = true;
    if (isTransformDependency) {
        for (const module of this.cache.modules) {
            if (module.transformDependencies.indexOf(id) === - 1) continue;
            // effective invalidation
            module.originalCode = null asany; }}// Call invalidate on watcher
    this.watcher.invalidate(id);
}
Copy the code

Invalidate method on Watcher

invalidate(id? : string) {if (id) {
        this.invalidatedIds.add(id);
    }
	// Prevent brushing
    if (this.running) {
        this.rerun = true;
        return;
    }
	
	// clear pre
    if (this.buildTimeout) clearTimeout(this.buildTimeout);

    this.buildTimeout = setTimeout((a)= > {
        this.buildTimeout = null;
        for (const id of this.invalidatedIds) {
            // Triggers the event that rollup. Rollup listens for
            this.emit('change', id);
        }
        this.invalidatedIds.clear();
        // Triggers the event that rollup. Rollup listens for
        this.emit('restart');
        // Go through the build again
        this.run();
    }, DELAY);
}
Copy the code

The FileWatcher class is as follows and can be read by itself


class FileWatcher {

	constructor(id: string, chokidarOptions: WatchOptions, group: Map<string, FileWatcher>) {
		this.id = id;
		this.tasks = new Set(a);this.transformDependencyTasks = new Set(a);let modifiedTime: number;

		// File status
		try {
			const stats = fs.statSync(id);
			modifiedTime = +stats.mtime;
		} catch (err) {
			if (err.code === 'ENOENT') {
				// can't watch files that don't exist (e.g. injected
				// by plugins somehow)
				return;
			}
			throw err;
		}

		// Handle different update states of files
		const handleWatchEvent = (event: string) = > {
			if (event === 'rename' || event === 'unlink') {
				// Triggered when renaming link
				this.close();
				group.delete(id);
				this.trigger(id);
				return;
			} else {
				let stats: fs.Stats;
				try {
					stats = fs.statSync(id);
				} catch (err) {
					// The file cannot be found
					if (err.code === 'ENOENT') {
						modifiedTime = - 1;
						this.trigger(id);
						return;
					}
					throw err;
				}
				// Restart the build and avoid repeated operations
				// debounce
				if (+stats.mtime - modifiedTime > 15) this.trigger(id); }};// Handle all file update status via handleWatchEvent
		this.fsWatcher = chokidarOptions
			? chokidar.watch(id, chokidarOptions).on('all', handleWatchEvent)
			: fs.watch(id, opts, handleWatchEvent);

		group.set(id, this);
	}

	addTask(task: Task, isTransformDependency: boolean) {
		if (isTransformDependency) this.transformDependencyTasks.add(task);
		else this.tasks.add(task);
	}

	close() {
		// Disable file listening
		if (this.fsWatcher) this.fsWatcher.close();
	}

	deleteTask(task: Task, group: Map<string, FileWatcher>) {
		let deleted = this.tasks.delete(task);
		deleted = this.transformDependencyTasks.delete(task) || deleted;

		if (deleted && this.tasks.size === 0 && this.transformDependencyTasks.size === 0) {
			group.delete(this.id);
			this.close();
		}
	}

	trigger(id: string) {
		for (const task of this.tasks) {
			task.invalidate(id, false);
		}
		for (const task of this.transformDependencyTasks) {
			task.invalidate(id, true); }}}Copy the code

conclusion

Rollup function of the watch is very clear, it is worth our using for reference to study, but he didn’t take the content into memory, but generated directly, speed will be slightly smaller than speaking, but this may have been the plugin support, do not discuss here, how do we know he is in motion, want to add something at random, dry it out, friends.

What will be coming next, plugin or tree shaking? Let me know if you have any ideas.

That’s about it. A little digression.

Time flies, the “winter vacation” is probably coming to an end, I always thought it would be great if I could work from home, but now I have experienced it, how can I do it?

Efficiency ah, a week’s work, two days to finish, also have time to do their own things, that feeling is not too cool, ha ha ha

It is estimated that the number of people who have this idea should also be part of it. Maybe there will be cloud office in the future, where everyone is an outsourcing company

Another thought came to mind:

A blunt soldier is sharpened, but with a whole body of work, the princes rise by its evils. Although there are wise men, they cannot be good afterwards. Therefore soldiers smell clumsy speed, not to see the long also.

Zeng Guofan understood the slow speed as slow preparation and fast operation.

That’s true. We should treat every need that way, be prepared and be quick, but that may not always be the case in the corporate world.


If this article is a little help to you, I hope to get your support, this is my biggest motivation, bye bye ~