From a88c7f867397fc061371367c31086693c2f89e4f Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Sun, 29 Dec 2024 18:29:20 -0500 Subject: [PATCH] feat(lib)!: [`resolver`] synchronous methods Signed-off-by: Lexus Drumgold --- README.md | 16 + __tests__/reporters/verbose.mts | 81 +-- build/make.mts | 2 +- package.json | 4 +- src/__snapshots__/index.e2e.snap | 8 + .../__tests__/file-system.spec-d.mts | 28 +- src/interfaces/file-system.mts | 17 +- src/internal/fs.browser.mts | 12 +- src/lib/__snapshots__/resolve-module.snap | 2 +- src/lib/__snapshots__/resolver.snap | 2 +- src/lib/__tests__/is-directory.spec.mts | 10 +- src/lib/__tests__/is-file.spec.mts | 10 +- .../__tests__/lookup-package-scope.spec.mts | 8 +- src/lib/__tests__/read-package-json.spec.mts | 12 +- src/lib/__tests__/resolver.spec.mts | 58 ++- src/lib/get-source.mts | 10 +- src/lib/is-directory.mts | 10 +- src/lib/is-file.mts | 10 +- src/lib/lookup-package-scope.mts | 10 +- src/lib/read-package-json.mts | 10 +- src/lib/resolver.mts | 482 ++++++++++++++++-- 21 files changed, 646 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 5f96446a..3ccef799 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,22 @@ import { isImportsSubpath, isRelativeSpecifier, legacyMainResolve, + legacyMainResolveSync, lookupPackageScope, moduleResolve, + moduleResolveSync, packageExportsResolve, + packageExportsResolveSync, packageImportsExportsResolve, + packageImportsExportsResolveSync, packageImportsResolve, + packageImportsResolveSync, packageResolve, + packageResolveSync, packageSelfResolve, + packageSelfResolveSync, packageTargetResolve, + packageTargetResolveSync, patternKeyCompare, patternMatch, readPackageJson, @@ -124,13 +132,21 @@ This package exports the following identifiers: - [`resolveModule`](./src/lib/resolve-module.mts) - [`resolver`](./src/lib/resolver.mts) - `legacyMainResolve` + - `legacyMainResolveSync` - `moduleResolve` + - `moduleResolveSync` - `packageExportsResolve` + - `packageExportsResolveSync` - `packageImportsExportsResolve` + - `packageImportsExportsResolveSync` - `packageImportsResolve` + - `packageImportsResolveSync` - `packageResolve` + - `packageResolveSync` - `packageSelfResolve` + - `packageSelfResolveSync` - `packageTargetResolve` + - `packageTargetResolveSync` - [`root`](./src/lib/root.mts) - [`toRelativeSpecifier`](./src/lib/to-relative-specifier.mts) - [`toUrl`](./src/lib/to-url.mts) diff --git a/__tests__/reporters/verbose.mts b/__tests__/reporters/verbose.mts index bc8735c6..5cdfcf33 100644 --- a/__tests__/reporters/verbose.mts +++ b/__tests__/reporters/verbose.mts @@ -7,21 +7,9 @@ import type { Task, TaskResultPack } from '@vitest/runner' import { getNames, getTests } from '@vitest/runner/utils' import colors, { type Colors } from 'tinyrainbow' -import type { RunnerTask } from 'vitest' +import type { RunnerTask, RunnerTestFile } from 'vitest' import { DefaultReporter, type Reporter } from 'vitest/reporters' -/** - * Verbose reporter options. - */ -interface Options { - /** - * Enable summary reporter? - * - * @default true - */ - summary?: boolean | null | undefined -} - /** * Verbose reporter. * @@ -43,13 +31,12 @@ class VerboseReporter extends DefaultReporter implements Reporter { /** * Create a new verbose reporter. - * - * @param {Options | null | undefined} [options] - * Reporter options */ - constructor(options?: Options | null | undefined) { - super({ summary: options?.summary ?? true }) + constructor() { + super({ summary: true }) + this.colors = colors + this.renderSucceed = true this.verbose = true } @@ -113,6 +100,29 @@ class VerboseReporter extends DefaultReporter implements Reporter { /** * Print tasks. * + * @see {@linkcode RunnerTestFile} + * + * @public + * @override + * @instance + * + * @param {RunnerTestFile[] | undefined} [files] + * List of test files + * @param {unknown[] | undefined} [errors] + * List of unhandled errors + * @return {undefined} + */ + public override onFinished( + files?: RunnerTestFile[] | undefined, + errors?: unknown[] | undefined + ): undefined { + if (files) { for (const task of files) this.printTask(task, true) } + return void super.onFinished(files, errors) + } + + /** + * Handle task updates. + * * @see {@linkcode TaskResultPack} * * @public @@ -124,21 +134,7 @@ class VerboseReporter extends DefaultReporter implements Reporter { * @return {undefined} */ public override onTaskUpdate(packs: TaskResultPack[]): undefined { - for (const pack of packs) { - /** - * Current task. - * - * @const {Task | undefined} task - */ - const task: Task | undefined = this.ctx.state.idMap.get(pack[0]) - - // print top-level suite task and recursively print tests - if (task && task.type === 'suite' && 'filepath' in task) { - void this.printTask(task) - } - } - - return void packs + return void (this.isTTY && void super.onTaskUpdate(packs)) } /** @@ -152,10 +148,20 @@ class VerboseReporter extends DefaultReporter implements Reporter { * * @param {Task | null | undefined} task * The task to handle + * @param {boolean | null | undefined} [force] + * Print `task` even when {@linkcode isTTY} is `false`? * @return {undefined} */ - protected override printTask(task: Task | null | undefined): undefined { - if (task && task.result?.state !== 'run') { + protected override printTask( + task: Task | null | undefined, + force?: boolean | null | undefined + ): undefined { + if ( + (!this.isTTY || force) && + task?.result?.state && + task.result.state !== 'queued' && + task.result.state !== 'run' + ) { /** * Task skipped? * @@ -197,7 +203,10 @@ class VerboseReporter extends DefaultReporter implements Reporter { state += skip ? this.colors.blackBright(suite) : suite this.log(state) - if (!skip) { for (const tsk of task.tasks) void this.printTask(tsk) } + + if (!skip) { + for (const subtask of task.tasks) void this.printTask(subtask, force) + } } } diff --git a/build/make.mts b/build/make.mts index 7d5b8195..7a928d5f 100644 --- a/build/make.mts +++ b/build/make.mts @@ -183,7 +183,7 @@ async function make( * * @const {PackageJson | null} pkg */ - const pkg: PackageJson | null = await readPackageJson( + const pkg: PackageJson | null = readPackageJson( pathe.pathToFileURL(absWorkingDir + pathe.sep) ) diff --git a/package.json b/package.json index 57991aa2..db2311a1 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "mlly": "./src/internal/fs.browser.mts", "default": "./dist/internal/fs.browser.mjs" }, - "node": "fs/promises", - "default": "fs/promises" + "node": "fs", + "default": "fs" }, "#internal/process": { "types": { diff --git a/src/__snapshots__/index.e2e.snap b/src/__snapshots__/index.e2e.snap index 15ebe8f7..c7c7dd5c 100644 --- a/src/__snapshots__/index.e2e.snap +++ b/src/__snapshots__/index.e2e.snap @@ -24,13 +24,21 @@ exports[`e2e:mlly > should expose public api 1`] = ` "resolveAlias", "resolveModule", "legacyMainResolve", + "legacyMainResolveSync", "moduleResolve", + "moduleResolveSync", "packageExportsResolve", + "packageExportsResolveSync", "packageImportsExportsResolve", + "packageImportsExportsResolveSync", "packageImportsResolve", + "packageImportsResolveSync", "packageResolve", + "packageResolveSync", "packageSelfResolve", + "packageSelfResolveSync", "packageTargetResolve", + "packageTargetResolveSync", "resolver", "root", "toRelativeSpecifier", diff --git a/src/interfaces/__tests__/file-system.spec-d.mts b/src/interfaces/__tests__/file-system.spec-d.mts index 28c9cfc2..4a92e7a1 100644 --- a/src/interfaces/__tests__/file-system.spec-d.mts +++ b/src/interfaces/__tests__/file-system.spec-d.mts @@ -4,11 +4,11 @@ */ import type TestSubject from '#interfaces/file-system' -import type { Awaitable, ModuleId, Stats } from '@flex-development/mlly' +import type { ModuleId, Stats } from '@flex-development/mlly' describe('unit-d:interfaces/FileSystem', () => { - describe('readFile', () => { - type Subject = TestSubject['readFile'] + describe('readFileSync', () => { + type Subject = TestSubject['readFileSync'] it('should match [this: void]', () => { expectTypeOf().thisParameter.toEqualTypeOf() @@ -21,16 +21,14 @@ describe('unit-d:interfaces/FileSystem', () => { }) describe('returns', () => { - it('should return Awaitable', () => { - expectTypeOf() - .returns - .toEqualTypeOf>() + it('should return Buffer | string', () => { + expectTypeOf().returns.toEqualTypeOf() }) }) }) - describe('realpath', () => { - type Subject = TestSubject['realpath'] + describe('realpathSync', () => { + type Subject = TestSubject['realpathSync'] it('should match [this: void]', () => { expectTypeOf().thisParameter.toEqualTypeOf() @@ -43,14 +41,14 @@ describe('unit-d:interfaces/FileSystem', () => { }) describe('returns', () => { - it('should return Awaitable', () => { - expectTypeOf().returns.toEqualTypeOf>() + it('should return string', () => { + expectTypeOf().returns.toEqualTypeOf() }) }) }) - describe('stat', () => { - type Subject = TestSubject['stat'] + describe('statSync', () => { + type Subject = TestSubject['statSync'] it('should match [this: void]', () => { expectTypeOf().thisParameter.toEqualTypeOf() @@ -63,8 +61,8 @@ describe('unit-d:interfaces/FileSystem', () => { }) describe('returns', () => { - it('should return Awaitable', () => { - expectTypeOf().returns.toEqualTypeOf>() + it('should return Stats', () => { + expectTypeOf().returns.toEqualTypeOf() }) }) }) diff --git a/src/interfaces/file-system.mts b/src/interfaces/file-system.mts index c224f9a6..c49b4501 100644 --- a/src/interfaces/file-system.mts +++ b/src/interfaces/file-system.mts @@ -3,7 +3,7 @@ * @module mlly/interfaces/FileSystem */ -import type { Awaitable, ModuleId, Stats } from '@flex-development/mlly' +import type { ModuleId, Stats } from '@flex-development/mlly' /** * File system API. @@ -12,7 +12,6 @@ interface FileSystem { /** * Get the contents of `id`. * - * @see {@linkcode Awaitable} * @see {@linkcode Buffer} * @see {@linkcode ModuleId} * @see https://nodejs.org/api/fs.html#fsreadfilepath-options-callback @@ -21,15 +20,14 @@ interface FileSystem { * * @param {ModuleId} id * The path or `file:` URL to handle - * @return {Awaitable} + * @return {Buffer | string} * File contents */ - readFile(this: void, id: ModuleId): Awaitable + readFileSync(this: void, id: ModuleId): Buffer | string /** * Get the resolved pathname for `id`. * - * @see {@linkcode Awaitable} * @see {@linkcode ModuleId} * @see https://nodejs.org/api/fs.html#fsrealpathpath-options-callback * @@ -37,15 +35,14 @@ interface FileSystem { * * @param {ModuleId} id * The path or `file:` URL to handle - * @return {Awaitable} + * @return {string} * Resolved pathname */ - realpath(this: void, id: ModuleId): Awaitable + realpathSync(this: void, id: ModuleId): string /** * Get information about a directory or file. * - * @see {@linkcode Awaitable} * @see {@linkcode ModuleId} * @see {@linkcode Stats} * @@ -53,10 +50,10 @@ interface FileSystem { * * @param {ModuleId} id * The path or `file:` URL to handle - * @return {Awaitable} + * @return {Stats} * Info about `id` */ - stat(this: void, id: ModuleId): Awaitable + statSync(this: void, id: ModuleId): Stats } export type { FileSystem as default } diff --git a/src/internal/fs.browser.mts b/src/internal/fs.browser.mts index eed89372..cd13f789 100644 --- a/src/internal/fs.browser.mts +++ b/src/internal/fs.browser.mts @@ -18,8 +18,8 @@ const fs: FileSystem = { * Never; not implemented * @throws {Error} */ - readFile(): never { - throw new Error('[readFile] not implemented') + readFileSync(): never { + throw new Error('[readFileSync] not implemented') }, /** @@ -29,8 +29,8 @@ const fs: FileSystem = { * Never; not implemented * @throws {Error} */ - realpath(): never { - throw new Error('[realpath] not implemented') + realpathSync(): never { + throw new Error('[realpathSync] not implemented') }, /** @@ -40,8 +40,8 @@ const fs: FileSystem = { * Never; not implemented * @throws {Error} */ - stat(): never { - throw new Error('[stat] not implemented') + statSync(): never { + throw new Error('[statSync] not implemented') } } diff --git a/src/lib/__snapshots__/resolve-module.snap b/src/lib/__snapshots__/resolve-module.snap index 4d4887f1..57f548e0 100644 --- a/src/lib/__snapshots__/resolve-module.snap +++ b/src/lib/__snapshots__/resolve-module.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`unit:lib/resolveModule > should return resolved URL ("#internal/fs") 1`] = `"node:fs/promises"`; +exports[`unit:lib/resolveModule > should return resolved URL ("#internal/fs") 1`] = `"node:fs"`; exports[`unit:lib/resolveModule > should return resolved URL ("../../../src") 1`] = `file://\${process.cwd()}/src/index.mts`; diff --git a/src/lib/__snapshots__/resolver.snap b/src/lib/__snapshots__/resolver.snap index 49fbd634..35b0e6b0 100644 --- a/src/lib/__snapshots__/resolver.snap +++ b/src/lib/__snapshots__/resolver.snap @@ -4,7 +4,7 @@ exports[`unit:lib/resolver > legacyMainResolve > should throw if main entry poin exports[`unit:lib/resolver > moduleResolve > should return resolved URL (0) 1`] = `file://\${process.cwd()}/__fixtures__/tsconfig.json`; -exports[`unit:lib/resolver > moduleResolve > should return resolved URL (1) 1`] = `"node:fs/promises"`; +exports[`unit:lib/resolver > moduleResolve > should return resolved URL (1) 1`] = `"node:fs"`; exports[`unit:lib/resolver > moduleResolve > should return resolved URL (2) 1`] = `file://\${process.cwd()}/src/internal/fs.browser.mts`; diff --git a/src/lib/__tests__/is-directory.spec.mts b/src/lib/__tests__/is-directory.spec.mts index c016a5dd..37d589cb 100644 --- a/src/lib/__tests__/is-directory.spec.mts +++ b/src/lib/__tests__/is-directory.spec.mts @@ -3,25 +3,25 @@ * @module mlly/lib/tests/unit/isDirectory */ +import fs from '#internal/fs' import cwd from '#lib/cwd' import testSubject from '#lib/is-directory' import type { ModuleId } from '@flex-development/mlly' import { pathToFileURL } from '@flex-development/pathe' -import fs from 'node:fs/promises' describe('unit:lib/isDirectory', () => { it.each([ 'directory', new URL('node:fs'), String(new URL('src/index.mts', cwd())) - ])('should return `false` if `id` is not a directory (%#)', async id => { - expect(await testSubject(id, fs)).to.be.false + ])('should return `false` if `id` is not a directory (%#)', id => { + expect(testSubject(id, fs)).to.be.false }) it.each([ pathToFileURL('src'), String(cwd()) - ])('should return `true` if `id` is a directory (%#)', async id => { - expect(await testSubject(id)).to.be.true + ])('should return `true` if `id` is a directory (%#)', id => { + expect(testSubject(id)).to.be.true }) }) diff --git a/src/lib/__tests__/is-file.spec.mts b/src/lib/__tests__/is-file.spec.mts index 7431fb86..31570107 100644 --- a/src/lib/__tests__/is-file.spec.mts +++ b/src/lib/__tests__/is-file.spec.mts @@ -3,25 +3,25 @@ * @module mlly/lib/tests/unit/isFile */ +import fs from '#internal/fs' import cwd from '#lib/cwd' import testSubject from '#lib/is-file' import type { ModuleId } from '@flex-development/mlly' import { pathToFileURL } from '@flex-development/pathe' -import fs from 'node:fs/promises' describe('unit:lib/isFile', () => { it.each([ 'file.mjs', new URL('node:fs'), String(cwd()) - ])('should return `false` if `id` is not a file (%#)', async id => { - expect(await testSubject(id, fs)).to.be.false + ])('should return `false` if `id` is not a file (%#)', id => { + expect(testSubject(id, fs)).to.be.false }) it.each([ pathToFileURL('package.json'), String(new URL('vitest.config.mts', cwd())) - ])('should return `true` if `id` is a file (%#)', async id => { - expect(await testSubject(id)).to.be.true + ])('should return `true` if `id` is a file (%#)', id => { + expect(testSubject(id)).to.be.true }) }) diff --git a/src/lib/__tests__/lookup-package-scope.spec.mts b/src/lib/__tests__/lookup-package-scope.spec.mts index e5dfe777..3a3c17d0 100644 --- a/src/lib/__tests__/lookup-package-scope.spec.mts +++ b/src/lib/__tests__/lookup-package-scope.spec.mts @@ -7,12 +7,12 @@ import testSubject from '#lib/lookup-package-scope' import { pathToFileURL, sep } from '@flex-development/pathe' describe('unit:lib/lookupPackageScope', () => { - it('should return `null` if package directory is not found', async () => { + it('should return `null` if package directory is not found', () => { // Arrange const url: URL = pathToFileURL('node_modules/@flex-development/404.mjs') // Act + Expect - expect(await testSubject(url)).to.be.null + expect(testSubject(url)).to.be.null }) it.each>([ @@ -24,9 +24,9 @@ describe('unit:lib/lookupPackageScope', () => { 'node_modules/@commitlint/cli/node_modules/@commitlint/config-validator/lib/validate.js' )], [pathToFileURL('node_modules/esbuild/lib/main.js')] - ])('should return URL of package directory (%#)', async url => { + ])('should return URL of package directory (%#)', url => { // Act - const result = await testSubject(url) + const result = testSubject(url) const resultStr = String(result) // Expect diff --git a/src/lib/__tests__/read-package-json.spec.mts b/src/lib/__tests__/read-package-json.spec.mts index abbcb6d1..c62d9f95 100644 --- a/src/lib/__tests__/read-package-json.spec.mts +++ b/src/lib/__tests__/read-package-json.spec.mts @@ -14,18 +14,18 @@ import { import pkg from '@flex-development/mlly/package.json' describe('unit:lib/readPackageJson', () => { - it('should return `null` if `package.json` file is not found', async () => { - expect(await testSubject(import.meta.url)).to.be.null + it('should return `null` if `package.json` file is not found', () => { + expect(testSubject(import.meta.url)).to.be.null }) - it('should return package manifest object', async () => { - expect(await testSubject(cwd())).to.eql(pkg) + it('should return package manifest object', () => { + expect(testSubject(cwd())).to.eql(pkg) }) it.each>([ [invalidJsonUrl, String(new URL('package.json', invalidJsonUrl))], [invalidJsonUrl, 'invalid-json', import.meta.url] - ])('should throw if package manifest is not valid JSON (%#)', async ( + ])('should throw if package manifest is not valid JSON (%#)', ( id, specifier, parent @@ -35,7 +35,7 @@ describe('unit:lib/readPackageJson', () => { // Act try { - await testSubject(id, specifier, parent) + testSubject(id, specifier, parent) } catch (e: unknown) { error = e as typeof error } diff --git a/src/lib/__tests__/resolver.spec.mts b/src/lib/__tests__/resolver.spec.mts index 729c271b..30777236 100644 --- a/src/lib/__tests__/resolver.spec.mts +++ b/src/lib/__tests__/resolver.spec.mts @@ -8,6 +8,7 @@ import parent from '#fixtures/parent' import chars from '#internal/chars' import cwd from '#lib/cwd' import defaultConditions from '#lib/default-conditions' +import defaultMainFields from '#lib/default-main-fields' import * as testSubject from '#lib/resolver' import exportsSugarA from '#node_modules/exports-sugar-a/package.json' import exportsSugar from '#node_modules/exports-sugar/package.json' @@ -24,7 +25,13 @@ import { import errnode from '@flex-development/errnode/package.json' import type { MainField } from '@flex-development/mlly' import pkg from '@flex-development/mlly/package.json' -import { dot, pathToFileURL, resolve, sep } from '@flex-development/pathe' +import { + dot, + isURL, + pathToFileURL, + resolve, + sep +} from '@flex-development/pathe' import type { Condition, PackageJson } from '@flex-development/pkg-types' import * as baseline from 'import-meta-resolve' @@ -270,6 +277,55 @@ describe('unit:lib/resolver', () => { }) }) + describe('packageImportsExportsResolve', () => { + it.each>([ + [ + '#internal/fs', + pkg.imports, + cwd(), + true, + fixtureConditions, + defaultMainFields, + import.meta.url + ], + [ + dot + sep + 'package.json', + pkg.exports, + cwd(), + false, + defaultConditions, + defaultMainFields, + import.meta.url + ] + ])('should return resolved package export or import URL (%j)', async ( + matchKey, + matchObject, + packageUrl, + isImports, + conditions, + mainFields, + parent, + fs + ) => { + // Act + const result = await testSubject.packageImportsExportsResolve( + matchKey, + matchObject, + packageUrl, + isImports, + conditions, + mainFields, + parent, + fs + ) + + // Expect + expect(result).to.not.be.null + expect(result).to.not.be.undefined + expect(result).to.satisfy(isURL).and.not.be.a('string') + }) + }) + describe('packageImportsResolve', () => { it('should throw if package import is not defined', async () => { // Arrange diff --git a/src/lib/get-source.mts b/src/lib/get-source.mts index 481a093f..9f17c531 100644 --- a/src/lib/get-source.mts +++ b/src/lib/get-source.mts @@ -134,19 +134,17 @@ function data(this: GetSourceContext, url: URL): Buffer { } /** - * @async - * * @this {GetSourceContext} * * @param {URL} url * Module URL - * @return {Promise} + * @return {Buffer | string | null} * Source code or `null` if module is not found */ -async function file( +function file( this: GetSourceContext, url: URL -): Promise { +): Buffer | string | null { ok(url.protocol === 'file:', 'expected `file:` URL') /** @@ -156,7 +154,7 @@ async function file( */ let code: Buffer | string | null = null - if (await isFile(url, this.fs)) code = await this.fs.readFile(url) + if (isFile(url, this.fs)) code = this.fs.readFileSync(url) return code } diff --git a/src/lib/is-directory.mts b/src/lib/is-directory.mts index 576936f7..9245b12b 100644 --- a/src/lib/is-directory.mts +++ b/src/lib/is-directory.mts @@ -12,22 +12,20 @@ import type { FileSystem, ModuleId } from '@flex-development/mlly' * @see {@linkcode FileSystem} * @see {@linkcode ModuleId} * - * @async - * * @param {ModuleId} id * Module id to check * @param {FileSystem | null | undefined} fs * File system API - * @return {Promise} + * @return {boolean} * `true` if directory exists at `id`, `false` otherwise */ -async function isDirectory( +function isDirectory( id: ModuleId, fs?: FileSystem | null | undefined -): Promise { +): boolean { try { if (typeof id === 'string' && id.startsWith('file:')) id = new URL(id) - return (await (fs ?? dfs).stat(id)).isDirectory() + return (fs ?? dfs).statSync(id).isDirectory() } catch { return false } diff --git a/src/lib/is-file.mts b/src/lib/is-file.mts index 516a6da4..f0bde375 100644 --- a/src/lib/is-file.mts +++ b/src/lib/is-file.mts @@ -12,22 +12,20 @@ import type { FileSystem, ModuleId } from '@flex-development/mlly' * @see {@linkcode FileSystem} * @see {@linkcode ModuleId} * - * @async - * * @param {ModuleId} id * Module id to check * @param {FileSystem | null | undefined} fs * File system API - * @return {Promise} + * @return {boolean} * `true` if file exists at `id`, `false` otherwise */ -async function isFile( +function isFile( id: ModuleId, fs?: FileSystem | null | undefined -): Promise { +): boolean { try { if (typeof id === 'string' && id.startsWith('file:')) id = new URL(id) - return (await (fs ?? dfs).stat(id)).isFile() + return (fs ?? dfs).statSync(id).isFile() } catch { return false } diff --git a/src/lib/lookup-package-scope.mts b/src/lib/lookup-package-scope.mts index bcc0bc8f..e9cf6fdf 100644 --- a/src/lib/lookup-package-scope.mts +++ b/src/lib/lookup-package-scope.mts @@ -16,22 +16,20 @@ import pathe from '@flex-development/pathe' * @see {@linkcode FileSystem} * @see {@linkcode ModuleId} * - * @async - * * @param {ModuleId} url * URL of module to get package scope for * @param {ModuleId | null | undefined} [end] * URL of directory to end search, defaults to {@linkcode root} * @param {FileSystem | null | undefined} [fs] * File system API - * @return {Promise} + * @return {URL | null} * URL of nearest directory containing `package.json` file or `null` */ -async function lookupPackageScope( +function lookupPackageScope( url: ModuleId, end?: ModuleId | null | undefined, fs?: FileSystem | null | undefined -): Promise { +): URL | null { /** * Scope URL. * @@ -49,7 +47,7 @@ async function lookupPackageScope( if (/node_modules[/\\]$/.test(scopeUrl.pathname)) break // return scopeUrl if `package.json` file exists in directory - if (await isFile(new URL('package.json', scopeUrl), fs)) return scopeUrl + if (isFile(new URL('package.json', scopeUrl), fs)) return scopeUrl } return null diff --git a/src/lib/read-package-json.mts b/src/lib/read-package-json.mts index b650c14e..cc305dc9 100644 --- a/src/lib/read-package-json.mts +++ b/src/lib/read-package-json.mts @@ -35,17 +35,17 @@ import type { PackageJson } from '@flex-development/pkg-types' * URL of parent module * @param {FileSystem | null | undefined} [fs] * File system API - * @return {Promise} + * @return {PackageJson | null} * Parsed file contents or `null` * @throws {ErrInvalidPackageConfig} * If `package.json` file does not parse as valid JSON */ -async function readPackageJson( +function readPackageJson( id: ModuleId, specifier?: string | null | undefined, parent?: ModuleId | null | undefined, fs?: FileSystem | null | undefined -): Promise { +): PackageJson | null { /** * URL of `package.json` file. * @@ -53,13 +53,13 @@ async function readPackageJson( */ const url: URL = new URL('package.json', id) - if (await isFile(url, fs)) { + if (isFile(url, fs)) { /** * Stringified package config. * * @const {string} data */ - const data: string = String(await (fs ?? dfs).readFile(url)) + const data: string = String((fs ?? dfs).readFileSync(url)) try { return JSON.parse(data) as PackageJson diff --git a/src/lib/resolver.mts b/src/lib/resolver.mts index fb446095..c5d0b949 100644 --- a/src/lib/resolver.mts +++ b/src/lib/resolver.mts @@ -72,13 +72,21 @@ declare module '@flex-development/errnode' { export { legacyMainResolve, + legacyMainResolveSync, moduleResolve, + moduleResolveSync, packageExportsResolve, + packageExportsResolveSync, packageImportsExportsResolve, + packageImportsExportsResolveSync, packageImportsResolve, + packageImportsResolveSync, packageResolve, + packageResolveSync, packageSelfResolve, - packageTargetResolve + packageSelfResolveSync, + packageTargetResolve, + packageTargetResolveSync } /** @@ -117,6 +125,51 @@ async function legacyMainResolve( parent?: ModuleId | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(legacyMainResolveSync( + packageUrl, + manifest, + mainFields, + parent, + fs + )) + }) +} + +/** + * Resolve the [`main`][main] package entry point using the legacy CommonJS + * resolution algorithm. + * + * [main]: https://github.com/nodejs/node/blob/v22.9.0/doc/api/packages.md#main + * + * @see {@linkcode ErrModuleNotFound} + * @see {@linkcode FileSystem} + * @see {@linkcode MainField} + * @see {@linkcode ModuleId} + * @see {@linkcode PackageJson} + * + * @param {ModuleId} packageUrl + * URL of package directory, `package.json` file, or module in the same + * directory as a `package.json` file + * @param {PackageJson | null | undefined} [manifest] + * Package manifest + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {ModuleId | null | undefined} [parent] + * URL of parent module + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL} + * Resolved URL + * @throws {ErrModuleNotFound} + */ +function legacyMainResolveSync( + packageUrl: ModuleId, + manifest?: PackageJson | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + parent?: ModuleId | null | undefined, + fs?: FileSystem | null | undefined +): URL { if (manifest) { for (const field of mainFields ?? defaultMainFields) { /** @@ -156,7 +209,7 @@ async function legacyMainResolve( for (const input of tries) { resolved = new URL(input, packageUrl) - if (await isFile(resolved, fs)) return resolved + if (isFile(resolved, fs)) return resolved } } } @@ -208,6 +261,57 @@ async function moduleResolve( preserveSymlinks?: boolean | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(moduleResolveSync( + specifier, + parent, + conditions, + mainFields, + preserveSymlinks, + fs + )) + }) +} + +/** + * Resolve a module `specifier`. + * + * Implements the `MODULE_RESOLVE` (`ESM_RESOLVE`) algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode ErrInvalidModuleSpecifier} + * @see {@linkcode ErrModuleNotFound} + * @see {@linkcode ErrUnsupportedDirImport} + * @see {@linkcode FileSystem} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {string} specifier + * Module specifier to resolve + * @param {ModuleId} parent + * URL of parent module + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {boolean | null | undefined} [preserveSymlinks] + * Keep symlinks instead of resolving them + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL} + * Resolved URL + * @throws {ErrInvalidModuleSpecifier} + * @throws {ErrModuleNotFound} + * @throws {ErrUnsupportedDirImport} + */ +function moduleResolveSync( + specifier: string, + parent: ModuleId, + conditions?: Condition[] | Set | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + preserveSymlinks?: boolean | null | undefined, + fs?: FileSystem | null | undefined +): URL { /** * URL protocol of parent module. * @@ -241,7 +345,7 @@ async function moduleResolve( throw error } } else if (protocol === 'file:' && isImportsSubpath(specifier)) { - resolved = await packageImportsResolve( + resolved = packageImportsResolveSync( specifier, parent, conditions, @@ -266,7 +370,7 @@ async function moduleResolve( throw error } - resolved = await packageResolve( + resolved = packageResolveSync( specifier, parent, conditions, @@ -289,7 +393,7 @@ async function moduleResolve( ) } - if (await isDirectory(resolved, fs)) { + if (isDirectory(resolved, fs)) { /** * Node error. * @@ -307,7 +411,7 @@ async function moduleResolve( throw error } - if (!(await isFile(resolved, fs))) { + if (!isFile(resolved, fs)) { throw new ERR_MODULE_NOT_FOUND( pathname, pathe.fileURLToPath(parent), @@ -317,7 +421,7 @@ async function moduleResolve( if (!preserveSymlinks) { fs ??= dfs - resolved = new URL(pathe.pathToFileURL(await fs.realpath(resolved))) + resolved = new URL(pathe.pathToFileURL(fs.realpathSync(resolved))) resolved.hash = hash resolved.search = search } @@ -368,6 +472,58 @@ async function packageExportsResolve( parent?: ModuleId | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageExportsResolveSync( + packageUrl, + subpath, + exports, + conditions, + parent, + fs + )) + }) +} + +/** + * Resolve a package export. + * + * Implements the `PACKAGE_EXPORTS_RESOLVE` algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode ErrInvalidPackageConfig} + * @see {@linkcode ErrPackagePathNotExported} + * @see {@linkcode Exports} + * @see {@linkcode FileSystem} + * @see {@linkcode Imports} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {ModuleId} packageUrl + * URL of package directory, `package.json` file, or module in the same + * directory as a `package.json` file + * @param {string} subpath + * Package subpath to resolve + * @param {Exports | undefined} exports + * Package exports + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {ModuleId | null | undefined} [parent] + * URL of parent module + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL} + * Resolved URL + * @throws {ErrInvalidPackageConfig} + * @throws {ErrPackagePathNotExported} + */ +function packageExportsResolveSync( + packageUrl: ModuleId, + subpath: string, + exports: Exports | undefined, + conditions?: Condition[] | Set | null | undefined, + parent?: ModuleId | null | undefined, + fs?: FileSystem | null | undefined +): URL { if (exports) { /** * Boolean indicating all {@linkcode exports} object keys must start with a @@ -427,7 +583,7 @@ async function packageExportsResolve( } if (mainExport !== undefined) { - resolved = await packageTargetResolve( + resolved = packageTargetResolveSync( packageUrl, mainExport, subpath, @@ -443,7 +599,7 @@ async function packageExportsResolve( ok(!Array.isArray(exports), 'expected `exports` to not be an array') ok(subpath.startsWith('./'), 'expected `subpath` to start with "./"') - resolved = await packageImportsExportsResolve( + resolved = packageImportsExportsResolveSync( subpath, exports, packageUrl, @@ -508,6 +664,61 @@ async function packageImportsExportsResolve( parent?: ModuleId | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageImportsExportsResolveSync( + matchKey, + matchObject, + packageUrl, + isImports, + conditions, + mainFields, + parent, + fs + )) + }) +} + +/** + * Resolve a package export or import. + * + * Implements the `PACKAGE_IMPORTS_EXPORTS_RESOLVE` algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode ExportsObject} + * @see {@linkcode FileSystem} + * @see {@linkcode Imports} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {string} matchKey + * Package subpath from module specifier or dot character (`'.'`) + * @param {ExportsObject | Imports | null | undefined} matchObject + * Package exports object or imports + * @param {ModuleId} packageUrl + * URL of directory containing `package.json` file + * @param {boolean | null | undefined} [isImports] + * Whether `matchObject` is internal to the package + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {ModuleId | null | undefined} [parent] + * URL of parent module + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL | null | undefined} + * Resolved URL + */ +function packageImportsExportsResolveSync( + matchKey: string, + matchObject: ExportsObject | Imports | null | undefined, + packageUrl: ModuleId, + isImports?: boolean | null | undefined, + conditions?: Condition[] | Set | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + parent?: ModuleId | null | undefined, + fs?: FileSystem | null | undefined +): URL | null | undefined { if (typeof matchObject === 'object' && matchObject) { /** * List containing expansion key and subpath pattern match. @@ -517,7 +728,7 @@ async function packageImportsExportsResolve( const match: PatternMatch | null = patternMatch(matchKey, matchObject) if (match) { - return packageTargetResolve( + return packageTargetResolveSync( packageUrl, matchObject[match[0]], ...match, @@ -545,6 +756,8 @@ async function packageImportsExportsResolve( * @see {@linkcode ModuleId} * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm * + * @async + * * @param {string} specifier * The import specifier to resolve * @param {ModuleId} parent @@ -567,6 +780,51 @@ async function packageImportsResolve( mainFields?: MainField[] | Set | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageImportsResolveSync( + specifier, + parent, + conditions, + mainFields, + fs + )) + }) +} + +/** + * Resolve a package import. + * + * Implements the `PACKAGE_IMPORTS_RESOLVE` algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode ErrInvalidModuleSpecifier} + * @see {@linkcode ErrPackageImportNotDefined} + * @see {@linkcode FileSystem} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {string} specifier + * The import specifier to resolve + * @param {ModuleId} parent + * URL of parent module + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL} + * Resolved URL + * @throws {ErrInvalidModuleSpecifier} + * @throws {ErrPackageImportNotDefined} + */ +function packageImportsResolveSync( + specifier: string, + parent: ModuleId, + conditions?: Condition[] | Set | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + fs?: FileSystem | null | undefined +): URL { if ( !specifier.startsWith(chars.hash) || specifier === chars.hash || @@ -584,7 +842,7 @@ async function packageImportsResolve( * * @const {URL | null} packageUrl */ - const packageUrl: URL | null = await lookupPackageScope(parent, null, fs) + const packageUrl: URL | null = lookupPackageScope(parent, null, fs) if (packageUrl) { /** @@ -592,7 +850,7 @@ async function packageImportsResolve( * * @const {PackageJson | null} pjson */ - const pjson: PackageJson | null = await readPackageJson( + const pjson: PackageJson | null = readPackageJson( packageUrl, specifier, parent, @@ -605,17 +863,16 @@ async function packageImportsResolve( * * @const {URL | null | undefined} resolved */ - const resolved: URL | null | undefined = - await packageImportsExportsResolve( - specifier, - pjson.imports, - packageUrl, - true, - conditions, - mainFields, - parent, - fs - ) + const resolved: URL | null | undefined = packageImportsExportsResolveSync( + specifier, + pjson.imports, + packageUrl, + true, + conditions, + mainFields, + parent, + fs + ) if (resolved) return resolved } @@ -673,6 +930,60 @@ async function packageResolve( mainFields?: MainField[] | Set | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageResolveSync( + specifier, + parent, + conditions, + mainFields, + fs + )) + }) +} + +/** + * Resolve a *bare specifier*. + * + * > *Bare specifiers* like `'some-package'` or `'some-package/shuffle'` refer + * > to the main entry point of a package by package name, or a specific feature + * > module within a package prefixed by the package name. Including the file + * > extension is only necessary for packages without an [`"exports"`][exports] + * > field. + * + * Implements the `PACKAGE_RESOLVE` algorithm. + * + * [exports]: https://nodejs.org/api/packages.html#exports + * + * @see {@linkcode Condition} + * @see {@linkcode ErrInvalidModuleSpecifier} + * @see {@linkcode ErrModuleNotFound} + * @see {@linkcode FileSystem} + * @see {@linkcode MainField} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {string} specifier + * The package specifier to resolve + * @param {ModuleId} parent + * Id of module to resolve `packageSpecifier` against + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL} + * Resolved URL + * @throws {ErrInvalidModuleSpecifier} + * @throws {ErrModuleNotFound} + */ +function packageResolveSync( + specifier: string, + parent: ModuleId, + conditions?: Condition[] | Set | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + fs?: FileSystem | null | undefined +): URL { if (isBuiltin(specifier)) return toUrl(specifier) /** @@ -737,7 +1048,7 @@ async function packageResolve( * * @const {URL | undefined} selfUrl */ - const selfUrl: URL | undefined = await packageSelfResolve( + const selfUrl: URL | undefined = packageSelfResolveSync( packageName, packageSubpath, parent, @@ -766,14 +1077,14 @@ async function packageResolve( parentUrl = new URL(pathe.dirname(parentUrl.href)) // continue if the folder at packageUrl does not exist - if (!(await isDirectory(packageUrl, fs))) continue + if (!isDirectory(packageUrl, fs)) continue /** * Package manifest. * * @const {PackageJson | null} pjson */ - const pjson: PackageJson | null = await readPackageJson( + const pjson: PackageJson | null = readPackageJson( packageUrl, null, parent, @@ -781,7 +1092,7 @@ async function packageResolve( ) if (pjson?.exports) { - return packageExportsResolve( + return packageExportsResolveSync( packageUrl, packageSubpath, pjson.exports, @@ -792,7 +1103,7 @@ async function packageResolve( } if (packageSubpath === pathe.dot) { - return legacyMainResolve(packageUrl, pjson, mainFields, parent, fs) + return legacyMainResolveSync(packageUrl, pjson, mainFields, parent, fs) } return new URL(packageSubpath, packageUrl) @@ -833,12 +1144,53 @@ async function packageSelfResolve( conditions?: Condition[] | Set | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageSelfResolveSync( + name, + subpath, + parent, + conditions, + fs + )) + }) +} + +/** + * Resolve the self-import of a package. + * + * Implements the `PACKAGE_SELF_RESOLVE` algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode FileSystem} + * @see {@linkcode ModuleId} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {string} name + * Package name + * @param {string} subpath + * Package subpath + * @param {ModuleId} parent + * URL of parent module + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export conditions + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL | undefined} + * Resolved URL + */ +function packageSelfResolveSync( + name: string, + subpath: string, + parent: ModuleId, + conditions?: Condition[] | Set | null | undefined, + fs?: FileSystem | null | undefined +): URL | undefined { /** * URL of package directory. * * @const {URL | null} packageUrl */ - const packageUrl: URL | null = await lookupPackageScope(parent, null, fs) + const packageUrl: URL | null = lookupPackageScope(parent, null, fs) if (packageUrl) { /** @@ -846,7 +1198,7 @@ async function packageSelfResolve( * * @const {PackageJson | null} pjson */ - const pjson: PackageJson | null = await readPackageJson( + const pjson: PackageJson | null = readPackageJson( packageUrl, null, parent, @@ -854,7 +1206,7 @@ async function packageSelfResolve( ) if (pjson?.exports && name === pjson.name) { - return packageExportsResolve( + return packageExportsResolveSync( packageUrl, subpath, pjson.exports, @@ -917,6 +1269,68 @@ async function packageTargetResolve( parent?: ModuleId | null | undefined, fs?: FileSystem | null | undefined ): Promise { + return new Promise(resolve => { + return void resolve(packageTargetResolveSync( + packageUrl, + target, + subpath, + patternMatch, + isImports, + conditions, + mainFields, + parent, + fs + )) + }) +} + +/** + * Resolve a package target. + * + * Implements the `PACKAGE_TARGET_RESOLVE` algorithm. + * + * @see {@linkcode Condition} + * @see {@linkcode ErrInvalidPackageConfig} + * @see {@linkcode ErrInvalidPackageTarget} + * @see {@linkcode FileSystem} + * @see {@linkcode ModuleId} + * @see {@linkcode Target} + * @see https://github.com/nodejs/node/blob/v22.9.0/doc/api/esm.md#resolution-algorithm + * + * @param {ModuleId} packageUrl + * URL of directory containing `package.json` file + * @param {unknown} target + * The package target to resolve + * @param {string} subpath + * Package subpath (key of `exports`/`imports`) + * @param {string | null | undefined} [patternMatch] + * Subpath pattern match + * @param {boolean | null | undefined} [isImports] + * Whether `target` is internal to the package + * @param {Condition[] | Set | null | undefined} [conditions] + * List of export/import conditions + * @param {MainField[] | Set | null | undefined} [mainFields] + * List of legacy main fields + * @param {ModuleId | null | undefined} [parent] + * URL of parent module + * @param {FileSystem | null | undefined} [fs] + * File system API + * @return {URL | null | undefined} + * Resolved URL + * @throws {ErrInvalidPackageConfig} + * @throws {ErrInvalidPackageTarget} + */ +function packageTargetResolveSync( + packageUrl: ModuleId, + target: unknown, + subpath: string, + patternMatch?: string | null | undefined, + isImports?: boolean | null | undefined, + conditions?: Condition[] | Set | null | undefined, + mainFields?: MainField[] | Set | null | undefined, + parent?: ModuleId | null | undefined, + fs?: FileSystem | null | undefined +): URL | null | undefined { if (typeof target === 'string') { if (!target.startsWith(pathe.dot + pathe.sep)) { if ( @@ -934,7 +1348,7 @@ async function packageTargetResolve( ) } - return packageResolve( + return packageResolveSync( typeof patternMatch === 'string' ? target.replace(chars.asterisk, patternMatch) : target, @@ -1011,7 +1425,7 @@ async function packageTargetResolve( let resolved: URL | null | undefined try { - resolved = await packageTargetResolve( + resolved = packageTargetResolveSync( packageUrl, targetValue, subpath, @@ -1059,7 +1473,7 @@ async function packageTargetResolve( * * @const {URL | null | undefined} resolved */ - const resolved: URL | null | undefined = await packageTargetResolve( + const resolved: URL | null | undefined = packageTargetResolveSync( packageUrl, (target as ConditionalTargets)[key], subpath,