Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: introduce parseAstAsync and parallelize parsing AST #5202

Merged
merged 10 commits into from
Oct 31, 2023
10 changes: 10 additions & 0 deletions browser/src/wasm.ts
@@ -1,2 +1,12 @@
// eslint-disable-next-line import/no-unresolved
export { parse, xxhash_base64_url as xxhashBase64Url } from '../../wasm/bindings_wasm.js';

// eslint-disable-next-line import/no-unresolved
import { parse } from '../../wasm/bindings_wasm.js';
export async function parseAsync(
code: string,
allowReturnOutsideFunction: boolean,
_signal?: AbortSignal | undefined | null
) {
return parse(code, allowReturnOutsideFunction);
}
26 changes: 25 additions & 1 deletion docs/javascript-api/index.md
Expand Up @@ -342,7 +342,7 @@ export default {
In order to parse arbitrary code using Rollup's parser, plugins can use [`this.parse`](../plugin-development/index.md#this-parse). To use this functionality outside the context of a Rollup build, the parser is also exposed as a separate export. It has the same signature as `this.parse`:

```js
import { parseAst } from 'rollup/parseAst';
import { parseAst, parseAstAsync } from 'rollup/parseAst';
import assert from 'node:assert';

assert.deepEqual(
Expand All @@ -368,4 +368,28 @@ assert.deepEqual(
sourceType: 'module'
}
);

assert.deepEqual(
await parseAstAsync('return 42;', { allowReturnOutsideFunction: true }),
{
type: 'Program',
start: 0,
end: 10,
body: [
{
type: 'ReturnStatement',
start: 0,
end: 10,
argument: {
type: 'Literal',
start: 7,
end: 9,
raw: '42',
value: 42
}
}
],
sourceType: 'module'
}
);
```
1 change: 1 addition & 0 deletions native.d.ts
Expand Up @@ -4,4 +4,5 @@
/* auto-generated by NAPI-RS */

export function parse(code: string, allowReturnOutsideFunction: boolean): Buffer
export function parseAsync(code: string, allowReturnOutsideFunction: boolean, signal?: AbortSignal | undefined | null): Promise<Buffer>
export function xxhashBase64Url(input: Uint8Array): string
3 changes: 2 additions & 1 deletion native.js
Expand Up @@ -46,9 +46,10 @@ If this is important to you, please consider supporting Rollup to make a native

const packageBase = imported.musl && isMusl() ? imported.musl : imported.base;
const localName = `./rollup.${packageBase}.node`;
const { parse, xxhashBase64Url } = require(
const { parse, parseAsync, xxhashBase64Url } = require(
existsSync(join(__dirname, localName)) ? localName : `@rollup/rollup-${packageBase}`
);

module.exports.parse = parse;
module.exports.parseAsync = parseAsync;
module.exports.xxhashBase64Url = xxhashBase64Url;
34 changes: 34 additions & 0 deletions rust/bindings_napi/src/lib.rs
Expand Up @@ -2,11 +2,45 @@ use napi::bindgen_prelude::*;
use napi_derive::napi;
use parse_ast::parse_ast;

pub struct ParseTask {
pub code: String,
pub allow_return_outside_function: bool,
}

#[napi]
impl Task for ParseTask {
type Output = Buffer;
type JsValue = Buffer;

fn compute(&mut self) -> Result<Self::Output> {
Ok(parse_ast(self.code.clone(), self.allow_return_outside_function).into())
}

fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
Ok(output)
}
}

#[napi]
pub fn parse(code: String, allow_return_outside_function: bool) -> Buffer {
parse_ast(code, allow_return_outside_function).into()
}

#[napi]
pub fn parse_async(
code: String,
allow_return_outside_function: bool,
signal: Option<AbortSignal>,
) -> AsyncTask<ParseTask> {
AsyncTask::with_optional_signal(
ParseTask {
code,
allow_return_outside_function,
},
signal,
)
}

#[napi]
pub fn xxhash_base64_url(input: Uint8Array) -> String {
xxhash::xxhash_base64_url(&input)
Expand Down
7 changes: 7 additions & 0 deletions scripts/publish-wasm-node-package.js
Expand Up @@ -14,6 +14,13 @@ const NATIVE_JS_CONTENT = `
const { parse } = require('./wasm-node/bindings_wasm.js');

exports.parse = parse
exports.parseAsync = async (
code,
allowReturnOutsideFunction,
_signal
) => {
return parse(code, allowReturnOutsideFunction);
}
`;

function getPath(...arguments_) {
Expand Down
16 changes: 12 additions & 4 deletions src/Module.ts
Expand Up @@ -68,7 +68,7 @@ import {
logShimmedExport,
logSyntheticNamedExportsNeedNamespaceExport
} from './utils/logs';
import { parseAst } from './utils/parseAst';
import { parseAst, parseAstAsync } from './utils/parseAst';
import {
doAttributesDiffer,
getAttributesFromImportExportDeclaration
Expand Down Expand Up @@ -785,7 +785,7 @@ export default class Module {
return { source, usesTopLevelAwait };
}

setSource({
async setSource({
ast,
code,
customTransformCache,
Expand All @@ -799,7 +799,7 @@ export default class Module {
}: TransformModuleJSON & {
resolvedIds?: ResolvedIdMap;
transformFiles?: EmittedFile[] | undefined;
}): void {
}): Promise<void> {
if (code.startsWith('#!')) {
const shebangEndPosition = code.indexOf('\n');
this.shebang = code.slice(2, shebangEndPosition);
Expand Down Expand Up @@ -831,7 +831,7 @@ export default class Module {
this.transformDependencies = transformDependencies;
this.customTransformCache = customTransformCache;
this.updateOptions(moduleOptions);
const moduleAst = ast ?? this.tryParse();
const moduleAst = ast ?? (await this.tryParseAsync());

timeEnd('generate ast', 3);
timeStart('analyze ast', 3);
Expand Down Expand Up @@ -1334,6 +1334,14 @@ export default class Module {
return this.error(logModuleParseError(error_, this.id), error_.pos);
}
}

private async tryParseAsync(): Promise<ProgramAst> {
try {
return (await parseAstAsync(this.info.code!)) as ProgramAst;
} catch (error_: any) {
return this.error(logModuleParseError(error_, this.id), error_.pos);
}
}
}

// if there is a cyclic import in the reexport chain, we should not
Expand Down
4 changes: 2 additions & 2 deletions src/ModuleLoader.ts
Expand Up @@ -311,10 +311,10 @@ export class ModuleLoader {
for (const emittedFile of cachedModule.transformFiles)
this.pluginDriver.emitFile(emittedFile);
}
module.setSource(cachedModule);
await module.setSource(cachedModule);
} else {
module.updateOptions(sourceDescription);
module.setSource(
await module.setSource(
await transform(sourceDescription, module, this.pluginDriver, this.options.onLog)
);
}
Expand Down
10 changes: 10 additions & 0 deletions src/rollup/types.d.ts
Expand Up @@ -206,6 +206,16 @@ export type ParseAst = (
options?: { allowReturnOutsideFunction?: boolean }
) => AstNode;

// declare AbortSignal here for environments without DOM lib or @types/node
declare global {
interface AbortSignal {}
}

export type ParseAstAsync = (
input: string,
options?: { allowReturnOutsideFunction?: boolean; signal?: AbortSignal }
) => Promise<AstNode>;

export interface PluginContext extends MinimalPluginContext {
addWatchFile: (id: string) => void;
cache: PluginCache;
Expand Down
17 changes: 14 additions & 3 deletions src/utils/parseAst.ts
@@ -1,10 +1,21 @@
import { parse } from '../../native';
import type { ParseAst } from '../rollup/types';
import { parse, parseAsync } from '../../native';
import type { ParseAst, ParseAstAsync } from '../rollup/types';
import { convertProgram } from './convert-ast';
import getReadStringFunction from './getReadStringFunction';

export const parseAst: ParseAst = (input, { allowReturnOutsideFunction = false } = {}) => {
const astBuffer = parse(input, allowReturnOutsideFunction);
const readString = getReadStringFunction(astBuffer);
return convertProgram(astBuffer.buffer, readString);
const result = convertProgram(astBuffer.buffer, readString);
return result;
};

export const parseAstAsync: ParseAstAsync = async (
input,
{ allowReturnOutsideFunction = false, signal } = {}
) => {
const astBuffer = await parseAsync(input, allowReturnOutsideFunction, signal);
const readString = getReadStringFunction(astBuffer);
const result = convertProgram(astBuffer.buffer, readString);
return result;
};
3 changes: 2 additions & 1 deletion src/utils/parseAstType.d.ts
@@ -1,3 +1,4 @@
import type { ParseAst } from '../rollup/types';
import type { ParseAst, ParseAstAsync } from '../rollup/types';

export const parseAst: ParseAst;
export const parseAstAsync: ParseAstAsync;
36 changes: 35 additions & 1 deletion test/misc/parse-ast.js
@@ -1,5 +1,5 @@
const assert = require('node:assert');
const { parseAst } = require('../../dist/parseAst');
const { parseAst, parseAstAsync } = require('../../dist/parseAst');

describe('parseAst', () => {
it('parses an AST', async () => {
Expand Down Expand Up @@ -109,3 +109,37 @@ describe('parseAst', () => {
assert.ok(key !== value);
});
});

describe('parseAstAsync', () => {
it('parses an AST', async () => {
assert.deepStrictEqual(await parseAstAsync('console.log("ok")'), {
type: 'Program',
start: 0,
end: 17,
body: [
{
type: 'ExpressionStatement',
start: 0,
end: 17,
expression: {
type: 'CallExpression',
start: 0,
end: 17,
arguments: [{ type: 'Literal', start: 12, end: 16, raw: '"ok"', value: 'ok' }],
callee: {
type: 'MemberExpression',
start: 0,
end: 11,
computed: false,
object: { type: 'Identifier', start: 0, end: 7, name: 'console' },
optional: false,
property: { type: 'Identifier', start: 8, end: 11, name: 'log' }
},
optional: false
}
}
],
sourceType: 'module'
});
});
});