Skip to content

Commit dfb0dcd

Browse files
committed
Load JS from node_modules
1 parent c7fcd02 commit dfb0dcd

5 files changed

Lines changed: 68 additions & 24 deletions

File tree

src/compiler/commandLineParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ namespace ts {
326326
name: "noImplicitUseStrict",
327327
type: "boolean",
328328
description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output
329+
},
330+
{
331+
name: "maxNodeModuleJsDepth",
332+
type: "number",
333+
description: Diagnostics.The_maximum_dependency_depth_to_search_under_node_modules_and_load_JavaScript_files
329334
}
330335
];
331336

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2576,6 +2576,10 @@
25762576
"category": "Message",
25772577
"code": 6112
25782578
},
2579+
"The maximum dependency depth to search under node_modules and load JavaScript files": {
2580+
"category": "Message",
2581+
"code": 6113
2582+
},
25792583

25802584
"Variable '{0}' implicitly has an '{1}' type.": {
25812585
"category": "Error",

src/compiler/program.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ namespace ts {
99
/* @internal */ export let ioWriteTime = 0;
1010

1111
/** The version of the TypeScript compiler release */
12+
export const version = "1.9.0";
1213

1314
const emptyArray: any[] = [];
14-
15-
export const version = "1.9.0";
15+
const startsWithDotSlashOrDotDotSlash = /^(\.\/|\.\.\/)/;
1616

1717
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean): string {
1818
let fileName = "tsconfig.json";
@@ -79,9 +79,7 @@ namespace ts {
7979
return false;
8080
}
8181

82-
const i = moduleName.lastIndexOf("./", 1);
83-
const startsWithDotSlashOrDotDotSlash = i === 0 || (i === 1 && moduleName.charCodeAt(0) === CharacterCodes.dot);
84-
return !startsWithDotSlashOrDotDotSlash;
82+
return !startsWithDotSlashOrDotDotSlash.test(moduleName);
8583
}
8684

8785
interface ModuleResolutionState {
@@ -448,11 +446,11 @@ namespace ts {
448446
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
449447
}
450448

451-
let jsonContent: { typings?: string };
449+
let jsonContent: { typings?: string; main?: string };
452450

453451
try {
454452
const jsonText = state.host.readFile(packageJsonPath);
455-
jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined };
453+
jsonContent = jsonText ? <{ typings?: string; main?: string }>JSON.parse(jsonText) : { typings: undefined, main: undefined };
456454
}
457455
catch (e) {
458456
// gracefully handle if readFile fails or returns not JSON
@@ -465,7 +463,7 @@ namespace ts {
465463
if (state.traceEnabled) {
466464
trace(state.host, Diagnostics.package_json_has_typings_field_0_that_references_1, jsonContent.typings, typingsFile);
467465
}
468-
const result = loadModuleFromFile(typingsFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state);
466+
const result = loadModuleFromFile(typingsFile, /* don't add extension */ [""], failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state);
469467
if (result) {
470468
return result;
471469
}
@@ -479,6 +477,15 @@ namespace ts {
479477
trace(state.host, Diagnostics.package_json_does_not_have_typings_field);
480478
}
481479
}
480+
// TODO (billti): tracing as per above
481+
if (typeof jsonContent.main === "string") {
482+
// If 'main' points to 'foo.js', we still want to try and load 'foo.d.ts' and 'foo.ts' first (and only 'foo.js' if 'allowJs' is set).
483+
const mainFile = normalizePath(combinePaths(candidate, removeFileExtension(jsonContent.main)));
484+
const result = loadModuleFromFile(mainFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(mainFile), state.host), state);
485+
if (result) {
486+
return result;
487+
}
488+
}
482489
}
483490
else {
484491
if (state.traceEnabled) {
@@ -499,12 +506,13 @@ namespace ts {
499506
const nodeModulesFolder = combinePaths(directory, "node_modules");
500507
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
501508
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
502-
// Load only typescript files irrespective of allowJs option if loading from node modules
503-
let result = loadModuleFromFile(candidate, supportedTypeScriptExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
509+
510+
const supportedExtensions = getSupportedExtensions(state.compilerOptions);
511+
let result = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
504512
if (result) {
505513
return result;
506514
}
507-
result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
515+
result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
508516
if (result) {
509517
return result;
510518
}
@@ -1397,7 +1405,7 @@ namespace ts {
13971405
}
13981406

13991407
// Get source file from normalized fileName
1400-
function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile {
1408+
function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number, isFileFromNodeSearch?: boolean): SourceFile {
14011409
if (filesByName.contains(path)) {
14021410
const file = filesByName.get(path);
14031411
// try to check if we've already seen this file but with a different casing in path
@@ -1406,6 +1414,13 @@ namespace ts {
14061414
reportFileNamesDifferOnlyInCasingError(fileName, file.fileName, refFile, refPos, refEnd);
14071415
}
14081416

1417+
// If this was a file found by a node_modules search, set the nodeModuleSearchDistance to parent distance + 1.
1418+
if (isFileFromNodeSearch) {
1419+
const newDistance = (refFile && refFile.nodeModuleSearchDistance) === undefined ? 1 : refFile.nodeModuleSearchDistance + 1;
1420+
// If already set on the file, don't overwrite if it was already found closer (which may be '0' if added as a root file)
1421+
file.nodeModuleSearchDistance = (typeof file.nodeModuleSearchDistance === "number") ? Math.min(file.nodeModuleSearchDistance, newDistance) : newDistance;
1422+
}
1423+
14091424
return file;
14101425
}
14111426

@@ -1424,6 +1439,12 @@ namespace ts {
14241439
if (file) {
14251440
file.path = path;
14261441

1442+
// Default to same distance as parent. Add one if found by a search.
1443+
file.nodeModuleSearchDistance = (refFile && refFile.nodeModuleSearchDistance) || 0;
1444+
if (isFileFromNodeSearch) {
1445+
file.nodeModuleSearchDistance++;
1446+
}
1447+
14271448
if (host.useCaseSensitiveFileNames()) {
14281449
// for case-sensitive file systems check if we've already seen some file with similar filename ignoring case
14291450
const existingFile = filesByNameIgnoreCase.get(path);
@@ -1468,28 +1489,37 @@ namespace ts {
14681489
}
14691490

14701491
function processImportedModules(file: SourceFile, basePath: string) {
1492+
const maxJsNodeModuleSearchDistance = options.maxNodeModuleJsDepth || 0;
14711493
collectExternalModuleReferences(file);
14721494
if (file.imports.length || file.moduleAugmentations.length) {
14731495
file.resolvedModules = {};
14741496
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
14751497
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
1498+
file.nodeModuleSearchDistance = file.nodeModuleSearchDistance || 0;
14761499
for (let i = 0; i < moduleNames.length; i++) {
14771500
const resolution = resolutions[i];
14781501
setResolvedModule(file, moduleNames[i], resolution);
14791502
// add file to program only if:
14801503
// - resolution was successful
14811504
// - noResolve is falsy
14821505
// - module name come from the list fo imports
1483-
const shouldAddFile = resolution &&
1484-
!options.noResolve &&
1485-
i < file.imports.length;
1506+
// - it's not a top level JavaScript module that exceeded the search max
1507+
const exceedsJsSearchDepth = resolution && resolution.isExternalLibraryImport &&
1508+
hasJavaScriptFileExtension(resolution.resolvedFileName) &&
1509+
file.nodeModuleSearchDistance >= maxJsNodeModuleSearchDistance;
1510+
const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !exceedsJsSearchDepth;
14861511

14871512
if (shouldAddFile) {
1488-
const importedFile = findSourceFile(resolution.resolvedFileName, toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /*isDefaultLib*/ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end);
1489-
1490-
if (importedFile && resolution.isExternalLibraryImport) {
1491-
// Since currently irrespective of allowJs, we only look for supportedTypeScript extension external module files,
1492-
// this check is ok. Otherwise this would be never true for javascript file
1513+
const importedFile = findSourceFile(resolution.resolvedFileName,
1514+
toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName),
1515+
/*isDefaultLib*/ false,
1516+
file,
1517+
skipTrivia(file.text, file.imports[i].pos),
1518+
file.imports[i].end,
1519+
resolution.isExternalLibraryImport);
1520+
1521+
// TODO (billti): Should we check here if a JavaScript file is a CommonJS file, or doesn't have /// references?
1522+
if (importedFile && resolution.isExternalLibraryImport && !hasJavaScriptFileExtension(importedFile.fileName)) {
14931523
if (!isExternalModule(importedFile) && importedFile.statements.length) {
14941524
const start = getTokenPosOfNode(file.imports[i], file);
14951525
fileProcessingDiagnostics.add(createFileDiagnostic(file, start, file.imports[i].end - start, Diagnostics.Exported_external_package_typings_file_0_is_not_a_module_Please_contact_the_package_author_to_update_the_package_definition, importedFile.fileName));

src/compiler/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,8 @@ namespace ts {
15361536
/* @internal */ externalModuleIndicator: Node;
15371537
// The first node that causes this file to be a CommonJS module
15381538
/* @internal */ commonJsModuleIndicator: Node;
1539+
// The number of times node_modules was searched to locate the package containing this file
1540+
/* @internal */ nodeModuleSearchDistance?: number;
15391541

15401542
/* @internal */ identifiers: Map<string>;
15411543
/* @internal */ nodeCount: number;
@@ -2419,6 +2421,7 @@ namespace ts {
24192421
traceModuleResolution?: boolean;
24202422
allowSyntheticDefaultImports?: boolean;
24212423
allowJs?: boolean;
2424+
maxNodeModuleJsDepth?: number;
24222425
noImplicitUseStrict?: boolean;
24232426
/* @internal */ stripInternal?: boolean;
24242427

src/compiler/utilities.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,7 +2035,8 @@ namespace ts {
20352035
else {
20362036
const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile];
20372037
for (const sourceFile of sourceFiles) {
2038-
if (!isDeclarationFile(sourceFile)) {
2038+
// Don't emit if source file is a declaration file, or was found by a search under 'node_modules'
2039+
if (!isDeclarationFile(sourceFile) && !sourceFile.nodeModuleSearchDistance) {
20392040
onSingleFileEmit(host, sourceFile);
20402041
}
20412042
}
@@ -2069,9 +2070,10 @@ namespace ts {
20692070
function onBundledEmit(host: EmitHost) {
20702071
// Can emit only sources that are not declaration file and are either non module code or module with --module or --target es6 specified
20712072
const bundledSources = filter(host.getSourceFiles(),
2072-
sourceFile => !isDeclarationFile(sourceFile) && // Not a declaration file
2073-
(!isExternalModule(sourceFile) || // non module file
2074-
(getEmitModuleKind(options) && isExternalModule(sourceFile)))); // module that can emit - note falsy value from getEmitModuleKind means the module kind that shouldn't be emitted
2073+
sourceFile => !isDeclarationFile(sourceFile) && // Not a declaration file
2074+
!sourceFile.nodeModuleSearchDistance && // Not loaded from searching under node_modules
2075+
(!isExternalModule(sourceFile) || // non module file
2076+
(getEmitModuleKind(options) && isExternalModule(sourceFile)))); // module that can emit - note falsy value from getEmitModuleKind means the module kind that shouldn't be emitted
20752077
if (bundledSources.length) {
20762078
const jsFilePath = options.outFile || options.out;
20772079
const emitFileNames: EmitFileNames = {

0 commit comments

Comments
 (0)