Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions e2e/nx/src/graph-ts-solution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
readJson,
runCLI,
runCommand,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
Expand Down Expand Up @@ -319,6 +320,155 @@ describe('Graph - TS solution setup', () => {
);
});

it('should detect dependencies correctly when using Node.js subpath imports', () => {
const pmc = getPackageManagerCommand();

const pkg1 = uniq('pkg1');
createPackage(pkg1, {
sourceFilePaths: [
'src/file1.ts',
'src/nested/file2.ts',
'src/file3.ts',
'src/file4.ts',
],
packageJsonEntryFields: {
imports: {
'#*': './src/*.ts',
'#file2': './src/nested/file2.ts',
'#file3': {
types: './src/file3.ts',
default: './src/file3.ts',
},
'#file4': 'external-package',
},
},
});
const pkg2 = uniq('pkg2');
createPackage(pkg2, {
sourceFilePaths: [
'src/file1.ts',
'src/nested/file2.ts',
'src/file3.ts',
'src/file4.ts',
],
packageJsonEntryFields: {
imports: {
'#*': './src/*.ts',
'#file2': './src/nested/file2.ts',
'#file3': {
types: './src/file3.ts',
default: './src/file3.ts',
},
'#file4': 'external-package',
},
},
});

// create a fake external package
createFile(
'temp-external-package/package.json',
JSON.stringify(
{
name: 'external-package',
version: '1.0.0',
exports: {
'.': {
types: './index.d.ts',
default: './index.js',
},
'./package.json': './package.json',
},
},
null,
2
)
);
createFile(
'temp-external-package/index.js',
`export const externalPackage = 'external-package';`
);
createFile(
'temp-external-package/index.d.ts',
`export const externalPackage: string;`
);
// create a tarball of the external package
runCommand('cd temp-external-package && npm pack');
// add the external package as a dependency
updateJson('package.json', (json) => {
json.dependencies ??= {};
json.dependencies['external-package'] =
'file:./temp-external-package/external-package-1.0.0.tgz';
return json;
});
// install dependencies to update the lock file
runCommand(pmc.install);

createFile(
`packages/${pkg1}/src/main.ts`,
`
import { file1 } from '#file1';
import { file2 } from '#file2';
import { file3 } from '#file3';
import { externalPackage } from '#file4';

export const main = file1 + file2 + file3 + externalPackage;
`
);
createFile(
`packages/${pkg2}/src/main.ts`,
`
import { file1 } from '#file1';
import { file2 } from '#file2';
import { file3 } from '#file3';
import { externalPackage } from '#file4';

export const main = file1 + file2 + file3 + externalPackage;
`
);

// force the graph to be generated
runCLI(`reset`);
runCLI(`report`);
const graph = readJson('.nx/workspace-data/project-graph.json');
// only `external-package` is detected as a dependency, the rest of the
// imports point to internal files of each project
expect(
graph.dependencies[`@proj/${pkg1}`].map((d) => d.target)
).toStrictEqual(['npm:external-package']);
expect(
graph.dependencies[`@proj/${pkg2}`].map((d) => d.target)
).toStrictEqual(['npm:external-package']);

// assert build succeeds, tsc outputs nothing when successful
expect(runCommand(`${pmc.exec} tsc -b packages/${pkg1}`)).toBe('');
checkFilesExist(
`packages/${pkg1}/dist/src/file1.js`,
`packages/${pkg1}/dist/src/file1.d.ts`,
`packages/${pkg1}/dist/src/nested/file2.js`,
`packages/${pkg1}/dist/src/nested/file2.d.ts`,
`packages/${pkg1}/dist/src/file3.js`,
`packages/${pkg1}/dist/src/file3.d.ts`,
`packages/${pkg1}/dist/src/file4.js`,
`packages/${pkg1}/dist/src/file4.d.ts`,
`packages/${pkg1}/dist/src/main.js`,
`packages/${pkg1}/dist/src/main.d.ts`
);
// assert build succeeds, tsc outputs nothing when successful
expect(runCommand(`${pmc.exec} tsc -b packages/${pkg2}`)).toBe('');
checkFilesExist(
`packages/${pkg2}/dist/src/file1.js`,
`packages/${pkg2}/dist/src/file1.d.ts`,
`packages/${pkg2}/dist/src/nested/file2.js`,
`packages/${pkg2}/dist/src/nested/file2.d.ts`,
`packages/${pkg2}/dist/src/file3.js`,
`packages/${pkg2}/dist/src/file3.d.ts`,
`packages/${pkg2}/dist/src/file4.js`,
`packages/${pkg2}/dist/src/file4.d.ts`,
`packages/${pkg2}/dist/src/main.js`,
`packages/${pkg2}/dist/src/main.d.ts`
);
});

function createPackage(
name: string,
options?: {
Expand All @@ -328,6 +478,7 @@ describe('Graph - TS solution setup', () => {
main?: string;
types?: string;
exports?: string | Record<string, any>;
imports?: Record<string, any>;
};
}
): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@ export class TargetProjectLocator {
}

if (this.tsConfig.config) {
// TODO(meeroslav): this block is probably obsolete
// and existed only because of the incomplete `paths` matching
// if import cannot be matched using tsconfig `paths` the compilation would fail anyway
// TODO: this can be removed once we rework resolveImportWithRequire below
// to properly handle ESM (exports, imports, conditions)
const resolvedProject = this.resolveImportWithTypescript(
importExpr,
filePath
Expand Down Expand Up @@ -369,28 +368,47 @@ export class TargetProjectLocator {
filePath: string
): string | undefined {
let resolvedModule: string;
if (this.typescriptResolutionCache.has(normalizedImportExpr)) {
resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr);
const projectName = findProjectForPath(filePath, this.projectRootMappings);
const cacheScope = projectName
? // fall back to the project name if the project root can't be determined
this.nodes[projectName]?.data?.root || projectName
: // fall back to the file path if the project can't be determined
filePath;
const cacheKey = `${normalizedImportExpr}__${cacheScope}`;
if (this.typescriptResolutionCache.has(cacheKey)) {
resolvedModule = this.typescriptResolutionCache.get(cacheKey);
} else {
resolvedModule = resolveModuleByImport(
normalizedImportExpr,
filePath,
this.tsConfig.absolutePath
);
this.typescriptResolutionCache.set(
normalizedImportExpr,
cacheKey,
resolvedModule ? resolvedModule : null
);
}

// TODO: vsavkin temporary workaround. Remove it once we reworking handling of npm packages.
if (resolvedModule && resolvedModule.indexOf('node_modules/') === -1) {
if (!resolvedModule) {
return;
}

const nodeModulesIndex = resolvedModule.lastIndexOf('node_modules/');
if (nodeModulesIndex === -1) {
const resolvedProject = this.findProjectOfResolvedModule(resolvedModule);
if (resolvedProject) {
return resolvedProject;
}
return resolvedProject;
}
return;

// strip the node_modules/ prefix from the resolved module path
const packagePath = resolvedModule.substring(
nodeModulesIndex + 'node_modules/'.length
);
const externalProject = this.findNpmProjectFromImport(
packagePath,
filePath
);

return externalProject;
}

private resolveImportWithRequire(
Expand Down
Loading