mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
As outlined in the previous commit which enabled the `esModuleInterop` TypeScript compiler option, we need to update all namespace imports for `typescript` to default imports. This is needed to allow for TypeScript to be imported at runtime from an ES module. Similar changes are needed for modules like `semver` where the types incorrectly suggest named exports that will not exist at runtime when imported from ESM. This commit refactors all imports to match with the lint rule we have configured in the previous commit. See the previous commit for more details on why certain imports have been changed. A special case are the imports to `@babel/core` and `@babel/types`. For these a special interop is needed as both default imports, or named imports break the other module format. e.g default imports would work well for ESM, but it breaks for CJS. For CJS, the named imports would only work, but in ESM, only the default export exist. We work around this for now until the devmode is using ESM as well (which would be consistent with prodmode and gives us more valuable test results). More details on the interop can be found in the `babel_core.ts` files (two interops are needed for both localize/or the compiler-cli). PR Close #43431
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import * as chokidar from 'chokidar';
|
|
import * as path from 'path';
|
|
import ts from 'typescript';
|
|
|
|
import {Diagnostics, exitCodeFromResult, ParsedConfiguration, performCompilation, PerformCompilationResult, readConfiguration} from './perform_compile';
|
|
import * as api from './transformers/api';
|
|
import {createCompilerHost} from './transformers/entry_points';
|
|
import {createMessageDiagnostic} from './transformers/util';
|
|
|
|
function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic {
|
|
let duration: string;
|
|
if (timeInMillis > 1000) {
|
|
duration = `${(timeInMillis / 1000).toPrecision(2)}s`;
|
|
} else {
|
|
duration = `${timeInMillis}ms`;
|
|
}
|
|
return {
|
|
category: ts.DiagnosticCategory.Message,
|
|
messageText: `Total time: ${duration}`,
|
|
code: api.DEFAULT_ERROR_CODE,
|
|
source: api.SOURCE,
|
|
};
|
|
}
|
|
|
|
export enum FileChangeEvent {
|
|
Change,
|
|
CreateDelete,
|
|
CreateDeleteDir,
|
|
}
|
|
|
|
export interface PerformWatchHost {
|
|
reportDiagnostics(diagnostics: Diagnostics): void;
|
|
readConfiguration(): ParsedConfiguration;
|
|
createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
|
|
createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
|
|
onFileChange(
|
|
options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void,
|
|
ready: () => void): {close: () => void};
|
|
setTimeout(callback: () => void, ms: number): any;
|
|
clearTimeout(timeoutId: any): void;
|
|
}
|
|
|
|
export function createPerformWatchHost(
|
|
configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void,
|
|
existingOptions?: ts.CompilerOptions,
|
|
createEmitCallback?: (options: api.CompilerOptions) =>
|
|
api.TsEmitCallback | undefined): PerformWatchHost {
|
|
return {
|
|
reportDiagnostics: reportDiagnostics,
|
|
createCompilerHost: options => createCompilerHost({options}),
|
|
readConfiguration: () => readConfiguration(configFileName, existingOptions),
|
|
createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
|
|
onFileChange: (options, listener, ready: () => void) => {
|
|
if (!options.basePath) {
|
|
reportDiagnostics([{
|
|
category: ts.DiagnosticCategory.Error,
|
|
messageText: 'Invalid configuration option. baseDir not specified',
|
|
source: api.SOURCE,
|
|
code: api.DEFAULT_ERROR_CODE
|
|
}]);
|
|
return {close: () => {}};
|
|
}
|
|
const watcher = chokidar.watch(options.basePath, {
|
|
// ignore .dotfiles, .js and .map files.
|
|
// can't ignore other files as we e.g. want to recompile if an `.html` file changes as well.
|
|
ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/,
|
|
ignoreInitial: true,
|
|
persistent: true,
|
|
});
|
|
watcher.on('all', (event: string, path: string) => {
|
|
switch (event) {
|
|
case 'change':
|
|
listener(FileChangeEvent.Change, path);
|
|
break;
|
|
case 'unlink':
|
|
case 'add':
|
|
listener(FileChangeEvent.CreateDelete, path);
|
|
break;
|
|
case 'unlinkDir':
|
|
case 'addDir':
|
|
listener(FileChangeEvent.CreateDeleteDir, path);
|
|
break;
|
|
}
|
|
});
|
|
watcher.on('ready', ready);
|
|
return {close: () => watcher.close(), ready};
|
|
},
|
|
setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
|
|
clearTimeout: (ts.sys.setTimeout && ts.sys.clearTimeout) || clearTimeout,
|
|
};
|
|
}
|
|
|
|
interface CacheEntry {
|
|
exists?: boolean;
|
|
sf?: ts.SourceFile;
|
|
content?: string;
|
|
}
|
|
|
|
interface QueuedCompilationInfo {
|
|
timerHandle: any;
|
|
modifiedResourceFiles: Set<string>;
|
|
}
|
|
|
|
/**
|
|
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
|
*/
|
|
export function performWatchCompilation(host: PerformWatchHost):
|
|
{close: () => void, ready: (cb: () => void) => void, firstCompileResult: Diagnostics} {
|
|
let cachedProgram: api.Program|undefined; // Program cached from last compilation
|
|
let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost cached from last compilation
|
|
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
|
|
let timerHandleForRecompilation: QueuedCompilationInfo|
|
|
undefined; // Handle for 0.25s wait timer to trigger recompilation
|
|
|
|
const ignoreFilesForWatch = new Set<string>();
|
|
const fileCache = new Map<string, CacheEntry>();
|
|
|
|
const firstCompileResult = doCompilation();
|
|
|
|
// Watch basePath, ignoring .dotfiles
|
|
let resolveReadyPromise: () => void;
|
|
const readyPromise = new Promise<void>(resolve => resolveReadyPromise = resolve);
|
|
// Note: ! is ok as options are filled after the first compilation
|
|
// Note: ! is ok as resolvedReadyPromise is filled by the previous call
|
|
const fileWatcher =
|
|
host.onFileChange(cachedOptions!.options, watchedFileChanged, resolveReadyPromise!);
|
|
|
|
return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
|
|
|
|
function cacheEntry(fileName: string): CacheEntry {
|
|
fileName = path.normalize(fileName);
|
|
let entry = fileCache.get(fileName);
|
|
if (!entry) {
|
|
entry = {};
|
|
fileCache.set(fileName, entry);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function close() {
|
|
fileWatcher.close();
|
|
if (timerHandleForRecompilation) {
|
|
host.clearTimeout(timerHandleForRecompilation.timerHandle);
|
|
timerHandleForRecompilation = undefined;
|
|
}
|
|
}
|
|
|
|
// Invoked to perform initial compilation or re-compilation in watch mode
|
|
function doCompilation(): Diagnostics {
|
|
if (!cachedOptions) {
|
|
cachedOptions = host.readConfiguration();
|
|
}
|
|
if (cachedOptions.errors && cachedOptions.errors.length) {
|
|
host.reportDiagnostics(cachedOptions.errors);
|
|
return cachedOptions.errors;
|
|
}
|
|
const startTime = Date.now();
|
|
if (!cachedCompilerHost) {
|
|
cachedCompilerHost = host.createCompilerHost(cachedOptions.options);
|
|
const originalWriteFileCallback = cachedCompilerHost.writeFile;
|
|
cachedCompilerHost.writeFile = function(
|
|
fileName: string, data: string, writeByteOrderMark: boolean,
|
|
onError?: (message: string) => void, sourceFiles: ReadonlyArray<ts.SourceFile> = []) {
|
|
ignoreFilesForWatch.add(path.normalize(fileName));
|
|
return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
|
};
|
|
const originalFileExists = cachedCompilerHost.fileExists;
|
|
cachedCompilerHost.fileExists = function(fileName: string) {
|
|
const ce = cacheEntry(fileName);
|
|
if (ce.exists == null) {
|
|
ce.exists = originalFileExists.call(this, fileName);
|
|
}
|
|
return ce.exists!;
|
|
};
|
|
const originalGetSourceFile = cachedCompilerHost.getSourceFile;
|
|
cachedCompilerHost.getSourceFile = function(
|
|
fileName: string, languageVersion: ts.ScriptTarget) {
|
|
const ce = cacheEntry(fileName);
|
|
if (!ce.sf) {
|
|
ce.sf = originalGetSourceFile.call(this, fileName, languageVersion);
|
|
}
|
|
return ce.sf!;
|
|
};
|
|
const originalReadFile = cachedCompilerHost.readFile;
|
|
cachedCompilerHost.readFile = function(fileName: string) {
|
|
const ce = cacheEntry(fileName);
|
|
if (ce.content == null) {
|
|
ce.content = originalReadFile.call(this, fileName);
|
|
}
|
|
return ce.content!;
|
|
};
|
|
// Provide access to the file paths that triggered this rebuild
|
|
cachedCompilerHost.getModifiedResourceFiles = function() {
|
|
if (timerHandleForRecompilation === undefined) {
|
|
return undefined;
|
|
}
|
|
return timerHandleForRecompilation.modifiedResourceFiles;
|
|
};
|
|
}
|
|
ignoreFilesForWatch.clear();
|
|
const oldProgram = cachedProgram;
|
|
// We clear out the `cachedProgram` here as a
|
|
// program can only be used as `oldProgram` 1x
|
|
cachedProgram = undefined;
|
|
const compileResult = performCompilation({
|
|
rootNames: cachedOptions.rootNames,
|
|
options: cachedOptions.options,
|
|
host: cachedCompilerHost,
|
|
oldProgram: oldProgram,
|
|
emitCallback: host.createEmitCallback(cachedOptions.options)
|
|
});
|
|
|
|
if (compileResult.diagnostics.length) {
|
|
host.reportDiagnostics(compileResult.diagnostics);
|
|
}
|
|
|
|
const endTime = Date.now();
|
|
if (cachedOptions.options.diagnostics) {
|
|
const totalTime = (endTime - startTime) / 1000;
|
|
host.reportDiagnostics([totalCompilationTimeDiagnostic(endTime - startTime)]);
|
|
}
|
|
const exitCode = exitCodeFromResult(compileResult.diagnostics);
|
|
if (exitCode == 0) {
|
|
cachedProgram = compileResult.program;
|
|
host.reportDiagnostics(
|
|
[createMessageDiagnostic('Compilation complete. Watching for file changes.')]);
|
|
} else {
|
|
host.reportDiagnostics(
|
|
[createMessageDiagnostic('Compilation failed. Watching for file changes.')]);
|
|
}
|
|
|
|
return compileResult.diagnostics;
|
|
}
|
|
|
|
function resetOptions() {
|
|
cachedProgram = undefined;
|
|
cachedCompilerHost = undefined;
|
|
cachedOptions = undefined;
|
|
}
|
|
|
|
function watchedFileChanged(event: FileChangeEvent, fileName: string) {
|
|
const normalizedPath = path.normalize(fileName);
|
|
|
|
if (cachedOptions && event === FileChangeEvent.Change &&
|
|
// TODO(chuckj): validate that this is sufficient to skip files that were written.
|
|
// This assumes that the file path we write is the same file path we will receive in the
|
|
// change notification.
|
|
normalizedPath === path.normalize(cachedOptions.project)) {
|
|
// If the configuration file changes, forget everything and start the recompilation timer
|
|
resetOptions();
|
|
} else if (
|
|
event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) {
|
|
// If a file was added or removed, reread the configuration
|
|
// to determine the new list of root files.
|
|
cachedOptions = undefined;
|
|
}
|
|
|
|
if (event === FileChangeEvent.CreateDeleteDir) {
|
|
fileCache.clear();
|
|
} else {
|
|
fileCache.delete(normalizedPath);
|
|
}
|
|
|
|
if (!ignoreFilesForWatch.has(normalizedPath)) {
|
|
// Ignore the file if the file is one that was written by the compiler.
|
|
startTimerForRecompilation(normalizedPath);
|
|
}
|
|
}
|
|
|
|
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
|
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
|
// off a new compilation.
|
|
function startTimerForRecompilation(changedPath: string) {
|
|
if (timerHandleForRecompilation) {
|
|
host.clearTimeout(timerHandleForRecompilation.timerHandle);
|
|
} else {
|
|
timerHandleForRecompilation = {
|
|
modifiedResourceFiles: new Set<string>(),
|
|
timerHandle: undefined
|
|
};
|
|
}
|
|
timerHandleForRecompilation.timerHandle = host.setTimeout(recompile, 250);
|
|
timerHandleForRecompilation.modifiedResourceFiles.add(changedPath);
|
|
}
|
|
|
|
function recompile() {
|
|
host.reportDiagnostics(
|
|
[createMessageDiagnostic('File change detected. Starting incremental compilation.')]);
|
|
doCompilation();
|
|
timerHandleForRecompilation = undefined;
|
|
}
|
|
}
|