Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Load JS from node_modules
  • Loading branch information
billti committed Feb 13, 2016
commit dfb0dcde0e2a5b4db9bd426dd8f49f34a947f3d0
5 changes: 5 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ namespace ts {
name: "noImplicitUseStrict",
type: "boolean",
description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output
},
{
name: "maxNodeModuleJsDepth",
type: "number",
description: Diagnostics.The_maximum_dependency_depth_to_search_under_node_modules_and_load_JavaScript_files
}
];

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2576,6 +2576,10 @@
"category": "Message",
"code": 6112
},
"The maximum dependency depth to search under node_modules and load JavaScript files": {
"category": "Message",
"code": 6113
},

"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
Expand Down
70 changes: 50 additions & 20 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ namespace ts {
/* @internal */ export let ioWriteTime = 0;

/** The version of the TypeScript compiler release */
export const version = "1.9.0";

const emptyArray: any[] = [];

export const version = "1.9.0";
const startsWithDotSlashOrDotDotSlash = /^(\.\/|\.\.\/)/;

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

const i = moduleName.lastIndexOf("./", 1);
const startsWithDotSlashOrDotDotSlash = i === 0 || (i === 1 && moduleName.charCodeAt(0) === CharacterCodes.dot);
return !startsWithDotSlashOrDotDotSlash;
return !startsWithDotSlashOrDotDotSlash.test(moduleName);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here is that looking up the fist 2 chars is cheeper than running a regexp. is there a reason why we need to change it?

}

interface ModuleResolutionState {
Expand Down Expand Up @@ -448,11 +446,11 @@ namespace ts {
trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath);
}

let jsonContent: { typings?: string };
let jsonContent: { typings?: string; main?: string };

try {
const jsonText = state.host.readFile(packageJsonPath);
jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined };
jsonContent = jsonText ? <{ typings?: string; main?: string }>JSON.parse(jsonText) : { typings: undefined, main: undefined };
}
catch (e) {
// gracefully handle if readFile fails or returns not JSON
Expand All @@ -465,7 +463,7 @@ namespace ts {
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_has_typings_field_0_that_references_1, jsonContent.typings, typingsFile);
}
const result = loadModuleFromFile(typingsFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state);
const result = loadModuleFromFile(typingsFile, /* don't add extension */ [""], failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state);
if (result) {
return result;
}
Expand All @@ -479,6 +477,15 @@ namespace ts {
trace(state.host, Diagnostics.package_json_does_not_have_typings_field);
}
}
// TODO (billti): tracing as per above
if (typeof jsonContent.main === "string") {
// 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).
const mainFile = normalizePath(combinePaths(candidate, removeFileExtension(jsonContent.main)));
const result = loadModuleFromFile(mainFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(mainFile), state.host), state);
if (result) {
return result;
}
}
}
else {
if (state.traceEnabled) {
Expand All @@ -499,12 +506,13 @@ namespace ts {
const nodeModulesFolder = combinePaths(directory, "node_modules");
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName));
// Load only typescript files irrespective of allowJs option if loading from node modules
let result = loadModuleFromFile(candidate, supportedTypeScriptExtensions, failedLookupLocations, !nodeModulesFolderExists, state);

const supportedExtensions = getSupportedExtensions(state.compilerOptions);
let result = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state);
if (result) {
return result;
}
result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state);
if (result) {
return result;
}
Expand Down Expand Up @@ -1397,7 +1405,7 @@ namespace ts {
}

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

// If this was a file found by a node_modules search, set the nodeModuleSearchDistance to parent distance + 1.
if (isFileFromNodeSearch) {
const newDistance = (refFile && refFile.nodeModuleSearchDistance) === undefined ? 1 : refFile.nodeModuleSearchDistance + 1;
// 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)
file.nodeModuleSearchDistance = (typeof file.nodeModuleSearchDistance === "number") ? Math.min(file.nodeModuleSearchDistance, newDistance) : newDistance;
}

return file;
}

Expand All @@ -1424,6 +1439,12 @@ namespace ts {
if (file) {
file.path = path;

// Default to same distance as parent. Add one if found by a search.
file.nodeModuleSearchDistance = (refFile && refFile.nodeModuleSearchDistance) || 0;
if (isFileFromNodeSearch) {
file.nodeModuleSearchDistance++;
}

if (host.useCaseSensitiveFileNames()) {
// for case-sensitive file systems check if we've already seen some file with similar filename ignoring case
const existingFile = filesByNameIgnoreCase.get(path);
Expand Down Expand Up @@ -1468,28 +1489,37 @@ namespace ts {
}

function processImportedModules(file: SourceFile, basePath: string) {
const maxJsNodeModuleSearchDistance = options.maxNodeModuleJsDepth || 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so is the default is meant to be 0 or are we going to give it a higher value and users can opt in to increase it if they do not see that fit?

collectExternalModuleReferences(file);
if (file.imports.length || file.moduleAugmentations.length) {
file.resolvedModules = {};
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
file.nodeModuleSearchDistance = file.nodeModuleSearchDistance || 0;
for (let i = 0; i < moduleNames.length; i++) {
const resolution = resolutions[i];
setResolvedModule(file, moduleNames[i], resolution);
// add file to program only if:
// - resolution was successful
// - noResolve is falsy
// - module name come from the list fo imports
const shouldAddFile = resolution &&
!options.noResolve &&
i < file.imports.length;
// - it's not a top level JavaScript module that exceeded the search max
const exceedsJsSearchDepth = resolution && resolution.isExternalLibraryImport &&
hasJavaScriptFileExtension(resolution.resolvedFileName) &&
file.nodeModuleSearchDistance >= maxJsNodeModuleSearchDistance;
const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !exceedsJsSearchDepth;

if (shouldAddFile) {
const importedFile = findSourceFile(resolution.resolvedFileName, toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /*isDefaultLib*/ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end);

if (importedFile && resolution.isExternalLibraryImport) {
// Since currently irrespective of allowJs, we only look for supportedTypeScript extension external module files,
// this check is ok. Otherwise this would be never true for javascript file
const importedFile = findSourceFile(resolution.resolvedFileName,
toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName),
/*isDefaultLib*/ false,
file,
skipTrivia(file.text, file.imports[i].pos),
file.imports[i].end,
resolution.isExternalLibraryImport);

// TODO (billti): Should we check here if a JavaScript file is a CommonJS file, or doesn't have /// references?
if (importedFile && resolution.isExternalLibraryImport && !hasJavaScriptFileExtension(importedFile.fileName)) {
if (!isExternalModule(importedFile) && importedFile.statements.length) {
const start = getTokenPosOfNode(file.imports[i], file);
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));
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,8 @@ namespace ts {
/* @internal */ externalModuleIndicator: Node;
// The first node that causes this file to be a CommonJS module
/* @internal */ commonJsModuleIndicator: Node;
// The number of times node_modules was searched to locate the package containing this file
/* @internal */ nodeModuleSearchDistance?: number;

/* @internal */ identifiers: Map<string>;
/* @internal */ nodeCount: number;
Expand Down Expand Up @@ -2419,6 +2421,7 @@ namespace ts {
traceModuleResolution?: boolean;
allowSyntheticDefaultImports?: boolean;
allowJs?: boolean;
maxNodeModuleJsDepth?: number;
noImplicitUseStrict?: boolean;
/* @internal */ stripInternal?: boolean;

Expand Down
10 changes: 6 additions & 4 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2035,7 +2035,8 @@ namespace ts {
else {
const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile];
for (const sourceFile of sourceFiles) {
if (!isDeclarationFile(sourceFile)) {
// Don't emit if source file is a declaration file, or was found by a search under 'node_modules'
if (!isDeclarationFile(sourceFile) && !sourceFile.nodeModuleSearchDistance) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a test with --noResolve for this.

onSingleFileEmit(host, sourceFile);
}
}
Expand Down Expand Up @@ -2069,9 +2070,10 @@ namespace ts {
function onBundledEmit(host: EmitHost) {
// Can emit only sources that are not declaration file and are either non module code or module with --module or --target es6 specified
const bundledSources = filter(host.getSourceFiles(),
sourceFile => !isDeclarationFile(sourceFile) && // Not a declaration file
(!isExternalModule(sourceFile) || // non module file
(getEmitModuleKind(options) && isExternalModule(sourceFile)))); // module that can emit - note falsy value from getEmitModuleKind means the module kind that shouldn't be emitted
sourceFile => !isDeclarationFile(sourceFile) && // Not a declaration file
!sourceFile.nodeModuleSearchDistance && // Not loaded from searching under node_modules
(!isExternalModule(sourceFile) || // non module file
(getEmitModuleKind(options) && isExternalModule(sourceFile)))); // module that can emit - note falsy value from getEmitModuleKind means the module kind that shouldn't be emitted
if (bundledSources.length) {
const jsFilePath = options.outFile || options.out;
const emitFileNames: EmitFileNames = {
Expand Down