Skip to content

Commit

Permalink
fix: linked module resolution (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph authored Jan 25, 2024
1 parent 83862ab commit 00eb9f2
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions __fixtures__/linked-dependencies/bundled/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@linked-dependencies/bundled",
"version": "1.0.0",
"main": "index.js",
"private": true,
"exports": {
"./reporter": "./reporters/default.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = class LinkedLocalReporter {};
9 changes: 9 additions & 0 deletions __fixtures__/linked-dependencies/external/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@linked-dependencies/external",
"version": "1.0.0",
"main": "index.js",
"private": true,
"exports": {
"./reporter": "./reporters/default.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = class LinkedExternalReporter {};
1 change: 0 additions & 1 deletion __fixtures__/linked-local-reporter/index.js

This file was deleted.

6 changes: 0 additions & 6 deletions __fixtures__/linked-local-reporter/package.json

This file was deleted.

7 changes: 5 additions & 2 deletions __fixtures__/simple-project/.esbuild-jestrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand All @@ -16,6 +16,9 @@ module.exports = {
return contents;
},
"package": {
"name": "custom-name"
"name": "custom-name",
"dependencies": {
"@linked-dependencies/external": "../linked-dependencies/external",
}
}
};
3 changes: 2 additions & 1 deletion __fixtures__/simple-project/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ module.exports = {
setupFilesAfterEnv: ['lodash/noop'],
reporters: [
'default',
'linked-local-reporter',
'@linked-dependencies/bundled/reporter',
'@linked-dependencies/external/reporter',
'<rootDir>/customReporter.js',
],
testMatch: [
Expand Down
6 changes: 4 additions & 2 deletions __fixtures__/simple-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
64 changes: 64 additions & 0 deletions __fixtures__/simple-project/verify.js
Original file line number Diff line number Diff line change
@@ -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, '<rootDir>/globalSetup.mjs', 'globalSetup');
assertEqual(reporters.length, 4, 'reporters length');
assertEqual(reporters[0][0], 'default', 'reporters[0]');
assertEqual(reporters[1][0], '<rootDir>/_.._/linked-dependencies/bundled/reporters/default.mjs', 'reporters[1]');
assertEqual(reporters[2][0], '@linked-dependencies/external/reporter', 'reporters[2]');
assertEqual(reporters[3][0], '<rootDir>/customReporter.mjs', 'reporters[3]');
assertEqual(setupFilesAfterEnv.length, 1, 'setupFilesAfterEnv.length');
assertEqual(setupFilesAfterEnv[0], '<rootDir>/bundled_externals/lodash/noop.mjs', 'setupFilesAfterEnv[0]');
assertEqual(testEnvironment, '<rootDir>/bundled_externals/jest-environment-emit/node.mjs', 'testEnvironment');
assertEqual(testMatch.length, 2, 'testMatch.length');
assertEqual(testMatch[0], '<rootDir>/src/entry1.test.mjs', 'testMatch[0]');
assertEqual(testMatch[1], '<rootDir>/src/entry2.test.mjs', 'testMatch[1]');
assertEqual(testRunner, 'jest-circus/runner', 'testRunner');
assertEqual(globalTeardown, '<rootDir>/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'));
}
36 changes: 26 additions & 10 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -17,13 +26,17 @@ export async function build(esbuildJestConfig = {}) {
...(esbuildBaseConfig.external || []),
];

const isExternal = (id) => {
const importLikePath = convertPathToImport(rootDir, id);
return !importLikePath.startsWith('<rootDir>') && 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('<rootDir>') && 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;

Expand All @@ -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 });
Expand All @@ -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,
Expand All @@ -92,7 +106,9 @@ export async function build(esbuildJestConfig = {}) {
}),
...(esbuildBaseConfig.plugins || []),
],
});
};

const buildResult = await logger.trace.complete(esbuildConfig, 'esbuild', () => esbuild(esbuildConfig));

return buildResult;
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 35 additions & 9 deletions utils/resolve-module.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down

0 comments on commit 00eb9f2

Please sign in to comment.