-
Notifications
You must be signed in to change notification settings - Fork 798
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
fix(compiler): prevent double full builds #3374
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,67 +1,124 @@ | ||
import type * as d from '../../declarations'; | ||
import { getTsOptionsToExtend } from './ts-config'; | ||
import { GENERATED_DTS } from '../output-targets/output-utils'; | ||
import ts from 'typescript'; | ||
|
||
/** | ||
* Create a TypeScript Program ({@link ts.Program}) to perform builds of a Stencil project using the provided | ||
* `buildCallback` entity | ||
* @param config a Stencil configuration to apply to a full build of a Stencil project | ||
* @param buildCallback a callback that invokes the actual transpilation of a Stencil project | ||
* @returns a Program that marries the TypeScript and Stencil compilers together. | ||
*/ | ||
export const createTsBuildProgram = async ( | ||
config: d.Config, | ||
buildCallback: (tsBuilder: ts.BuilderProgram) => Promise<void> | ||
) => { | ||
let isRunning = false; | ||
let timeoutId: any; | ||
): Promise<ts.WatchOfConfigFile<ts.EmitAndSemanticDiagnosticsBuilderProgram>> => { | ||
let isBuildRunning = false; | ||
let currentBuildTimeoutId: any; | ||
|
||
const optionsToExtend = getTsOptionsToExtend(config); | ||
|
||
/** | ||
* Create a {@link ts.System}. The System is responsible for handling all interactions between the TypeScript compiler | ||
* and the host operating system. | ||
*/ | ||
const tsWatchSys: ts.System = { | ||
...ts.sys, | ||
|
||
watchFile(path, callback) { | ||
if (path.endsWith(`/${GENERATED_DTS}`)) { | ||
return ts.sys.watchFile(path, callback); | ||
} | ||
/** | ||
* Watch changes in source files, missing files needed to update the program or config file | ||
* @returns a no-op file watcher | ||
*/ | ||
watchFile(): ts.FileWatcher { | ||
return { | ||
close() {}, | ||
}; | ||
}, | ||
watchDirectory() { | ||
/** | ||
* Watch a resolved module's failed lookup locations, config file specs, type roots where auto type reference | ||
* directives are added | ||
* @returns a no-op file watcher | ||
*/ | ||
watchDirectory(): ts.FileWatcher { | ||
return { | ||
close() {}, | ||
}; | ||
}, | ||
setTimeout(callback, time) { | ||
timeoutId = setInterval(() => { | ||
if (!isRunning) { | ||
/** | ||
* Set delayed compilation, so that multiple changes in short span are compiled together | ||
* @param callback a callback to invoke upon the completion of compilation. this function is provided to Stencil by | ||
* the TypeScript compiler. | ||
* @param timeoutMs the minimum time to wait (in milliseconds) before checking if compilation is complete or not | ||
* @returns the identifier for the interval that's created | ||
*/ | ||
setTimeout(callback: (...args: any[]) => void, timeoutMs: number): any { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we get an error if we change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For both this and export interface System {
...
setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any;
clearTimeout?(timeoutId: any): void;
} I think this is due to the fact that in some cases we can use a number, but in Node these functions are typed as accepting
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok yeah that makes sense - I had a feeling this could be a browser <-> node difference |
||
currentBuildTimeoutId = setInterval(() => { | ||
if (!isBuildRunning) { | ||
callback(); | ||
clearInterval(timeoutId); | ||
clearInterval(currentBuildTimeoutId); | ||
} | ||
}, config.sys.watchTimeout || time); | ||
return timeoutId; | ||
}, config.sys.watchTimeout || timeoutMs); | ||
return currentBuildTimeoutId; | ||
}, | ||
|
||
clearTimeout(id) { | ||
return clearInterval(id); | ||
/** | ||
* Reset existing delayed compilation | ||
* @param timeoutId the current build timeout identifier to clear | ||
*/ | ||
clearTimeout(timeoutId: any): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #3374 (comment) - I think these should be left as |
||
clearInterval(timeoutId); | ||
}, | ||
}; | ||
|
||
config.sys.addDestory(() => tsWatchSys.clearTimeout(timeoutId)); | ||
config.sys.addDestory(() => tsWatchSys.clearTimeout(currentBuildTimeoutId)); | ||
|
||
const tsWatchHost = ts.createWatchCompilerHost( | ||
config.tsconfig, | ||
optionsToExtend, | ||
tsWatchSys, | ||
ts.createEmitAndSemanticDiagnosticsBuilderProgram, | ||
(reportDiagnostic) => { | ||
config.logger.debug('watch reportDiagnostic:' + reportDiagnostic.messageText); | ||
}, | ||
(reportWatchStatus) => { | ||
config.logger.debug(reportWatchStatus.messageText); | ||
} | ||
); | ||
/** | ||
* Create a {@link ts.WatchCompilerHost}. A CompilerHost allows a {@link ts.Program} to interact with the | ||
* {@link ts.System}, by acting as an intermediary: | ||
* ``` | ||
* ┌────────────┐ ┌──────────────────────┐ ┌───────────┐ ┌──────────────────┐ | ||
* │ ts.Program │<->│ ts.WatchCompilerHost │<->│ ts.System │<->│ Operating System │ | ||
* └────────────┘ └──────────────────────┘ └───────────┘ └──────────────────┘ | ||
* ``` | ||
* | ||
* Strictly speaking, the created entity is a subclass of a WatchCompilerHost. The | ||
* {@link ts.WatchCompilerHostOfConfigFile} class has the following features that makes it useful to Stencil (even | ||
* when Stencil is performing a single, full build): | ||
* - it provides the opportunity to extend/alter an existing tsconfig file, allowing users to override specific | ||
* configuration options via {@link ts.WatchCompilerHostOfConfigFile#optionsToExtend}, which is a provided as an | ||
* argument in the constructor | ||
* - it includes the {@link ts.WatchCompilerHost#afterProgramCreate} function in its interface, which Stencil | ||
* overrides to invoke a build callback (not as a part of this object's creation) | ||
*/ | ||
const tsWatchHost: ts.WatchCompilerHostOfConfigFile<ts.EmitAndSemanticDiagnosticsBuilderProgram> = | ||
ts.createWatchCompilerHost( | ||
config.tsconfig, | ||
optionsToExtend, | ||
tsWatchSys, | ||
ts.createEmitAndSemanticDiagnosticsBuilderProgram, | ||
(reportDiagnostic) => { | ||
config.logger.debug('watch reportDiagnostic:' + reportDiagnostic.messageText); | ||
}, | ||
(reportWatchStatus) => { | ||
config.logger.debug(reportWatchStatus.messageText); | ||
} | ||
); | ||
|
||
tsWatchHost.afterProgramCreate = async (tsBuilder) => { | ||
isRunning = true; | ||
/** | ||
* Override {@link ts.WatchCompilerHost#afterProgramCreate} to invoke the build callback that was provided as an | ||
* argument to this function. | ||
* @param tsBuilder a {@link ts.BuilderProgram} to manage the {@link ts.Program} in the provided build context | ||
*/ | ||
tsWatchHost.afterProgramCreate = async (tsBuilder: ts.EmitAndSemanticDiagnosticsBuilderProgram): Promise<void> => { | ||
isBuildRunning = true; | ||
await buildCallback(tsBuilder); | ||
isRunning = false; | ||
isBuildRunning = false; | ||
}; | ||
|
||
/** | ||
* Create the initial {@link ts.Program} using Stencil's custom {@link ts.WatchCompilerHostOfConfigFile}. The Program | ||
* represents the _TypeScript_ compiler context, that will work in tandem with Stencil's compiler context and build | ||
* context | ||
*/ | ||
return ts.createWatchProgram(tsWatchHost); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this basically the offending code here? I was just thinking it might be good to have a brief explanation as to why we have a no-op file watcher here (and under
watchDirectory
below)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! That's it.
So admission time - I'm not entirely sure if/why
close
was overridden the way it was. I followed the pattern to avoid breaking anything further (since this isn't under test ATM). Unfortunately there isn't a design doc or anything ingit blame
that makes the justification for this immediately obvious 😢There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm the mystery deepends...in that case I think it's fine as is 😄