Description
openedon Jun 10, 2015
Listening to @nycdotnet has me fired up to tackle this one. Thanks, Steve. (btw, you can check out his good interview here: http://www.dotnetrocks.com/default.aspx?showNum=1149)
The proposal here was first started in times prehistoric (even before #11), when dinosaurs walked the scorched earth. While nothing in this proposal is novel, per se, I believe it's high time we tackled the issue. Steve's own proposal is #3394.
The Problem
Currently, in TypeScript, it's rather easy to start and get going, and we're making it easier with each day (with the help of things like #2338 and the work on System.js). This is wonderful. But there is a bit of a hurdle as project size grows. We currently have a mental model that goes something like this:
- Small-sized projects: use tsconfig.json, keep most of your source in the current directory
- Large-sized projects: use custom builds, put source where you need it
For small-sized projects, tsconfig.json gives you an easy-to-setup way of getting going with any of the editors in a cross platform way. For large-scale projects, you will likely end up switching to build systems because of the varied requirements of large-scale projects, and the end result will be something that works for your scenarios but is difficult to tool because it's far too difficult to tool the variety of build systems and options.
Steve, in his interview, points out that this isn't quite the right model of the world, and I tend to agree with him. Instead, there are three sizes of project:
- Small-sized projects: use tsconfig.json, keep most of your source in the current directory
- Medium-sized projects: those with standard builds and shared components
- Large-sized projects: use custom builds, put source where you need it
As you scale in size of project, Steve argues, you need to be able to scale through the medium step, or tool support falls off too quickly.
Proposal
To solve this, I propose we support "medium-sized" projects. These projects have standard build steps that could be described in tsconfig.json today, with the exception that the project is built from multiple components. The hypothesis here is that there are quite a number of projects at this level that could be well-served by this support.
Goals
Provide an easy-to-use experience for developers creating "medium-sized" projects for both command-line compilation and when working with these projects in an IDE.
Non-goals
This proposal does not include optional compilation, or any steps outside of what the compiler handles today. This proposal also does not cover bundling or packaging, which will be handled in a separate proposal. In short, as mentioned in the name, this proposal covers only the 'medium-sized' projects and not the needs of those at large scales.
Design
To support medium-sized projects, we focus on the use case of one tsconfig.json referencing another.
Example tsconfig.json of today:
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"sourceMap": true
},
"files": [
"core.ts",
"sys.ts"
]
}
Proposed tsconfig.json 'dependencies' section:
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"sourceMap": true
},
"dependencies": [
"../common",
"../util"
],
"files": [
"core.ts",
"sys.ts"
]
}
Dependencies point to either:
- A directory, where a tsconfig.json can be found
- A tsconfig.json directly
Dependencies are hierarchical. To edit the full project, you need to open the correct directory that contains the root tsconfig.json. This implies that dependencies can't be cyclic. While it may be possible to handle cyclic dependencies in some cases, other cases, namely those with types that have circular dependencies, it may not be possible to do a full resolution.
How it works
Dependencies are built first, in the order they are listed in the 'dependencies' section. If a dependency fails to build, the compiler will exit with an error and not continue to build the rest of the project.
As each dependency completes, a '.d.ts' file representing the outputs is made available to the current build. Once all dependencies complete, the current project is built.
If the user specifies a subdirectory as a dependency, and also implies its compilation by not providing a 'files' section, the dependency is compiled during dependency compilation and is also removed from the compilation of the current project.
The language service can see into each dependency. Because each dependency will be driven off its own tsconfig.json, this may mean that multiple language service instances would need to be created. The end result would be a coordinated language service that was capable of refactoring, code navigation, find all references, etc across dependencies.
Limitations
Adding a directory as a dependency that has no tsconfig.json is considered an error.
Outputs of dependencies are assumed to be self-contained and separate from the current project. This implies that you can't concatenate the output .js of a dependency with the current project via tsconfig.json. External tools, of course, can provide this functionality.
As mentioned earlier, circular dependencies are considered an error. In the simple case:
A - B
\ C
A is the 'current project' and depends on two dependencies: B and C. If B and C do not themselves have dependencies, this case is trivial. If C depends on B, B is made available to C. This is not considered to be circular. If, however, B depends on A, this is considered circular and would be an error.
If, in the example, B depends on C and C is self-contained, this would not be considered a cycle. In this case, the compilation order would be C, B, A, which follows the logic we have for ///ref.
Optional optimizations/improvements
If a dependency does not to be rebuilt, then its build step is skipped and the '.d.ts' representation from the previous build is reused. This could be extended to handle if the compilation of a dependencies has built dependencies that will show up later in the 'dependencies' list of the current project (as happened in the example given in the Limitations section).
Rather than treating directories passed as dependencies that do not have a tsconfig.json as error cases, we could optionally apply default 'files' and the settings of the current project to that dependency.