diff --git a/.cspell.json b/.cspell.json index 17708557..5f1a69a3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -30,7 +30,7 @@ "patches/", "yarn.lock" ], - "ignoreRegExpList": [], + "ignoreRegExpList": ["/data:.+;base64,.+/"], "ignoreWords": [], "language": "en-US", "patterns": [], diff --git a/README.md b/README.md index 5a00210a..6d59ad29 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ import { defaultMainFields, extensionFormatMap, formats, + getSource, isAbsoluteSpecifier, isArrayIndex, isBareSpecifier, @@ -106,6 +107,7 @@ This package exports the following identifiers: - [`defaultMainFields`](./src/lib/default-main-fields.mts) - [`extensionFormatMap`](./src/lib/extension-format-map.mts) - [`formats`](./src/lib/formats.mts) +- [`getSource`](./src/lib/get-source.mts) - [`isAbsoluteSpecifier`](./src/lib/is-absolute-specifier.mts) - [`isArrayIndex`](./src/lib/is-array-index.mts) - [`isBareSpecifier`](./src/lib/is-bare-specifier.mts) diff --git a/eslint.base.config.mjs b/eslint.base.config.mjs index 1033df69..0a05b4af 100644 --- a/eslint.base.config.mjs +++ b/eslint.base.config.mjs @@ -171,10 +171,12 @@ export default [ globals: { ...globals.es2024, ...globals.node, + BufferEncoding: 'readonly', Chai: 'readonly', Console: 'readonly', NodeJS: 'readonly', - React: fs.existsSync('node_modules/react') ? 'readonly' : false + React: fs.existsSync('node_modules/react') ? 'readonly' : false, + RequestInit: 'readonly' }, parser: /** @type {Parser} */ (ts.parser), parserOptions: { diff --git a/package.json b/package.json index 3716764c..fda63702 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,18 @@ "node": "fs", "default": "fs" }, + "#internal/process": { + "types": { + "mlly": "./src/internal/process.d.mts", + "default": "./dist/internal/process.d.mts" + }, + "browser": { + "mlly": "./src/internal/process.browser.mts", + "default": "./dist/internal/process.browser.mjs" + }, + "node": "process", + "default": "process" + }, "#internal/*": { "mlly": "./src/internal/*.mts", "default": "./dist/internal/*.mjs" diff --git a/src/__snapshots__/index.e2e.snap b/src/__snapshots__/index.e2e.snap index 6a50ad39..8294d058 100644 --- a/src/__snapshots__/index.e2e.snap +++ b/src/__snapshots__/index.e2e.snap @@ -9,6 +9,7 @@ exports[`e2e:mlly > should expose public api 1`] = ` "defaultMainFields", "extensionFormatMap", "formats", + "getSource", "isAbsoluteSpecifier", "isArrayIndex", "isBareSpecifier", diff --git a/src/interfaces/__tests__/context-get-source.spec-d.mts b/src/interfaces/__tests__/context-get-source.spec-d.mts new file mode 100644 index 00000000..466524d8 --- /dev/null +++ b/src/interfaces/__tests__/context-get-source.spec-d.mts @@ -0,0 +1,43 @@ +/** + * @file Type Tests - GetSourceContext + * @module mlly/interfaces/tests/unit-d/GetSourceContext + */ + +import type TestSubject from '#interfaces/context-get-source' +import type { + FileSystem, + GetSourceHandlers, + GetSourceOptions +} from '@flex-development/mlly' + +describe('unit-d:interfaces/GetSourceContext', () => { + it('should extend GetSourceOptions', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should match [error: boolean]', () => { + expectTypeOf().toHaveProperty('error').toEqualTypeOf() + }) + + it('should match [fs: FileSystem]', () => { + expectTypeOf().toHaveProperty('fs').toEqualTypeOf() + }) + + it('should match [handlers: GetSourceHandlers]', () => { + expectTypeOf() + .toHaveProperty('handlers') + .toEqualTypeOf() + }) + + it('should match [req: RequestInit]', () => { + expectTypeOf() + .toHaveProperty('req') + .toEqualTypeOf() + }) + + it('should match [schemes: Set', () => { + expectTypeOf() + .toHaveProperty('schemes') + .toEqualTypeOf>() + }) +}) diff --git a/src/interfaces/__tests__/options-get-source.spec-d.mts b/src/interfaces/__tests__/options-get-source.spec-d.mts new file mode 100644 index 00000000..d9c43a25 --- /dev/null +++ b/src/interfaces/__tests__/options-get-source.spec-d.mts @@ -0,0 +1,54 @@ +/** + * @file Type Tests - GetSourceOptions + * @module mlly/interfaces/tests/unit-d/GetSourceOptions + */ + +import type TestSubject from '#interfaces/options-get-source' +import type { + FileSystem, + GetSourceHandlers, + ModuleFormat +} from '@flex-development/mlly' +import type { Nilable } from '@flex-development/tutils' + +describe('unit-d:interfaces/GetSourceOptions', () => { + it('should allow empty object', () => { + assertType({}) + }) + + it('should match [format?: ModuleFormat | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('format') + .toEqualTypeOf>() + }) + + it('should match [fs?: FileSystem | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('fs') + .toEqualTypeOf>() + }) + + it('should match [handlers?: GetSourceHandlers | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('handlers') + .toEqualTypeOf>() + }) + + it('should match [ignoreErrors?: boolean | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('ignoreErrors') + .toEqualTypeOf>() + }) + + it('should match [req?: RequestInit | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('req') + .toEqualTypeOf>() + }) + + it('should match [schemes?: Set | readonly string[] | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('schemes') + .toEqualTypeOf | readonly string[]>>() + }) +}) diff --git a/src/interfaces/__tests__/protocol-map.spec-d.mts b/src/interfaces/__tests__/protocol-map.spec-d.mts index ea6d37c6..5eb8c92f 100644 --- a/src/interfaces/__tests__/protocol-map.spec-d.mts +++ b/src/interfaces/__tests__/protocol-map.spec-d.mts @@ -6,79 +6,91 @@ import type TestSubject from '#interfaces/protocol-map' describe('unit-d:interfaces/ProtocolMap', () => { - it('should have property "blob:"', () => { - expectTypeOf().toHaveProperty('blob:') + it('should match [blob: "blob:"]', () => { + expectTypeOf().toHaveProperty('blob').toEqualTypeOf<'blob:'>() }) - it('should have property "content:"', () => { - expectTypeOf().toHaveProperty('content:') + it('should match [content: "content:"]', () => { + expectTypeOf() + .toHaveProperty('content') + .toEqualTypeOf<'content:'>() }) - it('should have property "cvs:"', () => { - expectTypeOf().toHaveProperty('cvs:') + it('should match [cvs: "cvs:"]', () => { + expectTypeOf().toHaveProperty('cvs').toEqualTypeOf<'cvs:'>() }) - it('should have property "data:"', () => { - expectTypeOf().toHaveProperty('data:') + it('should match [data: "data:"]', () => { + expectTypeOf().toHaveProperty('data').toEqualTypeOf<'data:'>() }) - it('should have property "dns:"', () => { - expectTypeOf().toHaveProperty('dns:') + it('should match [dns: "dns:"]', () => { + expectTypeOf().toHaveProperty('dns').toEqualTypeOf<'dns:'>() }) - it('should have property "file:"', () => { - expectTypeOf().toHaveProperty('file:') + it('should match [file: "file:"]', () => { + expectTypeOf().toHaveProperty('file').toEqualTypeOf<'file:'>() }) - it('should have property "fish:"', () => { - expectTypeOf().toHaveProperty('fish:') + it('should match [fish: "fish:"]', () => { + expectTypeOf().toHaveProperty('fish').toEqualTypeOf<'fish:'>() }) - it('should have property "ftp:"', () => { - expectTypeOf().toHaveProperty('ftp:') + it('should match [ftp: "ftp:"]', () => { + expectTypeOf().toHaveProperty('ftp').toEqualTypeOf<'ftp:'>() }) - it('should have property "git:"', () => { - expectTypeOf().toHaveProperty('git:') + it('should match [git: "git:"]', () => { + expectTypeOf().toHaveProperty('git').toEqualTypeOf<'git:'>() }) - it('should have property "http:"', () => { - expectTypeOf().toHaveProperty('http:') + it('should match [http: "http:"]', () => { + expectTypeOf().toHaveProperty('http').toEqualTypeOf<'http:'>() }) - it('should have property "https:"', () => { - expectTypeOf().toHaveProperty('https:') + it('should match [https: "https:"]', () => { + expectTypeOf() + .toHaveProperty('https') + .toEqualTypeOf<'https:'>() }) - it('should have property "mvn:"', () => { - expectTypeOf().toHaveProperty('mvn:') + it('should match [mvn: "mvn:"]', () => { + expectTypeOf().toHaveProperty('mvn').toEqualTypeOf<'mvn:'>() }) - it('should have property "redis:"', () => { - expectTypeOf().toHaveProperty('redis:') + it('should match [node: "node:"]', () => { + expectTypeOf().toHaveProperty('node').toEqualTypeOf<'node:'>() }) - it('should have property "sftp:"', () => { - expectTypeOf().toHaveProperty('sftp:') + it('should match [redis: "redis:"]', () => { + expectTypeOf() + .toHaveProperty('redis') + .toEqualTypeOf<'redis:'>() }) - it('should have property "ssh:"', () => { - expectTypeOf().toHaveProperty('ssh:') + it('should match [sftp: "sftp:"]', () => { + expectTypeOf().toHaveProperty('sftp').toEqualTypeOf<'sftp:'>() }) - it('should have property "svn:"', () => { - expectTypeOf().toHaveProperty('svn:') + it('should match [ssh: "ssh:"]', () => { + expectTypeOf().toHaveProperty('ssh').toEqualTypeOf<'ssh:'>() }) - it('should have property "view-source:"', () => { - expectTypeOf().toHaveProperty('view-source:') + it('should match [svn: "svn:"]', () => { + expectTypeOf().toHaveProperty('svn').toEqualTypeOf<'svn:'>() }) - it('should have property "ws:"', () => { - expectTypeOf().toHaveProperty('ws:') + it('should match [viewSource: "view-source:"]', () => { + expectTypeOf() + .toHaveProperty('viewSource') + .toEqualTypeOf<'view-source:'>() }) - it('should have property "wss:"', () => { - expectTypeOf().toHaveProperty('wss:') + it('should match [ws: "ws:"]', () => { + expectTypeOf().toHaveProperty('ws').toEqualTypeOf<'ws:'>() + }) + + it('should match [wss: "wss:"]', () => { + expectTypeOf().toHaveProperty('wss').toEqualTypeOf<'wss:'>() }) }) diff --git a/src/interfaces/context-get-source.mts b/src/interfaces/context-get-source.mts new file mode 100644 index 00000000..7b767d54 --- /dev/null +++ b/src/interfaces/context-get-source.mts @@ -0,0 +1,63 @@ +/** + * @file Interfaces - GetSourceContext + * @module mlly/interfaces/GetSourceContext + */ + +import type { + FileSystem, + GetSourceHandlers, + GetSourceOptions +} from '@flex-development/mlly' + +/** + * Source code retrieval context. + * + * @see {@linkcode GetSourceOptions} + * + * @extends {GetSourceOptions} + */ +interface GetSourceContext extends GetSourceOptions { + /** + * Throw [`ERR_UNSUPPORTED_ESM_URL_SCHEME`][err]? + * + * [err]: https://nodejs.org/api/errors.html#err_unsupported_esm_url_scheme + */ + error: boolean + + /** + * File system API. + * + * @see {@linkcode FileSystem} + * + * @override + */ + fs: FileSystem + + /** + * URL handler map. + * + * @see {@linkcode GetSourceHandlers} + * + * @override + */ + handlers: GetSourceHandlers + + /** + * Request options for network based modules. + * + * > 👉 **Note**: Only applicable if {@linkcode network} is + * > enabled. + * + * @override + */ + req: RequestInit + + /** + * List of supported URL schemes. + * + * @override + */ + schemes: Set +} + +export type { GetSourceContext as default } diff --git a/src/interfaces/index.mts b/src/interfaces/index.mts index 3a5ab6ee..5d7084db 100644 --- a/src/interfaces/index.mts +++ b/src/interfaces/index.mts @@ -4,9 +4,15 @@ */ export type { default as Aliases } from '#interfaces/aliases' +export type { + default as GetSourceContext +} from '#interfaces/context-get-source' export type { default as FileSystem } from '#interfaces/file-system' export type { default as MainFieldMap } from '#interfaces/main-field-map' export type { default as ModuleFormatMap } from '#interfaces/module-format-map' +export type { + default as GetSourceOptions +} from '#interfaces/options-get-source' export type { default as ResolveAliasOptions } from '#interfaces/options-resolve-alias' diff --git a/src/interfaces/options-get-source.mts b/src/interfaces/options-get-source.mts new file mode 100644 index 00000000..24d27725 --- /dev/null +++ b/src/interfaces/options-get-source.mts @@ -0,0 +1,59 @@ +/** + * @file Interfaces - GetSourceOptions + * @module mlly/interfaces/GetSourceOptions + */ + +import type { + FileSystem, + GetSourceHandlers, + ModuleFormat +} from '@flex-development/mlly' + +/** + * Source code retrieval options. + */ +interface GetSourceOptions { + /** + * Module format hint. + * + * @see {@linkcode ModuleFormat} + */ + format?: ModuleFormat | null | undefined + + /** + * File system API. + * + * @see {@linkcode FileSystem} + */ + fs?: FileSystem | null | undefined + + /** + * URL handler map. + * + * @see {@linkcode GetSourceHandlers} + */ + handlers?: GetSourceHandlers | null | undefined + + /** + * Ignore [`ERR_UNSUPPORTED_ESM_URL_SCHEME`][err] if thrown. + * + * [err]: https://nodejs.org/api/errors.html#err_unsupported_esm_url_scheme + */ + ignoreErrors?: boolean | null | undefined + + /** + * Request options for network based modules. + * + * > 👉 **Note**: Only applicable if {@linkcode network} is enabled. + */ + req?: RequestInit | null | undefined + + /** + * List of supported URL schemes. + * + * @default ['data','file','http','https','node'] + */ + schemes?: Set | readonly string[] | null | undefined +} + +export type { GetSourceOptions as default } diff --git a/src/interfaces/protocol-map.mts b/src/interfaces/protocol-map.mts index 9b83a706..389f97b8 100644 --- a/src/interfaces/protocol-map.mts +++ b/src/interfaces/protocol-map.mts @@ -11,26 +11,27 @@ * @see https://url.spec.whatwg.org/#special-scheme */ interface ProtocolMap { - 'blob:': true - 'content:': true - 'cvs:': true - 'data:': true - 'dns:': true - 'file:': true - 'fish:': true - 'ftp:': true - 'git:': true - 'http:': true - 'https:': true - 'mvn:': true - 'redis:': true - 'sftp:': true - 'ssh:': true - 'svn:': true - 'urn:': true - 'view-source:': true - 'ws:': true - 'wss:': true + blob: 'blob:' + content: 'content:' + cvs: 'cvs:' + data: 'data:' + dns: 'dns:' + file: 'file:' + fish: 'fish:' + ftp: 'ftp:' + git: 'git:' + http: 'http:' + https: 'https:' + mvn: 'mvn:' + node: 'node:' + redis: 'redis:' + sftp: 'sftp:' + ssh: 'ssh:' + svn: 'svn:' + urn: 'urn:' + viewSource: 'view-source:' + ws: 'ws:' + wss: 'wss:' } export type { ProtocolMap as default } diff --git a/src/internal/fs.browser.mts b/src/internal/fs.browser.mts index bf6e1fc2..cd13f789 100644 --- a/src/internal/fs.browser.mts +++ b/src/internal/fs.browser.mts @@ -1,6 +1,6 @@ /** * @file Internal - fs/browser - * @module mlly/lib/fs/browser + * @module mlly/internal/fs/browser */ import type { FileSystem } from '@flex-development/mlly' diff --git a/src/internal/process.browser.mts b/src/internal/process.browser.mts new file mode 100644 index 00000000..b55915ca --- /dev/null +++ b/src/internal/process.browser.mts @@ -0,0 +1,9 @@ +/** + * @file Internal - process/browser + * @module mlly/internal/process/browser + */ + +export default { + browser: true, + platform: 'browser' +} diff --git a/src/internal/process.d.mts b/src/internal/process.d.mts new file mode 100644 index 00000000..138acea8 --- /dev/null +++ b/src/internal/process.d.mts @@ -0,0 +1,4 @@ +declare module '#internal/process' { + const process: NodeJS.Process + export default process +} diff --git a/src/lib/__snapshots__/get-source.snap b/src/lib/__snapshots__/get-source.snap new file mode 100644 index 00000000..203b2745 --- /dev/null +++ b/src/lib/__snapshots__/get-source.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unit:lib/getSource > should throw on unsupported url scheme (%#) 1`] = `[Error: Only URLs with a scheme in: file are supported by the default ESM loader. Received protocol 'git:']`; diff --git a/src/lib/__tests__/get-source.spec.mts b/src/lib/__tests__/get-source.spec.mts new file mode 100644 index 00000000..29abf6c2 --- /dev/null +++ b/src/lib/__tests__/get-source.spec.mts @@ -0,0 +1,50 @@ +/** + * @file Unit Tests - getSource + * @module mlly/lib/tests/unit/getSource + */ + +import testSubject from '#lib/get-source' +import { + codes, + isNodeError, + type NodeError +} from '@flex-development/errnode' +import pathe from '@flex-development/pathe' + +describe('unit:lib/getSource', () => { + it.each>([ + ['os'], + [new URL('node:test')] + ])('should return `undefined` if `id` is builtin module (%j)', async id => { + expect(await testSubject(id)).to.be.undefined + }) + + it.each>([ + ['data:application/javascript;base64,SGVsbG8sIFdvcmxkIQ=='], + ['data:text/javascript,console.log("hello!");'], + ['https://esm.sh/@flex-development/mlly'], + [pathe.fileURLToPath(import.meta.url)], + [pathe.pathToFileURL('build.config.mts')] + ])('should return source code for `id` (%#)', async (id, options) => { + expect(await testSubject(id, options)).to.be.a('string') + }) + + it('should throw on unsupported url scheme (%#)', async () => { + // Arrange + let error!: NodeError + + // Act + try { + await testSubject('git://github.com/flex-development/mlly.git', { + schemes: ['file'] + }) + } catch (e: unknown) { + error = e as typeof error + } + + // Expect + expect(error).to.satisfy(isNodeError) + expect(error).to.have.property('code', codes.ERR_UNSUPPORTED_ESM_URL_SCHEME) + expect(error).toMatchSnapshot() + }) +}) diff --git a/src/lib/can-parse-url.mts b/src/lib/can-parse-url.mts index 13b00709..6d2caf48 100644 --- a/src/lib/can-parse-url.mts +++ b/src/lib/can-parse-url.mts @@ -13,19 +13,16 @@ import type { ModuleId } from '@flex-development/mlly' * * @see {@linkcode ModuleId} * - * @param {ModuleId} input + * @param {unknown} input * The absolute or relative input URL to parse - * @param {ModuleId | null | undefined} [base] + * @param {unknown} [base] * Base URL to resolve against if `input` is not absolute * @return {boolean} * `true` if `input` can be parsed to a `URL` */ -function canParseUrl( - input: ModuleId, - base?: ModuleId | null | undefined -): boolean { +function canParseUrl(input: unknown, base?: unknown): boolean { try { - new URL(input, base ?? undefined) + new URL(input as ModuleId, (base ?? undefined) as ModuleId | undefined) return true } catch { return false diff --git a/src/lib/get-source.mts b/src/lib/get-source.mts new file mode 100644 index 00000000..ec049f81 --- /dev/null +++ b/src/lib/get-source.mts @@ -0,0 +1,193 @@ +/** + * @file getSource + * @module mlly/lib/getSource + */ + +import fs from '#internal/fs' +import process from '#internal/process' +import canParseUrl from '#lib/can-parse-url' +import isFile from '#lib/is-file' +import { + ERR_UNSUPPORTED_ESM_URL_SCHEME, + type ErrUnsupportedEsmUrlScheme +} from '@flex-development/errnode' +import { isBuiltin } from '@flex-development/is-builtin' +import type { + GetSourceContext, + GetSourceHandler, + GetSourceOptions, + ModuleId, + Protocol +} from '@flex-development/mlly' +import pathe from '@flex-development/pathe' +import { ok } from 'devlop' + +export default getSource + +/** + * Get the source code for `id`. + * + * > 👉 **Note**: If `id` is not a [builtin module][builtin-module] and also + * > cannot be parsed as an {@linkcode URL}, it will be assumed to be a path and + * > converted to a [`file:` URL][file-url]. + * + * [builtin-module]: https://nodejs.org/api/esm.html#builtin-modules + * [file-url]: https://nodejs.org/api/esm.html#file-urls + * + * @see {@linkcode ErrUnsupportedEsmUrlScheme} + * @see {@linkcode GetSourceOptions} + * @see {@linkcode ModuleId} + * + * @async + * + * @this {void} + * + * @param {ModuleId} id + * Module id to handle + * @param {GetSourceOptions | null | undefined} [options] + * Source code retrieval options + * @return {Promise} + * Source code for `id` + * @throws {ErrUnsupportedEsmUrlScheme} + */ +async function getSource( + this: void, + id: ModuleId, + options?: GetSourceOptions | null | undefined +): Promise { + /** + * Source code retrieval context. + * + * @const {GetSourceContext} context + */ + const context: GetSourceContext = { + ...options, + error: false, + fs: options?.fs ?? fs, + handlers: { + 'data:': data, + 'file:': file, + 'http:': https, + 'https:': https, + 'node:': node, + ...options?.handlers + }, + req: { ...options?.req }, + schemes: new Set( + options?.schemes + ? [...options.schemes] + : ['data', 'file', 'http', 'https', 'node'] + ) + } + + /** + * Module url. + * + * @const {URL} url + */ + const url: URL = isBuiltin(id) + ? new URL('node:' + String(id).replace(/^node:/, '')) + : canParseUrl(id) + ? new URL(id) + : (ok(typeof id === 'string', 'expected string'), pathe.pathToFileURL(id)) + + /** + * Source code handler for {@linkcode url}. + * + * @const {GetSourceHandler} handle + */ + const handle: GetSourceHandler | null | undefined = + context.handlers[url.protocol as Protocol] + + /** + * Source code. + * + * @var {Uint8Array | string | null | undefined} code + */ + let code: Uint8Array | string | null | undefined + + // get source code + if (handle) { + code = await handle.call(context, url) + } else { + context.error = true + } + + // throw on unsupported url scheme + if (context.error && !context.ignoreErrors) { + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME( + url, + [...context.schemes], + process.platform === 'win32' + ) + } + + if (code !== null && code !== undefined) code = String(code) + return code +} + +/** + * @this {GetSourceContext} + * + * @param {URL} url + * Module URL + * @return {Buffer} + * Source code buffer + */ +function data(this: GetSourceContext, url: URL): Buffer { + const [mime, data = ''] = url.pathname.split(',') + + ok(url.protocol === 'data:', 'expected `data:` URL') + ok(mime !== undefined, 'expected `mime` to be a string') + + return Buffer.from( + decodeURIComponent(data), + mime.endsWith('base64') ? 'base64' : 'utf8' + ) +} + +/** + * @this {GetSourceContext} + * + * @param {URL} url + * Module URL + * @return {Buffer | string | null} + * Source code or `null` if module is not found + */ +function file(this: GetSourceContext, url: URL): Buffer | string | null { + ok(url.protocol === 'file:', 'expected `file:` URL') + + /** + * Source code. + * + * @var {Buffer | string | null} code + */ + let code: Buffer | string | null = null + + if (isFile(url, this.fs)) code = this.fs.readFileSync(url) + return code +} + +/** + * @this {GetSourceContext} + * + * @param {URL} url + * Module URL + * @return {Promise} + * Source code + */ +async function https(this: GetSourceContext, url: URL): Promise { + ok(/^https?:$/.test(url.protocol), 'expected `http:` or `https:` URL') + return (await fetch(url.href, this.req)).text() +} + +/** + * @this {GetSourceContext} + * + * @param {URL} url + * Module URL + * @return {undefined} + */ +function node(this: GetSourceContext, url: URL): undefined { + return void ok(url.protocol === 'node:', 'expected `node:` URL') +} diff --git a/src/lib/index.mts b/src/lib/index.mts index 3d4493a4..9651d6c8 100644 --- a/src/lib/index.mts +++ b/src/lib/index.mts @@ -20,6 +20,7 @@ export { default as defaultExtensions } from '#lib/default-extensions' export { default as defaultMainFields } from '#lib/default-main-fields' export { default as extensionFormatMap } from '#lib/extension-format-map' export { default as formats } from '#lib/formats' +export { default as getSource } from '#lib/get-source' export { default as isAbsoluteSpecifier } from '#lib/is-absolute-specifier' export { default as isArrayIndex } from '#lib/is-array-index' export { default as isBareSpecifier } from '#lib/is-bare-specifier' diff --git a/src/types/__tests__/awaitable.spec-d.mts b/src/types/__tests__/awaitable.spec-d.mts new file mode 100644 index 00000000..c4307b48 --- /dev/null +++ b/src/types/__tests__/awaitable.spec-d.mts @@ -0,0 +1,18 @@ +/** + * @file Type Tests - Awaitable + * @module mlly/types/tests/unit-d/Awaitable + */ + +import type TestSubject from '#types/awaitable' + +describe('unit-d:types/Awaitable', () => { + type T = Uint8Array | string | null | undefined + + it('should extract PromiseLike', () => { + expectTypeOf>().extract>().not.toBeNever() + }) + + it('should extract T', () => { + expectTypeOf>().extract().not.toBeNever() + }) +}) diff --git a/src/types/__tests__/get-source-handler.spec-d.mts b/src/types/__tests__/get-source-handler.spec-d.mts new file mode 100644 index 00000000..464a761e --- /dev/null +++ b/src/types/__tests__/get-source-handler.spec-d.mts @@ -0,0 +1,27 @@ +/** + * @file Type Tests - GetSourceHandler + * @module mlly/types/tests/unit-d/GetSourceHandler + */ + +import type TestSubject from '#types/get-source-handler' +import type { Awaitable, GetSourceContext } from '@flex-development/mlly' + +describe('unit-d:types/GetSourceHandler', () => { + it('should match [this: GetSourceContext]', () => { + expectTypeOf().thisParameter.toEqualTypeOf() + }) + + describe('parameters', () => { + it('should be callable with [URL]', () => { + expectTypeOf().parameters.toEqualTypeOf<[URL]>() + }) + }) + + describe('returns', () => { + it('should return Awaitable', () => { + expectTypeOf() + .returns + .toEqualTypeOf>() + }) + }) +}) diff --git a/src/types/__tests__/get-source-handlers.spec-d.mts b/src/types/__tests__/get-source-handlers.spec-d.mts new file mode 100644 index 00000000..2032b46f --- /dev/null +++ b/src/types/__tests__/get-source-handlers.spec-d.mts @@ -0,0 +1,16 @@ +/** + * @file Type Tests - GetSourceHandlers + * @module mlly/types/tests/unit-d/GetSourceHandlers + */ + +import type TestSubject from '#types/get-source-handlers' +import type { GetSourceHandler, Protocol } from '@flex-development/mlly' +import type { Nilable } from '@flex-development/tutils' + +describe('unit-d:types/GetSourceHandlers', () => { + it('should match [[key: Protocol]: GetSourceHandler | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('' as Protocol) + .toEqualTypeOf>() + }) +}) diff --git a/src/types/__tests__/protocol.spec-d.mts b/src/types/__tests__/protocol.spec-d.mts index 4c424b91..7f20c6b0 100644 --- a/src/types/__tests__/protocol.spec-d.mts +++ b/src/types/__tests__/protocol.spec-d.mts @@ -7,8 +7,7 @@ import type TestSubject from '#types/protocol' import type { ProtocolMap } from '@flex-development/mlly' describe('unit-d:types/Protocol', () => { - it('should equal Extract', () => { - expectTypeOf() - .toEqualTypeOf>() + it('should equal ProtocolMap[keyof ProtocolMap]', () => { + expectTypeOf().toEqualTypeOf() }) }) diff --git a/src/types/awaitable.mts b/src/types/awaitable.mts new file mode 100644 index 00000000..5babe042 --- /dev/null +++ b/src/types/awaitable.mts @@ -0,0 +1,16 @@ +/** + * @file Type Aliases - Awaitable + * @module mlly/types/Awaitable + */ + +/** + * Create a union of `T` and a `T` as a promise-like value. + * + * @see {@linkcode PromiseLike} + * + * @template {any} T + * Value + */ +type Awaitable = PromiseLike | T + +export type { Awaitable as default } diff --git a/src/types/get-source-handler.mts b/src/types/get-source-handler.mts new file mode 100644 index 00000000..539d7624 --- /dev/null +++ b/src/types/get-source-handler.mts @@ -0,0 +1,27 @@ +/** + * @file Type Aliases - GetSourceHandler + * @module mlly/types/GetSourceHandler + */ + +import type { Awaitable, GetSourceContext } from '@flex-development/mlly' + +/** + * Get the source code for `url`. + * + * @see {@linkcode Awaitable} + * @see {@linkcode GetSourceContext} + * @see {@linkcode URL} + * + * @this {GetSourceContext} + * + * @param {URL} url + * Module URL + * @return {Awaitable} + * Source code + */ +type GetSourceHandler = ( + this: GetSourceContext, + url: URL +) => Awaitable + +export type { GetSourceHandler as default } diff --git a/src/types/get-source-handlers.mts b/src/types/get-source-handlers.mts new file mode 100644 index 00000000..b0ccd722 --- /dev/null +++ b/src/types/get-source-handlers.mts @@ -0,0 +1,20 @@ +/** + * @file Type Aliases - GetSourceHandlers + * @module mlly/types/GetSourceHandlers + */ + +import type { GetSourceHandler, Protocol } from '@flex-development/mlly' + +/** + * Map where key is a URL protocol, and each value is `null`, `undefined`, or a + * source code handler. + * + * @see {@linkcode GetSourceHandler} + * @see {@linkcode Protocol} + */ +type GetSourceHandlers = Partial> + +export type { GetSourceHandlers as default } diff --git a/src/types/index.mts b/src/types/index.mts index 2d3049dd..05959f1e 100644 --- a/src/types/index.mts +++ b/src/types/index.mts @@ -3,7 +3,10 @@ * @module mlly/types */ +export type { default as Awaitable } from '#types/awaitable' export type { default as ChangeExtFn } from '#types/change-ext-fn' +export type { default as GetSourceHandler } from '#types/get-source-handler' +export type { default as GetSourceHandlers } from '#types/get-source-handlers' export type { default as MainField } from '#types/main-field' export type { default as ModuleFormat } from '#types/module-format' export type { default as ModuleId } from '#types/module-id' diff --git a/src/types/protocol.mts b/src/types/protocol.mts index 6a57aa1b..b85b6971 100644 --- a/src/types/protocol.mts +++ b/src/types/protocol.mts @@ -11,6 +11,6 @@ import type { ProtocolMap } from '@flex-development/mlly' * To register new protocols, augment {@linkcode ProtocolMap}. They will be * added to this union automatically. */ -type Protocol = Extract +type Protocol = ProtocolMap[keyof ProtocolMap] export type { Protocol as default } diff --git a/tsconfig.build.json b/tsconfig.build.json index 848147bb..82c00b07 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,19 +4,21 @@ "allowJs": false, "checkJs": false, "customConditions": ["mlly"], - "declaration": true, + "declarationMap": false, "noEmitOnError": true, "outDir": "./dist", "paths": {}, "rootDir": "./src", "skipLibCheck": true, + "sourceMap": false, "target": "es2023" }, "exclude": ["**/__tests__/**"], "extends": "./tsconfig.json", "files": [ - "typings/@flex-development/pkg-types/index.d.mts", - "typings/typescript/lib.es5.d.ts" + "./typings/@flex-development/pkg-types/index.d.mts", + "./typings/node/process.d.ts", + "./typings/typescript/lib.es5.d.ts" ], - "include": ["./dist/**/*", "./src/**/*"] + "include": ["./dist/**/*", "./src/**/**.mts"] } diff --git a/tsconfig.json b/tsconfig.json index 54d5932d..99a5262d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,8 @@ "alwaysStrict": true, "checkJs": true, "customConditions": ["mlly", "development"], - "declaration": false, - "declarationMap": false, + "declaration": true, + "declarationMap": true, "emitDecoratorMetadata": false, "esModuleInterop": true, "exactOptionalPropertyTypes": true, diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index ee6ae2bd..c8cf85e2 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -9,6 +9,7 @@ "files": [ "./typings/@faker-js/faker/global.d.ts", "./typings/@flex-development/pkg-types/index.d.mts", + "./typings/node/process.d.ts", "./typings/typescript/lib.es5.d.ts", "./vitest-env.d.mts" ], diff --git a/typings/node/process.d.ts b/typings/node/process.d.ts new file mode 100644 index 00000000..99eb55e6 --- /dev/null +++ b/typings/node/process.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface Process { + browser?: boolean | null | undefined + } +}