-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* checkpoint import from node_modules * it works! * minify * only resolve bare imports * coalesce imports; ignore errors * preload transitive dependencies * DRY parseImports * parseImports tests * fix windows? * add logging * test only windows * don’t resolve input * restore tests * build _node * fix import order * 30s timeout * adopt @rollup/plugin-node-resolve * more tests * fix file path resolution * restore tests * add missing dependency * adopt pkg-dir --------- Co-authored-by: Philippe Rivière <[email protected]>
- Loading branch information
Showing
25 changed files
with
365 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import {existsSync} from "node:fs"; | ||
import {copyFile, readFile, writeFile} from "node:fs/promises"; | ||
import {createRequire} from "node:module"; | ||
import op from "node:path"; | ||
import {extname, join} from "node:path/posix"; | ||
import {pathToFileURL} from "node:url"; | ||
import {nodeResolve} from "@rollup/plugin-node-resolve"; | ||
import {packageDirectory} from "pkg-dir"; | ||
import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup"; | ||
import {rollup} from "rollup"; | ||
import esbuild from "rollup-plugin-esbuild"; | ||
import {prepareOutput, toOsPath} from "./files.js"; | ||
import type {ImportReference} from "./javascript/imports.js"; | ||
import {isJavaScript, parseImports} from "./javascript/imports.js"; | ||
import {parseNpmSpecifier} from "./npm.js"; | ||
import {isPathImport} from "./path.js"; | ||
import {faint} from "./tty.js"; | ||
|
||
export async function resolveNodeImport(root: string, spec: string): Promise<string> { | ||
return resolveNodeImportInternal(op.join(root, ".observablehq", "cache", "_node"), root, spec); | ||
} | ||
|
||
const bundlePromises = new Map<string, Promise<void>>(); | ||
|
||
async function resolveNodeImportInternal(cacheRoot: string, packageRoot: string, spec: string): Promise<string> { | ||
const {name, path = "."} = parseNpmSpecifier(spec); | ||
const require = createRequire(pathToFileURL(op.join(packageRoot, "/"))); | ||
const pathResolution = require.resolve(spec); | ||
const packageResolution = await packageDirectory({cwd: op.dirname(pathResolution)}); | ||
if (!packageResolution) throw new Error(`unable to resolve package.json: ${spec}`); | ||
const {version} = JSON.parse(await readFile(op.join(packageResolution, "package.json"), "utf-8")); | ||
const resolution = `${name}@${version}/${extname(path) ? path : path === "." ? "index.js" : `${path}.js`}`; | ||
const outputPath = op.join(cacheRoot, toOsPath(resolution)); | ||
if (!existsSync(outputPath)) { | ||
let promise = bundlePromises.get(outputPath); | ||
if (!promise) { | ||
promise = (async () => { | ||
process.stdout.write(`${spec} ${faint("→")} ${resolution}\n`); | ||
await prepareOutput(outputPath); | ||
if (isJavaScript(pathResolution)) { | ||
await writeFile(outputPath, await bundle(spec, cacheRoot, packageResolution)); | ||
} else { | ||
await copyFile(pathResolution, outputPath); | ||
} | ||
})(); | ||
bundlePromises.set(outputPath, promise); | ||
promise.catch(() => {}).then(() => bundlePromises.delete(outputPath)); | ||
} | ||
await promise; | ||
} | ||
return `/_node/${resolution}`; | ||
} | ||
|
||
/** | ||
* Resolves the direct dependencies of the specified node import path, such as | ||
* "/_node/[email protected]/src/index.js", returning a set of node import paths. | ||
*/ | ||
export async function resolveNodeImports(root: string, path: string): Promise<ImportReference[]> { | ||
if (!path.startsWith("/_node/")) throw new Error(`invalid node path: ${path}`); | ||
return parseImports(join(root, ".observablehq", "cache"), path); | ||
} | ||
|
||
/** | ||
* Given a local npm path such as "/_node/[email protected]/src/index.js", returns | ||
* the corresponding npm specifier such as "[email protected]/src/index.js". | ||
*/ | ||
export function extractNodeSpecifier(path: string): string { | ||
if (!path.startsWith("/_node/")) throw new Error(`invalid node path: ${path}`); | ||
return path.replace(/^\/_node\//, ""); | ||
} | ||
|
||
async function bundle(input: string, cacheRoot: string, packageRoot: string): Promise<string> { | ||
const bundle = await rollup({ | ||
input, | ||
plugins: [ | ||
nodeResolve({browser: true, rootDir: packageRoot}), | ||
importResolve(input, cacheRoot, packageRoot), | ||
esbuild({ | ||
target: ["es2022", "chrome96", "firefox96", "safari16", "node18"], | ||
exclude: [], // don’t exclude node_modules | ||
minify: true | ||
}) | ||
], | ||
onwarn(message, warn) { | ||
if (message.code === "CIRCULAR_DEPENDENCY") return; | ||
warn(message); | ||
} | ||
}); | ||
try { | ||
const output = await bundle.generate({format: "es"}); | ||
const code = output.output.find((o): o is OutputChunk => o.type === "chunk")!.code; // TODO don’t assume one chunk? | ||
return code; | ||
} finally { | ||
await bundle.close(); | ||
} | ||
} | ||
|
||
function importResolve(input: string, cacheRoot: string, packageRoot: string): Plugin { | ||
async function resolve(specifier: string | AstNode): Promise<ResolveIdResult> { | ||
return typeof specifier !== "string" || // AST node? | ||
isPathImport(specifier) || // relative path, e.g., ./foo.js | ||
/^\w+:/.test(specifier) || // windows file path, https: URL, etc. | ||
specifier === input // entry point | ||
? null // don’t do any additional resolution | ||
: {id: await resolveNodeImportInternal(cacheRoot, packageRoot, specifier), external: true}; // resolve bare import | ||
} | ||
return { | ||
name: "resolve-import", | ||
resolveId: resolve, | ||
resolveDynamicImport: resolve | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ import {simple} from "acorn-walk"; | |
import {rsort, satisfies} from "semver"; | ||
import {isEnoent} from "./error.js"; | ||
import type {ExportNode, ImportNode, ImportReference} from "./javascript/imports.js"; | ||
import {findImports, isImportMetaResolve} from "./javascript/imports.js"; | ||
import {isImportMetaResolve, parseImports} from "./javascript/imports.js"; | ||
import {parseProgram} from "./javascript/parse.js"; | ||
import type {StringLiteral} from "./javascript/source.js"; | ||
import {getStringLiteralValue, isStringLiteral} from "./javascript/source.js"; | ||
|
@@ -252,30 +252,14 @@ export async function resolveNpmImport(root: string, specifier: string): Promise | |
return `/_npm/${name}@${await resolveNpmVersion(root, {name, range})}/${path.replace(/\+esm$/, "_esm.js")}`; | ||
} | ||
|
||
const npmImportsCache = new Map<string, Promise<ImportReference[]>>(); | ||
|
||
/** | ||
* Resolves the direct dependencies of the specified npm path, such as | ||
* "/_npm/[email protected]/_esm.js", returning the corresponding set of npm paths. | ||
*/ | ||
export async function resolveNpmImports(root: string, path: string): Promise<ImportReference[]> { | ||
if (!path.startsWith("/_npm/")) throw new Error(`invalid npm path: ${path}`); | ||
let promise = npmImportsCache.get(path); | ||
if (promise) return promise; | ||
promise = (async function () { | ||
try { | ||
const filePath = await populateNpmCache(root, path); | ||
if (!/\.(m|c)?js$/i.test(path)) return []; // not JavaScript; TODO traverse CSS, too | ||
const source = await readFile(filePath, "utf-8"); | ||
const body = parseProgram(source); | ||
return findImports(body, path, source); | ||
} catch (error: any) { | ||
console.warn(`unable to fetch or parse ${path}: ${error.message}`); | ||
return []; | ||
} | ||
})(); | ||
npmImportsCache.set(path, promise); | ||
return promise; | ||
await populateNpmCache(root, path); | ||
return parseImports(join(root, ".observablehq", "cache"), path); | ||
} | ||
|
||
/** | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
test/input/packages/node_modules/test-browser-condition/browser.js
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
1 change: 1 addition & 0 deletions
1
test/input/packages/node_modules/test-browser-condition/default.js
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.