rollup-preserve-directives
Version:
Rollup plugin to preserve directives
169 lines (166 loc) • 8.01 kB
JavaScript
import { extname } from 'path';
import MagicString from 'magic-string';
const availableESExtensionsRegex = /\.(m|c)?(j|t)sx?$/;
const directiveRegex = /^use (\w+)$/;
function preserveDirectives() {
const meta = {
shebangs: new Map(),
directives: {}
};
return {
name: 'preserve-directives',
transform: {
order: 'post',
handler (code, id) {
const ext = extname(id);
if (!availableESExtensionsRegex.test(ext)) {
return null;
}
const magicString = new MagicString(code);
// MagicString's `hasChanged()` is slow, so we track the change manually
let hasChanged = false;
/**
* Here we are making 3 assumptions:
* - shebang can only be at the first line of the file, otherwise it will not be recognized
* - shebang can only contains one line
* - shebang must starts with # and !
*
* Those assumptions are also made by acorn, babel and swc:
*
* - acorn: https://github.com/acornjs/acorn/blob/8da1fdd1918c9a9a5748501017262ce18bb2f2cc/acorn/src/state.js#L78
* - babel: https://github.com/babel/babel/blob/86fee43f499c76388cab495c8dcc4e821174d4e0/packages/babel-parser/src/tokenizer/index.ts#L574
* - swc: https://github.com/swc-project/swc/blob/7bf4ab39b0e49759d9f5c8d7f989b3ed010d81a7/crates/swc_ecma_parser/src/lexer/mod.rs#L204
*/ if (code[0] === '#' && code[1] === '!') {
let firstNewLineIndex = 0;
for(let i = 2, len = code.length; i < len; i++){
const charCode = code.charCodeAt(i);
if (charCode === 10 || charCode === 13 || charCode === 0x2028 || charCode === 0x2029) {
firstNewLineIndex = i;
break;
}
}
if (firstNewLineIndex) {
meta.shebangs.set(id, code.slice(0, firstNewLineIndex));
magicString.remove(0, firstNewLineIndex + 1);
hasChanged = true;
}
}
/**
* rollup's built-in parser returns an extended version of ESTree Node.
*/ let ast = null;
try {
ast = this.parse(magicString.toString(), {
allowReturnOutsideFunction: true,
// @ts-expect-error
// rollup 2 built-in parser doesn't have `allowShebang`, we need to use the sliced code here. Hence the `magicString.toString()`
allowShebang: true
});
} catch (e) {
this.warn({
code: 'PARSE_ERROR',
message: `[rollup-preserve-directives]: failed to parse "${id}" and extract the directives. make sure you have added "rollup-preserve-directives" to the last of your plugins list, after swc/babel/esbuild/typescript or any other transform plugins.`
});
return null;
}
// Exit if the root of the AST is not a Program
if (ast.type !== 'Program') {
return null;
}
for (const node of ast.body){
// Only parse the top level directives, once reached to the first non statement literal node, stop parsing
if (node.type !== 'ExpressionStatement') {
break;
}
let directive = null;
/**
* rollup and estree defines `directive` field on the `ExpressionStatement` node:
* https://github.com/rollup/rollup/blob/fecf0cfe14a9d79bb0eff4ad475174ce72775ead/src/ast/nodes/ExpressionStatement.ts#L10
*/ if ('directive' in node) {
directive = node.directive;
} else if (node.expression.type === 'Literal' && typeof node.expression.value === 'string' && directiveRegex.test(node.expression.value)) {
directive = node.expression.value;
}
if (directive) {
var _meta_directives, _id;
(_meta_directives = meta.directives)[_id = id] || (_meta_directives[_id] = new Set());
meta.directives[id].add(directive);
/**
* rollup has extended acorn node with the `start` and the `end` field
* https://github.com/rollup/rollup/blob/fecf0cfe14a9d79bb0eff4ad475174ce72775ead/src/ast/nodes/shared/Node.ts#L33
*
* However, typescript doesn't know that, so we add type guards for typescript
* to infer.
*/ if ('start' in node && typeof node.start === 'number' && 'end' in node && typeof node.end === 'number') {
magicString.remove(node.start, node.end);
hasChanged = true;
}
}
}
if (!hasChanged) {
// If nothing has changed, we can avoid the expensive `toString()` and `generateMap()` calls
return null;
}
const metaState = {
preserveDirectives: {
directives: Array.from(meta.directives[id] || []),
shebang: meta.shebangs.get(id) || null
}
};
return {
code: magicString.toString(),
map: magicString.generateMap({
hires: true
}),
meta: metaState
};
}
},
renderChunk (code, chunk, { sourcemap }) {
/**
* chunk.moduleIds is introduced in rollup 3
* Add a fallback for rollup 2
*/ const moduleIds = 'moduleIds' in chunk ? chunk.moduleIds : Object.keys(chunk.modules);
const outputDirectives = moduleIds.map((id)=>{
if (meta.directives[id]) {
return meta.directives[id];
}
return null;
}).reduce((acc, directives)=>{
if (directives) {
directives.forEach((directive)=>acc.add(directive));
}
return acc;
}, new Set());
let magicString = null;
if (outputDirectives.size) {
magicString || (magicString = new MagicString(code));
magicString.prepend(`${Array.from(outputDirectives).map((directive)=>`'${directive}';`).join('\n')}\n`);
}
// determine if any of the modules have a shebang
// if so, prepend it to the output
const shebang = chunk.facadeModuleId ? meta.shebangs.get(chunk.facadeModuleId) || null : null;
if (shebang) {
magicString || (magicString = new MagicString(code));
magicString.prepend(`${shebang}\n`);
}
// Neither outputDirectives nor meta.shebang is present, no change is needed
if (!magicString) {
return null;
}
return {
code: magicString.toString(),
map: sourcemap ? magicString.generateMap({
hires: true
}) : null
};
},
onLog (level, log) {
if (log.code === 'MODULE_LEVEL_DIRECTIVE' && level === 'warn') {
return false;
}
return this.warn(log);
}
};
}
const preserveDirective = preserveDirectives;
export { preserveDirectives as default, preserveDirective };