The original rollup that simple piece of rollup.generate + rollup.write

Hello everyone, I am a light rain light rain, committed to sharing interesting and useful technical articles.
Translation and original content is divided into, if there are questions, please feel free to comment or private letter, and we hope that progress.
Easy to share, hoping to get everyone's support and concern.

plan

rollup series intends to release a chapter by chapter, leaner and more specific content easier to understand

Currently we intend to look into chapters:

  • rollup.rollup
  • rollup.generate + rollup.write <==== Current Articles
  • rollup.watch
  • Achieve plugin

TL;DR

Text book connected, we know the configuration of the inlet rollup.rollup been resolved, depending mount, the data of these operations, one of chunks are eventually returned, then return a number of ways:

rollup() {
    const chunks = await graph.build();
    return {
        generate,
        // ...
    }
}
这其中利用了闭包的原理,以便后续方法可以访问到rollup结果

This issue, we would generate in-depth approach to look at it内心世界

Or the old routine, before looking at the code, the first white whole process under the words, rollup.generate () in the following steps:

  1. Configuration standardization, create a plug-in drive
  2. chunks, assets collected
  3. preserveModules mode processing
  4. Pre-rendered
  5. chunk optimization
  6. Source render
  7. Output filtering, sorting

Recently seen such a sentence:

"The person wisdom, letters, Ren, Yong, Yan also '

Refers to the quality of the person, the order represents the importance of each capability:

Chi: Veda, strategy
faith: faith, credit
Jen: benevolence, reputation
courage: bravery, decisive
Yan: iron law, Notary

Today, still work, even if it is placed in the field. Although not directly take over, but the content is the same.

We want to do it in this line, first to its own hardware (Chi), then the output quality (the letter), tacit cooperation among colleagues (ren), judge of things (Yong) and the requirements of the team as well as rewards and penalties ( strict).

important point

!!! version => read the rollup version: 1.32.0

Tip! => Marked with TODO to specific implementation details will be subject to analysis.

Note! => Inside each sub-title is achieved parent title (function)

!!! emphasize => rollup in module (file) is the file id of address, so similar meaning resolveID this is to parse the file address, we can return to the file id we want to return (that is, an address, a relative path, the path decision) to let rollup load

It is a rollup core, only the most basic things, such as providing a default module (file) loading mechanism , such as content packaged into different styles, our plug-in provides a path to load the file, parse the file contents (processing ts, sass, etc.) and other operations, is a plug-in design, and webpack similar
plug-in is a very flexible and long-term iterative updated design, which is the core of a large framework, Rencheliliangda ~

The main module and universal meaning

  1. Graph: FIG globally unique, comprising an inlet and various dependent relationships, methods of operation, caching. It is the core of rollup
  2. PathTracker: no side effects depend on the path tracking module
  3. PluginDriver: Plug-drive, calling the plug-ins and provide environmental context, etc.
  4. FileEmitter: Resource operator
  5. GlobalScope: Global role Bureau, there are relatively localized
  6. ModuleLoader: Module Loader
  7. NodeBase: ast respective syntax (ArrayExpression, AwaitExpression etc.) base class constructor

Main flow analysis

  • generate method:

Call encapsulated built private method returns Promise, one by one, the first term getOutputOptionsAndPluginDriver;

generate: ((rawOutputOptions: GenericConfigObject) => {
    // 过滤output配置选项,并创建output的插件驱动器
    const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver(
        rawOutputOptions
    );
    const promise = generate(outputOptions, false, outputPluginDriver).then(result =>
        createOutput(result)
    );
    // 丢弃老版本字段
    Object.defineProperty(promise, 'code', throwAsyncGenerateError);
    Object.defineProperty(promise, 'map', throwAsyncGenerateError);
    return promise;
})
  • getOutputOptionsAndPluginDriver:

The output method configured to generate and output a standardized configuration plug drive

PluginDriver class exposes methods createOutputPluginDriver

class PluginDriver {
    // ...
    public createOutputPluginDriver(plugins: Plugin[]): PluginDriver {
        return new PluginDriver(
            this.graph,
            plugins,
            this.pluginCache,
            this.preserveSymlinks,
            this.watcher,
            this
        );
    }
    // ...
}

The reference method to create output plug-in drive: graph.pluginDriver.createOutputPluginDriver

const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver(
    // 统一化插件
    normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX)
);

Generating a standard output simpler configuration, before calling rollup.rollup used in the method for extracting of MergeOptions (Reference mergeOptions.ts) input configuration method, after acquiring the configuration process, calls outputOptionshook function, this hook can be read soon to pass to generate write configuration / make changes, but more rollup recommended operational changes such as in renderStart in. After a series eventually returned check AnalyzingourputOptions

function normalizeOutputOptions(
    inputOptions: GenericConfigObject,
    rawOutputOptions: GenericConfigObject,
    hasMultipleChunks: boolean,
    outputPluginDriver: PluginDriver
): OutputOptions {
    const mergedOptions = mergeOptions({
        config: {
            output: {
                ...rawOutputOptions,
                // 可以用output里的覆盖
                ...(rawOutputOptions.output as object),
                // 不过input里的output优先级最高,但是不是每个地方都返回,有的不会使用
                ...(inputOptions.output as object)
            }
        }
    });

    // 如果merge过程中出错了
    if (mergedOptions.optionError) throw new Error(mergedOptions.optionError);

    // 返回的是数组,但是rollup不支持数组,所以获取第一项,目前也只会有一项
    const mergedOutputOptions = mergedOptions.outputOptions[0];

    const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) =>
        result || outputOptions;

    // 触发钩子函数
    const outputOptions = outputPluginDriver.hookReduceArg0Sync(
        'outputOptions',
        [mergedOutputOptions],
        outputOptionsReducer,
        pluginContext => {
            const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook());
            return {
                ...pluginContext,
                emitFile: emitError,
                setAssetSource: emitError
            };
        }
    );

    // 检查经过插件处理过的output配置
    checkOutputOptions(outputOptions);

    // output.file 和 output.dir是互斥的
    if (typeof outputOptions.file === 'string') {
        if (typeof outputOptions.dir === 'string')
            return error({
                code: 'INVALID_OPTION',
                message:
                    'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.'
            });
        if (inputOptions.preserveModules) {
            return error({
                code: 'INVALID_OPTION',
                message:
                    'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.'
            });
        }
        if (typeof inputOptions.input === 'object' && !Array.isArray(inputOptions.input))
            return error({
                code: 'INVALID_OPTION',
                message: 'You must set "output.dir" instead of "output.file" when providing named inputs.'
            });
    }

    if (hasMultipleChunks) {
        if (outputOptions.format === 'umd' || outputOptions.format === 'iife')
            return error({
                code: 'INVALID_OPTION',
                message: 'UMD and IIFE output formats are not supported for code-splitting builds.'
            });
        if (typeof outputOptions.file === 'string')
            return error({
                code: 'INVALID_OPTION',
                message:
                    'You must set "output.dir" instead of "output.file" when generating multiple chunks.'
            });
    }

    return outputOptions;
}
  • generate internal generatemethod

After obtaining the output plug and the mating drive after standardization method to generate a built-in, which accepts three parameters, wherein the second parameter identifies whether to write, that is the method used to generate simultaneous write and next in.

First name of the user-defined access to resources, if not the default value

const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]';

Acquiring chunks of the directory intersection, which is the common root directory

const inputBase = commondir(getAbsoluteEntryModulePaths(chunks));

Get all chunks id getAbsoluteEntryModulePaths absolute path, commondir reference node-commondir module, the principle is to get the path of a file transfer be split into an array (set A), then traverse all the remaining file id, for comparison, find the index is not equal, then re-assigned to a, for the next cycle, until the end, we get a public directory.

function commondir(files: string[]) {
    if (files.length === 0) return '/';
    if (files.length === 1) return path.dirname(files[0]);
    const commonSegments = files.slice(1).reduce((commonSegments, file) => {
        const pathSegements = file.split(/\/+|\\+/);
        let i;
        for (
            i = 0;
            commonSegments[i] === pathSegements[i] &&
            i < Math.min(commonSegments.length, pathSegements.length);
            i++
        );
        return commonSegments.slice(0, i);
    }, files[0].split(/\/+|\\+/));

    // Windows correctly handles paths with forward-slashes
    return commonSegments.length > 1 ? commonSegments.join('/') : '/';
}

Create an all objects chunks of information and assets included

const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null);

SetOutputBundle calls the plug on the drive to set the output created above outputBundleWithPlaceholderson.

outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames);

setOutputBundle on FileEmitter class implementation, instances of the class on the insert driver (PluginDriver), and assign a common plug drive method.
reserveFileNameInBundle method on outputBundleWithPlaceholders mount the file chunks.
finalizeAsset method only processing resources, the resource after formatting, added to the outputBundleWithPlaceholders. The format is:

{
    fileName,
    get isAsset(): true {
        graph.warnDeprecation(
            'Accessing "isAsset" on files in the bundle is deprecated, please use "type === \'asset\'" instead',
            false
        );

        return true;
    },
    source,
    type: 'asset'
};
class FileEmitter {
    // ...
    setOutputBundle = (
        outputBundle: OutputBundleWithPlaceholders,
        assetFileNames: string
    ): void => {
        this.output = {
            // 打包出来的命名
            assetFileNames,
            // 新建的空对象 => Object.create(null)
            bundle: outputBundle
        };
        // filesByReferenceId是通过rollup.rollup中emitChunks的时候设置的,代表已使用的chunks
        // 处理文件
        for (const emittedFile of this.filesByReferenceId.values()) {
            if (emittedFile.fileName) {
                // 文件名挂在到this.output上,作为key,值为: FILE_PLACEHOLDER
                reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph);
            }
        }
        // 遍历set 处理资源
        for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) {
            // 插件中定义了source的情况
            if (consumedFile.type === 'asset' && consumedFile.source !== undefined) {
                // 给this.output上绑定资源
                this.finalizeAsset(consumedFile, consumedFile.source, referenceId, this.output);
            }
        }
    };
    // ...
}

Call the renderStarthook function is used to access the output and input configuration, and we may see a lot of method calls the hook function, such as hookParallel, hookSeq etc., which are used to trigger the plug-in hook functions provided, but the implementation is different, some parallel, some serial, and some can only be performed by a so on, which would have taken it alone.

await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]);

Execution footer banner intro outro hook function, internal functions that perform these hooks, the default value option [footer | banner | intro | outro], and finally returns the string result to be spliced.

const addons = await createAddons(outputOptions, outputPluginDriver);

PreserveModules processing mode, that is, whether the pack as little as possible, rather than each module is a chunk
if it is packaged as little as possible, it will be exported chunks of multi-mount to the exportNames chunks of property, for later use
if each module is a chunk, then deduced derivation pattern

for (const chunk of chunks) {
    // 尽可能少的打包模块
    // 设置chunk的exportNames
    if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions);

    // 尽可能多的打包模块
    if (inputOptions.preserveModules || (chunk.facadeModule && chunk.facadeModule.isEntryPoint))
        // 根据导出,去推断chunk的导出模式
        chunk.exportMode = getExportMode(chunk, outputOptions, chunk.facadeModule!.id);
}

Pre-rendered chunks.
Using magic-string module source management, initialization render configuration, dependency parsing, added to the current dependencies properties chunks to sort a list of dependency are in execution order, the processing module is ready dynamic introduced, provided unique identifier (?)

for (const chunk of chunks) {
    chunk.preRender(outputOptions, inputBase);
}

Optimization chunks

if (!optimized && inputOptions.experimentalOptimizeChunks) {
    optimizeChunks(chunks, outputOptions, inputOptions.chunkGroupingSize!, inputBase);
    optimized = true;
}

The chunkId assigned to outputBundleWithPlaceholders created above

assignChunkIds(
    chunks,
    inputOptions,
    outputOptions,
    inputBase,
    addons,
    outputBundleWithPlaceholders,
    outputPluginDriver
);

Set up chunks of the object, that is, in accordance with chunks id set to outputBundleWithPlaceholders, this time has a complete chunk information on outputBundleWithPlaceholders

outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders);

Syntax tree parsing code generation operation, and finally return outputBundle.

await Promise.all(
    chunks.map(chunk => {
        const outputChunk = outputBundleWithPlaceholders[chunk.id!] as OutputChunk;
        return chunk
            .render(outputOptions, addons, outputChunk, outputPluginDriver)
            .then(rendered => {
                // 引用类型,outputBundleWithPlaceholders上的也变化了,所以outputBundle也变化了,最后返回outputBundle
                outputChunk.code = rendered.code;
                outputChunk.map = rendered.map;

                return outputPluginDriver.hookParallel('ongenerate', [
                    { bundle: outputChunk, ...outputOptions },
                    outputChunk
                ]);
            });
    })
);

return outputBundle;
  • generate internal createOutputmethod

receiving a return value createOutput generate, and the generated filter and sort OutputBundle

function createOutput(outputBundle: Record<string, OutputChunk | OutputAsset | {}>): RollupOutput {
    return {
        output: (Object.keys(outputBundle)
            .map(fileName => outputBundle[fileName])
            .filter(outputFile => Object.keys(outputFile).length > 0) as (
            | OutputChunk
            | OutputAsset
        )[]).sort((outputFileA, outputFileB) => {
            const fileTypeA = getSortingFileType(outputFileA);
            const fileTypeB = getSortingFileType(outputFileB);
            if (fileTypeA === fileTypeB) return 0;
            return fileTypeA < fileTypeB ? -1 : 1;
        }) as [OutputChunk, ...(OutputChunk | OutputAsset)[]]
    };
}
  • rollup.write

write method and generate almost the same method, but is the second parameter generate method is true, for generateBundle hook function is used, it has been shown that the current stage is wirte or generate.
After the acquisition is the current number of chunks, export more time will detect the configuration file and then throw an error and sourcemapFile

let chunkCount = 0; //计数
for (const fileName of Object.keys(bundle)) {
    const file = bundle[fileName];
    if (file.type === 'asset') continue;
    chunkCount++;
    if (chunkCount > 1) break;
}
if (chunkCount > 1) {
    // sourcemapFile配置
    if (outputOptions.sourcemapFile)
        return error({
            code: 'INVALID_OPTION',
            message: '"output.sourcemapFile" is only supported for single-file builds.'
        });
    // file字段
    if (typeof outputOptions.file === 'string')
        return error({
            code: 'INVALID_OPTION',
            message:
                'When building multiple chunks, the "output.dir" option must be used, not "output.file".' +
                (typeof inputOptions.input !== 'string' ||
                inputOptions.inlineDynamicImports === true
                    ? ''
                    : ' To inline dynamic imports, set the "inlineDynamicImports" option.')
        });
}

After calling a write method: writeOutputFile

await Promise.all(
    Object.keys(bundle).map(chunkId =>
        writeOutputFile(result, bundle[chunkId], outputOptions, outputPluginDriver)
    )
);

The method is very intuitive writeOutputFile, resolution path

const fileName = resolve(outputOptions.dir || dirname(outputOptions.file!), outputFile.fileName);

After different treatment based on the type of chunk, assets direct access to the code, chunks if needed according to sourcemap option sourcemp appended to the code.

if (outputFile.type === 'asset') {
    source = outputFile.source;
} else {
    source = outputFile.code;
    if (outputOptions.sourcemap && outputFile.map) {
        let url: string;
        if (outputOptions.sourcemap === 'inline') {
            url = outputFile.map.toUrl();
        } else {
            url = `${basename(outputFile.fileName)}.map`;
            writeSourceMapPromise = writeFile(`${fileName}.map`, outputFile.map.toString());
        }
        if (outputOptions.sourcemap !== 'hidden') {
            source += `//# ${SOURCEMAPPING_URL}=${url}\n`;
        }
    }
}

The last call fs module for file creation and content can be written

function writeFile(dest: string, data: string | Buffer) {
    return new Promise<void>((fulfil, reject) => {
        mkdirpath(dest);

        fs.writeFile(dest, data, err => {
            if (err) {
                reject(err);
            } else {
                fulfil();
            }
        });
    });
}

These are the analysis section of the code flow, with reference to the specific details of the code base Notes

Specific features of the analysis section

  • slightly

to sum up

With the deepening Read on to discover rollup many details of the operation, are complex and require more time to polish the words, being the first analysis of the main flow, specific implementation details such as optimization chunks, etc., as the case after prerender talk about it.

But also learned a few things, rollup of all types ast divided into one class, a class devoted to a type ast, only when calls need to traverse ast body, get each item type, and then dynamically call on it , it is to use. For students ast no imagery can be seen here => ast online analytical

rollup to a package from the build, through three major steps:

Loading, parsing => analysis (dependency analysis, citations, useless analysis module, the type of analysis) => generates

Seemingly simple, but in reality complex. Like it a point to rollup.

Guess you like

Origin www.cnblogs.com/xiaoyuxy/p/12526650.html