diff --git a/README.md b/README.md index d53676b9..307ea62a 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ This package exports the following identifiers: - `ERR_INVALID_PACKAGE_CONFIG` - `ERR_INVALID_PACKAGE_TARGET` - `ERR_INVALID_RETURN_VALUE` + - `ERR_INVALID_URL_SCHEME` - `ERR_INVALID_URL` - `ERR_METHOD_NOT_IMPLEMENTED` - `ERR_MISSING_OPTION` diff --git a/__tests__/utils/node-versions.ts b/__tests__/utils/node-versions.ts index 46e4d4af..ccfc1b41 100644 --- a/__tests__/utils/node-versions.ts +++ b/__tests__/utils/node-versions.ts @@ -15,7 +15,8 @@ const NODE_VERSIONS: SemanticVersion[] = [ '19.9.0', '20.17.0', '21.7.3', - '22.7.0' + '22.7.0', + '22.8.0' ] export default NODE_VERSIONS diff --git a/loader.mjs b/loader.mjs deleted file mode 100644 index ccb7953c..00000000 --- a/loader.mjs +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @file Custom Loader Hooks - * @module loader - * @see https://nodejs.org/api/esm.html#loaders - */ - -import { DECORATOR_REGEX } from '@flex-development/decorator-regex' -import * as esm from '@flex-development/esm-types' -import * as mlly from '@flex-development/mlly' -import * as pathe from '@flex-development/pathe' -import * as tutils from '@flex-development/tutils' -import * as esbuild from 'esbuild' -import { URL, fileURLToPath, pathToFileURL } from 'node:url' -import ts from 'typescript' -import tsconfig from './tsconfig.json' assert { type: 'json' } - -// add support for extensionless files in "bin" scripts -// https://github.com/nodejs/modules/issues/488 -mlly.EXTENSION_FORMAT_MAP.set('', mlly.Format.COMMONJS) - -/** - * URL of current working directory. - * - * @type {URL} - * @const cwd - */ -const cwd = pathToFileURL(tsconfig.compilerOptions.baseUrl) - -/** - * Determines how the given module `url` should be interpreted, retrieved, and - * parsed. - * - * @see {@linkcode esm.LoadHookContext} - * @see {@linkcode esm.LoadHookResult} - * @see {@linkcode esm.LoadHook} - * @see {@linkcode esm.ResolvedModuleUrl} - * @see https://nodejs.org/api/esm.html#loadurl-context-nextload - * - * @async - * - * @param {esm.ResolvedModuleUrl} url - * Resolved module URL - * @param {esm.LoadHookContext} context - * Hook context - * @return {Promise} - * Hook result - */ -export const load = async (url, context) => { - // get module format - context.conditions = context.conditions ?? [] - context.format = context.format ?? (await mlly.getFormat(url)) - - // validate import assertions - mlly.validateAssertions(url, context.format, context.importAssertions) - - /** - * File extension of {@linkcode url}. - * - * @type {pathe.Ext | tutils.EmptyString} - * @const ext - */ - const ext = pathe.extname(url) - - /** - * Module source code. - * - * @type {tutils.Optional>} - * @var source - */ - let source = await mlly.getSource(url, { format: context.format }) - - // add custom conditions - if (tutils.isArray(tsconfig.compilerOptions.customConditions)) { - context.conditions = [ - ...context.conditions, - ...tsconfig.compilerOptions.customConditions - ] - } - - // emit decorator metadata - DECORATOR_REGEX.lastIndex = 0 - if (DECORATOR_REGEX.test(source)) { - const { outputText } = ts.transpileModule(source, { - compilerOptions: { ...tsconfig.compilerOptions, inlineSourceMap: true }, - fileName: url - }) - - source = outputText - } - - // transform typescript files - if (/^\.(?:cts|mts|tsx?)$/.test(ext) && !/\.d\.(?:cts|mts|ts)$/.test(url)) { - // push require condition for .cts files and update format - if (ext === '.cts') { - context.conditions.unshift('require', 'node') - context.format = mlly.Format.MODULE - } - - // resolve path aliases - source = await mlly.resolveAliases(source, { - aliases: tsconfig.compilerOptions.paths, - conditions: new Set(context.conditions), - cwd, - ext: '', - parent: url - }) - - // resolve modules - source = await mlly.resolveModules(source, { - conditions: new Set(context.conditions), - parent: url - }) - - // transpile source code - const { code } = await esbuild.transform(source, { - format: 'esm', - keepNames: true, - loader: ext.slice(/^\.[cm]/.test(ext) ? 2 : 1), - minify: false, - sourcefile: fileURLToPath(url), - sourcemap: 'inline', - target: `node${process.versions.node}`, - tsconfigRaw: { compilerOptions: tsconfig.compilerOptions } - }) - - // set source code to transpiled source - source = code - } - - return { - format: context.format, - shortCircuit: true, - source: tutils.ifelse(context.format === mlly.Format.COMMONJS, null, source) - } -} - -/** - * Resolves the given module `specifier`, and its module format as a hint to the - * {@linkcode load} hook. - * - * Adds supports for: - * - * - Path alias resolution - * - Extensionless file and directory index resolution - * - * @see {@linkcode esm.ResolveHookContext} - * @see {@linkcode esm.ResolveHookResult} - * @see {@linkcode esm.ResolveHook} - * @see https://nodejs.org/api/esm.html#resolvespecifier-context-nextresolve - * - * @async - * - * @param {string} specifier - * Module specifier - * @param {esm.ResolveHookContext} context - * Hook context - * @return {Promise} - * Hook result - */ -export const resolve = async (specifier, context) => { - const { conditions, parentURL: parent } = context - - // resolve path alias - specifier = await mlly.resolveAlias(specifier, { - aliases: tsconfig.compilerOptions.paths, - conditions, - cwd, - parent - }) - - /** - * Resolved module {@linkcode URL}. - * - * @type {URL} - * @const url - */ - const url = await mlly.resolveModule(specifier, { - conditions, - parent: parent?.startsWith('file:') ? parent : specifier - }) - - return { - format: await mlly.getFormat(url), - shortCircuit: true, - url: url.href - } -} diff --git a/package.json b/package.json index f7ceece8..50a4eeb5 100644 --- a/package.json +++ b/package.json @@ -34,22 +34,33 @@ "files": [ "CHANGELOG.md", "LICENSE.md", - "dist", - "src" + "README.md", + "dist" ], "exports": { - ".": "./dist/index.mjs", + ".": { + "vitest": null, + "default": "./dist/index.mjs" + }, "./package.json": "./package.json" }, "imports": { "#format-message": { + "ts-node": "./src/internal/format-message.development.ts", "development": "./dist/internal/format-message.development.mjs", "production": "./dist/internal/format-message.mjs", "default": "./dist/internal/format-message.mjs" }, - "#hide-stack-frames": "./dist/internal/hide-stack-frames.mjs", - "#k-is-node-error": "./dist/internal/k-is-node-error.mjs", + "#hide-stack-frames": { + "ts-node": "./src/internal/hide-stack-frames.ts", + "default": "./dist/internal/hide-stack-frames.mjs" + }, + "#k-is-node-error": { + "ts-node": "./src/internal/k-is-node-error.ts", + "default": "./dist/internal/k-is-node-error.mjs" + }, "#stack-trace": { + "ts-node": "./src/internal/stack-trace.ts", "browser": "./dist/internal/stack-trace.browser.mjs", "node": "./dist/internal/stack-trace.mjs", "default": "./dist/internal/stack-trace.mjs" @@ -106,11 +117,8 @@ "@commitlint/types": "19.0.3", "@eslint/js": "9.9.1", "@flex-development/commitlint-config": "1.0.1", - "@flex-development/decorator-regex": "2.0.0", - "@flex-development/esm-types": "2.0.0", "@flex-development/grease": "3.0.0-alpha.9", "@flex-development/mkbuild": "1.0.0-alpha.23", - "@flex-development/mlly": "1.0.0-alpha.18", "@flex-development/pathe": "2.0.0", "@flex-development/pkg-types": "4.0.0", "@stylistic/eslint-plugin": "2.7.2", diff --git a/src/__snapshots__/errors.integration.snap b/src/__snapshots__/errors.integration.snap index 79b3b1ee..c5d5ffe3 100644 --- a/src/__snapshots__/errors.integration.snap +++ b/src/__snapshots__/errors.integration.snap @@ -30,6 +30,8 @@ exports[`integration:errors > ERR_INVALID_RETURN_VALUE > #toString > should retu exports[`integration:errors > ERR_INVALID_URL > #toString > should return string representation of error 1`] = `TypeError [ERR_INVALID_URL]: Invalid URL`; +exports[`integration:errors > ERR_INVALID_URL_SCHEME > #toString > should return string representation of error 1`] = `TypeError [ERR_INVALID_URL_SCHEME]: The URL must be of scheme file`; + exports[`integration:errors > ERR_METHOD_NOT_IMPLEMENTED > #toString > should return string representation of error 1`] = `Error [ERR_METHOD_NOT_IMPLEMENTED]: The _transform() method is not implemented`; exports[`integration:errors > ERR_MISSING_OPTION > #toString > should return string representation of error 1`] = `TypeError [ERR_MISSING_OPTION]: init.highWaterMark is required`; diff --git a/src/__snapshots__/index.e2e.snap b/src/__snapshots__/index.e2e.snap index 0e0388f8..a186e23a 100644 --- a/src/__snapshots__/index.e2e.snap +++ b/src/__snapshots__/index.e2e.snap @@ -20,6 +20,7 @@ exports[`e2e:errnode > should expose public api 1`] = ` "ERR_INVALID_PACKAGE_TARGET", "ERR_INVALID_RETURN_VALUE", "ERR_INVALID_URL", + "ERR_INVALID_URL_SCHEME", "ERR_METHOD_NOT_IMPLEMENTED", "ERR_MISSING_OPTION", "ERR_MODULE_NOT_FOUND", diff --git a/src/__tests__/errors.integration.spec.ts b/src/__tests__/errors.integration.spec.ts index fa4e469d..bbfd7161 100644 --- a/src/__tests__/errors.integration.spec.ts +++ b/src/__tests__/errors.integration.spec.ts @@ -77,6 +77,7 @@ describe('integration:errors', () => { ], [codes.ERR_INVALID_RETURN_VALUE, TypeError, 'null', 'body', 13], [codes.ERR_INVALID_URL, TypeError, pathe.sep, 'http://[127.0.0.1]:8000'], + [codes.ERR_INVALID_URL_SCHEME, TypeError, 'file'], [codes.ERR_METHOD_NOT_IMPLEMENTED, Error, '_transform()'], [codes.ERR_MISSING_OPTION, TypeError, 'init.highWaterMark'], [ diff --git a/src/errors/__snapshots__/err-invalid-url-scheme.snap b/src/errors/__snapshots__/err-invalid-url-scheme.snap new file mode 100644 index 00000000..326206a1 --- /dev/null +++ b/src/errors/__snapshots__/err-invalid-url-scheme.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unit:errors/ERR_INVALID_URL_SCHEME > should build message from parameters (0) 1`] = `"The URL must be of scheme file"`; + +exports[`unit:errors/ERR_INVALID_URL_SCHEME > should build message from parameters (1) 1`] = `"The URL must be one of scheme file or node"`; diff --git a/src/errors/__tests__/err-invalid-url-scheme.spec-d.ts b/src/errors/__tests__/err-invalid-url-scheme.spec-d.ts new file mode 100644 index 00000000..aefe1003 --- /dev/null +++ b/src/errors/__tests__/err-invalid-url-scheme.spec-d.ts @@ -0,0 +1,36 @@ +/** + * @file Type Tests - ERR_INVALID_URL_SCHEME + * @module errnode/errors/tests/unit-d/ERR_INVALID_URL_SCHEME + */ + +import { codes } from '#src/enums' +import type { NodeError, NodeErrorConstructor } from '#src/interfaces' +import type * as TestSubject from '../err-invalid-url-scheme' + +describe('unit-d:errors/ERR_INVALID_URL_SCHEME', () => { + describe('ERR_INVALID_URL_SCHEME', () => { + it('should be ErrInvalidUrlSchemeConstructor', () => { + expectTypeOf() + .toEqualTypeOf() + }) + }) + + describe('ErrInvalidUrlScheme', () => { + it('should extend NodeError', () => { + expectTypeOf() + .toMatchTypeOf>() + }) + }) + + describe('ErrInvalidUrlSchemeConstructor', () => { + it('should match NodeErrorConstructor', () => { + // Arrange + type T = TestSubject.ErrInvalidUrlScheme + type Args = TestSubject.ErrInvalidUrlSchemeArgs + + // Expect + expectTypeOf() + .toMatchTypeOf>() + }) + }) +}) diff --git a/src/errors/__tests__/err-invalid-url-scheme.spec.ts b/src/errors/__tests__/err-invalid-url-scheme.spec.ts new file mode 100644 index 00000000..a7640543 --- /dev/null +++ b/src/errors/__tests__/err-invalid-url-scheme.spec.ts @@ -0,0 +1,22 @@ +/** + * @file Unit Tests - ERR_INVALID_URL_SCHEME + * @module errnode/errors/tests/unit/ERR_INVALID_URL_SCHEME + */ + +import TestSubject, { + type ErrInvalidUrlScheme +} from '../err-invalid-url-scheme' + +describe('unit:errors/ERR_INVALID_URL_SCHEME', () => { + it.each>([ + ['file'], + [['file', 'node']] + ])('should build message from parameters (%#)', (...args) => { + // Act + const subject: ErrInvalidUrlScheme = new TestSubject(...args) + + // Expect + expect(subject).to.have.property('message').be.a('string').that.is.not.empty + expect(subject.message).toMatchSnapshot() + }) +}) diff --git a/src/errors/err-invalid-url-scheme.ts b/src/errors/err-invalid-url-scheme.ts new file mode 100644 index 00000000..b2001c37 --- /dev/null +++ b/src/errors/err-invalid-url-scheme.ts @@ -0,0 +1,99 @@ +/** + * @file Errors - ERR_INVALID_URL_SCHEME + * @module errnode/errors/ERR_INVALID_URL_SCHEME + * @see https://github.com/nodejs/node/blob/v22.8.0/lib/internal/errors.js#L1543-L1552 + */ + +import E from '#e' +import { codes } from '#src/enums' +import type { NodeError, NodeErrorConstructor } from '#src/interfaces' +import formatList from '#src/utils/format-list' +import { ok } from 'devlop' + +/** + * `ERR_INVALID_URL_SCHEME` schema. + * + * @see {@linkcode NodeError} + * @see https://nodejs.org/api/errors.html#err_invalid_url_scheme + * + * @extends {NodeError} + * @extends {TypeError} + */ +interface ErrInvalidUrlScheme + extends NodeError, TypeError {} + +/** + * `ERR_INVALID_URL_SCHEME` message arguments. + */ +type Args = [expected: string | readonly string[]] + +/** + * `ERR_INVALID_URL_SCHEME` constructor. + * + * @see {@linkcode Args} + * @see {@linkcode ErrInvalidUrlScheme} + * @see {@linkcode NodeErrorConstructor} + * + * @extends {NodeErrorConstructor} + */ +interface ErrInvalidUrlSchemeConstructor + extends NodeErrorConstructor { + /** + * Create a new `ERR_INVALID_URL_SCHEME` error. + * + * @see {@linkcode ErrInvalidUrlScheme} + * + * @param {ReadonlyArray | string} expected + * Expected scheme or list of expected schemes + * @return {ErrInvalidUrlScheme} + */ + new (expected: string | readonly string[]): ErrInvalidUrlScheme +} + +/** + * `ERR_INVALID_URL_SCHEME` model. + * + * Thrown when an attempt is made to use a URL of an incompatible scheme + * (protocol). + * + * @see {@linkcode ErrInvalidUrlSchemeConstructor} + * + * @type {ErrInvalidUrlSchemeConstructor} + * + * @class + */ +const ERR_INVALID_URL_SCHEME: ErrInvalidUrlSchemeConstructor = E( + codes.ERR_INVALID_URL_SCHEME, + TypeError, + /** + * @param {ReadonlyArray | string} expected + * Expected scheme or list of expected schemes + * @return {string} + * Error message + */ + function message(expected: string | readonly string[]): string { + ok(expected.length, 'expected `expected` to not be empty') + + /** + * Error message. + * + * @var {string} message + */ + let message: string = 'The URL must be ' + + message += Array.isArray(expected) && expected.length > 1 + ? 'one of scheme' + : 'of scheme' + + message += ` ${formatList(expected, 'or')}` + + return message + } +) + +export { + ERR_INVALID_URL_SCHEME as default, + type ErrInvalidUrlScheme, + type Args as ErrInvalidUrlSchemeArgs, + type ErrInvalidUrlSchemeConstructor +} diff --git a/src/errors/index.ts b/src/errors/index.ts index 5730adeb..4713e870 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -94,6 +94,12 @@ export { type ErrInvalidUrlArgs, type ErrInvalidUrlConstructor } from './err-invalid-url' +export { + default as ERR_INVALID_URL_SCHEME, + type ErrInvalidUrlScheme, + type ErrInvalidUrlSchemeArgs, + type ErrInvalidUrlSchemeConstructor +} from './err-invalid-url-scheme' export { default as ERR_METHOD_NOT_IMPLEMENTED, type ErrMethodNotImplemented, diff --git a/tsconfig.build.json b/tsconfig.build.json index 883befe5..adff39ac 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,13 +5,10 @@ "noEmitOnError": true, "paths": { "#e": ["src/e"], - "#format-message": ["#format-message"], - "#k-is-node-error": ["#k-is-node-error"], - "#stack-trace": ["#stack-trace"], "#src/*": ["src/*"] }, "skipLibCheck": false, - "target": "es2022" + "target": "es2023" }, "exclude": [], "extends": "./tsconfig.json", diff --git a/tsconfig.json b/tsconfig.json index 7a30a2eb..6ca3e076 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "alwaysStrict": false, "baseUrl": ".", "checkJs": false, - "customConditions": ["development", "import", "node"], + "customConditions": ["development", "import", "node", "ts-node"], "declaration": false, "declarationMap": false, "emitDecoratorMetadata": false, @@ -31,14 +31,7 @@ "paths": { "#e": ["src/e"], "#fixtures/*": ["__fixtures__/*"], - "#format-message": ["src/internal/format-message.development"], - "#hide-stack-frames": ["src/internal/hide-stack-frames"], - "#k-is-node-error": ["src/internal/k-is-node-error"], - "#is-stack-trace-limit-writable": [ - "src/internal/is-stack-trace-limit-writable" - ], "#src/*": ["src/*"], - "#stack-trace": ["src/internal/stack-trace"], "#tests/*": ["__tests__/*"] }, "preserveConstEnums": true, diff --git a/vitest.config.ts b/vitest.config.ts index 6f3deb53..f4b25f36 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -42,6 +42,9 @@ const config: UserConfigExport = defineConfig((env: ConfigEnv): UserConfig => { return { define: {}, plugins: [tsconfigPaths({ projects: [tsconfig] })], + resolve: { + conditions: ['vitest', 'ts-node'] + }, test: { allowOnly: !ci, benchmark: { diff --git a/yarn.lock b/yarn.lock index db33f806..376a2e4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1524,11 +1524,8 @@ __metadata: "@commitlint/types": "npm:19.0.3" "@eslint/js": "npm:9.9.1" "@flex-development/commitlint-config": "npm:1.0.1" - "@flex-development/decorator-regex": "npm:2.0.0" - "@flex-development/esm-types": "npm:2.0.0" "@flex-development/grease": "npm:3.0.0-alpha.9" "@flex-development/mkbuild": "npm:1.0.0-alpha.23" - "@flex-development/mlly": "npm:1.0.0-alpha.18" "@flex-development/pathe": "npm:2.0.0" "@flex-development/pkg-types": "npm:4.0.0" "@flex-development/tutils": "npm:6.0.0-alpha.25" @@ -1654,17 +1651,6 @@ __metadata: languageName: unknown linkType: soft -"@flex-development/esm-types@npm:2.0.0": - version: 2.0.0 - resolution: "@flex-development/esm-types@npm:2.0.0::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Fesm-types%2F2.0.0%2Fbd7aef7a92e089250bd8a17e8c73b16bf8ae248c" - dependencies: - "@flex-development/tutils": "npm:6.0.0-alpha.11" - peerDependencies: - "@types/node": ">=16.18.23" - checksum: 10/5e9030a8528485a6098fea9d4babf1457803bb85b22690f986460c097f62572ae04fd9f37646c55f6a4e449732ec3129e6ed962d8710d6b8230cfeb3f6d562ec - languageName: node - linkType: hard - "@flex-development/export-regex@npm:1.0.2": version: 1.0.2 resolution: "@flex-development/export-regex@npm:1.0.2::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Fexport-regex%2F1.0.2%2F9f8b8a1ca3fd1f2e259f9da14d4750f79622ee43" @@ -1922,17 +1908,6 @@ __metadata: languageName: node linkType: hard -"@flex-development/tutils@npm:6.0.0-alpha.11": - version: 6.0.0-alpha.11 - resolution: "@flex-development/tutils@npm:6.0.0-alpha.11::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Ftutils%2F6.0.0-alpha.11%2Fcd1832885d050e2b6403e471bae0b7f7ffc70680" - dependencies: - dequal: "npm:2.0.3" - peerDependencies: - typescript: ">=5.0.4" - checksum: 10/1a2f7a1a6c7b6a10cc70efe3b23df8007a86e1a55ad2a1fd9480e1453fc3c10da0ba8680e177b316c526c79871210b5c185ae27ff1a6b815f2d6c5673a413d50 - languageName: node - linkType: hard - "@flex-development/tutils@npm:6.0.0-alpha.12": version: 6.0.0-alpha.12 resolution: "@flex-development/tutils@npm:6.0.0-alpha.12::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Ftutils%2F6.0.0-alpha.12%2F09e3edc5c0533b5a51a88aa87c8b13bf48aeceb9"