From b240202b6d85c43bebfd8a79e5503f6224f30a17 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 25 Jan 2024 19:35:17 +0200 Subject: [PATCH] fix: linked module resolution --- .github/workflows/ci.yml | 3 + .../linked-dependencies/bundled/package.json | 9 +++ .../bundled/reporters/default.js | 1 + .../linked-dependencies/external/package.json | 9 +++ .../external/reporters/default.js | 1 + __fixtures__/linked-local-reporter/index.js | 1 - .../linked-local-reporter/package.json | 6 -- .../simple-project/.esbuild-jestrc.js | 7 +- __fixtures__/simple-project/jest.config.js | 3 +- __fixtures__/simple-project/package.json | 6 +- __fixtures__/simple-project/verify.js | 64 +++++++++++++++++++ index.mjs | 36 ++++++++--- package.json | 1 + utils/resolve-module.mjs | 44 ++++++++++--- 14 files changed, 160 insertions(+), 31 deletions(-) create mode 100644 __fixtures__/linked-dependencies/bundled/package.json create mode 100644 __fixtures__/linked-dependencies/bundled/reporters/default.js create mode 100644 __fixtures__/linked-dependencies/external/package.json create mode 100644 __fixtures__/linked-dependencies/external/reporters/default.js delete mode 100644 __fixtures__/linked-local-reporter/index.js delete mode 100644 __fixtures__/linked-local-reporter/package.json create mode 100644 __fixtures__/simple-project/verify.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25dacc5..f1dc874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - name: Bundle project run: npm run build working-directory: __fixtures__/simple-project + - name: Verify the bundle + run: npm run verify + working-directory: __fixtures__/simple-project - name: Set up bundled project uses: bahmutov/npm-install@v1 with: diff --git a/__fixtures__/linked-dependencies/bundled/package.json b/__fixtures__/linked-dependencies/bundled/package.json new file mode 100644 index 0000000..9927537 --- /dev/null +++ b/__fixtures__/linked-dependencies/bundled/package.json @@ -0,0 +1,9 @@ +{ + "name": "@linked-dependencies/bundled", + "version": "1.0.0", + "main": "index.js", + "private": true, + "exports": { + "./reporter": "./reporters/default.js" + } +} diff --git a/__fixtures__/linked-dependencies/bundled/reporters/default.js b/__fixtures__/linked-dependencies/bundled/reporters/default.js new file mode 100644 index 0000000..b7e329a --- /dev/null +++ b/__fixtures__/linked-dependencies/bundled/reporters/default.js @@ -0,0 +1 @@ +module.exports = class LinkedLocalReporter {}; diff --git a/__fixtures__/linked-dependencies/external/package.json b/__fixtures__/linked-dependencies/external/package.json new file mode 100644 index 0000000..24c71fc --- /dev/null +++ b/__fixtures__/linked-dependencies/external/package.json @@ -0,0 +1,9 @@ +{ + "name": "@linked-dependencies/external", + "version": "1.0.0", + "main": "index.js", + "private": true, + "exports": { + "./reporter": "./reporters/default.js" + } +} diff --git a/__fixtures__/linked-dependencies/external/reporters/default.js b/__fixtures__/linked-dependencies/external/reporters/default.js new file mode 100644 index 0000000..c432783 --- /dev/null +++ b/__fixtures__/linked-dependencies/external/reporters/default.js @@ -0,0 +1 @@ +module.exports = class LinkedExternalReporter {}; diff --git a/__fixtures__/linked-local-reporter/index.js b/__fixtures__/linked-local-reporter/index.js deleted file mode 100644 index 511b491..0000000 --- a/__fixtures__/linked-local-reporter/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = class LinkedReporter {}; diff --git a/__fixtures__/linked-local-reporter/package.json b/__fixtures__/linked-local-reporter/package.json deleted file mode 100644 index 7f942b3..0000000 --- a/__fixtures__/linked-local-reporter/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "linked-local-reporter", - "version": "1.0.0", - "main": "index.js", - "private": true -} diff --git a/__fixtures__/simple-project/.esbuild-jestrc.js b/__fixtures__/simple-project/.esbuild-jestrc.js index 072d16e..c154d7a 100644 --- a/__fixtures__/simple-project/.esbuild-jestrc.js +++ b/__fixtures__/simple-project/.esbuild-jestrc.js @@ -6,7 +6,7 @@ module.exports = { "outExtension": { ".js": ".mjs" }, - "external": ["chalk", "dtrace-provider"], + "external": ["chalk", "dtrace-provider", "@linked-dependencies/external"], }, "preTransform": (path, contents) => { if (path.includes('lodash/noop')) { @@ -16,6 +16,9 @@ module.exports = { return contents; }, "package": { - "name": "custom-name" + "name": "custom-name", + "dependencies": { + "@linked-dependencies/external": "../linked-dependencies/external", + } } }; diff --git a/__fixtures__/simple-project/jest.config.js b/__fixtures__/simple-project/jest.config.js index bbc0743..7db0e8a 100644 --- a/__fixtures__/simple-project/jest.config.js +++ b/__fixtures__/simple-project/jest.config.js @@ -5,7 +5,8 @@ module.exports = { setupFilesAfterEnv: ['lodash/noop'], reporters: [ 'default', - 'linked-local-reporter', + '@linked-dependencies/bundled/reporter', + '@linked-dependencies/external/reporter', '/customReporter.js', ], testMatch: [ diff --git a/__fixtures__/simple-project/package.json b/__fixtures__/simple-project/package.json index a535017..45e3d51 100644 --- a/__fixtures__/simple-project/package.json +++ b/__fixtures__/simple-project/package.json @@ -5,15 +5,17 @@ "main": "index.js", "scripts": { "build": "esbuild-jest", - "test": "jest" + "test": "jest", + "verify": "node verify.js" }, "devDependencies": { + "@linked-dependencies/bundled": "../linked-dependencies/bundled", + "@linked-dependencies/external": "../linked-dependencies/external", "esbuild": "^0.19.8", "esbuild-jest-cli": "../..", "jest": "^29.5.0", "jest-environment-emit": "^1.0.3", "jest-allure2-reporter": "^2.0.0-beta.1", - "linked-local-reporter": "../linked-local-reporter", "lodash": "^4.17.21" } } diff --git a/__fixtures__/simple-project/verify.js b/__fixtures__/simple-project/verify.js new file mode 100644 index 0000000..324507e --- /dev/null +++ b/__fixtures__/simple-project/verify.js @@ -0,0 +1,64 @@ +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const rootDir = path.join(__dirname, '..', 'simple-project-bundled'); + +main(); + +function main() { + console.log('Verifying that the bundled project is correct...'); + verifyDirectoryStructure(); + verifyJestConfig(); + console.log('Success!'); +} + +function verifyDirectoryStructure() { + assertExists('package.json'); + assertExists('jest.config.json'); + assertExists('globalSetup.mjs'); + assertExists('globalTeardown.mjs'); + assertExists('customReporter.mjs'); + assertExists('src/entry1.test.mjs'); + assertExists('src/entry2.test.mjs'); + assertExists('_.._/linked-dependencies/bundled/reporters/default.mjs'); + assertExists('bundled_externals/jest-environment-emit/node.mjs'); + assertExists('bundled_externals/lodash/noop.mjs'); + assertDoesNotExist('node_modules'); + assertDoesNotExist('_.._/linked-dependencies/external'); +} + +function verifyJestConfig() { + const { globalSetup, reporters, setupFilesAfterEnv, testEnvironment, testMatch, testRunner, globalTeardown } = parseJSON('jest.config.json'); + + assertEqual(globalSetup, '/globalSetup.mjs', 'globalSetup'); + assertEqual(reporters.length, 4, 'reporters length'); + assertEqual(reporters[0][0], 'default', 'reporters[0]'); + assertEqual(reporters[1][0], '/_.._/linked-dependencies/bundled/reporters/default.mjs', 'reporters[1]'); + assertEqual(reporters[2][0], '@linked-dependencies/external/reporter', 'reporters[2]'); + assertEqual(reporters[3][0], '/customReporter.mjs', 'reporters[3]'); + assertEqual(setupFilesAfterEnv.length, 1, 'setupFilesAfterEnv.length'); + assertEqual(setupFilesAfterEnv[0], '/bundled_externals/lodash/noop.mjs', 'setupFilesAfterEnv[0]'); + assertEqual(testEnvironment, '/bundled_externals/jest-environment-emit/node.mjs', 'testEnvironment'); + assertEqual(testMatch.length, 2, 'testMatch.length'); + assertEqual(testMatch[0], '/src/entry1.test.mjs', 'testMatch[0]'); + assertEqual(testMatch[1], '/src/entry2.test.mjs', 'testMatch[1]'); + assertEqual(testRunner, 'jest-circus/runner', 'testRunner'); + assertEqual(globalTeardown, '/globalTeardown.mjs', 'globalTeardown'); +} + +function assertExists(fileName) { + assert(fs.existsSync(path.join(rootDir, fileName)), `${fileName} should exist`); +} + +function assertDoesNotExist(fileName) { + assert(!fs.existsSync(path.join(rootDir, fileName)), `${fileName} should not exist`); +} + +function assertEqual(actual, expected, name) { + assert(actual === expected, `${name} should be ${expected}, but was: ${actual}`); +} + +function parseJSON(fileName) { + return JSON.parse(fs.readFileSync(path.join(rootDir, fileName), 'utf8')); +} diff --git a/index.mjs b/index.mjs index 20f3b6b..1c3985e 100644 --- a/index.mjs +++ b/index.mjs @@ -3,10 +3,19 @@ import { build as esbuild } from 'esbuild'; import esbuildJest from './plugin.mjs'; import {ESM_REQUIRE_SHIM} from "./utils/esm-require-shim.mjs"; -import {convertPathToImport} from "./utils/resolve-module.mjs"; -import {importViaChain,importViaChainUnsafe} from "./utils/resolve-via-chain.mjs"; import {isBuiltinReporter} from "./utils/is-builtin-reporter.mjs"; import {JEST_DEPENDENCIES} from "./utils/jest-dependencies.mjs"; +import {logger, optimizedLogger, optimizeTracing} from "./utils/logger.mjs"; +import {convertPathToImport} from "./utils/resolve-module.mjs"; +import {importViaChain, importViaChainUnsafe} from "./utils/resolve-via-chain.mjs"; + +const __RESOLVED__ = optimizeTracing((id, resolved) => { + optimizedLogger.trace({ id }, `resolved: ${resolved}`); +}); + +const __IS_EXTERNAL__ = optimizeTracing((id, external) => { + optimizedLogger.trace(`mark as ${external ? 'external' : 'internal'}: ${id}`); +}); export async function build(esbuildJestConfig = {}) { const rootDir = process.cwd(); @@ -17,13 +26,17 @@ export async function build(esbuildJestConfig = {}) { ...(esbuildBaseConfig.external || []), ]; - const isExternal = (id) => { - const importLikePath = convertPathToImport(rootDir, id); - return !importLikePath.startsWith('') && externalModules.some(id => { - // TODO: This is not enough, we need to support wildcards and maybe some more syntax options - return id === importLikePath || importLikePath.startsWith(`${id}/`); + const isExternal = (id) => + optimizedLogger.trace.complete(`isExternal: ${id}`, () => { + const importLikePath = convertPathToImport(rootDir, id); + __RESOLVED__(id, importLikePath); + const result = !importLikePath.startsWith('') && externalModules.some(id => { + // TODO: This is not enough, we need to support wildcards and maybe some more syntax options + return id === importLikePath || importLikePath.startsWith(`${id}/`); + }); + __IS_EXTERNAL__(id, result); + return result; }); - } let buildArgv; @@ -49,6 +62,7 @@ export async function build(esbuildJestConfig = {}) { */ const fullConfig = await readConfig(jestArgv, rootDir, false); const { configPath, globalConfig, projectConfig } = fullConfig; + logger.trace({ configPath, globalConfig, projectConfig }, 'read Jest config'); const { default: Runtime } = importViaChain(rootDir, ['jest', '@jest/core'], 'jest-runtime'); const testContext = await Runtime.createContext(projectConfig, { maxWorkers: 1, watch: false, watchman: false }); @@ -66,7 +80,7 @@ export async function build(esbuildJestConfig = {}) { globalConfig.globalTeardown, ].filter(p => p && !isExternal(p)); - const buildResult = await esbuild({ + const esbuildConfig = { ...esbuildBaseConfig, bundle: true, @@ -92,7 +106,9 @@ export async function build(esbuildJestConfig = {}) { }), ...(esbuildBaseConfig.plugins || []), ], - }); + }; + + const buildResult = await logger.trace.complete(esbuildConfig, 'esbuild', () => esbuild(esbuildConfig)); return buildResult; } diff --git a/package.json b/package.json index 2472d91..128d6f6 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "bunyan": "^2.0.0", "bunyan-debug-stream": "^3.1.0", "cosmiconfig": "^8.1.3", + "find-up": "^7.0.0", "lodash.merge": "^4.6.2", "import-from": "^4.0.0", "resolve-from": "^5.0.0" diff --git a/utils/resolve-module.mjs b/utils/resolve-module.mjs index 8690bc4..bee6b8e 100644 --- a/utils/resolve-module.mjs +++ b/utils/resolve-module.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { findUpSync } from 'find-up'; import resolveFrom from "resolve-from"; export function convertPathToImport(rootDir, absolutePath) { @@ -33,16 +34,20 @@ function resolveFromEntries(rootDir, absolutePath) { return undefined; } - const packageExports = packageJson.exports || {}; - for (const innerEntry of Object.keys(packageExports)) { - const packageEntry = path.posix.join(packageName, innerEntry) - const resolvedPath = resolveFrom.silent(rootDir, packageEntry); - if (resolvedPath === absolutePath) { - return packageEntry; + const packageExports = packageJson.exports; + if (packageExports) { + for (const innerEntry of Object.keys(packageExports)) { + const packageEntry = path.posix.join(packageName, innerEntry) + const resolvedPath = resolveFrom.silent(rootDir, packageEntry); + if (resolvedPath === absolutePath) { + return packageEntry; + } } + } else { + return path.posix.join(packageName, ...path.relative(packagePath, absolutePath).split(path.sep)); } - return path.posix.join(packageName, ...path.relative(packagePath, absolutePath).split(path.sep)); + return undefined; } function resolveFromRootDirectory(rootDir, absolutePath) { @@ -56,7 +61,7 @@ function inferPackageInfo(rootDir, absolutePath) { const pathParts = relativePath.split(path.sep); const nodeModulesIndex = pathParts.indexOf('node_modules'); if (nodeModulesIndex < 0) { - return result; + return inferLinkedPackageInfo(rootDir, absolutePath) || result; } const isInnerModule = pathParts.lastIndexOf('node_modules') > nodeModulesIndex; @@ -78,11 +83,32 @@ function inferPackageInfo(rootDir, absolutePath) { return result; } +/** + * @param {string} rootDir + * @param {string} absolutePath + * @returns {{ packageName: string, packagePath: string } | undefined} + */ +function inferLinkedPackageInfo(rootDir, absolutePath) { + const packageJsonPath = findUpSync('package.json', { cwd: path.dirname(absolutePath) }); + if (!packageJsonPath || path.dirname(packageJsonPath) === rootDir) { + return undefined; + } + + const packageJson = parsePackageJson(packageJsonPath); + return { + packageName: packageJson.name, + packagePath: path.dirname(packageJsonPath), + }; +} + function readPackageJson(packagePath) { const packageJsonPath = path.join(packagePath, 'package.json'); + return parsePackageJson(packageJsonPath); +} +function parsePackageJson(filePath) { try { - return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }