diff --git a/.changeset/cuddly-comics-poke.md b/.changeset/cuddly-comics-poke.md new file mode 100644 index 0000000..cb12ed2 --- /dev/null +++ b/.changeset/cuddly-comics-poke.md @@ -0,0 +1,5 @@ +--- +"@arethetypeswrong/core": patch +--- + +Use `@loaderkit/resolve` for module resolution diff --git a/packages/core/package.json b/packages/core/package.json index a8fcf80..1d522d0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", "cjs-module-lexer": "^1.2.3", "fflate": "^0.8.2", "lru-cache": "^10.4.3", diff --git a/packages/core/src/internal/esm/cjsNamespace.ts b/packages/core/src/internal/esm/cjsNamespace.ts index 6aae5e5..45099a0 100644 --- a/packages/core/src/internal/esm/cjsNamespace.ts +++ b/packages/core/src/internal/esm/cjsNamespace.ts @@ -1,6 +1,6 @@ import { Package } from "../../createPackage.js"; import { getCjsModuleBindings } from "./cjsBindings.js"; -import { cjsResolve } from "./cjsResolve.js"; +import { cjsResolve } from "./resolve.js"; export function getCjsModuleNamespace(fs: Package, file: URL, seen = new Set()): Set { seen.add(file.pathname); @@ -17,9 +17,9 @@ export function getCjsModuleNamespace(fs: Package, file: URL, seen = new Set exports.add(name)); } } catch {} diff --git a/packages/core/src/internal/esm/cjsResolve.ts b/packages/core/src/internal/esm/cjsResolve.ts deleted file mode 100644 index 4a0582b..0000000 --- a/packages/core/src/internal/esm/cjsResolve.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Package } from "../../createPackage.js"; -import { - esmFileFormat, - lookupPackageScope, - packageExportsResolve, - packageImportsResolve, - readPackageJson, -} from "./esmResolve.js"; -import { nodeCoreModules } from "./nodeModules.js"; - -// require(X) from module at path Y -export function cjsResolve(fs: Package, fragment: string, parentURL: URL) { - // 1. If X is a core module, - // a. return the core module - // b. STOP - if (fragment.startsWith("node:")) { - return { format: "node", resolved: new URL(fragment) }; - } else if (nodeCoreModules.includes(fragment)) { - return { format: "node", resolved: new URL(`node:${fragment}`) }; - } - - // 2. If X begins with '/' - if (fragment.startsWith("/")) { - // a. set Y to be the file system root - // nb: omitted - throw new Error("not implemented"); - } - - // 3. If X begins with './' or '/' or '../' - if (fragment.startsWith("./") || fragment.startsWith("../")) { - // a. LOAD_AS_FILE(Y + X) - const asFile = loadAsFile(fs, fragment, parentURL); - if (asFile) { - return asFile; - } - - // b. LOAD_AS_DIRECTORY(Y + X) - const asDirectory = loadAsDirectory(fs, new URL(`${fragment}/`, parentURL)); - if (asDirectory) { - return asDirectory; - } - - // c. THROW "not found" - throw new Error("not found"); - } - - // 4. If X begins with '#' - if (fragment.startsWith("#")) { - // a. LOAD_PACKAGE_IMPORTS(X, dirname(Y)) - const asPackageImports = loadPackageImports(fs, fragment, new URL("./", parentURL)); - if (asPackageImports) { - return asPackageImports; - } - } - - // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) - const asSelf = loadPackageSelf(fs, fragment, new URL("./", parentURL)); - if (asSelf) { - return asSelf; - } - - // 6. LOAD_NODE_MODULES(X, dirname(Y)) - const asNodeModules = loadNodeModules(fs, fragment, new URL("./", parentURL)); - if (asNodeModules) { - return asNodeModules; - } - - // 7. THROW "not found" - throw new Error("not found"); -} - -// LOAD_AS_FILE(X) -function loadAsFile(fs: Package, fragment: string, parentURL: URL) { - // 1. If X is a file, load X as its file extension format. STOP - const asFile = new URL(fragment, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asFile))) { - return loadWithFormat(fs, asFile); - } - - // 2. If X.js is a file, load X.js as JavaScript text. STOP - const asJsFile = new URL(`${fragment}.js`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asJsFile))) { - return loadWithFormat(fs, asJsFile); - } - - // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP - const asJsonFile = new URL(`${fragment}.json`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asJsonFile))) { - return loadWithFormat(fs, asJsonFile); - } - - // 4. If X.node is a file, load X.node as binary addon. STOP - const asNodeFile = new URL(`${fragment}.node`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asNodeFile))) { - return { format: "node", resolved: asNodeFile }; - } -} - -// LOAD_INDEX(X) -function loadIndex(fs: Package, fragment: string, parentURL: URL) { - // 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP - const asJsIndex = new URL(`${fragment}/index.js`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asJsIndex))) { - return loadWithFormat(fs, asJsIndex); - } - - // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP - const asJsonIndex = new URL(`${fragment}/index.json`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asJsonIndex))) { - return loadWithFormat(fs, asJsonIndex); - } - - // 3. If X/index.node is a file, load X/index.node as binary addon. STOP - const asNodeIndex = new URL(`${fragment}/index.node`, parentURL); - if (fs.fileExists(verbatimFileURLToPath(asNodeIndex))) { - return { format: "native", resolved: asNodeIndex }; - } -} - -// LOAD_AS_DIRECTORY(X) -function loadAsDirectory(fs: Package, path: URL) { - // 1. If X/package.json is a file, - // a. Parse X/package.json, and look for "main" field. - const pjson = readPackageJson(fs, path); - // b. If "main" is a falsy value, GOTO 2. - if (pjson === null || !pjson.name) { - // c. let M = X + (json main field) - // d. LOAD_AS_FILE(M) - // e. LOAD_INDEX(M) - // f. LOAD_INDEX(X) DEPRECATED - // g. THROW "not found" - } - // 2. LOAD_INDEX(X) - return loadIndex(fs, ".", path); -} - -function loadWithFormat(fs: Package, resolved: URL) { - // nb: The algorithm doesn't specify this but the implementation seems to do something similar. - // You cannot require a bare `.js` file from a `.cjs` parent with a `{"type":"module"}` - // `package.json`. - const format = esmFileFormat(fs, resolved); - return { format, resolved }; -} - -// LOAD_NODE_MODULES(X, START) -function loadNodeModules(fs: Package, fragment: string, parentURL: URL) { - // 1. let DIRS = NODE_MODULES_PATHS(START) - // 2. for each DIR in DIRS: - for (const dir of nodeModulesPaths(parentURL)) { - // a. LOAD_PACKAGE_EXPORTS(X, DIR) - const asPackageExports = loadPackageExports(fs, fragment, dir); - if (asPackageExports) { - return asPackageExports; - } - - // b. LOAD_AS_FILE(DIR/X) - const asFile = loadAsFile(fs, fragment, dir); - if (asFile) { - return asFile; - } - - // c. LOAD_AS_DIRECTORY(DIR/X) - const asDirectory = loadAsDirectory(fs, new URL(`${fragment}/`, dir)); - if (asDirectory) { - return asDirectory; - } - } -} - -// NODE_MODULES_PATHS(START) -function* nodeModulesPaths(path: URL) { - // 1. let PARTS = path split(START) - // 2. let I = count of PARTS - 1 - // 3. let DIRS = [] - // 4. while I >= 0, - if (path.protocol !== "file:") { - return; - } - do { - // a. if PARTS[I] = "node_modules", GOTO d. - if (!path.pathname.endsWith("/node_modules/")) { - // b. DIR = path join(PARTS[0 .. I] + "node_modules") - // c. DIRS = DIR + DIRS - yield new URL("./node_modules/", path); - } - // d. let I = I - 1 - path = new URL("../", path); - } while (path.pathname !== "/"); - // 5. return DIRS + GLOBAL_FOLDERS -} - -// LOAD_PACKAGE_IMPORTS(X, DIR) -function loadPackageImports(fs: Package, fragment: string, parentURL: URL) { - // 1. Find the closest package scope SCOPE to DIR. - const packageURL = lookupPackageScope(fs, parentURL); - - // 2. If no scope was found, return. - if (packageURL === null) { - return; - } - - // 3. If the SCOPE/package.json "imports" is null or undefined, return. - const pjson = readPackageJson(fs, packageURL); - if (pjson.imports == null) { - return; - } - - // 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in - // the ESM resolver. - const match = packageImportsResolve(fs, fragment, packageURL, ["node", "require"]); - - // 5. RESOLVE_ESM_MATCH(MATCH). - return resolveEsmMatch(fs, match); -} - -// LOAD_PACKAGE_EXPORTS(X, DIR) -function loadPackageExports(fs: Package, fragment: string, parentURL: URL) { - // 1. Try to interpret X as a combination of NAME and SUBPATH where the name - // may have a @scope/ prefix and the subpath begins with a slash (`/`). - const matches = /^((?:@[^/]+\/)?[^/]+)(.*)$/.exec(fragment); - // 2. If X does not match this pattern or DIR/NAME/package.json is not a file, - // return. - if (matches === null) { - return; - } - const dir = new URL(`${matches[1]}/`, parentURL); - const subpath = matches[2]; - - // 3. Parse DIR/NAME/package.json, and look for "exports" field. - const pjson = readPackageJson(fs, dir); - if (pjson === null) { - return; - } - - // 4. If "exports" is null or undefined, return. - if (pjson.exports == null) { - return; - } - - // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, `package.json` - // "exports", ["node", "require"]) defined in the ESM resolver. - const match = packageExportsResolve(fs, dir, `.${subpath}`, pjson.exports, ["node", "require"]); - - // 6. RESOLVE_ESM_MATCH(MATCH) - return resolveEsmMatch(fs, match); -} - -// LOAD_PACKAGE_SELF(X, DIR) -function loadPackageSelf(fs: Package, fragment: string, parentURL: URL) { - // 1. Find the closest package scope SCOPE to DIR. - const packageURL = lookupPackageScope(fs, parentURL); - - // 2. If no scope was found, return. - if (packageURL === null) { - return; - } - - // 3. If the SCOPE/package.json "exports" is null or undefined, return. - const pjson = readPackageJson(fs, packageURL); - if (pjson.exports == null) { - return; - } - - // 4. If the SCOPE/package.json "name" is not the first segment of X, return. - if (fragment !== pjson.name && !fragment.startsWith(`${pjson.name}/`)) { - return; - } - - // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE), "." + X.slice("name".length), - // `package.json` "exports", ["node", "require"]) defined in the ESM resolver. - const match = packageExportsResolve(fs, packageURL, `./${fragment.slice(pjson.name.length)}`, pjson.exports, [ - "node", - "require", - ]); - - // 6. RESOLVE_ESM_MATCH(MATCH) - return resolveEsmMatch(fs, match); -} - -// RESOLVE_ESM_MATCH(MATCH) -function resolveEsmMatch(fs: Package, match: URL) { - // 1. let RESOLVED_PATH = fileURLToPath(MATCH) - const resolvedPath = verbatimFileURLToPath(match); - - // 2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension format. STOP - if (fs.fileExists(resolvedPath)) { - return loadWithFormat(fs, match); - } - - // 3. THROW "not found" - throw new Error("not found"); -} - -// nb: We use URLs for the path traversal convenience, but `require("./file.cjs?")` should be read -// as-is. -function verbatimFileURLToPath(url: URL) { - return `${url.pathname}${url.search}${url.hash}`; -} diff --git a/packages/core/src/internal/esm/esmNamespace.ts b/packages/core/src/internal/esm/esmNamespace.ts index ed99c16..80fb477 100644 --- a/packages/core/src/internal/esm/esmNamespace.ts +++ b/packages/core/src/internal/esm/esmNamespace.ts @@ -1,7 +1,7 @@ import { Package } from "../../createPackage.js"; import { getEsmModuleBindings } from "./esmBindings.js"; -import { esmResolve } from "./esmResolve.js"; import { getCjsModuleNamespace } from "./cjsNamespace.js"; +import { esmResolve } from "./resolve.js"; // Note: this doesn't handle ambiguous indirect exports which probably isn't worth the // implementation complexity. @@ -13,28 +13,28 @@ export function getEsmModuleNamespace( seen = new Set(), ): string[] { // Resolve specifier - const { format, resolved } = esmResolve(fs, specifier, parentURL); + const { format, url } = esmResolve(fs, specifier, parentURL); // Don't recurse for circular indirect exports - if (seen.has(resolved.pathname)) { + if (seen.has(url.pathname)) { return []; } - seen.add(resolved.pathname); + seen.add(url.pathname); if (format === "commonjs") { - return [...getCjsModuleNamespace(fs, resolved)]; + return [...getCjsModuleNamespace(fs, url)]; } // Parse module bindings const bindings = (format ?? "module") === "module" - ? getEsmModuleBindings(fs.readFile(resolved.pathname)) + ? getEsmModuleBindings(fs.readFile(url.pathname)) : // Maybe JSON, WASM, etc { exports: ["default"], reexports: [] }; // Concat indirect exports const indirect = bindings.reexports - .flatMap((specifier) => getEsmModuleNamespace(fs, specifier, resolved, seen)) + .flatMap((specifier) => getEsmModuleNamespace(fs, specifier, url, seen)) .filter((name) => name !== "default"); return [...new Set([...bindings.exports, ...indirect])]; } diff --git a/packages/core/src/internal/esm/esmResolve.ts b/packages/core/src/internal/esm/esmResolve.ts deleted file mode 100644 index 86ca56a..0000000 --- a/packages/core/src/internal/esm/esmResolve.ts +++ /dev/null @@ -1,689 +0,0 @@ -import { Package } from "../../createPackage.js"; -import { nodeCoreModules } from "./nodeModules.js"; - -// defaultConditions is the conditional environment name array, [ "node", "import" ]. -const defaultConditions = ["node", "import"]; - -// ESM_RESOLVE(specifier, parentURL) -export function esmResolve(fs: Package, specifier: string, parentURL: URL) { - // 1. Let resolved be undefined. - const resolved = (() => { - try { - // 2. If specifier is a valid URL, then - // 1. Set resolved to the result of parsing and reserializing specifier as a URL. - return new URL(specifier); - } catch {} - - // 3. Otherwise, if specifier starts with "/", "./", or "../", then - // 1. Set resolved to the URL resolution of specifier relative to parentURL. - if (/^(\/|\.\.?\/)/.test(specifier)) { - return new URL(specifier, parentURL); - } - - // 4. Otherwise, if specifier starts with "#", then - // 1. Set resolved to the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions). - if (specifier.startsWith("#")) { - return packageImportsResolve(fs, specifier, parentURL, defaultConditions); - } - - // 5. Otherwise, - // 1. Note: specifier is now a bare specifier. - // 2. Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL). - return packageResolve(fs, specifier, parentURL); - })(); - - // 6. Let format be undefined. - const format = (() => { - // 7. If resolved is a "file:" URL, then - if (resolved.protocol === "file:") { - // 1. If resolved contains any percent encodings of "/" or "\" ("%2F" and "%5C" respectively), then - if (/%2F|%5C/.test(resolved.href)) { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - - // 2. If the file at resolved is a directory, then - if (fs.directoryExists(resolved.pathname)) { - // 1. Throw an Unsupported Directory Import error. - throw new Error("Unsupported Directory Import"); - } - - // 3. If the file at resolved does not exist, then - if (!fs.fileExists(resolved.pathname)) { - // 1. Throw a Module Not Found error. - throw new Error("Module Not Found"); - } - - // 4. Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components. - // 5. Set format to the result of ESM_FILE_FORMAT(resolved). - return esmFileFormat(fs, resolved); - } - - // 8. Otherwise, - if (resolved.protocol === "node:") { - // 1. Set format the module format of the content type associated with the URL resolved. - return "node"; - } - - // nb: otherwise omitted - return; - })(); - - // 9. Return format and resolved to the loading phase - return { format, resolved }; -} - -// PACKAGE_RESOLVE(packageSpecifier, parentURL) -function packageResolve(fs: Package, packageSpecifier: string, parentURL: URL) { - // 1. Let packageName be undefined. - const packageName = (() => { - // 2. If packageSpecifier is an empty string, then - if (packageSpecifier === "") { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - - // 3. If packageSpecifier is a Node.js builtin module name, then - if (nodeCoreModules.includes(packageSpecifier)) { - // 1. Return the string "node:" concatenated with packageSpecifier. - return `node:${packageSpecifier}`; - } - - // 4. If packageSpecifier does not start with "@", then - if (!packageSpecifier.startsWith("@")) { - // 1. Set packageName to the substring of packageSpecifier until the first "/" separator or - // the end of the string. - return packageSpecifier.split("/")[0]; - } - - // 5. Otherwise, - const matches = /^([^/]*\/[^/]*)/.exec(packageSpecifier); - // 1. If packageSpecifier does not contain a "/" separator, then - if (matches === null) { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - // 2. Set packageName to the substring of packageSpecifier until the second "/" separator or the - // end of the string. - return matches[1]; - })(); - - // 6. If packageName starts with "." or contains "\" or "%", then - if (packageName.startsWith(".") || packageName.includes("\\") || packageName.includes("%")) { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - - // 7. Let packageSubpath be "." concatenated with the substring of packageSpecifier from the - // position at the length of packageName. - const packageSubpath = `.${packageSpecifier.substring(packageName.length)}`; - - // 8. If packageSubpath ends in "/", then - if (packageSubpath.endsWith("/")) { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - - // 9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL). - const selfUrl = packageSelfResolve(fs, packageName, packageSubpath, parentURL); - - // 10. If selfUrl is not undefined, return selfUrl. - if (selfUrl !== undefined) { - return selfUrl; - } - - // 11. While parentURL is not the file system root, - // nb: Modified to search up to "root" - do { - // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with - // packageSpecifier, relative to parentURL. - const packageURL = new URL(`node_modules/${packageSpecifier}/`, parentURL); - - // 2. Set parentURL to the parent folder URL of parentURL. - parentURL = new URL("../", parentURL); - - // 3. If the folder at packageURL does not exist, then - if (!fs.directoryExists(packageURL.pathname)) { - // 1. Continue the next loop iteration. - continue; - } - - // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL). - const pjson = readPackageJson(fs, packageURL); - - // 5. If pjson is not null and pjson.exports is not null or undefined, then - if (pjson !== null && pjson.exports != null) { - // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, - // defaultConditions). - return packageExportsResolve(fs, packageURL, packageSubpath, pjson.exports, defaultConditions); - } - - // 6. Otherwise, if packageSubpath is equal to ".", then - if (packageSubpath === ".") { - // 1. If pjson.main is a string, then - if (typeof pjson.main === "string") { - // 1. Return the URL resolution of main in packageURL. - return new URL(pjson.main, packageURL); - } - - // 2. Otherwise, - // 1. Return the URL resolution of packageSubpath in packageURL. - return new URL(packageSubpath, packageURL); - } - } while (parentURL.pathname !== "/"); - - // 12. Throw a Module Not Found error - throw new Error("Module Not Found"); -} - -// PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL) -function packageSelfResolve(fs: Package, packageName: string, packageSubpath: string, parentURL: URL) { - // 1. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL). - const packageURL = lookupPackageScope(fs, parentURL); - - // 2. If packageURL is null, then - if (packageURL === null) { - // 1. Return undefined. - return; - } - - // 3. Let pjson be the result of READ_PACKAGE_JSON(packageURL). - const pjson = readPackageJson(fs, packageURL); - - // 4. If pjson is null or if pjson.exports is null or undefined, then - if (pjson === null || pjson.exports == null) { - // 1. Return undefined. - return; - } - - // 5. If pjson.name is equal to packageName, then - if (pjson.name === packageName) { - // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions). - return packageExportsResolve(fs, packageURL, packageSubpath, pjson.exports, defaultConditions); - } - - // 6. Otherwise, return undefined. - return; -} - -// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) -export function packageExportsResolve( - fs: Package, - packageURL: URL, - subpath: string, - exports: unknown, - conditions: readonly string[], -) { - // 1. If exports is an Object with both a key starting with "." and a key not starting with ".", - // throw an Invalid Package Configuration error. - const exportsIsObject = typeof exports === "object" && exports !== null; - const exportsKeys = exportsIsObject ? Object.keys(exports) : undefined; - const hasDotKeys = exportsKeys?.some((key) => key.startsWith(".")); - const hasNonDotKeys = exportsKeys?.some((key) => !key.startsWith(".")); - if (hasDotKeys && hasNonDotKeys) { - throw new Error("Invalid Package Configuration"); - } - - // 2. If subpath is equal to ".", then - if (subpath === ".") { - // 1. Let mainExport be undefined. - const mainExport = (() => { - // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then - if (typeof exports === "string" || Array.isArray(exports) || !hasDotKeys) { - // 1. Set mainExport to exports. - return exports; - } - - // 3. Otherwise if exports is an Object containing a "." property, then - if (exportsIsObject && "." in exports) { - // 1. Set mainExport to the value of the "." property in exports. - return exports["."]; - } - })(); - - // 4. If mainExport is not undefined, then - if (mainExport !== undefined) { - // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE(packageURL, mainExport, null, - // false, conditions). - const resolved = packageTargetResolve(fs, packageURL, mainExport, null, false, conditions); - - // 2. If resolved is not null or undefined, return resolved. - if (resolved != null) { - return resolved; - } - } - } - - // 3. Otherwise, if exports is an Object and all keys of exports start with ".", then - if (exportsIsObject && hasDotKeys && !hasNonDotKeys) { - // 1. Assert: subpath begins with "./". - // 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE(subpath, exports, - // packageURL, false, conditions). - const resolved = packageImportsExportsResolve( - fs, - subpath, - exports satisfies object as Record, - packageURL, - false, - conditions, - ); - - // 3. If resolved is not null or undefined, return resolved. - if (resolved != null) { - return resolved; - } - } - - // 4. Throw a Package Path Not Exported error. - throw new Error("Package Path Not Exported"); -} - -// PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions) -export function packageImportsResolve(fs: Package, specifier: string, parentURL: URL, conditions: readonly string[]) { - // 1. Assert: specifier begins with "#". - // 2. If specifier is exactly equal to "#" or starts with "#/", then - if (specifier === "#" || specifier.startsWith("#/")) { - // 1. Throw an Invalid Module Specifier error. - throw new Error("Invalid Module Specifier"); - } - - // 3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL). - const packageURL = lookupPackageScope(fs, parentURL); - - // 4. If packageURL is not null, then - if (packageURL !== null) { - // 1. Let pjson be the result of READ_PACKAGE_JSON(packageURL). - const pjson = readPackageJson(fs, packageURL); - - // 2. If pjson.imports is a non-null Object, then - if (typeof pjson.imports === "object" && pjson?.imports !== null) { - // 1. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, - // packageURL, true, conditions). - const resolved = packageImportsExportsResolve(fs, specifier, pjson.imports, packageURL, true, conditions); - - // 2. If resolved is not null or undefined, return resolved. - if (resolved != null) { - return resolved; - } - } - } - - // 5. Throw a Package Import Not Defined error. - throw new Error("Package Import Not Defined"); -} - -// PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions) -function packageImportsExportsResolve( - fs: Package, - matchKey: string, - matchObj: Record, - packageURL: URL, - isImports: boolean, - conditions: readonly string[], -) { - // 1. If matchKey is a key of matchObj and does not contain "*", then - if (matchKey in matchObj && !matchKey.includes("*")) { - // 1. Let target be the value of matchObj[matchKey]. - const target = matchObj[matchKey]; - - // 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, - // conditions). - return packageTargetResolve(fs, packageURL, target, null, isImports, conditions); - } - - // 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by - // the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity. - const expansionKeys = Object.keys(matchObj) - .filter((key) => { - const ii = key.indexOf("*"); - return ii !== -1 && ii === key.lastIndexOf("*"); - }) - .sort(patternKeyCompare); - - // 3. For each key expansionKey in expansionKeys, do - for (const key of expansionKeys) { - // 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" - // character. - const patternBase = key.substring(0, key.indexOf("*")); - - // 2. If matchKey starts with but is not equal to patternBase, then - if (matchKey.startsWith(patternBase) && matchKey !== patternBase) { - // 1. Let patternTrailer be the substring of expansionKey from the index after the first "*" - // character. - const patternTrailer = key.substring(key.indexOf("*") + 1); - - // 2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the - // length of matchKey is greater than or equal to the length of expansionKey, then - if (patternTrailer.length === 0 || (matchKey.endsWith(patternTrailer) && matchKey.length >= key.length)) { - // 1. Let target be the value of matchObj[expansionKey]. - const target = matchObj[key]; - - // 2. Let patternMatch be the substring of matchKey starting at the index of the length of - // patternBase up to the length of matchKey minus the length of patternTrailer. - const patternMatch = matchKey.substring(patternBase.length, matchKey.length - patternTrailer.length); - - // 3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, - // isImports, conditions). - return packageTargetResolve(fs, packageURL, target, patternMatch, isImports, conditions); - } - } - } - - // 4. Return null - return null; -} - -// PATTERN_KEY_COMPARE(keyA, keyB) -function patternKeyCompare(keyA: string, keyB: string) { - // 1. Assert: keyA ends with "/" or contains only a single "*". - // 2. Assert: keyB ends with "/" or contains only a single "*". - - // 3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise. - const baseLengthA = keyA.includes("*") ? keyA.indexOf("*") + 1 : keyA.length; - - // 4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise. - const baseLengthB = keyB.includes("*") ? keyB.indexOf("*") + 1 : keyB.length; - - // 5. If baseLengthA is greater than baseLengthB, return -1. - // 6. If baseLengthB is greater than baseLengthA, return 1. - const baseDifference = baseLengthB - baseLengthA; - if (baseDifference !== 0) { - return baseDifference; - } - - // 7. If keyA does not contain "*", return 1. - if (!keyA.includes("*")) { - return 1; - } - - // 8. If keyB does not contain "*", return -1. - if (!keyB.includes("*")) { - return -1; - } - - // 9. If the length of keyA is greater than the length of keyB, return -1. - // 10. If the length of keyB is greater than the length of keyA, return 1. - const difference = keyB.length - keyA.length; - if (difference !== 0) { - return difference; - } - - // 11. Return 0. - return 0; -} - -// PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions) -function packageTargetResolve( - fs: Package, - packageURL: URL, - target: unknown, - patternMatch: string | null, - isImports: boolean, - conditions: readonly string[], -): URL | null | undefined { - // 1. If target is a String, then - if (typeof target === "string") { - // 1. If target does not start with "./", then - if (!target.startsWith("./")) { - // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid - // URL, then - if (!isImports || target.startsWith("../") || target.startsWith("/") || URL.canParse(target)) { - // 1. Throw an Invalid Package Target error. - throw new Error("Invalid Package Target error"); - } - - // 2. If patternMatch is a String, then - if (patternMatch !== null) { - // 1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, - // packageURL + "/"). - return packageResolve(fs, target.replace(/\*/g, patternMatch), new URL(`${packageURL}/`)); - } else { - // 3. Return PACKAGE_RESOLVE(target, packageURL + "/"). - return packageResolve(fs, target, new URL(`${packageURL}/`)); - } - } - - // 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after - // the first "." segment, case insensitive and including percent encoded variants, throw an - // Invalid Package Target error. - if ( - target - .slice(2) - .split(/\/|\\/) - .some((segment) => segment === "" || segment === "." || segment === ".." || segment === "node_modules") - ) { - throw new Error("Invalid Package Target error"); - } - - // 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. - const resolvedTarget = new URL(target, packageURL); - - // 4. Assert: packageURL is contained in resolvedTarget. - // 5. If patternMatch is null, then - if (patternMatch === null) { - // 1. Return resolvedTarget. - return resolvedTarget; - } - - // 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" - // segments, case insensitive and including percent encoded variants, throw an Invalid Module - // Specifier error. - if ( - patternMatch - .split(/\/|\\/) - .some((segment) => segment === "" || segment === "." || segment === ".." || segment === "node_modules") - ) { - throw new Error("Invalid Module Specifier"); - } - - // 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch. - return new URL(resolvedTarget.href.replace(/\*/g, patternMatch)); - } - - // 2. Otherwise, if target is a non-null Object, then - if (typeof target === "object" && target !== null) { - // 1. If target contains any index property keys, as defined in ECMA-262 6.1.7 Array Index , - // throw an Invalid Package Configuration error. - if (Object.keys(target).some((key) => /^[0-9]+$/.test(key))) { - throw new Error("Invalid Package Configuration error"); - } - - // 2. For each property p of target, in object insertion order as, - for (const [property, targetValue] of Object.entries(target)) { - // 1. If p equals "default" or conditions contains an entry for p, then - if (property === "default" || conditions.includes(property)) { - // 1. Let targetValue be the value of the p property in target. - // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE(packageURL, targetValue, patternMatch, - // isImports, conditions). - const resolved = packageTargetResolve(fs, packageURL, targetValue, patternMatch, isImports, conditions); - - // 3. If resolved is undefined, continue the loop. - if (resolved === undefined) { - continue; - } - - // 4. Return resolved. - return resolved; - } - } - - // 3. Return undefined. - return; - } - - // 3. Otherwise, if target is an Array, then - if (Array.isArray(target)) { - // 1. If target.length is zero, return null. - if (target.length === 0) { - return null; - } - - // 2. For each item targetValue in target, do - for (const targetValue of target) { - // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE(packageURL, targetValue, patternMatch, - // isImports, conditions). - const resolved = packageTargetResolve(fs, packageURL, targetValue, patternMatch, isImports, conditions); - - // 2. If resolved is undefined, continue the loop. - if (resolved === undefined) { - continue; - } - - // 3. Return resolved. - return resolved; - } - - // 3. Return or throw the last fallback resolution null return or error. - // nb: ???? - return null; - } - - // 4. Otherwise, if target is null, return null. - if (target === null) { - return null; - } - - // 5. Otherwise throw an Invalid Package Target error. - throw new Error("Invalid Package Target error"); -} - -// ESM_FILE_FORMAT(url) -export function esmFileFormat(fs: Package, url: URL) { - // 1. Assert: url corresponds to an existing file. - // 2. If url ends in ".mjs", then - if (url.pathname.endsWith(".mjs")) { - // 1. Return "module". - return "module"; - } - - // 3. If url ends in ".cjs", then - if (url.pathname.endsWith(".cjs")) { - // 1. Return "commonjs". - return "commonjs"; - } - - // 4. If url ends in ".json", then - if (url.pathname.endsWith(".json")) { - // 1. Return "json". - return "json"; - } - - // 5. If --experimental-wasm-modules is enabled and url ends in ".wasm", then - if (url.pathname.endsWith(".wasm")) { - // 1. Return "wasm". - return "wasm"; - } - - // 6. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url). - const packageURL = lookupPackageScope(fs, url); - if (packageURL === null) { - // nb: The algorithm seems to be poorly specified here because `READ_PACKAGE_JSON` does not - // handle the null case, but `LOOKUP_PACKAGE_SCOPE` is allowed to return `null`. - throw new Error("Invalid Module Specifier"); - } - - // 7. Let pjson be the result of READ_PACKAGE_JSON(packageURL). - const pjson = readPackageJson(fs, packageURL); - - // 8. Let packageType be null. - // 9. If pjson?.type is "module" or "commonjs", then - // 1. Set packageType to pjson.type. - const packageType = pjson.type === "module" || pjson.type === "commonjs" ? (pjson.type as string) : null; - - // 10. If url ends in ".js", then - if (url.pathname.endsWith(".js")) { - // 1. If packageType is not null, then - if (packageType !== null) { - // 1. Return packageType. - if (typeof packageType !== "string") { - throw new Error("Invalid Package Configuration"); - } - return packageType; - } - - // 2. If --experimental-detect-module is enabled and the source of module contains static - // import or export syntax, then - // 1. Return "module". - // nb: omitted - - // 3. Return "commonjs". - return "commonjs"; - } - - // 11. If url does not have any extension, then - const segments = url.pathname.split("/"); - if (!segments[segments.length - 1].includes(".")) { - // 1. If packageType is "module" and --experimental-wasm-modules is enabled and the file at url - // contains the header for a WebAssembly module, then - // 1. Return "wasm". - // nb: omitted - - // 2. If packageType is not null, then - if (packageType !== null) { - // 1. Return packageType. - return packageType; - } - - // 3. If --experimental-detect-module is enabled and the source of module contains static import - // or export syntax, then - // 1. Return "module". - // nb: omitted - - // 4. Return "commonjs". - return "commonjs"; - } - - // 12. Return undefined (will throw during load phase). - return; -} - -// LOOKUP_PACKAGE_SCOPE(url) -export function lookupPackageScope(fs: Package, url: URL) { - if (url.protocol !== "file:") { - return null; - } - - // 1. Let scopeURL be url. - let scopeURL = url; - - // 2. While scopeURL is not the file system root, - // nb: Modified to search to include "root", also for "parent URL" operation. - do { - // 2. If scopeURL ends in a "node_modules" path segment, return null. - if (scopeURL.pathname.endsWith("/node_modules/")) { - return null; - } - - // 3. Let pjsonURL be the resolution of "package.json" within scopeURL. - const pjsonURL = new URL("package.json", scopeURL); - - // 4. if the file at pjsonURL exists, then - if (fs.fileExists(pjsonURL.pathname)) { - // 1. Return scopeURL. - return scopeURL; - } - - // 1. Set scopeURL to the parent URL of scopeURL. - scopeURL = new URL("../", scopeURL); - } while (url.pathname !== "/"); - - // 3. Return null. - return null; -} - -// READ_PACKAGE_JSON(packageURL) -export function readPackageJson(fs: Package, packageURL: URL) { - // 1. Let pjsonURL be the resolution of "package.json" within packageURL. - const pjsonURL = new URL("package.json", packageURL); - - // 2. If the file at pjsonURL does not exist, then - if (!fs.fileExists(pjsonURL.pathname)) { - // 1. Return null. - return null; - } - - // 3. If the file at packageURL does not parse as valid JSON, then - // 1. Throw an Invalid Package Configuration error. - // 4. Return the parsed JSON source of the file at pjsonURL. - return JSON.parse(fs.readFile(pjsonURL.pathname)); -} diff --git a/packages/core/src/internal/esm/nodeModules.ts b/packages/core/src/internal/esm/nodeModules.ts deleted file mode 100644 index 582c393..0000000 --- a/packages/core/src/internal/esm/nodeModules.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Object.keys(process.binding("natives")).filter(name => !/^(?:_|internal\/)/.test(name)) -export const nodeCoreModules = [ - "assert", - "assert/strict", - "async_hooks", - "buffer", - "child_process", - "cluster", - // "configs", - "console", - "constants", - "crypto", - "dgram", - "diagnostics_channel", - "dns", - "dns/promises", - "domain", - "events", - "fs", - "fs/promises", - "http", - "http2", - "https", - "inspector", - "inspector/promises", - "module", - "net", - "os", - "path", - "path/posix", - "path/win32", - "perf_hooks", - "process", - "punycode", - "querystring", - "readline", - "readline/promises", - "repl", - "stream", - "stream/consumers", - "stream/promises", - "stream/web", - "string_decoder", - "sys", - "test", - "test/reporters", - "timers", - "timers/promises", - "tls", - "trace_events", - "tty", - "url", - "util", - "util/types", - "v8", - "vm", - "wasi", - "worker_threads", - "zlib", -]; diff --git a/packages/core/src/internal/esm/resolve.ts b/packages/core/src/internal/esm/resolve.ts new file mode 100644 index 0000000..d92a663 --- /dev/null +++ b/packages/core/src/internal/esm/resolve.ts @@ -0,0 +1,21 @@ +import type { Package } from "#createPackage.js"; +import * as cjs from "@loaderkit/resolve/cjs"; +import * as esm from "@loaderkit/resolve/esm"; +import type { FileSystemSync } from "@loaderkit/resolve/fs"; + +function makeFileSystemAdapter(fs: Package): FileSystemSync { + return { + directoryExists: url => fs.directoryExists(url.pathname), + fileExists: url => fs.fileExists(url.pathname), + readFileJSON: (url) => JSON.parse(fs.readFile(url.pathname)), + readLink: () => undefined, + } +} + +export function cjsResolve(fs: Package, specifier: string, parentURL: URL) { + return cjs.resolveSync(makeFileSystemAdapter(fs), specifier, parentURL); +} + +export function esmResolve(fs: Package, specifier: string, parentURL: URL) { + return esm.resolveSync(makeFileSystemAdapter(fs), specifier, parentURL); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a30830..50aeaea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,10 +6,10 @@ settings: patchedDependencies: cjs-module-lexer@1.4.0: - hash: gwwdgkalbuq7pt3qbrdws6vbs4 + hash: 014f76e37750df8a231384fd74630cb96fe50393f64ead0205d93766127278c2 path: patches/cjs-module-lexer@1.4.0.patch lru-cache@10.4.3: - hash: pmipjznrdotd3z6bhpktlo2giu + hash: 1b0bd126921f919521ebf14aab49cca0ecbb03dadbe19eae05aab57edefa6714 path: patches/lru-cache@10.4.3.patch importers: @@ -71,15 +71,18 @@ importers: '@andrewbranch/untar.js': specifier: ^1.0.3 version: 1.0.3 + '@loaderkit/resolve': + specifier: ^1.0.2 + version: 1.0.2 cjs-module-lexer: specifier: ^1.2.3 - version: 1.4.0(patch_hash=gwwdgkalbuq7pt3qbrdws6vbs4) + version: 1.4.0(patch_hash=014f76e37750df8a231384fd74630cb96fe50393f64ead0205d93766127278c2) fflate: specifier: ^0.8.2 version: 0.8.2 lru-cache: specifier: ^10.4.3 - version: 10.4.3(patch_hash=pmipjznrdotd3z6bhpktlo2giu) + version: 10.4.3(patch_hash=1b0bd126921f919521ebf14aab49cca0ecbb03dadbe19eae05aab57edefa6714) semver: specifier: ^7.5.4 version: 7.5.4 @@ -147,7 +150,7 @@ importers: version: 3.13.0 types-registry: specifier: latest - version: 0.1.685 + version: 0.1.708 packages/web: dependencies: @@ -199,6 +202,7 @@ packages: '@azure/core-http@3.0.3': resolution: {integrity: sha512-QMib3wXotJMFhHgmJBPUF9YsyErw34H0XDFQd9CauH7TPB+RGcyl9Ayy7iURtJB04ngXhE6YwrQsWDXlSLrilg==} engines: {node: '>=14.0.0'} + deprecated: This package is no longer supported. Please migrate to use @azure/core-rest-pipeline '@azure/core-lro@2.5.4': resolution: {integrity: sha512-3GJiMVH7/10bulzOKGrrLeG/uCBH/9VtxqaMcB9lIqAeamI/xYQSHJL/KcsLDuH+yTjYpro/u6D/MuRe4dN70Q==} @@ -228,6 +232,9 @@ packages: resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} engines: {node: '>=6.9.0'} + '@braidai/lang@1.0.0': + resolution: {integrity: sha512-Ckpah5j8iAzDfc4YEP4uqnxyUznuAt6hRR093JSEYUgh2trQjCibQ2pfxHxzfz7y9vkUn9/rBxjFpGY+SPudHA==} + '@changesets/apply-release-plan@7.0.5': resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==} @@ -555,6 +562,9 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@loaderkit/resolve@1.0.2': + resolution: {integrity: sha512-yTCCjuQapvRz6S30B8DyqHu1WYsbYRCww6uNsmbQU4GQVf5gJzJSB60qUHj+qBSxReLtRL/mhmhYhrIc9jVFTw==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1730,8 +1740,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - types-registry@0.1.685: - resolution: {integrity: sha512-dC3Nqu/q2Ey8Jj/YV0plEub0oycMiu3VaAf+tf8X5uhtiRS4iF1BubY5cra1LIaKr+fTblxQvoVAYIPeVj1A9g==} + types-registry@0.1.708: + resolution: {integrity: sha512-psSqNGOPZsNF2t1kR7/CgrvvYyfhdm7nwbZy4TXh+940gf8MXP93tN9lc3+7/O9/WpPRvAAxjJAmngwuyDkTRg==} typescript@5.6.1-rc: resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} @@ -1936,6 +1946,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.0 + '@braidai/lang@1.0.0': {} + '@changesets/apply-release-plan@7.0.5': dependencies: '@changesets/config': 3.0.3 @@ -2224,6 +2236,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@loaderkit/resolve@1.0.2': + dependencies: + '@braidai/lang': 1.0.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.23.2 @@ -2497,7 +2513,7 @@ snapshots: ci-info@3.9.0: {} - cjs-module-lexer@1.4.0(patch_hash=gwwdgkalbuq7pt3qbrdws6vbs4): {} + cjs-module-lexer@1.4.0(patch_hash=014f76e37750df8a231384fd74630cb96fe50393f64ead0205d93766127278c2): {} clean-stack@2.2.0: {} @@ -2900,7 +2916,7 @@ snapshots: lodash.startcase@4.4.0: {} - lru-cache@10.4.3(patch_hash=pmipjznrdotd3z6bhpktlo2giu): {} + lru-cache@10.4.3(patch_hash=1b0bd126921f919521ebf14aab49cca0ecbb03dadbe19eae05aab57edefa6714): {} lru-cache@4.1.5: dependencies: @@ -3193,7 +3209,7 @@ snapshots: path-scurry@1.10.1: dependencies: - lru-cache: 10.4.3(patch_hash=pmipjznrdotd3z6bhpktlo2giu) + lru-cache: 10.4.3(patch_hash=1b0bd126921f919521ebf14aab49cca0ecbb03dadbe19eae05aab57edefa6714) minipass: 7.0.4 path-type@4.0.0: {} @@ -3463,7 +3479,7 @@ snapshots: tunnel@0.0.6: {} - types-registry@0.1.685: {} + types-registry@0.1.708: {} typescript@5.6.1-rc: {}