Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support project references #817

Merged
merged 44 commits into from
Sep 23, 2018
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
92f23b3
Pass project references to language service
andrewbranch Aug 9, 2018
55d8efa
Use parsed project references, not raw content
andrewbranch Aug 9, 2018
75a5bdb
Don’t fail while trying to emit/diagnose files in project references;…
andrewbranch Aug 9, 2018
05ee14d
Be tolerant of older TypeScript versions
andrewbranch Aug 9, 2018
157211e
Add project references to transpileOnly mode
andrewbranch Aug 9, 2018
ee61c63
Add project references to experimental watch API path
andrewbranch Aug 10, 2018
0b81aa3
Use JS output for files in project references
andrewbranch Aug 13, 2018
0a42045
Fix loader emit typings
andrewbranch Aug 14, 2018
c01af17
Get program out of language service too
andrewbranch Aug 14, 2018
0d2854c
Add comparison test for project references without source maps
andrewbranch Aug 14, 2018
615bcbb
Add comparison test for project references with source map (no warnings)
andrewbranch Aug 14, 2018
2f4ff59
Add test for unbuilt project references
andrewbranch Aug 14, 2018
1e765b2
Only warn once per project per compilation about source maps
andrewbranch Aug 20, 2018
d262970
Make comments more accurate
andrewbranch Aug 20, 2018
700e967
Implement getOutputJavaScriptFileName
andrewbranch Sep 1, 2018
741bf9b
Add test for project references with outDir (and fix implementation)
andrewbranch Sep 1, 2018
f36a841
Be safer in getProjectReferenceForFile
andrewbranch Sep 1, 2018
05ab35d
Fix type error
andrewbranch Sep 1, 2018
565dab2
Add JS files instead of TS files to loader dependencies for project refs
andrewbranch Sep 3, 2018
a224efc
Add comments
andrewbranch Sep 3, 2018
80528ea
Cache project reference info in files map
andrewbranch Sep 3, 2018
47515ed
Update test for updated stack trace
andrewbranch Sep 3, 2018
451c462
Don’t repeatedly check files that don’t have project refs for project…
andrewbranch Sep 3, 2018
266a4ba
Rename test project reference output to a folder that’s not gitignored 😂
andrewbranch Sep 3, 2018
cc90401
Merge branch 'master' into project-references
johnnyreilly Sep 12, 2018
d532f78
Don’t ignore map files important to projectReferences tests
andrewbranch Sep 16, 2018
29d02b9
Normalize windows paths before comparing
Sep 17, 2018
433630b
Use only relative paths in error messages
andrewbranch Sep 17, 2018
80a28cd
Update expected output to remove absolute path from error message
andrewbranch Sep 18, 2018
fe888e2
Ignore path differences in TS error message
andrewbranch Sep 18, 2018
ddc5cc7
Theoretically protect against getProjectReferences changes in typescr…
andrewbranch Sep 20, 2018
6c87188
Update README with project references
andrewbranch Sep 21, 2018
611e8d9
Put projectReferences behind a loaderOption
andrewbranch Sep 21, 2018
32e2882
Update validateLoaderOptions and test loader options
andrewbranch Sep 21, 2018
e47664b
Update validateLoaderOptionNames test
andrewbranch Sep 21, 2018
2f196b3
ignore projectReferencesOutDir on windows in non-transpile mode
johnnyreilly Sep 21, 2018
b235502
add execution test for project references
johnnyreilly Sep 22, 2018
5d190fa
General tweaks; I should really get tslint properly set up
johnnyreilly Sep 22, 2018
16a9e7c
some yarn lint stuff
johnnyreilly Sep 23, 2018
b6510a0
further tslint fixes
johnnyreilly Sep 23, 2018
fbed24b
fix broken appenSuffixes
johnnyreilly Sep 23, 2018
308f0dd
add linting to build
johnnyreilly Sep 23, 2018
a6b203c
update changelog
johnnyreilly Sep 23, 2018
ca2c576
Merge branch 'master' into project-references
johnnyreilly Sep 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/after-compile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as path from 'path';
import { collectAllDependants, formatErrors } from './utils';

import {
collectAllDependants,
formatErrors,
isUsingProjectReferences
} from './utils';
import * as constants from './constants';
import {
TSFiles,
Expand Down Expand Up @@ -60,6 +65,7 @@ export function makeAfterCompile(

instance.filesWithErrors = filesWithErrors;
instance.modifiedFiles = null;
instance.projectsMissingSourceMaps = new Set();

callback();
};
Expand Down Expand Up @@ -181,6 +187,15 @@ function provideErrorsToWebpack(
}

const sourceFile = program && program.getSourceFile(filePath);

// If the source file is undefined, that probably means it’s actually part of an unbuilt project reference,
// which will have already produced a more useful error than the one we would get by proceeding here.
// If it’s undefined and we’re not using project references at all, I guess carry on so the user will
// get a useful error about which file was unexpectedly missing.
if (isUsingProjectReferences(instance) && !sourceFile) {
continue;
}

const errors = program
? [
...program.getSyntacticDiagnostics(sourceFile),
Expand Down
85 changes: 80 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import * as loaderUtils from 'loader-utils';
import * as typescript from 'typescript';

import { getTypeScriptInstance, getEmitOutput } from './instances';
import { appendSuffixesIfMatch, arrify, formatErrors } from './utils';
import {
appendSuffixesIfMatch,
arrify,
formatErrors,
getProjectReferenceForFile,
validateSourceMapOncePerProject
} from './utils';
import * as constants from './constants';
import {
AsyncCallback,
Expand Down Expand Up @@ -63,11 +69,80 @@ function successLoader(
: rawFilePath;

const fileVersion = updateFileInCache(filePath, contents, instance);
const referencedProject = getProjectReferenceForFile(filePath, instance);
if (referencedProject) {
if (referencedProject.commandLine.options.outFile) {
throw new Error(
`The referenced project at ${
referencedProject.sourceFile.fileName
} is using the ` +
`outFile' option, which is not supported with ts-loader.`
);
}

const { outputText, sourceMapText } = options.transpileOnly
? getTranspilationEmit(filePath, contents, instance, loader)
: getEmit(rawFilePath, filePath, instance, loader);
const jsFileName = instance.compiler.getOutputJavaScriptFileName(
Copy link

Choose a reason for hiding this comment

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

given that the whole tsbuild API is now internal, this uses an internal function? I couldn't find a custom implementation if it in this PR

filePath,
referencedProject.commandLine
);

if (!instance.compiler.sys.fileExists(jsFileName)) {
throw new Error(
`Could not find output JavaScript file for input ${filePath} ` +
`(looked at ${jsFileName}).\nThe input file is part of a project ` +
`reference located at ${referencedProject.sourceFile.fileName}, ` +
'so ts-loader is looking for the project’s pre-built output on ' +
'disk. Try running `tsc --build` to build project references.'
);
}

validateSourceMapOncePerProject(
instance,
loader,
jsFileName,
referencedProject
);

const mapFileName = jsFileName + '.map';
const outputText = instance.compiler.sys.readFile(jsFileName);
const sourceMapText = instance.compiler.sys.readFile(mapFileName);
makeSourceMapAndFinish(
sourceMapText,
outputText,
filePath,
contents,
loader,
options,
fileVersion,
callback
);
} else {
const { outputText, sourceMapText } = options.transpileOnly
? getTranspilationEmit(filePath, contents, instance, loader)
: getEmit(rawFilePath, filePath, instance, loader);

makeSourceMapAndFinish(
sourceMapText,
outputText,
filePath,
contents,
loader,
options,
fileVersion,
callback
);
}
}

function makeSourceMapAndFinish(
sourceMapText: string | undefined,
outputText: string | undefined,
filePath: string,
contents: string,
loader: Webpack,
options: LoaderOptions,
fileVersion: number,
callback: AsyncCallback
) {
if (outputText === null || outputText === undefined) {
const additionalGuidance =
!options.allowTsInNodeModules && filePath.indexOf('node_modules') !== -1
Expand All @@ -77,7 +152,7 @@ function successLoader(
: '';

throw new Error(
`Typescript emitted no output for ${filePath}.${additionalGuidance}`
`TypeScript emitted no output for ${filePath}.${additionalGuidance}`
);
}

Expand Down
65 changes: 36 additions & 29 deletions src/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { makeAfterCompile } from './after-compile';
import { getConfigFile, getConfigParseResult } from './config';
import { EOL, dtsDtsxOrDtsDtsxMapRegex } from './constants';
import { getCompilerOptions, getCompiler } from './compilerSetup';
import { makeError, formatErrors } from './utils';
import {
makeError,
formatErrors,
ensureProgram,
isUsingProjectReferences
} from './utils';
import * as logger from './logger';
import { makeServicesHost, makeWatchHost } from './servicesHost';
import { makeWatchRun } from './watch-run';
Expand All @@ -23,23 +28,6 @@ import {

const instances = <TSInstances>{};

function ensureProgram(instance: TSInstance) {
if (instance && instance.watchHost) {
if (instance.hasUnaccountedModifiedFiles) {
if (instance.changedFilesList) {
instance.watchHost.updateRootFileNames();
}
if (instance.watchOfFilesAndCompilerOptions) {
instance.program = instance.watchOfFilesAndCompilerOptions
.getProgram()
.getProgram();
}
instance.hasUnaccountedModifiedFiles = false;
}
return instance.program;
}
return undefined;
}
/**
* The loader is executed once for each file seen by webpack. However, we need to keep
* a persistent instance of TypeScript that contains all of the files in the program
Expand Down Expand Up @@ -167,7 +155,13 @@ function successfulTypeScriptInstance(
if (loaderOptions.transpileOnly) {
// quick return for transpiling
// we do need to check for any issues with TS options though
const program = compiler!.createProgram([], compilerOptions);
const program = configParseResult.projectReferences
? compiler!.createProgram({
rootNames: configParseResult.fileNames,
options: configParseResult.options,
projectReferences: configParseResult.projectReferences
})
: compiler!.createProgram([], compilerOptions);

// happypack does not have _module.errors - see https://github.com/TypeStrong/ts-loader/issues/336
if (!loaderOptions.happyPackMode) {
Expand All @@ -190,6 +184,7 @@ function successfulTypeScriptInstance(
loaderOptions,
files,
otherFiles,
program,
dependencyGraph: {},
reverseDependencyGraph: {},
transformers: getCustomTransformers(),
Expand Down Expand Up @@ -257,7 +252,8 @@ function successfulTypeScriptInstance(
loader,
instance,
loaderOptions.appendTsSuffixTo,
loaderOptions.appendTsxSuffixTo
loaderOptions.appendTsxSuffixTo,
configParseResult.projectReferences
);
instance.watchOfFilesAndCompilerOptions = compiler.createWatchProgram(
instance.watchHost
Expand All @@ -266,7 +262,13 @@ function successfulTypeScriptInstance(
.getProgram()
.getProgram();
} else {
const servicesHost = makeServicesHost(scriptRegex, log, loader, instance);
const servicesHost = makeServicesHost(
scriptRegex,
log,
loader,
instance,
configParseResult.projectReferences
);
instance.languageService = compiler.createLanguageService(
servicesHost,
compiler.createDocumentRegistry()
Expand Down Expand Up @@ -298,16 +300,21 @@ export function getEmitOutput(instance: TSInstance, filePath: string) {
writeByteOrderMark: boolean
) => outputFiles.push({ name: fileName, writeByteOrderMark, text });
const sourceFile = program.getSourceFile(filePath);
program.emit(
sourceFile,
writeFile,
/*cancellationToken*/ undefined,
/*emitOnlyDtsFiles*/ false,
instance.transformers
);
// The source file will be undefined if it’s part of an unbuilt project reference
if (sourceFile || !isUsingProjectReferences(instance)) {
program.emit(
sourceFile,
writeFile,
/*cancellationToken*/ undefined,
/*emitOnlyDtsFiles*/ false,
instance.transformers
);
}
return outputFiles;
} else {
// Emit Javascript
return instance.languageService!.getEmitOutput(filePath).outputFiles;
return instance.languageService!.getProgram()!.getSourceFile(filePath)
? instance.languageService!.getEmitOutput(filePath).outputFiles
: [];
}
}
10 changes: 8 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ export interface Webpack {
/**
* Emit a warning.
*/
emitWarning: (message: string) => void;
emitWarning: (message: Error) => void;
/**
* Emit an error.
*/
emitError: (message: string) => void;
emitError: (message: Error) => void;
/**
* Emit a file. This is webpack-specific
*/
Expand Down Expand Up @@ -239,6 +239,12 @@ export interface TSInstance {
* contains the modified files - cleared each time after-compile is called
*/
modifiedFiles?: TSFiles | null;
/**
* Paths to project references that are missing source maps.
* Cleared each time after-compile is called. Used to dedupe
* warnings about source maps during a single compilation.
*/
projectsMissingSourceMaps?: Set<string>;
languageService?: typescript.LanguageService | null;
version?: number;
dependencyGraph: DependencyGraph;
Expand Down
39 changes: 36 additions & 3 deletions src/servicesHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function makeServicesHost(
scriptRegex: RegExp,
log: logger.Logger,
loader: Webpack,
instance: TSInstance
instance: TSInstance,
projectReferences?: ReadonlyArray<typescript.ProjectReference>
) {
const {
compiler,
Expand Down Expand Up @@ -61,6 +62,8 @@ export function makeServicesHost(
const servicesHost: typescript.LanguageServiceHost = {
getProjectVersion: () => `${instance.version}`,

getProjectReferences: () => projectReferences,

getScriptFileNames: () =>
[...files.keys()].filter(filePath => filePath.match(scriptRegex)),

Expand Down Expand Up @@ -149,7 +152,8 @@ export function makeWatchHost(
loader: Webpack,
instance: TSInstance,
appendTsSuffixTo: RegExp[],
appendTsxSuffixTo: RegExp[]
appendTsxSuffixTo: RegExp[],
projectReferences?: ReadonlyArray<typescript.ProjectReference>
) {
const { compiler, compilerOptions, files, otherFiles } = instance;

Expand Down Expand Up @@ -239,7 +243,9 @@ export function makeWatchHost(
);
}
},
createProgram: compiler.createAbstractBuilder
createProgram: projectReferences
? createBuilderProgramWithReferences
: compiler.createAbstractBuilder
};
return watchHost;

Expand Down Expand Up @@ -366,6 +372,33 @@ export function makeWatchHost(
callback
);
}

function createBuilderProgramWithReferences(
rootNames: ReadonlyArray<string> | undefined,
options: typescript.CompilerOptions | undefined,
host: typescript.CompilerHost | undefined,
oldProgram: typescript.BuilderProgram | undefined,
configFileParsingDiagnostics:
| ReadonlyArray<typescript.Diagnostic>
| undefined
) {
const program = compiler.createProgram({
rootNames: rootNames!,
options: options!,
host,
oldProgram: oldProgram && oldProgram.getProgram(),
configFileParsingDiagnostics,
projectReferences
});

const builderProgramHost: typescript.BuilderProgramHost = host!;
return compiler.createAbstractBuilder(
program,
builderProgramHost,
oldProgram,
configFileParsingDiagnostics
);
}
}

function resolveModuleNames(
Expand Down