From 624aa0d4d643bfb6da1cbf8b91291997b0c12c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Thu, 5 Oct 2023 19:08:03 +0100 Subject: [PATCH] feat(module): improve Module.resolve() --- package-lock.json | 4 +- package.json | 6 +- src/helpers/Module.ts | 99 ++++++++++++++------ src/types/ModuleResolveOptions.ts | 22 +++++ src/types/index.ts | 1 + tests/unit/ModuleTest.ts | 145 +++++++++++++++++++++++++++--- 6 files changed, 234 insertions(+), 43 deletions(-) create mode 100644 src/types/ModuleResolveOptions.ts diff --git a/package-lock.json b/package-lock.json index dc1f5a2..2fe04ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/common", - "version": "4.17.1", + "version": "4.17.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@athenna/common", - "version": "4.17.1", + "version": "4.17.2", "license": "MIT", "dependencies": { "@fastify/formbody": "^7.4.0", diff --git a/package.json b/package.json index 7528e5e..43b6d20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/common", - "version": "4.17.1", + "version": "4.17.2", "description": "The Athenna common helpers to use in any Node.js ESM project.", "license": "MIT", "author": "João Lenon ", @@ -22,8 +22,8 @@ "scripts": { "build": "node node_modules/@athenna/tsconfig/src/build.js", "lint:fix": "eslint \"{bin,src,tests}/**/*.ts\" --fix", - "test": "npm run --silent lint:fix && node --import=@athenna/tsconfig bin/test.ts", - "test:debug": "cross-env NODE_DEBUG=athenna:* node --inspect --import=@athenna/tsconfig bin/test.ts", + "test": "npm run --silent lint:fix && node --enable-source-maps --import=@athenna/tsconfig bin/test.ts", + "test:debug": "cross-env NODE_DEBUG=athenna:* node --inspect --enable-source-maps --import=@athenna/tsconfig bin/test.ts", "test:coverage": "c8 npm run --silent test" }, "files": [ diff --git a/src/helpers/Module.ts b/src/helpers/Module.ts index 2796996..47c14c1 100644 --- a/src/helpers/Module.ts +++ b/src/helpers/Module.ts @@ -8,16 +8,16 @@ */ import { debug } from '#src/debug' -import { Path, File, Folder } from '#src' import { createRequire } from 'node:module' import { fileURLToPath, pathToFileURL } from 'node:url' -import { extname, dirname, resolve, isAbsolute } from 'node:path' +import { dirname, extname, isAbsolute } from 'node:path' +import { Path, File, Folder, type ModuleResolveOptions, Options } from '#src' export class Module { /** * Get the module first export match or default. */ - public static async get(module: any | Promise): Promise { + public static async get(module: any | Promise): Promise { module = await module if (module.default) { @@ -47,10 +47,10 @@ export class Module { * console.log(alias) // 'App/Services/MyService' * console.log(module) // [class MyService] */ - public static async getWithAlias( + public static async getWithAlias( module: any | Promise, subAlias: string - ): Promise<{ alias: string; module: any }> { + ): Promise<{ alias: string; module: T }> { module = await Module.get(module) if (!subAlias.endsWith('/')) { @@ -96,7 +96,7 @@ export class Module { /** * Same as get method, but import the path directly. */ - public static async getFrom(path: string): Promise { + public static async getFrom(path: string): Promise { const module = await Module.import(path) return Module.get(module) @@ -105,10 +105,10 @@ export class Module { /** * Same as getWithAlias method, but import the path directly. */ - public static async getFromWithAlias( + public static async getFromWithAlias( path: string, subAlias: string - ): Promise<{ alias: string; module: any }> { + ): Promise<{ alias: string; module: T }> { const module = await Module.import(path) return Module.getWithAlias(module, subAlias) @@ -160,7 +160,7 @@ export class Module { * Import a full path using the path href to ensure compatibility * between OS's. */ - public static async import(path: string): Promise { + public static async import(path: string): Promise { debug('trying to import the path: %s', path) if (!isAbsolute(path)) { @@ -175,7 +175,7 @@ export class Module { * module does not exist, catching the error throw from bad * import. */ - public static async safeImport(path: string): Promise { + public static async safeImport(path: string): Promise { try { return await Module.import(path) } catch (err) { @@ -184,40 +184,87 @@ export class Module { } /** - * Resolve the module path by meta url and import it. + * Resolve the module path by parent URL. */ - public static async resolve(path: string, meta: string): Promise { + public static async resolve( + path: string, + parentURL: string, + options: ModuleResolveOptions = {} + ): Promise { + options = Options.create(options, { + import: true, + getModule: true + }) + const splitted = path.split('?') const queries = splitted[1] || '' path = splitted[0] - if (!path.startsWith('#') && extname(path)) { - path = resolve(path) + const resolve = async (path: string) => { + if (queries) { + path = path.concat('?', queries) + } + + if (!options.import) { + return path + } + + if (!options.getModule) { + return import(path) + } + + return Module.get(import(path)) } if (isAbsolute(path)) { - path = pathToFileURL(path).href + debug( + "path is absolute and don't need to be resolved, importing path: %s and query params: %s", + path, + queries + ) + + return resolve(pathToFileURL(path).href) } + if (!path.startsWith('#') && extname(path)) { + debug( + 'trying to resolve relative path: %s, with parent URL: %s and query params: %s', + path, + parentURL, + queries + ) + + return resolve(new URL(path, parentURL).href) + } + + if (process.argv.includes('--experimental-import-meta-resolve')) { + debug( + 'trying to resolve import alias path: %s with parent URL: %s and query params: %s using import.meta.resolve', + path, + parentURL, + queries + ) + + return resolve(await import.meta.resolve(path, parentURL)) + } + + const require = Module.createRequire(parentURL) + debug( - 'trying to resolve path: %s, with parent URL: %s and query params: %s', + 'trying to resolve import alias path: %s with parent URL: %s and query params: %s using require.resolve', path, - meta, + parentURL, queries ) - // `await` is not needed for `import.meta.resolve` method, - // but TypeScript complains on it. - let resolvedPath = await import.meta.resolve(path, meta) - - debug('resolved path: %s', resolvedPath) - - if (queries) { - resolvedPath = resolvedPath.concat('?', queries) + try { + path = require.resolve(path) + } catch (error) { + path = error.message.match(/'(.*?)'/)[1] } - return Module.get(import(resolvedPath)) + return resolve(pathToFileURL(path).href) } /** diff --git a/src/types/ModuleResolveOptions.ts b/src/types/ModuleResolveOptions.ts new file mode 100644 index 0000000..3bd0f9b --- /dev/null +++ b/src/types/ModuleResolveOptions.ts @@ -0,0 +1,22 @@ +/** + * @athenna/common + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type ModuleResolveOptions = { + /** + * Automatically import the module instead of returning + * the module path. + */ + import?: boolean + + /** + * Automatically get the imported module using `Module.get()` + * method. + */ + getModule?: boolean +} diff --git a/src/types/index.ts b/src/types/index.ts index 31d6c9f..69ed56e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,6 +37,7 @@ export * from '#src/types/json/FileJson' export * from '#src/types/json/FolderJson' export * from '#src/types/json/ExceptionJson' +export * from '#src/types/ModuleResolveOptions' export * from '#src/types/pagination/PaginationOptions' export * from '#src/types/pagination/PaginatedResponse' diff --git a/tests/unit/ModuleTest.ts b/tests/unit/ModuleTest.ts index debca0e..d4e9fbf 100644 --- a/tests/unit/ModuleTest.ts +++ b/tests/unit/ModuleTest.ts @@ -7,10 +7,17 @@ * file that was distributed with this source code. */ -import { Module, Path } from '#src' -import { Test, type Context } from '@athenna/test' +import { Exception, Module, Path } from '#src' +import { Test, type Context, AfterEach } from '@athenna/test' export default class ModuleTest { + @AfterEach() + public afterEach() { + if (process.argv.includes('--experimental-import-meta-resolve')) { + process.argv.splice(process.argv.indexOf('--experimental-import-meta-resolve'), 1) + } + } + @Test() public async shouldBeAbleToGetTheModuleFirstExportMatchOrDefault({ assert }: Context) { const moduleDefault = await Module.get(import('../fixtures/config/app.js')) @@ -95,58 +102,172 @@ export default class ModuleTest { } @Test() - public async shouldBeAbleToResolveImportAliasByMetaUrlAndImportIt({ assert }: Context) { + public async shouldBeAbleToResolveImportAliasByParentURLUsingRequireResolveAndImportIt({ assert }: Context) { + const Exception = await Module.resolve('#src/helpers/Exception', import.meta.url) + + assert.equal(Exception.name, 'Exception') + } + + @Test() + public async shouldBeAbleToResolveImportAliasByParentURLUsingImportMetaResolveAndImportIt({ assert }: Context) { + process.argv.push('--experimental-import-meta-resolve') + const Exception = await Module.resolve('#src/helpers/Exception', import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveImportAliasWithDotsInThePathByMetaUrlAndImportIt({ assert }: Context) { + public async shouldBeAbleToResolveImportAliasWithDotsInThePathByParentURLUsingRequireResolveAndImportIt({ + assert + }: Context) { const AppController = await Module.resolve('#tests/fixtures/controllers/app.controller', import.meta.url) assert.equal(AppController.name, 'AppController') } @Test() - public async shouldBeAbleToResolvePartialPathsByMetaUrlAndImportIt({ assert }: Context) { - const Exception = await Module.resolve('./src/helpers/Exception.js', import.meta.url) + public async shouldBeAbleToResolveImportAliasWithDotsInThePathByParentURLUsingImportMetaResolveAndImportIt({ + assert + }: Context) { + process.argv.push('--experimental-import-meta-resolve') + + const AppController = await Module.resolve('#tests/fixtures/controllers/app.controller', import.meta.url) + + assert.equal(AppController.name, 'AppController') + } + + @Test() + public async shouldBeAbleToResolveRelativePathsByParentURLUsingURLAndImportIt({ assert }: Context) { + const Exception = await Module.resolve('../../src/helpers/Exception.js', import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveAbsolutePathsByMetaUrlAndImportIt({ assert }: Context) { + public async shouldBeAbleToResolveRelativePathsWithDotsByParentURLUsingURLAndImportIt({ assert }: Context) { + const AppController = await Module.resolve('../fixtures/controllers/app.controller.js', import.meta.url) + + assert.equal(AppController.name, 'AppController') + } + + @Test() + public async shouldBeAbleToResolveAbsolutePathsByParentURLAndImportIt({ assert }: Context) { const Exception = await Module.resolve(Path.src('helpers/Exception.js'), import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveVersionedImportAliasByMetaUrlAndImportIt({ assert }: Context) { + public async shouldBeAbleToResolveAbsolutePathsWithDotsByParentURLAndImportIt({ assert }: Context) { + const AppController = await Module.resolve(Path.fixtures('controllers/app.controller.js'), import.meta.url) + + assert.equal(AppController.name, 'AppController') + } + + @Test() + public async shouldBeAbleToResolveVersionedImportAliasByParentURLAndImportIt({ assert }: Context) { const Exception = await Module.resolve(`#src/helpers/Exception?version=${Math.random()}`, import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveVersionedPartialPAthsByMetaUrlAndImportIt({ assert }: Context) { - const Exception = await Module.resolve(`./src/helpers/Exception.js?version=${Math.random()}`, import.meta.url) + public async shouldBeAbleToResolveVersionedRelativePathsByParentURLAndImportIt({ assert }: Context) { + const Exception = await Module.resolve(`../../src/helpers/Exception.js?version=${Math.random()}`, import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveVersionedAbsolutePathsByMetaUrlAndImportIt({ assert }: Context) { + public async shouldBeAbleToResolveVersionedAbsolutePathsByParentURLAndImportIt({ assert }: Context) { const Exception = await Module.resolve(Path.src(`helpers/Exception.js?version=${Math.random()}`), import.meta.url) assert.equal(Exception.name, 'Exception') } @Test() - public async shouldBeAbleToResolveModulesFromNodeModulesUsingResolve({ assert }: Context) { + public async shouldBeAbleToResolveModulesFromNodeModulesUsingRequireResolve({ assert }: Context) { + const chalk = await Module.resolve('chalk', import.meta.url) + + assert.deepEqual(chalk, (await import('chalk')).default) + } + + @Test() + public async shouldBeAbleToResolveModulesFromNodeModulesUsingImportMetaResolve({ assert }: Context) { + process.argv.push('--experimental-import-meta-resolve') + const chalk = await Module.resolve('chalk', import.meta.url) assert.deepEqual(chalk, (await import('chalk')).default) } + + @Test() + public async shouldBeAbleToResolveImportAliasPathAndGetThePathAsResult({ assert }: Context) { + const path = await Module.resolve('#src/helpers/Exception', import.meta.url, { import: false }) + + assert.isTrue(path.startsWith('file:')) + assert.isTrue(path.includes('src/helpers/Exception.js')) + } + + @Test() + public async shouldBeAbleToResolveRelativePathAndGetThePathAsResult({ assert }: Context) { + const path = await Module.resolve('../../src/helpers/Exception.js', import.meta.url, { import: false }) + + assert.isTrue(path.startsWith('file:')) + assert.isTrue(path.includes('src/helpers/Exception.js')) + } + + @Test() + public async shouldBeAbleToResolveAbsolutePathAndGetThePathAsResult({ assert }: Context) { + const path = await Module.resolve(Path.src('helpers/Exception.js'), import.meta.url, { import: false }) + + assert.isTrue(path.startsWith('file:')) + assert.isTrue(path.includes('src/helpers/Exception.js')) + } + + @Test() + public async shouldBeAbleToResolveLibsFromNodeModulesAndGetThePathAsResult({ assert }: Context) { + const path = await Module.resolve('chalk', import.meta.url, { import: false }) + + assert.isTrue(path.startsWith('file:')) + assert.isTrue(path.includes('node_modules/chalk/source/index.js')) + } + + @Test() + public async shouldBeAbleToResolveImportAliasPathAndGetAllTheModuleAsResult({ assert }: Context) { + const module = await Module.resolve('#src/helpers/Exception', import.meta.url, { import: true, getModule: false }) + + assert.deepEqual(module.Exception, Exception) + } + + @Test() + public async shouldBeAbleToResolveRelativePathAndGetAllTheModuleAsResult({ assert }: Context) { + const module = await Module.resolve('../../src/helpers/Exception.js', import.meta.url, { + import: true, + getModule: false + }) + + assert.deepEqual(module.Exception, Exception) + } + + @Test() + public async shouldBeAbleToResolveAbsolutePathAndGetAllTheModuleAsResult({ assert }: Context) { + const module = await Module.resolve(Path.src('helpers/Exception.js'), import.meta.url, { + import: true, + getModule: false + }) + + assert.deepEqual(module.Exception, Exception) + } + + @Test() + public async shouldBeAbleToResolveLibsFromNodeModulesAndGetAllTheModuleThePathAsResult({ assert }: Context) { + const module = await Module.resolve('chalk', import.meta.url, { + import: true, + getModule: false + }) + + assert.deepEqual(module, await import('chalk')) + } }