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
Proposal: Conditional Compilation #3538
Comments
|
I don't quite like that the proposal isn't addressing conditional imports. I think that is the core issue of having conditionals in the first place.
I'm not sure if I like the runtime property check. We are all developers and we know TypeScript is a compiled language. Why do we need to have a solution when we don't define any flag values? I would rather see it compile as if all conditionals where true then. I also like the design of conditional flags in C# and C++ because they look like "commented code". Which kind of infer that they only interfere on compile time and not during runtime. They are also simple to understand just like a simple if statement. I guess they are also simpler for the compiler, just let the scanner scan or skip characters depending on if the conditional are true or false. Instead of having a complex tree shaking that removes dead code. With your solution you are also using runtime statements |
I think that would be a different solution. That is mainly because it is not likely the same pattern can be used with imports without breaking the functionality. The way we addressed this in Dojo, which then worked for both runtime and build time, was to utilise the AMD loader plugin mechanism to evaluate the "magical" MID string expressed as a ternary expression to determine if a certain
Sometimes a developer will want their code to be emitted as isomorphic, especially if they want to distribute it as a library without the end user having to be aware of TypeScript. This is aligned to design goal "4. Emit clean, idiomatic, recognizable JavaScript code." as well as "7. Preserve runtime behavior of all JavaScript code." and "10. Be a cross-platform development tool." What the runtime code is, is up to the developer and whether it gets emitted or not is up to the developer.
Maybe you should come up with an alternative proposal. Also, it would seem that that is something that TypeScript has largely avoided, "compiler hints". I dislike "auto-magic" comments/compiler hints personally and seeing it avoided in TypeScript made me happy. You often end up with surprises and the TypeScript Design Goals also state that TypeScript should not "introduce behaviour that is likely to surprise users".
Not necessarily, you will still be modifying the AST if you expect things like intellisense to continue to work. Ignoring written code under certain conditions is never straight forward. I am not sure why you feel comments make this process any easier for the compiler.
Exactly, but only when supplied with compile time values. Why do you feel that is a bad thing? |
|
I would also like that TS supports conditional compilation, but more in the C++/C# way too. With a C++/C# implementation: Additionally, if TS would provide "macros" for the current filename and line number, I would be able to retrieve them relative to the TS source code, whereas today a stack trace reports line numbers relative to the generated JS file... Some points from your proposal:
|
@mhegazy said "Pre-processor directives (i.e.#ifdefs) are not desirable." I took him at his word. As far as your example, I am a bit lost on how the following would eliminate the function call? Doesn't it generate an assert as a noop anyways, just like you did in TypeScript?
Totally different topic... You do know about the sourceMap compiler option?
There is a big difference between build/compile time value and a runtime value. The intent of my proposal is to allow both, without changing the source code. You can provide all your "feature" logic in your code and then you can choose to have it compiled out, or resolved runtime. In theory, you would always want to have some runtime value. Other examples would be like if you where trying to shim things like Promises or Object.observe. You would write the code to handle both cases, with a clear feature/condition and then you can choose to have that resolved at runtime (and the whole set of code is emitted, including the resolution logic) or you could choose to have two builds, both optimised for the features being there or not.
Why I proposed it was because I felt it would lead to less surprises. Again, according to the TypeScript Design Goals, TypeScript should not "introduce behaviour that is likely to surprise users". By leveraging |
I just wanted to report my need for conditional compilation, which doesn't seem supported by your proposal. At the end I don't really care of the exact mechanism which might be implemented.
When
My goal is not to debug the code under a browser, but to create a mail with the exception stack trace when an uncaught exception occurs at client side.
OK, I misunderstood your example. In |
|
@stephanedr Just jumping in, the idea of giving the compiler hints using design-time decorators like |
Again, side topic... Mozilla's Source Map would allow you to determine the TypeScript original positions for the source and actually rewrite the stack trace. There are a few other more complete tools out there that leverage source-map and would do the heavy lifting for you. I suspect even with your macros, it would be hard to cover all the transforms that occur during transpilation, where this is more certain. |
|
@RichiCoder1, correct me if I'm wrong, but using decorators will only allow to empty the assert function. What I would like is to remove all the assert calls. @kitsonk, providing file name / line number should be quite easy for a compiler, as it already maintains them to report compilation errors. |
|
Regarding passing the flags to process, I have a suggestion inspired by some of the node.js based conventions (among which JSCS has aced in this aspect by incorporating every approach): Note: (in case of redefinitions / clashes) precedence order: descending
|
|
Webpack has this feature in the box https://webpack.github.io/docs/list-of-plugins.html#defineplugin new webpack.DefinePlugin({
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object")
})console.log("Running App version " + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv"); |
|
Hi everyone, Conditional compilation is a must-have feature in Typescript. The idea of both runtime et precompilation time constants is also a very good idea. But I think we should use a C#/C++ syntax but adapted to JavaScript : Sample source#define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]
#if HOST_NODE
console.log("I'm running in Node.JS");
#else
console.log("I'm running in browser");
#endif
#if COMPILE_OPTIONS.indexOf("default") !== -1
console.log("Default option is configured");
#endifEdit: Remove equals signs to keep C-style syntax as suggested by @stephanedr . Runtime compilationThis would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as: const __tsc__HOST_NODE = (typeof process == "object" && process.versions && process.versions.node && process.versions.v8));
const __tsc__COMPILE_OPTIONS = ["some", "default", "options"];
if (__tsc__HOST_NODE) {
console.log("I'm running in Node.JS");
} else {
console.log("I'm running in browser");
}
if (__tsc__COMPILE_OPTIONS.indexOf("default") !== -1) {
console.log("Default option is configured");
}tsconfig.json configurationAssuming you define some constants in your {
"defines": {
"HOST_NODE": false,
"COMPILE_OPTIONS": ["some", "other", "options"]
}
}This would then emit as : console.log("I'm running in browser");CLI configurationOr if you define contants in an other way by using CLI : $ tsc --define=HOST_NODE:true --define=COMPILE_OPTIONS:["some", "default", "options"]This would then emit as : console.log("I'm running in NodeJS");
console.log("Default option is configured");Typings emitting and interpretationAssuming you have a module designed like this : #define HOST_NODE (typeof process == "object" && process.versions && process.versions.node && process.versions.v8))
#define COMPILE_OPTIONS ["some", "default", "options"]
export function commonFunction() { }
#if HOST_NODE
export function nodeSpecificFunction() { }
#endif
#if COMPILE_OPTIONS.indexOf("default") !== -1
export function dynamicOptionFunction() { }
#endif
export class MyClass {
common() { }
#if HOST_NODE
nodeSpecific() { }
#endif
}Edit: Remove equals signs to keep C-style syntax as suggested by @stephanedr . If no definitions are configured, it should be interpreted like this : export function commonFunction(): void;
export function nodeSpecificFunction?(): void;
export function dynamicOptionFunction?(): void;
export class MyClass {
common(): void;
nodeSpecific?(): void;
}Edit: Added Class case as suggested by @stephanedr . If you define some constants in your {
"defines": {
"HOST_NODE": false,
"COMPILE_OPTIONS": ["some", "default", "options"]
}
}Then, it should be interpreted like this : export function commonFunction(): void;
export function dynamicOptionFunction(): void;
export class MyClass {
common(): void;
}Edit: Added Class case as suggested by @stephanedr . It allows compiler and EDIs to ignore some parts of the code based on compiler configuration. Function-like syntaxBased on @stephanedr comments. Assuming following sample source #define DEBUG !!process.env.DEBUG
#if DEBUG
function _assert(cond: boolean): void {
if (!cond)
throw new AssertionError();
}
#define assert(cond: boolean): void _assert(cond)
#endif
type BasicConstructor = { new (...args: Object[]) => T };
#if DEBUG
function _cast<T>(type: BasicConstructor, object: Object): T {
#assert(object instanceof type);
return <T>object;
}
#define cast<T>(type: BasicConstructor, object: T) _cast(type, object)
#else
#define cast<T>(type: BasicConstructor, object: T) <T>object
#endif
class C {
f(a: number) {
#assert(a >= 0 && a <= 10);
let div = #cast(HTMLDivElement, document.getElementById(...));
}
}This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as: const __tsc__EMPTY = function () { return; };
const __tsc__DEBUG= !!process.env.DEBUG;
let __tsc__assert = __tsc__EMPTY;
if (__tsc__DEBUG) {
function _assert(cond) {
if (!cond)
throw new AssertionError();
}
__tsc__assert = function (cond) { return _assert(cond); };
}
let __tsc__cast = __tsc__EMPTY;
if (__tsc__DEBUG) {
function _cast(type, object) {
__tsc__assert(object instanceof type);
return object;
}
__tsc__cast = function (type, object) { return _cast(type, object); };
}
else {
__tsc__cast = function (type, object) { return object; };
}
class C {
f(a) {
__tsc__assert(a >= 0 && a <= 10);
let div = __tsc__cast(HTMLDivElement, document.getElementById(...));
}
}Assuming you define function _assert(cond) {
if (!cond)
throw new AssertionError();
}
function _cast(type, object) {
_assert(object instanceof type);
return object;
}
class C {
f(a) {
_assert(a >= 0 && a <= 10);
let div = _cast(HTMLDivElement, document.getElementById(...));
}
}Now, assuming you define class C {
f(a) {
let div = document.getElementById(...);
}
}ConclusionI think it allows a more granular conditional compilation by using the power of JavaScript. Moreover it clearly separates (in both code-style and evaluation) the compiler directives from your code, like it used to be on C-style compiled languages. What do you think ? |
|
@SomaticIT A few remarks: 1/ For people who know C/C# syntax, the "=" sign between the name and the value may be a bit disturbing. Why not keeping the C/C# syntax? 2/ It should allow something like: (for sure, here DEBUG needs to evaluate to a constant to generate valid ES6 code). 3/ It should also support function-like syntax, e.g.: The simplest to get |
|
1/ I agree with you, I edited my comment to remove 2/ I also agree with you, I edited my comment to add this exemple in Typings emitting and interpretation part. 3/ I think this case is really interesting but I think we should improve compilation emitting and typings interpretation in this particular case. |
|
@SomaticIT
3b/ I'm not sure this can always be parsed, particularly with return union types (where the type ends? where the "body" starts?). 3c/ A same function-like define can be implemented several times (here 2, but might be more). Where should we put the JSDoc (avoiding duplication)? An alternate syntax might be: Note that I'm not especially attached to a "#" syntax, so might also be: |
|
Sorry I've used the C# decorator syntax. Let's use the TypeScript's one. |
|
My project needs conditional compilation in order to support partial builds - when the user chooses that he wants to have a version of the library that only has components 1, 3, X... in it. While most of that can be supported by splitting the code into separate files, there are some cases when I need to define that certain class members are for one component only. For this I currently have a script that uses the compiler API and uses a regex to apply simple conditional text replacements in the source code. |
|
Some background: C/C++ and C# support preprocessors, Java doesn't. However, one can use the C++ preprocessor system in Java, see this post. I was somewhat curious about this feature in TS and I think there are valid cases where preprocessors could be used without abusing the TS/JS dev and build process. But I'm not convinced that this should be done during the TS compilation. Gulp offers some preprocessing right now, see preprocess or gulp-preprocess or Webpack's similar feature mentioned by @cevek. Here is a simple experimenting of mine to check out gulp-preprocess with TS. (Don't expect much.) This preprocessor seems to be kinda useful, not with all of the C++ preprocessor features though. Currently I see these major weaknesses of external preprocessors.
So I think that using preprocessors in a TS environment is a very special requirement (i.e. not general) that can be solved by using already existing tools. I recommend reconsidering this feature after at least a half or one year. There are more important and way more general feature requests now. |
|
This would be very useful for targeting different platforms when necessary, such as React Native differences between iOS and Android. |
|
This issue already lives now for more than 5 years and many duplicates/alternatives have been closed already. I was wondering if somebody is using an unofficial alternative in their projects successfully. I personally can imagine some variants to implement this but I would like to avoid re-implementing the wheel:
My direct use case is that I have some "debug" mode of my library where I render some additional guides and hints for better testing and analysis purposes. This feature is only reasonable during development time and I want to remove this code on production for a smaller footprint. |
|
@Danielku15 couldn't agree more! Currently we're running into the same issue but there's no clear path to resolve it. The most confusing part is that: Optimizers like https://github.com/webpack-contrib/terser-webpack-plugin can provide functionalities like removing dead code. But when it comes to the combination of webpack && TypeScript && terser, it does not work at all. I believe if we're using JS rather than TS, things can improve a bit. However, we need to move forward on this topic for better TS toolchain. |
|
Hello, I would like to know if we can reopen the discussion about preprocessor directives: #3538 (comment) Thanks |
|
I would like to see this functionality to compile project for different target web servers. |
|
My personal instinct after reading much of this thread is that there a desire to build the "Taj Mahal" of conditional compilation when I think for a lot of people, simple and basic conditional compilation would be a fantastic first step. I dislike how the capabilities of C#/C++ are being conflated together. What C++ can do, relative to C#, makes them two somewhat different beasts. C# provides a straightfoward means via #if/#endif directives to include, or not include, code. For me, this might be debugging code in a debug build, or some internal validation code in a debug build, and stuff like that. For others it might be some conditional compilation around web server support. Because the C# style of behaviour acts like a preprocessor, it works for all portions of a code file, and so can target import statements, class definitions, or whatever else you wish. My general opinion is this feature is being over-thought and over-baked. If the C# example tells us anything, it's that even a basic capability (which is what C# offers relative to C++) still can be immensely useful relative to nothing at all. |
|
Currently, I'm using Tree Shaking in webpack (https://webpack.js.org/guides/tree-shaking/) and the DefinePlugin to implement condition build for different targets: Android, iOS, web, test. You could turn off sideEffects to make sure unused imports are not bundled: https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free |
|
I just wanna express the need I'm having right now for conditional compilation based on the target #if tsconfig.compilerOptions.module === "ESNext"
const script = await import(filename);
#else
const script = require(filename);
#end Perhaps all we need is a way to provide custom variables at the compilation level (+ default ones such as tsconfig options) and filter out AST branches that do not meet special conditional expressions. Branches filtered out do not need to be checked/compiled. I understand this is quite complex though! |
|
I'd like to give a shout out to the https://github.com/jarred-sumner/atbuild |
|
Conditional compilation could also be used to provide conditional exporting so that items can be exported only when running in development mode for the purpose of unit testing, while not being exported in production code. For example: export function ComplexFunction(args: ComplexFunctionArguments) {
...
}
#if DEBUG
export
#endif
function HelperFunction(args: HelperFunctionArguments) {
...
}That way, unit test code (which will always be running in development mode) can directly call/mock/etc |
|
I came across a situation where I wanted to do a conditional compile and searching led to this this open issue. Node.js 14.17.0 introduced a new "randomUUID" function. in the "crypto" package. As a built-in now, a module is no longer required and I prefer to use built-in's wherever possible. Prior to this new built-in, I was referencing the "v4" function of the "uuid" package on npm. The problem I am having is one module I have runs on an old Node.js 8.17.0 system as well as newer Node.js 14 and 16 systems. It would be nice to have the TypeScript code compile one way for the ES2018 target (Node.js 8) and another for ES2020 target (Node.js 14+) which I compile using to different tsconfig.json files. For now I just hand edit the one built module that could use the conditional option so that it is one of these statements: This is obviously a very minor case but it could be more complex issue for other people as I read through the comments. |
|
Strongly support, especially needed for cross platform (e.g. Electron / Cordova) |
|
I agree with @customautosys. As a reminder, I put again a link to my proposal: |
|
Would it not be better for this to be implemented as a form of triple-slash directive? This would be much like we have with references and the amd module stuff. Something like: doGeneralStuff()
/// <conditional platform="election" ...other props perhaps... >
electrionOnly()
/// </conditional>
/// <conditional platform="node" ...other props perhaps... >
nodeOnly()
/// </conditional>
doMoreGeneralStuff()This syntax wouldn't break parsers like eslint's typescript parser and countless other projects, it's already familiar to those who have used triple-slashes in typescript and have used xml or a like. I also like it allows for the use of more than one condition specified as a prop on the conditional node. How this ties into the tsconfig and what parts are dynamic I'll leave that to the better of the conversation here. Just an off the cuff thought I wanted to share and see what you all thought. |
|
This proposal has been long-standing (years). |
That's also OK. It doesn't really matter what the syntax is as long as there is a way to select / remove code at compile time based on a compile time constant / definition. Whether it's /// or #define / #if / #ifdef / #ifndef doesn't matter. |
|
@customautosys It does matter a little. If existing tokenizers can consume the code without exploding that is a win. Things like linters don't need to be aware of conditional behavior to do their job but they can't do it at all if they can't parse the code. There are many other kinds of tools that would run into similar problems I'm sure. |
|
I'm really not a fan of the |
|
Whenever TypeScript adds a new syntax feature, existing parsers need to be updated, that's just a fact of life. TypeScript adds new syntax fairly often, the most recent example is template string types in 4.1 |
|
I thought about it a bit more, and I've come around on the syntax. The triple slash stuff is currently used for js files where typescript syntax can not be used. |
|
I just re-read it and it seems the proposal does not support conditional compilation for imports. This makes it a lot less useful then. We should have a way to allow for conditional imports. |
Proposal: Conditional Compilation
Problem Statement
At design time, developers often find that they need to deal with certain scenarios to make their code ubiquitous and runs in every environment and under every runtime condition. At build time however, they want to emit code that is more suited for the runtime environment that they are targetting by not emitting code that is relevant to that environment.
This is directly related to #449 but it also covers some other issues in a similar problem space.
Similar Functionality
There are several other examples of apporaches to solving this problem:
Considerations
Most of the solutions above use "magic" language features that significantly affect the AST of the code. One of the benefits of the has.js approach is that the code is transparent for runtime feature detection and build time optimisation. For example, the following would be how design time would work:
If you then wanted to do a build that targeted NodeJS, then you would simply assert to the build tool (
staticHasFlags) that instead of detecting that feature at runtime,host-nodewas in facttrue. The build tool would then realise that theelsebranch was unreachable and remove that branch from the built code.Because the solution sits entirely within the language syntax without any sort of "magical" directives or syntax, it does not take a lot of knowledge for a developer to leverage it.
Also by doing this, you do not have to do heavy changes to the AST as part of the complication process and it should be easy to identify branches that are "dead" and can be dropped out of the emit.
Of course this approach doesn't specifically address conditionality of other language features, like the ability to conditionally load modules or conditional classes, though there are other features being introduced in TypeScript (e.g. local types #3266) which when coupled with this would address conditionality of other language features.
Proposed Changes
In order to support conditional compile time emitting, there needs to be a language mechanic to identify blocks of code that should be emitted under certain conditions and a mechanism for determining if they are to be emitted. There also needs to be a mechanism to determine these conditions at compile time.
Defining a Conditional Identifier at Design Time
It is proposed that a new keyword is introduced to allow the introduction of a different class of identifier that is neither a variable or a constant. Introduction of a TypeScript only keyword should not be taken lightly and it is proposed that either
conditionorhasis used to express these identifiers. When expressed at design time, the identifier will be given a value which can be evaluated at runtime, with block scope. This then can be substituted though a compile time with another value.Of the two keywords, this proposal suggests that
hasis more functional in meaning, but might be less desirable because of potential for existing code breakage, but examples utlise thehaskeyword.For example, in TypeScript the following would be a way of declaring a condition:
This would then emit, assuming there is no compile time substitutional value available (and targeting ES6) as:
Defining the value of a Conditional Identifier at Compile Time
In order to provide the compile time values, an augmentation of the
tsconfig.jsonis proposed. A new attribute will be proposed that will be named in line with the keyword of eitherconditionValuesorhasValues. Differenttsconfig.jsoncan be used for the different builds desired. Not considered in this proposal is consideration of how these values might be passed totscdirectly.Here is an example of
tsconfig.json:{ "version": "1.6.0", "compilerOptions": { "target": "es5", "module": "umd", "declaration": false, "noImplicitAny": true, "removeComments": true, "noLib": false, "sourceMap": true, "outDir": "./" }, "hasValues": { "hostNode": true } }Compiled Code
So given the
tsconfig.jsonabove and the following TypeScript:You would expect the following to be emitted:
As the compiler would replace the symbol of hostNode with the value provided in
tsconfig.jsonand then substitute that value in the AST. It would then realise that the one of the branches was unreachable at compile time and then collapse the AST branch and only emit the reachable code.The text was updated successfully, but these errors were encountered: