<p dir="auto">Extension to <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="54532184" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/1692" data-hovercard-type="pull_request" data-hovercard-url="/microsoft/TypeScript/pull/1692/hovercard" href="https://github.com/microsoft/TypeScript/pull/1692">#1692</a>.</p>
<p dir="auto">Motivation:<br>
A common file layout for some projects is to organize code by feature, rather by the kind of the file. This means that the implementation of a feature, along with its unit tests and end to end tests are all grouped into a single directory structure. I use this convention in my own projects.</p>
<p dir="auto">Currently, however, tsconfig.json assumes that sub-projects are split by directories. With the feature grouped files convention, however, we have multiple 'projects' interleaved in the same directories. For example, end to end tests have different project settings than unit tests, which may have different project settings from the application code itself.</p>
<p dir="auto">Proposal:<br>
Support sub-projects within a single tsconfig.json. This allows for more flexibility in how files are laid out in an application.</p>
<p dir="auto">Example:</p>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
  &quot;compilerOptions&quot;: {
    &quot;noImplicitAny&quot;: true,
    &quot;removeComments&quot;: false,
    &quot;sourceMap&quot;: true
  },
  &quot;files&quot;: [
     &quot;common-files/commonFiles.ts&quot;,
  ],
  &quot;projects&quot;: {
     &quot;app&quot;: {
         &quot;compilerOptions&quot;: {
             &quot;module&quot;: &quot;amd&quot;,
             &quot;removeComments&quot;: true,
             &quot;out&quot;: &quot;app/app.js&quot;
         },
         &quot;files&quot;: [
             &quot;app/feature1/feature1.ts&quot;
         ]
     },
     &quot;unit&quot;: {
         &quot;compilerOptions&quot;: {
             &quot;out&quot;: &quot;tmp/unit/&quot;
         },
         &quot;files&quot;: [
           &quot;app/feature1/feature1_test.ts&quot;,
         ]
    },
    &quot;e2e&quot;: {
        &quot;compilerOptions&quot;: {
            &quot;module&quot;: &quot;commonjs&quot;,
            &quot;out&quot;: &quot;tmp/e2e/&quot;
        },
        &quot;files&quot;: [
            &quot;app/feature1/feature1PageObject_e2e.ts&quot;,
            &quot;app/feature1/feature1_e2e.ts&quot;
         ]
    }       
}"><pre class="notranslate">{
  <span class="pl-ent">"compilerOptions"</span>: {
    <span class="pl-ent">"noImplicitAny"</span>: <span class="pl-c1">true</span>,
    <span class="pl-ent">"removeComments"</span>: <span class="pl-c1">false</span>,
    <span class="pl-ent">"sourceMap"</span>: <span class="pl-c1">true</span>
  },
  <span class="pl-ent">"files"</span>: [
     <span class="pl-s"><span class="pl-pds">"</span>common-files/commonFiles.ts<span class="pl-pds">"</span></span>,
  ],
  <span class="pl-ent">"projects"</span>: {
     <span class="pl-ent">"app"</span>: {
         <span class="pl-ent">"compilerOptions"</span>: {
             <span class="pl-ent">"module"</span>: <span class="pl-s"><span class="pl-pds">"</span>amd<span class="pl-pds">"</span></span>,
             <span class="pl-ent">"removeComments"</span>: <span class="pl-c1">true</span>,
             <span class="pl-ent">"out"</span>: <span class="pl-s"><span class="pl-pds">"</span>app/app.js<span class="pl-pds">"</span></span>
         },
         <span class="pl-ent">"files"</span>: [
             <span class="pl-s"><span class="pl-pds">"</span>app/feature1/feature1.ts<span class="pl-pds">"</span></span>
         ]
     },
     <span class="pl-ent">"unit"</span>: {
         <span class="pl-ent">"compilerOptions"</span>: {
             <span class="pl-ent">"out"</span>: <span class="pl-s"><span class="pl-pds">"</span>tmp/unit/<span class="pl-pds">"</span></span>
         },
         <span class="pl-ent">"files"</span>: [
           <span class="pl-s"><span class="pl-pds">"</span>app/feature1/feature1_test.ts<span class="pl-pds">"</span></span>,
         ]
    },
    <span class="pl-ent">"e2e"</span>: {
        <span class="pl-ent">"compilerOptions"</span>: {
            <span class="pl-ent">"module"</span>: <span class="pl-s"><span class="pl-pds">"</span>commonjs<span class="pl-pds">"</span></span>,
            <span class="pl-ent">"out"</span>: <span class="pl-s"><span class="pl-pds">"</span>tmp/e2e/<span class="pl-pds">"</span></span>
        },
        <span class="pl-ent">"files"</span>: [
            <span class="pl-s"><span class="pl-pds">"</span>app/feature1/feature1PageObject_e2e.ts<span class="pl-pds">"</span></span>,
            <span class="pl-s"><span class="pl-pds">"</span>app/feature1/feature1_e2e.ts<span class="pl-pds">"</span></span>
         ]
    }       
}</pre></div>
<p dir="auto">Building with tsc:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="tsc -p tsconfig.json
-&gt; builds root and all sub-projects, including those in sub-directories, recursively.

tsc -p tsconfig.json:app
-&gt; builds root and app sub-project."><pre class="notranslate"><code class="notranslate">tsc -p tsconfig.json
-&gt; builds root and all sub-projects, including those in sub-directories, recursively.

tsc -p tsconfig.json:app
-&gt; builds root and app sub-project.
</code></pre></div>
<p dir="auto">Notes:</p>
<ul dir="auto">
<li>Projects are recursive. You can have sub-projects of projects. Building the root builds all sub-projects and their sub-projects.</li>
<li>To build a specific sub-project, use ':', for example tsc -p jsconfig.json:app:feature1 if "feature1" were a subproject of "app" (not shown in the example above).</li>
<li>Child projects inherit options and files from their parent projects, unless options are overwritten. Files are additive.</li>
<li>tsconfig.json files in sub-directories are evaluated independently. That is, they do not inherit anything from tsconfig.json files in a parent directory.</li>
<li>IDE and editor extensions may decide to support switching the current "context" of a file, i.e. the project or sub-project, or the tsconfig.json file if more than one reference the same file in one of the parent directories.</li>
</ul>
<p dir="auto">The default context is determined as follows:</p>
<ul dir="auto">
<li>Find the tsconfig.json in the same directory as the file, or recursively search parent directories.</li>
<li>If the root project has a reference to the file, use the root settings as the default context.</li>
<li>Recursively look in each sub-project, finding the first project with a reference to the file.</li>
</ul>
<p dir="auto">Listening to <a class="user-mention notranslate" data-hovercard-type="user" data-hovercard-url="/users/nycdotnet/hovercard" data-octo-click="hovercard-link-click" data-octo-dimensions="link_type:self" href="https://github.com/nycdotnet">@nycdotnet</a> has me fired up to tackle this one.  Thanks, Steve.  (btw, you can check out his good interview here: <a href="http://www.dotnetrocks.com/default.aspx?showNum=1149" rel="nofollow">http://www.dotnetrocks.com/default.aspx?showNum=1149</a>)</p>
<p dir="auto">The proposal here was first started in times prehistoric (even before <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="37903783" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/11" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/11/hovercard" href="https://github.com/microsoft/TypeScript/issues/11">#11</a>), 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 <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="85547804" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/3394" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/3394/hovercard" href="https://github.com/microsoft/TypeScript/issues/3394">#3394</a>.</p>
<h1 dir="auto">The Problem</h1>
<p dir="auto">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 <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="60991541" data-permission-text="Title is private" data-url="https://github.com/microsoft/TypeScript/issues/2338" data-hovercard-type="issue" data-hovercard-url="/microsoft/TypeScript/issues/2338/hovercard" href="https://github.com/microsoft/TypeScript/issues/2338">#2338</a> 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:</p>
<ul dir="auto">
<li>Small-sized projects: use tsconfig.json, keep most of your source in the current directory</li>
<li>Large-sized projects: use custom builds, put source where you need it</li>
</ul>
<p dir="auto">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.</p>
<p dir="auto">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:</p>
<ul dir="auto">
<li>Small-sized projects: use tsconfig.json, keep most of your source in the current directory</li>
<li>Medium-sized projects: those with standard builds and shared components</li>
<li>Large-sized projects: use custom builds, put source where you need it</li>
</ul>
<p dir="auto">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.</p>
<h1 dir="auto">Proposal</h1>
<p dir="auto">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.</p>
<h2 dir="auto">Goals</h2>
<p dir="auto">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.</p>
<h2 dir="auto">Non-goals</h2>
<p dir="auto">This proposal does <em>not</em> 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.</p>
<h2 dir="auto">Design</h2>
<p dir="auto">To support medium-sized projects, we focus on the use case of one tsconfig.json referencing another.</p>
<p dir="auto">Example <a href="https://github.com/Microsoft/TypeScript/wiki/tsconfig.json">tsconfig.json</a> of today:</p>
<div class="highlight highlight-source-js notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
    &quot;compilerOptions&quot;: {
        &quot;module&quot;: &quot;commonjs&quot;,
        &quot;noImplicitAny&quot;: true,
        &quot;sourceMap&quot;: true
    },
    &quot;files&quot;: [
        &quot;core.ts&quot;,
        &quot;sys.ts&quot;
    ]
}"><pre class="notranslate"><span class="pl-kos">{</span>
    <span class="pl-s">"compilerOptions"</span>: <span class="pl-kos">{</span>
        <span class="pl-s">"module"</span>: <span class="pl-s">"commonjs"</span><span class="pl-kos">,</span>
        <span class="pl-s">"noImplicitAny"</span>: <span class="pl-c1">true</span><span class="pl-kos">,</span>
        <span class="pl-s">"sourceMap"</span>: <span class="pl-c1">true</span>
    <span class="pl-kos">}</span><span class="pl-kos">,</span>
    <span class="pl-s">"files"</span>: <span class="pl-kos">[</span>
        <span class="pl-s">"core.ts"</span><span class="pl-kos">,</span>
        <span class="pl-s">"sys.ts"</span>
    <span class="pl-kos">]</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto">Proposed tsconfig.json 'dependencies' section:</p>
<div class="highlight highlight-source-js notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="{
    &quot;compilerOptions&quot;: {
        &quot;module&quot;: &quot;commonjs&quot;,
        &quot;noImplicitAny&quot;: true,
        &quot;sourceMap&quot;: true
    },
    &quot;dependencies&quot;: [
        &quot;../common&quot;, 
        &quot;../util&quot;
    ],
    &quot;files&quot;: [
        &quot;core.ts&quot;,
        &quot;sys.ts&quot;
    ]
}"><pre class="notranslate"><span class="pl-kos">{</span>
    <span class="pl-s">"compilerOptions"</span>: <span class="pl-kos">{</span>
        <span class="pl-s">"module"</span>: <span class="pl-s">"commonjs"</span><span class="pl-kos">,</span>
        <span class="pl-s">"noImplicitAny"</span>: <span class="pl-c1">true</span><span class="pl-kos">,</span>
        <span class="pl-s">"sourceMap"</span>: <span class="pl-c1">true</span>
    <span class="pl-kos">}</span><span class="pl-kos">,</span>
    <span class="pl-s">"dependencies"</span>: <span class="pl-kos">[</span>
        <span class="pl-s">"../common"</span><span class="pl-kos">,</span> 
        <span class="pl-s">"../util"</span>
    <span class="pl-kos">]</span><span class="pl-kos">,</span>
    <span class="pl-s">"files"</span>: <span class="pl-kos">[</span>
        <span class="pl-s">"core.ts"</span><span class="pl-kos">,</span>
        <span class="pl-s">"sys.ts"</span>
    <span class="pl-kos">]</span>
<span class="pl-kos">}</span></pre></div>
<p dir="auto">Dependencies point to either:</p>
<ul dir="auto">
<li>A directory, where a tsconfig.json can be found</li>
<li>A tsconfig.json directly</li>
</ul>
<p dir="auto">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.</p>
<h1 dir="auto">How it works</h1>
<p dir="auto">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.</p>
<p dir="auto">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.</p>
<p dir="auto">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.</p>
<p dir="auto">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.</p>
<h1 dir="auto">Limitations</h1>
<p dir="auto">Adding a directory as a dependency that has no tsconfig.json is considered an error.</p>
<p dir="auto">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.</p>
<p dir="auto">As mentioned earlier, circular dependencies are considered an error.  In the simple case:</p>
<p dir="auto">A - B<br>
\ C</p>
<p dir="auto">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.</p>
<p dir="auto">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.</p>
<h1 dir="auto">Optional optimizations/improvements</h1>
<p dir="auto">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).</p>
<p dir="auto">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.</p>