Skip to content

Commit

Permalink
feat: parallel ast operations
Browse files Browse the repository at this point in the history
  • Loading branch information
hansSchall committed Sep 7, 2024
1 parent 98f2af7 commit d7afae3
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 50 deletions.
12 changes: 10 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
"name": "@deno-plc/vite-plugin-deno",
"version": "2.0.3",
"exports": "./mod.ts",
"compilerOptions": {
"lib": [
"deno.window",
"deno.ns",
"ESNext",
"webworker"
]
},
"fmt": {
"indentWidth": 4,
"lineWidth": 120
},
"tasks": {
"check": "deno fmt && deno lint && deno publish --dry-run --allow-dirty && deno test --parallel",
"check-ci": "deno fmt --check && deno lint && deno publish --dry-run && deno test --parallel"
"check": "deno fmt && deno lint && deno publish --dry-run --allow-dirty && deno test -A --parallel",
"check-ci": "deno fmt --check && deno lint && deno publish --dry-run && deno test -A --parallel"
},
"lint": {
"rules": {
Expand Down
10 changes: 5 additions & 5 deletions src/ast-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
import { assert } from "@std/assert";
import { has_default_export } from "./ast-ops.ts";

Deno.test("has default export", () => {
assert(has_default_export(`export default function foo(){}`) === true);
assert(has_default_export(`function foo(){}\n export {foo as default};`) === true);
assert(has_default_export(`// export default`) === false);
assert(has_default_export(`// export {foo as default}`) === false);
Deno.test("has default export", async () => {
assert(await has_default_export(`export default function foo(){}`) === true);
assert(await has_default_export(`function foo(){}\n export {foo as default};`) === true);
assert(await has_default_export(`// export default`) === false);
assert(await has_default_export(`// export {foo as default}`) === false);
});
63 changes: 24 additions & 39 deletions src/ast-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,26 @@
* USA or see <https://www.gnu.org/licenses/>.
*/

import { transform } from "lebab";
import { parse } from "acorn";
import { simple as walk_simple } from "acorn-walk";
import { WorkerPool } from "./ast-pool.ts";

export function toESM(raw_code: string) {
const pool = new WorkerPool(navigator.hardwareConcurrency ?? 4);

export async function toESM(raw_code: string, id: string) {
if (raw_code.includes("require") || raw_code.includes("module")) {
const { code, warnings } = transform(
return (await pool.run({
task_id: pool.get_task_id(),
kind: "cjs-to-esm",
id,
raw_code,
["commonjs"],
);
for (const $ of warnings) {
console.log($);
}
return code;
})).code!;
} else {
return raw_code;
}
return raw_code;
}

const default_export_cache = new Map<string, boolean>();
const default_export_cache = new Map<string, Promise<boolean> | boolean>();

export function has_default_export(code: string, id: string = code) {
export async function has_default_export(code: string, id: string = code): Promise<boolean> {
if (default_export_cache.has(id)) {
return default_export_cache.get(id)!;
}
Expand All @@ -50,30 +49,16 @@ export function has_default_export(code: string, id: string = code) {
return false;
}

const ast = parse(code, {
ecmaVersion: 2023,
sourceType: "module",
});

let hasDefaultExport = false;

walk_simple(ast, {
ExportDefaultDeclaration(_node) {
hasDefaultExport = true;
},
ExportNamedDeclaration(node) {
if (node.specifiers) {
for (const specifier of node.specifiers) {
// @ts-ignore missing typedef
if (specifier.type === "ExportSpecifier" && specifier.exported?.name === "default") {
hasDefaultExport = true;
}
}
}
},
});

default_export_cache.set(id, hasDefaultExport);
const pr = (async () =>
(await pool.run({
task_id: pool.get_task_id(),
kind: "default-exports",
id,
raw_code: code,
})).has_default_export!)();

return hasDefaultExport;
default_export_cache.set(id, pr);
const has_default_export = await pr;
default_export_cache.set(id, has_default_export);
return has_default_export;
}
81 changes: 81 additions & 0 deletions src/ast-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @license LGPL-2.1-or-later
*
* vite-plugin-deno
*
* Copyright (C) 2024 Hans Schallmoser
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA or see <https://www.gnu.org/licenses/>.
*/

import type { AstResult, AstTask } from "./ast-worker.ts";
import { assert } from "@std/assert/assert";

export class WorkerPool {
constructor(readonly concurrency: number) {
assert(concurrency > 0);
assert(concurrency % 1 === 0); // int
for (let i = 0; i < concurrency; i++) {
this.#pool.push(this.#spawn_worker());
}
}
#spawn_worker() {
const worker = new Worker(new URL("./ast-worker.ts", import.meta.url), {
type: "module",
});
worker.addEventListener("message", (ev) => {
const result = ev.data as AstResult;
const listener = this.#listener.get(result.task_id);
listener?.(result);
this.#listener.delete(result.task_id);

const waiting = this.#waiting.shift();
if (waiting) {
waiting(worker);
} else {
this.#free_pool.push(worker);
}
});
this.#free_pool.push(worker);
return worker;
}
#pool: Worker[] = [];
#free_pool: Worker[] = [];
#waiting: ((worker: Worker) => void)[] = [];
#listener = new Map<number, (res: AstResult) => void>();
#run_task(task: AstTask, worker: Worker): Promise<AstResult> {
return new Promise((resolve) => {
this.#listener.set(task.task_id, resolve);
worker.postMessage(task);
});
}
public run(task: AstTask): Promise<AstResult> {
const worker = this.#free_pool.shift();
if (worker) {
return this.#run_task(task, worker);
} else {
return new Promise((resolve) => {
this.#waiting.push((worker) => {
this.#run_task(task, worker).then(resolve);
});
});
}
}
#task_id = 0;
public get_task_id(): number {
return this.#task_id++;
}
}
93 changes: 93 additions & 0 deletions src/ast-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @license LGPL-2.1-or-later
*
* vite-plugin-deno
*
* Copyright (C) 2024 Hans Schallmoser
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA or see <https://www.gnu.org/licenses/>.
*/

import { transform } from "lebab";
import { parse } from "acorn";
import { simple as walk_simple } from "acorn-walk";

export interface AstTask {
task_id: number;
kind: "cjs-to-esm" | "default-exports";
id: string;
raw_code: string;
}

export interface AstResult {
task_id: number;
code?: string;
has_default_export?: boolean;
}

function run_task(task: AstTask): Omit<AstResult, "task_id"> {
if (task.kind === "cjs-to-esm") {
const { code, warnings } = transform(
task.raw_code,
["commonjs"],
);
for (const $ of warnings) {
console.log($);
}

return {
code,
};
} else {
const ast = parse(task.raw_code, {
ecmaVersion: 2023,
sourceType: "module",
});

let has_default_export = false;

walk_simple(ast, {
ExportDefaultDeclaration(_node) {
has_default_export = true;
},
ExportNamedDeclaration(node) {
if (node.specifiers) {
for (const specifier of node.specifiers) {
// @ts-ignore missing typedef
if (specifier.type === "ExportSpecifier" && specifier.exported?.name === "default") {
has_default_export = true;
}
}
}
},
});

return {
has_default_export,
};
}
}

self.onmessage = (e) => {
const task = e.data as AstTask;

const res: AstResult = {
task_id: task.task_id,
...run_task(task),
};

self.postMessage(res);
};
16 changes: 12 additions & 4 deletions src/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,32 @@ export class NPMPackage {
}
}

const npm_data_transform_cache = new Map<string, string>();
const npm_data_transform_cache = new Map<string, string | Promise<string>>();

export async function getNPMData(specifier: ModuleSpecifier) {
if (npm_data_transform_cache.has(specifier.href)) {
return npm_data_transform_cache.get(specifier.href)!;
}

const pr = getNPMDataInner(specifier);
npm_data_transform_cache.set(specifier.href, pr);
const code = await pr;
npm_data_transform_cache.set(specifier.href, code);
return code;
}

async function getNPMDataInner(specifier: ModuleSpecifier) {
const id = parseNPMExact(specifier.pathname);
assert(id);
const raw_code = await Deno.readTextFile(join(await getNPMPath(id.name, id.version), id.path));
const code = toESM(raw_code);
npm_data_transform_cache.set(specifier.href, code);
const code = toESM(raw_code, specifier.href);
return code;
}

export async function get_npm_import_link(info: NPMImportInfo): Promise<string> {
const importedCode = await getNPMData(info.module);

if (has_default_export(importedCode, info.specifier.href)) {
if (await has_default_export(importedCode, info.specifier.href)) {
return `
// npm:${info.package.name}@${format(info.package.version)}
Expand Down

0 comments on commit d7afae3

Please sign in to comment.