Typescript: 支持“中型”项目

创建于 2015-06-10  ·  147评论  ·  资料来源: microsoft/TypeScript

@nycdotnet让我

这里的提议最初是在史前时代(甚至在#11 之前)开始的,当时恐龙在焦土上行走。 尽管该提案本身没有任何新意,但我认为现在是我们解决该问题的时候了。 史蒂夫自己的提议是#3394。

问题

目前,在 TypeScript 中,开始和开始使用起来相当容易,而且我们每天都在让它变得更容易(借助诸如 #2338 和 System.js 之类的工作)。 这太棒了。 但随着项目规模的增长,存在一些障碍。 我们目前有一个类似这样的心智模型:

  • 小型项目:使用 tsconfig.json,将大部分源代码保存在当前目录中
  • 大型项目:使用自定义构建,将源代码放在需要的地方

对于小型项目,tsconfig.json 为您提供了一种易于设置的方式,可以跨平台方式使用任何编辑器。 对于大型项目,由于大型项目的需求多种多样,您最终可能会切换到构建系统,最终结果将是适用于您的场景但难以工具化的东西,因为工具化太难了各种构建系统和选项。

史蒂夫在他的采访中指出,这不是世界的正确模型,我倾向于同意他的观点。 相反,有三种规模的项目:

  • 小型项目:使用 tsconfig.json,将大部分源代码保存在当前目录中
  • 中型项目:具有标准构建和共享组件的项目
  • 大型项目:使用自定义构建,将源代码放在需要的地方

Steve 认为,随着项目规模的扩大,您需要能够通过中间步骤进行扩展,否则工具支持会下降得太快。

提议

为了解决这个问题,我建议我们支持“中型”项目。 这些项目有标准的构建步骤,可以在今天的 tsconfig.json 中描述,但项目是由多个组件构建的。 这里的假设是,在这个级别上有相当多的项目可以通过这种支持得到很好的服务。

目标

为开发人员为命令行编译和在 IDE 中处理这些项目时创建“中型”项目提供易于使用的体验。

非目标

该提案_不_包括可选编译,或编译器今天处理的任何步骤之外的任何步骤。 该提案也不包括捆绑或打包,这将在单独的提案中处理。 简而言之,顾名思义,这个提案只涵盖“中型”项目,不涉及大型项目的需求。

设计

为了支持中型项目,我们专注于一个 tsconfig.json 引用另一个的用例。

今天的tsconfig.json示例:

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true
    },
    "files": [
        "core.ts",
        "sys.ts"
    ]
}

提议的 tsconfig.json 'dependencies' 部分:

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true
    },
    "dependencies": [
        "../common", 
        "../util"
    ],
    "files": [
        "core.ts",
        "sys.ts"
    ]
}

依赖项指向:

  • 一个目录,可以在其中找到 tsconfig.json
  • 一个 tsconfig.json 直接

依赖是分层的。 要编辑完整的项目,您需要打开包含根 tsconfig.json 的正确目录。 这意味着依赖项不能是循环的。 虽然在某些情况下可以处理循环依赖,但在其他情况下,即具有循环依赖的类型,可能无法进行完整解析。

这个怎么运作

首先构建依赖关系,按照它们在“依赖关系”部分中列出的顺序。 如果依赖项构建失败,编译器将退出并显示错误并且不会继续构建项目的其余部分。

当每个依赖项完成时,代表输出的“.d.ts”文件可用于当前构建。 完成所有依赖项后,将构建当前项目。

如果用户将子目录指定为依赖项,并且还通过不提供“文件”部分来暗示其编译,则在依赖项编译期间会编译该依赖项,并且还会从当前项目的编译中删除该依赖项。

语言服务可以查看每个依赖项。 因为每个依赖项都会从它自己的 tsconfig.json 驱动,这可能意味着需要创建多个语言服务实例。 最终结果将是一个协调的语言服务,能够跨依赖项进行重构、代码导航、查找所有引用等。

限制

添加目录作为没有 tsconfig.json 的依赖项被视为错误。

假设依赖项的输出是自包含的并且与当前项目分开。 这意味着您不能通过 tsconfig.json 将依赖项的输出 .js 与当前项目连接起来。 当然,外部工具可以提供此功能。

如前所述,循环依赖被认为是错误的。 在简单的情况下:

甲乙
\ C

A 是“当前项目”并且依赖于两个依赖项:B 和 C。如果 B 和 C 本身没有依赖项,则这种情况是微不足道的。 如果 C 依赖于 B,则 B 可用于 C。这不被认为是循环的。 但是,如果 B 依赖于 A,则这被认为是循环的并且将是错误的。

如果在示例中,B 依赖于 C 并且 C 是自包含的,则这不会被视为循环。 在这种情况下,编译顺序将是 C、B、A,这遵循我们对 ///ref 的逻辑。

可选的优化/改进

如果不重新构建依赖项,则跳过其构建步骤并重用先前构建中的“.d.ts”表示。 如果依赖项的编译构建了依赖项,这些依赖项将在稍后显示在当前项目的“依赖项”列表中(如限制部分给出的示例中所发生的那样),则可以扩展此处理。

我们可以选择将默认“文件”和当前项目的设置应用于该依赖项,而不是将作为没有 tsconfig.json 的依赖项传递的目录视为错误情况。

Committed Monorepos & Cross-Project References Suggestion

最有用的评论

下面的文档/博客文章正在进行中(将根据反馈进行编辑)

我鼓励任何关注此线程的人尝试一下。 我现在正在研究 monorepo 场景,以解决那里的任何最后的错误/功能,并且应该很快就会有一些指导


项目参考

项目引用是 TypeScript 3.0 中的一项新功能,它允许您将 TypeScript 程序组织成更小的部分。

通过这样做,您可以大大缩短构建时间,强制执行组件之间的逻辑分离,并以新的更好的方式组织您的代码。

我们还为tsc引入了一种新模式,即--build标志,它与项目引用协同工作以实现更快的 TypeScript 构建。

示例项目

让我们看一个相当普通的程序,看看项目引用如何帮助我们更好地组织它。
假设您有一个包含两个模块converterunits ,以及每个模块对应的测试文件:

/src/converter.ts
/src/units.ts
/test/converter-tests.ts
/test/units-tests.ts
/tsconfig.json

测试文件导入实现文件并进行一些测试:

// converter-tests.ts
import * as converter from "../converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

以前,如果您使用单个 tsconfig 文件,则使用此结构会很尴尬:

  • 实现文件可以导入测试文件
  • 不可能同时构建testsrc而没有src出现在输出文件夹名称中,这是您可能不想要的
  • 在实现文件更改只是内部需要再次类型检查测试,尽管这不会造成过新的错误
  • 仅更改测试需要再次对实现进行类型检查,即使没有任何更改

您可以使用多个 tsconfig 文件来解决其中一些问题,但会出现新的问题:

  • 没有内置的最新检查,所以你最终总是运行tsc两次
  • 两次调用tsc会导致更多的启动时间开销
  • tsc -w不能同时在多个配置文件上运行

项目参考可以解决所有这些问题,甚至更多。

什么是项目参考?

tsconfig.json文件有一个新的顶级属性references 。 它是一个对象数组,用于指定要引用的项目:

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../src" }
    ]
}

每个引用的path属性可以指向包含tsconfig.json文件的目录,或指向配置文件本身(可以有任何名称)。

当您引用一个项目时,会发生新的事情:

  • 从引用的项目导入模块将改为加载其输出声明文件 ( .d.ts )
  • 如果引用的项目生成outFile ,则输出文件.d.ts文件的声明将在此项目中可见
  • 如果需要,构建模式(见下文)将自动构建引用的项目

通过分成多个项目,您可以大大提高类型检查和编译的速度,减少使用编辑器时的内存使用量,并提高程序逻辑分组的执行情况。

composite

引用的项目必须启用新的composite设置。
需要此设置以确保 TypeScript 可以快速确定在哪里可以找到引用项目的输出。
启用composite标志会改变一些事情:

  • rootDir设置,如果没有明确设置,默认为包含tsconfig文件的目录
  • 所有实现文件必须与include模式匹配或列在files数组中。 如果违反此约束, tsc将通知您未指定哪些文件
  • declaration必须开启

declarationMaps

我们还添加了对声明源映射的支持。
如果您启用--declarationMap ,您将能够使用“转到定义”和重命名等编辑器功能在受支持的编辑器中跨项目边界透明地导航和编辑代码。

prependoutFile

您还可以在引用中使用prepend选项启用依赖项的输出:

   "references": [
       { "path": "../utils", "prepend": true }
   ]

预先准备一个项目将包括该项目的输出高于当前项目的输出。
这适用于.js文件和.d.ts文件,并且源映射文件也将正确发出。

tsc将只使用磁盘上的现有文件来执行此过程,因此可能会创建一个无法生成正确输出文件的项目,因为某些项目的输出将在结果文件中多次出现.
例如:

  ^ ^ 
 /   \
B     C
 ^   ^
  \ /
   D

在这种情况下,重要的是不要在每个引用前添加,因为您最终会在D的输出中得到A两个副本 - 这可能会导致意外结果。

项目参考注意事项

项目引用有一些您应该注意的权衡。

由于依赖项目使用从其依赖项构建的.d.ts文件,因此您必须签入某些构建输出在克隆后构建项目,然后才能在编辑器中导航项目而不会看到虚假错误。
我们正在开发一个应该能够缓解这种情况的幕后 .d.ts 生成过程,但现在我们建议通知开发人员他们应该在克隆后进行构建。

此外,为了保持与现有构建工作流程的兼容性,除非使用--build开关调用,否则tsc不会自动构建依赖项。
让我们了解更多关于--build

TypeScript 的构建模式

期待已久的功能是针对 TypeScript 项目的智能增量构建。
在 3.0 中,您可以将--build标志与tsc
这实际上是tsc一个新入口点,它的行为更像是一个构建协调器,而不是一个简单的编译器。

运行tsc --build (简称tsc -b )将执行以下操作:

  • 查找所有引用的项目
  • 检测它们是否是最新的
  • 以正确的顺序构建过时的项目

您可以为tsc -b提供多个配置文件路径(例如tsc -b src test )。
就像tsc -p ,如果名为tsconfig.json则无需指定配置文件名本身。

tsc -b命令行

您可以指定任意数量的配置文件:

 > tsc -b                                # Build the tsconfig.json in the current directory
 > tsc -b src                            # Build src/tsconfig.json
 > tsc -b foo/release.tsconfig.json bar  # Build foo/release.tsconfig.json and bar/tsconfig.json

不要担心对命令行上传递的文件进行排序 - tsc会在需要时重新排序它们,以便始终首先构建依赖项。

还有一些特定于tsc -b标志:

  • --verbose :打印详细日志以解释正在发生的事情(可以与任何其他标志结合使用)
  • --dry :显示将要完成的操作,但实际上并未构建任何内容
  • --clean :删除指定项目的输出(可以与--dry结合使用)
  • --force :就好像所有项目都过时一样
  • --watch :监视模式(不能与除--verbose之外的任何标志结合使用)

注意事项

通常, tsc会在存在语法或类型错误的情况下产生输出( .js.d.ts ),除非打开noEmitOnError
在增量构建系统中执行此操作将非常糟糕 - 如果您的过时依赖项之一出现新错误,您只会看到一次,因为后续构建将跳过构建现在最新的项目。
出于这个原因, tsc -b作用就像为所有项目启用了noEmitOnError

如果您签入任何构建输出( .js.d.ts.d.ts.map等),您可能需要在某些源代码控制后运行--force构建操作取决于您的源代码控制工具是否保留本地副本和远程副本之间的时间映射。

msbuild

如果您有 msbuild 项目,则可以通过添加来开启启用构建模式

    <TypeScriptBuildMode>true</TypeScriptBuildMode>

到您的项目文件。 这将启用自动增量构建和清理。

请注意,与tsconfig.json / -p ,不会考虑现有的 TypeScript 项目属性 - 所有设置都应使用您的 tsconfig 文件进行管理。

一些团队已经设置了基于 msbuild 的工作流,其中 tsconfig 文件与它们配对的托管项目具有相同的隐式图形排序。
如果您的解决方案是这样,您可以继续使用msbuildtsc -p以及项目引用; 这些是完全可互操作的。

指导

整体结构

有了更多的tsconfig.json文件,您通常会希望使用配置文件继承来集中您的常用编译器选项。
通过这种方式,您可以更改一个文件中的设置,而不必编辑多个文件。

另一个好的做法是拥有一个“解决方案” tsconfig.json文件,该文件仅包含所有叶节点项目的references
这提供了一个简单的入口点; 例如,在 TypeScript 存储库中,我们只需运行tsc -b src来构建所有端点,因为我们列出了src/tsconfig.json中的所有子项目
请注意,从 3.0 开始,如果tsconfig.json文件中至少有一个reference ,则files数组为空不再是错误。

您可以在 TypeScript 存储库中看到这些模式 - 请参阅src/tsconfig_base.jsonsrc/tsconfig.jsonsrc/tsc/tsconfig.json作为关键示例。

构建相关模块

通常,使用相关模块转换 repo 不需要太多。
只需在给定父文件夹的每个子目录中放置一个tsconfig.json文件,然后将reference添加到这些配置文件中以匹配程序的预期分层。
您需要将outDir设置为输出文件夹的显式子文件夹,或者将rootDir设置为所有项目文件夹的公共根目录。

构造 outFiles

使用outFile编译的布局更加灵活,因为相对路径并不重要。
要记住的一件事是,您通常希望在“最后一个”项目之前不要使用prepend - 这将缩短构建时间并减少任何给定构建所需的 I/O 量。
TypeScript 存储库本身就是一个很好的参考——我们有一些“库”项目和一些“端点”项目; “端点”项目尽可能小,并且只引入他们需要的库。

Monorepos 的结构化

TODO:进行更多实验并弄清楚这一点。 Rush 和 Lerna 似乎有不同的模型,这意味着我们的结局不同

所有147条评论

哦,我的天啊!

:+1:

是的! 这对于您提供的用例非常有意义。 让我们使用的工具更好地了解我们的代码是如何被使用的,这绝对是正确的做法。 每次我错误地 F12 进入 .d.ts 文件时,我都感觉像扼杀了一只小猫!

乔纳森,

非常感谢您的友好反馈并决定接受这个。 TypeScript 是一个非常棒的工具,这个功能将帮助许多想要将他们的中型代码库组件化的人,这些代码库无法证明依赖于严格关注点划分的大型项目(例如 Azure 门户或项目 Monacos拥有 > 100kloc 和许多独立团队的世界)。 换句话说,这将真正帮助“普通人”。 此外,其他人已经为此提出了一些建议,例如@NoelAbrahams (#2180) 和其他人,所以我不能在这里声明原创性。 这只是我一直需要的东西。

我认为你的提议很好。 与我现在已经关闭的提案 (#3394) 相比,我看到的唯一缺点是缺乏参考的后备机制。

考虑我在此处详述的以下真实场景: https :

我有一个依赖于不同项目 csproj2ts 的 TypeScript 项目 grunt-ts。 几乎没有任何在 grunt-ts 上工作的人也想在 csproj2ts 上工作,因为它的功能范围非常有限。 但是,对于像我这样的人 - 能够同时处理两个项目并进行重构/转到定义/找到它们之间的所有引用会很棒。

当我提出我的建议时,我建议依赖对象是一个具有命名回退的对象文字。 更符合您的建议的版本是:

"dependencies": {
   "csproj2ts": ["../csproj2ts","node_modules/csproj2ts/csproj2ts.d.ts"],
   "SomeRequiredLibrary": "../SomeRequiredLibraryWithNoFallback"
}

为了简化它仍然是一个数组,我建议以下替代实现 grunt-ts tsconfig.json文件的未来假设dependencies部分:

"dependencies": [
   ["../csproj2ts","node_modules/csproj2ts/csproj2ts.d.ts"],
   "../SomeRequiredLibraryWithNoFallback"
]

dependencies每个数组类型项的解析规则是:在每个项中找到的_first_ 项是使用的项,其余项被忽略。 字符串类型的项目按照 Jonathan 的提议进行处理。

这是一个实现起来稍微复杂一些的解决方案,但是它为开发人员(和库作者)提供了_远大_的灵活性。 对于不需要在 csproj2ts 上开发(因此没有../csproj2ts/tsconfig.json文件)的开发人员,依赖项将只是一个添加到编译上下文的定义文件。 对于 _do_ 拥有../csproj2ts/tsconfig.json文件的开发人员,该提案将完全按照您在上面描述的方式工作。

在上面的示例中, "../SomeRequiredLibraryWithNoFallback"将需要像在您现有的提案中一样存在,并且它的缺失将是编译器错误。

非常感谢您考虑这一点。

这里有两个问题需要我们拆分,一个是构建,另一个是语言服务支持。

对于 Build,我不认为 tsconfig 是合适的地方。 这显然是一个构建系统问题。 有了这个提议,打字稿编译器需要从事以下业务:

  • 找出依赖
  • 最新的断言检查
  • 配置管理(发布与调试)

这些显然都是构建系统的责任; 这些都是难题,并且已经有工具可以做到这一点,例如 MSBuild、grunt、gulp 等。
一旦 tsconfig 和 tsc 成为构建驱动程序,您会希望它使用所有 CPU 来构建不相关的子树,或者为每个项目设置 post 和 pre build 命令,并可能构建其他项目。 再说一次,我认为有一些构建工具擅长他们的工作,我们不需要重新创建它。

对于语言服务,
我认为工具可以了解多个 tsconfig 文件并从中进行推断并为您提供更多帮助,但这不应该影响您的构建。 我会考虑这样的事情:

"files" : [
    "file1.ts",
    {
        "path": "../projectB/out/projectB.d.ts",
         "sourceProject": "../projectB/"
     }
]

tsc 只会查看“路径”,但工具可以查看其他信息并尽可能提供帮助。

我承认问题的存在,但不认为将构建和工具混为一谈是正确的解决方案。 tsconfig.json 应该仍然是一个配置包(即响应文件的 json 替代品),而不是成为构建系统。 一个 tsconfig.json 代表单个 tsc 调用。 tsc 应保留为单个项目编译器。

VS 中的 MSBuild 项目是使用构建系统构建 IDE 功能的一个例子,现在 ppl 对它不满意,因为它太大了。

感谢您的回复,穆罕默德。 让我重述一下,看看我是否理解:

  • 您认为协调多项目构建的任务应该仍然是专用构建工具的领域。
  • 您认为此建议可能对 TypeScript 语言服务有所帮助。
  • 您认为在tsconfig.json tsc --project上运行tsc file1.ts ../project/out/project.d.ts 。 但是,在 VS 或其他具有 TypeScript 语言服务的编辑器中打开这样的项目将允许“转到定义”将开发人员带到定义功能的 _actual TypeScript 文件_(而不是projectB.d.ts的定义)

我有这个权利吗?

如果是这样,我认为这是非常公平的。 在我最初的提案 (https://github.com/Microsoft/TypeScript/issues/3394) 中,我确实说过我的想法不完整,因为它不包括从输出结果复制发出的结果的步骤在引用库中引用库在运行时期望它们的位置。 我认为您是在说“当真正需要的是语言服务支持时,为什么要半途而废”。

要在您的示例中稍微更改数据,您是否愿意支持这样的事情?

"files" : [
    "file1.ts",
    {
        "path": "externalLibraries/projectB.d.ts",
         "sourceProject": "../projectB/"
     }
]

假设当前项目将附带默认使用的 projectB 定义,但如果 projectB 的实际源可用,则将使用实际源。

@nycdotnet你总结得对; 我想创建一个松散耦合的系统,允许将不同的构建工具与不同的 IDE 混合和匹配,但仍然可以获得很好的设计时体验。

听起来很棒!

我同意@mhegazy ,实际上我认为 TypeScript 停止将自己视为“编译器”并开始将自己视为“类型检查器”和“转译器”很重要。 现在有单文件转译支持,除了在运行时/捆绑之外,我看不出有任何理由创建已编译的 JavaScript 文件。 当实际的打字稿源可用时,我也不明白为什么有必要在类型检查期间生成外部参考定义。

文件和包解析是您正在使用的构建系统(浏览器、systemjs、webpack 等)的责任,因此为了使工具工作,语言服务需要能够以与您使用的任何构建系统/平台相同的方式解析文件正在使用。 这要么意味着为每个构建系统实现一个自定义 LanguageServicesHost,要么为每个构建系统提供一个工具,在 tsconfig.json 中生成正确的映射条目。 这两个都是可以接受的。

@nycdotnet我认为使用npm link ../csproj2ts可以更好地处理多个回退路径的用例?

我同意@mhegazy 的观点,即我们应该将构建与编译器/转译器分开。 我确实喜欢在 files 部分中包含 tsconfig -> tsconfig 依赖项,假设如果该部分没有列出任何 *.ts 文件,它仍然会扫描它们。 例如

“文件”:[
{
"path": "externalLibraries/projectB.d.ts",
"sourceProject": "../projectB/"
}
]

仍将包含包含 tsconfig.json 文件的目录和子目录中的所有 ts 文件。

@dbaeumer然后您想将它放在不同的属性中,对吗? 当前,如果定义了文件,则始终使用它,我们忽略包含 *.ts 部分。

@mhegazy不一定是不同的部分,尽管它最终会使事情变得更清楚。 如果我使用 tsconfig -> tsconfig 依赖项,我想要避免的就是被迫列出所有文件。 在上面的例子中,我仍然不想列出任何 *.ts 文件来将它们输入到编译器中。

我认为这是非常需要的。 但是,我认为我们无法避免构建问题。 这并不意味着我们需要与此提案一起实施构建系统,但我们应该考虑一些在建议的配置中有效的良好指导。 正确处理并非易事(我们确实需要为 Visual Studio 解决这个问题)。

在上面的提议中(其中引用了 .d.ts 和源),它是否检测 .d.ts 是否过期(即需要重建)? 像重构/重命名这样的操作是否可以跨项目工作(即更新引用的项目源中的名称,而不仅仅是它的 .d.ts 文件,它会在下一次构建时被覆盖)? GoToDef 是否会将我带到引用项目中的原始代码(而不是整个项目的巨大 .d.ts 文件的中间)? 这些似乎意味着需要解析并在某些情况下分析引用项目的来源,在这种情况下 .d.ts 有用吗?

我们今天拥有的通用解决方案是,您将 .d.ts 作为一个项目的构建输出,然后在另一个项目中作为输入引用。 这适用于构建,因此无需更改。

问题是编辑场景。 您不想在编辑时浏览生成的文件。 我提出的解决方案是提供生成的 .d.ts 来自何处的“提示”。 然后语言服务不会加载 .d.ts,而是从提示路径加载“项目”。 这种方式 goto def 将带您到实现文件而不是 .d.ts 并且类似的错误将在不需要编译的情况下工作。

像重命名这样的操作,将从一个项目“传播”到另一个项目,类似地找到引用,都可以。

今天,完全由主机(IDE 等)来查找 tsconfig.json 文件,尽管 TS 提供了 API 来读取和解析它。 如果有多个 tsconfig.json 以分层方式放置,您会如何设想这种工作? 主机是否仍然负责解析初始文件而不是其他文件,还是主机负责解析所有 tsconfigs?

似乎在便利/惯例和灵活性之间存在权衡。

这不是从能够像#2568 中描述的那样构建 d.ts 文件(或至少具有相对导入)开始的吗?

@spion我不确定我在这里看到了依赖。 你可以有一个项目的多个输出,它没有你是一个单一的 delectation 文件。 构建系统应该能够知道这一点并将它们作为输入连接到相关项目。

@mhegazy哎呀,对不起。 再看问题,看来这和语言服务有关。 我阅读了以下内容

  • 中型项目:具有标准构建和共享组件的项目

并自动假定它与更好地支持 npm/browserify(或 webpack)构建工作流有关,其中项目的一部分是外部模块。

AFAIK 还没有办法为外部模块生成 .d.ts 文件吗? 如果是这样,语言服务可以链接导入外部模块的项目的唯一方法是在 tsconfig.json 中有这样的内容:

{ 
  "provides": "external-module-name"
}

当项目在另一个tsconfig.json被引用时,它会通知 LS

AFAIK 还没有办法为外部模块生成 .d.ts 文件吗?

我不认为这是真的。 调用tsc --m --d将生成一个声明文件,该文件本身就是一个外部模块。 解析逻辑将尝试查找 .ts,如果没有,则查找同名的 .d.ts,

@spion TypeScript 可以像@mhegazy所说的那样为外部模块生成 d.ts 文件,但这会导致定义与源文件的比例为 1:1,这与通常使用库的 TypeScript 定义的方式不同。 解决这个问题的一种方法是这个 TypeStrong 库: https :

@mhegazy抱歉,我的意思是“环境外部模块”,即如果我在 TypeScript 中编写external-module-name并从另一个模块导入它的一个类:

import {MyClass} from 'external-module-name'

没有办法让tsc生成声明'external-module-name'的相应 .d.ts 文件

@nycdotnet我知道 dts-bundle 和dts-generator但如果语言服务要知道我的另一个项目的来源,它也应该知道它提供的模块名称以便能够正确跟踪导入

此功能的状态如何? 对于中型项目来说,这似乎是一个重要的选择。 您如何使用特定的“requirejs”配置配置在不同文件夹中具有源的项目?

@llgcode请查看https://github.com/Microsoft/TypeScript/issues/5039 ,这应该在typescript@next可用。

我真的不明白用编译子项目的任务编写一个 gulpfile 有什么困难,这正是中型项目所需的。 我什至在小型项目中也这样做。 我使用 tsconfig.json 的唯一原因是为了 VS Code

首先我不使用gulp。 其次,这可能是一个大项目,您不想每次都重新编译。 但是如果你有一个很好的 gulp 解决方案,请告诉我如何做到这一点。

@llgcode好吧, gulp.task()定义任意数量的任务。 在您的任务中,您可以使用gulp.src()输入文件流,然后通过转换管道将它们输入.pipe() ,例如编译、连接、缩小、源映射、复制资产......您可以使用 Node 和 NPM 模块做任何可能的事情。
如果您必须编译多个项目,只需定义一个执行此操作的任务即可。 如果你想使用多个 tsconfig.json,gulp - typescript 支持,或者你可以只读取 json 文件。 增量构建也是可能的。 我不知道您的项目是如何构建的,如果您将它们放在不同的存储库中并使用子模块,或者其他什么。 但是 gulp 是 100% 灵活的。

好的,谢谢似乎是一个很棒的工具。 如果我需要像 require("mylibs/lib") 这样的映射,并且我的文件例如在文件夹 project/src/lib.js 中,那么完成将无法在 atom 中工作,我不知道 typescript 或 gulp 将如何解决使用“mylibs”和本地路径完成的映射/配置。 所以我认为#5039 中的这个新选项路径是这个问题的一个很好的解决方案。

@llgcode使用glob获取所有文件(包括 .d.ts 文件),请参阅https://www.npmjs.com/package/gulp-typescript#resolving -files。

我只是认为 TypeScript 在这里尝试做很多事情,它是一个转译器而不是构建工具。 管理提案中提到的“依赖项”实际上是包管理器或版本控制系统的任务,并将它们连接在一起是构建工具的任务。

@felixfbecker我不同意。 我知道的每个编译器(带类型检查)都有这样的选项。 例如:
gcc -> 包含文件
java -> 类路径和源路径
去 -> GOPATH
蟒蛇-> PYTHONPATH
编译器/转译器需要知道哪些源文件需要转译哪些源文件只是包含/lib 文件。
需要像 gulp 这样的构建工具来知道文件更改时要做什么。

同意@llgcode 。 此外,除了作为编译器公开之外,TypeScript 还作为语言服务公开,为 IDE 提供语法高亮(实际上是检测)和完成功能。 而且还需要遍历依赖树。

@llgcode @unional有效点。 另一件可能有帮助的事情是使 tsconfig.json 中的files属性接受globs,以便您可以定义要包含的所有文件夹中的所有文件。 但是我知道你来自哪里以及为什么人们可能需要多个 tsconfig.json 用于更大的项目。

AFAIK for CommonJS 这已经通过node_modulesnpm link ../path/to/other-project

一旦您开始跨项目重用库,npm link 就不会起作用。 如果您在自己的两个独立项目之间使用公共库,(以 rxjs 为例)打字稿会告诉您“Observable 不可分配给 Observable”。 这是因为包含路径跟随符号链接文件夹到两个不同的 node_modules 文件夹,尽管它们是同一个库。 变通方法导致构建 gulp 任务或本地/私有 npm 存储库,基本上回到大型项目选项。

@EricABC 那可能是因为他们使用环境外部模块声明,在这种情况下,他们还应该包括新支持的基于node_modules的 .d.ts 文件的定义。 除此之外,应该没有问题,因为 TS 类型只是在结构上检查,所以只要结构匹配,它们是否来自不同的模块或具有不同的名称都没有关系。

谢谢@spion ,只是假设它是基于文件的,看起来你会让我免于一些自我折磨。

也可能有帮助的一件事是使 tsconfig.json 中的 files 属性接受 globs ...

讨论中有include属性

问题和评论:

  • dependencies应该允许完整的 tsconfig.json 路径,因为tsc 允许它
  • files已经存在并且很好时,为什么要引入一个新关键字( dependencies )?
    例子:
{
    "compilerOptions": {
        // ...
    },
    "files": [
        "../common/tsconfig.json", // <== takes the `files` part of the tsconfig.json
        "../common/tsconfig.util.json", // <==
        "core.ts",
        "sys.ts"
    ]
}
  • 如果依赖项 tsconfig.json 也指定了compilerOptions什么?


让我们更进一步/狂野:-) 并可能允许(在未来) compilerOptionsexclude ... 引用另一个 tsconfig.json:

// File app/tsconfig.json
{
    "compilerOptions": "../common/tsconfig.compilerOptions.json",
    "files": [
        "../common/tsconfig.json",
        "../common/tsconfig.util.json",
        "core.ts",
        "sys.ts"
    ],
    "exclude": "../common/exclude.json"
}

// File ../common/tsconfig.compilerOptions.json
{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true
    }
}

// File ../common/exclude.json
{
    "exclude": [
        "node_modules",
        "wwwroot"
    ]
}

// File ../common/tsconfig.util.json
{
    "files": [
        "foo.ts",
        "bar.ts"
    ]
}

你得到了逻辑: files , compilerOptions , exclude ... 可以引用其他 tsconfig.json 文件,它只会从其他 tsconfig 中“获取”匹配的关键字部分.json 文件 => 简单且可扩展。 因此,您可以根据需要将 tsconfig.json 拆分为多个文件并重新使用它们。

阅读您讨论的一部分,最重要的是让这个“语言服务”/goto 定义正确。 JavaScript 调试器使用 sourceMaps。 现在,如果 tsc 不仅在 .js 中而且还在 .d.ts 文件中生成 sourceMap 数据......

除此之外,我真的没有看到从 tsconfig.json 文件中触发子项目的构建有什么好处。 如果您需要这种基本的构建时依赖项,一个简单的 shell 脚本就可以完成这项工作。 另一方面,如果您需要智能增量构建,则提出的方法似乎太简单了。 在许多情况下,tsc 最终只是构建步骤之一。 在 tsconfig.json 中为 tsc 编写依赖项有多奇怪,而其余部分则为其他一些文件? 同样,对于简单的事情,tsc 是 shell 脚本可以执行的唯一构建步骤。

无论如何,如何像在 .js 文件中一样在 .d.ts 文件中生成源映射?

我们简单地使用节点模块 + npm 链接,唯一不起作用的是 moduleResolution: node 与 ES6 模块不兼容,这启用了内联/摇树优化(另请参见 #11103 )

不要偏离主题,但在某些方面,这似乎与在本地处理多个 Node 包项目的挑战并行。 我认为这并不像使用“npm 链接”那么简单。 它们可能有不同的构建脚本,您需要以正确的顺序运行所有脚本,增量执行更难,使用监视模式更难,调试更难,源映射更难解析。 根据您选择的编辑器,这可能更具挑战性。

通常我只是放弃,把它全部放在一个巨大的项目中,然后在它们稳定后将其拆分成单独的包,但这只会有所帮助,因为面临的挑战不那么频繁了。 我觉得整个经历真的非常烦人。 我错过了什么吗?

所以,不管结果如何,我只希望我最终可以为整个开发体验找到一个优雅的解决方案。

只是说这对我们的日常工作非常有用!

由于 Web 开发的性质,我们有多个 ts 项目,每个项目包含多个 ts 文件,这些文件被编译成单个 js 文件( --outFile )。 这些要么是类似应用程序的项目(它们执行特定的事情或添加特定功能),要么是类似库的项目(应用程序的可重用代码)。 通常我们同时处理多个这些 ts 项目,增强库以促进应用程序的开发。 但是我不认为我的任何开发人员在任何时候都在他们的本地环境中拥有我们所有的 ts 项目。

为了改进我们的工作流程,我们目前的选择是

  • 把所有东西都扔进 1 ts 项目中

    • 我们可以直接引用 .ts 文件,这比使用 d.ts 文件要好得多

    • 但是我们需要一个外部工具来映射和连接文件集,因为我们需要限制通过互联网请求的 js 文件,同时保持应用程序的模块化(特定于功能或页面)。

    • 如前所述,并非所有人都始终需要所有这些 ts 项目,因此这会给文件系统增加很多膨胀。 从事项目 X 的​​人可能需要项目 A、B 和 D,但从事项目 Y 的人可能需要 A 和 C。

  • 保持项目分离(我们的现状)

    • 我们需要从其他 ts 项目中引用编译的 d.ts 文件,因为直接包含 .ts 文件会将其添加到输出中。 如果我们可以直接 f12 进入我们自己的源代码,速度会快很多。

    • 我们需要添加工具/脚本来同时编译多个这些项目。 目前,我们要么从多个终端启动tsc -d -w命令,要么启动一个脚本,为找到 tsconfig 的所有子目录执行此操作。

    • 我知道其他工具可以帮助解决这个问题(例如 gulp),或者我们可以用 tsconfigs 中的文件 glob 来解决问题,但这对 d.ts 文件的第一个问题没有帮助。

我们的项目不够大,无法将库的开发与应用程序严格分开,但也不够小,以至于我们可以简单地将所有内容放在一起。 我们发现自己在打字稿方面缺乏优雅的选择。

如果dependencies可以成为两全其美的选择,那就太棒了; 的功能标记到所有依赖项的 ts 文件,但根据该依赖项自己的 tsconfig 编译输出。

是否有关于此主题的任何更新。 我们如何拥有多个相互独立编译的打字稿项目? 我们如何在 tsconfig.json 中描述这样的依赖?

这现在包含在未来部分的路线图中,标题为“支持项目引用”。 所以,我猜一个.tsconfig文件将能够链接另一个.tsconfig文件作为依赖项。

是否有关于此主题的任何更新。 我们如何拥有多个相互独立编译的打字稿项目? 我们如何在 tsconfig.json 中描述这样的依赖?

构建依赖应该在你的构建系统中编码。 构建系统,如 gulp、grunt、broccoli、msbuild、basal 等,都是为处理此类情况而构建的。
对于类型信息,一个项目的输出应该包括一个 .d.ts 并且应该作为输入传递给另一个。

@mhegazy我们的项目是这样工作的。 我们在 lerna monorepo 中有许多包,每个包在 package.json 中有自己的依赖项,并且在tsconfig.json"types"属性中具有相同的依赖项。 每个项目都用--outFile编译(这是一个旧项目,还没有转移到 ES 模块),并且"typings" package.json键指向捆绑的.d.ts文件。

我们使用 gulp 进行捆绑/观看。

它在大多数情况下有效,但存在一些问题:

  • 由于#15488 和#15487 之类的问题,我们需要有一个显式链接才能使引用正常工作。
  • Go-to-definition 将带您到一个捆绑的.d.ts文件。 理想情况下,这会将您带到另一个项目中的源代码。
  • 进行完整构建的最快方法是lerna run build --sort (在每个目录中实际上是tsc ),它有额外的开销,因为它会为每个包生成一个 TypeScript 编译器进程,执行大量重复工作.

我正在密切关注这个问题,因为我们也处于其他人描述的相同情况下。
多个“项目”,每个项目都有其 tsconfig.json 文件。

我们的构建过程就像@mhegazy指出的那样:每个项目发出一个.d.ts文件,用作依赖项目的输入。

真正的问题是 IDE 支持:在查找引用时,它们只能在单个tsconfig.json的范围内找到。 更糟糕的是,更改文件的级联效果不会跨项目传播,因为tsconfig.json范围之外的依赖文件不会重新编译。 这对我们项目的维护非常不利,有时会导致可能在 IDE 中捕获的构建错误。

It's happening

哦,我的天啊

我很想拥有一个更新的场景,其中涉及 React 组件。 我们有一个组件存储库,其中包含 JSX 模块(原子、分子和有机体),这些模块呈现适用于我们公司所有应用程序的 UI 组件。 所有前端开发人员在处理各自的应用程序时都使用此组件存储库。 如果我能拥有 TypeScript 语言服务体验,让我能够编辑特定应用程序的 UI 并“转到定义”到通用 UI 组件存储库中,那就太好了。 今天我们必须单独捆绑这些组件并复制它们。 这是我希望修复的“制作自己的项目管道”问题(在 .NET 世界中有一个很好的故事,其中有一个解决方案下的项目)。

项目参考:TypeScript 的内置可扩展性

介绍

TypeScript 已扩展到数十万行的项目,但本身并不支持这种扩展作为内置行为。 团队已经开发了不同效率的变通方法,并且没有一种标准化的方式来表示大型项目。 虽然随着时间的推移,我们已经极大地提高了类型检查器的性能,但在 TS 可以合理获得多快的速度方面仍然存在硬限制,
以及诸如 32 位地址空间之类的限制,这些限制会阻止语言服务在交互式场景中“无限”扩展。

直观地说,更改前端组件中的一行 JSX应该需要对 500,000 LOC 项目的整个核心业务逻辑组件进行重新类型检查。 项目引用的目标是为开发人员提供工具来他们的代码划分为更小的块。 通过使工具能够一次处理较小的工作块,我们可以提高响应能力并收紧核心开发循环。

我们相信,就性能或内存消耗方面的大幅改进而言,我们当前的架构几乎没有剩余的“免费午餐”。 相反,这种分区是一种显式的权衡,它以牺牲一些前期工作为代价来提高速度。 开发人员将不得不花一些时间来推理他们系统的依赖关系图,并且在我们进一步增强工具之前,某些交互功能(例如跨项目重命名)可能不可用。

我们将确定该系统强加的关键约束,并为项目规模、目录结构和构建模式建立指导方针。

场景

需要考虑三种主要情况。

相关模块

一些项目广泛使用相对导入。 这些导入明确解析为磁盘上的另一个文件。 像../../core/utils/otherMod这样的路径很常见,尽管在这些存储库中通常更喜欢扁平的目录结构。

例子

以下是可汗学院 perseus 项目的一个例子:

改编自https://github.com/Khan/perseus/blob/master/src/components/graph.jsx

const Util = require("../util.js");
const GraphUtils = require("../util/graph-utils.js");
const {interactiveSizes} = require("../styles/constants.js");
const SvgImage = require("../components/svg-image.jsx");

观察

虽然目录结构暗示了项目结构,但它不一定是确定的。 在上面的可汗学院示例中,will 可以推断utilstylescomponents可能是他们自己的项目。 但也有可能这些目录非常小,实际上会被分组到一个构建单元中。

单一回购

Mono-repo 由许多通过非相对路径导入的模块组成。 从子模块(例如import * as C from 'core/thing )导入可能很常见。 通常,但并非总是如此,每个根模块实际上都发布在 NPM 上。

例子

改编自https://github.com/angular/angular/blob/master/packages/forms/src/validators.ts

import {InjectionToken, ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core';
import {forkJoin} from 'rxjs/observable/forkJoin';
import {map} from 'rxjs/operator/map';
import {AbstractControl, FormControl} from './model';

观察

划分的单位不一定是模块名称的开头部分。 例如, rxjs实际上单独编译它的子部分( observableoperator ),就像任何作用域包(例如@angular/core )一样。

外档

TypeScript 可以将其输入文件连接成单个输出 JavaScript 文件。 参考指令或 tsconfig.json 中的文件排序为结果文件创建确定性输出顺序。 这很少用于新项目,但在旧代码库(包括 TypeScript 本身)中仍然流行。

例子

https://github.com/Microsoft/TypeScript/blob/master/src/compiler/tsc.ts

/// <reference path="program.ts"/>
/// <reference path="watch.ts"/>
/// <reference path="commandLineParser.ts"/>

https://github.com/Microsoft/TypeScript/blob/master/src/harness/unittests/customTransforms.ts

/// <reference path="..\..\compiler\emitter.ts" />
/// <reference path="..\harness.ts" />

观察

一些使用此配置的解决方案将通过单独的script标签(或等效标签)加载每个outFile ,但其他解决方案(例如 TypeScript 本身)需要连接先前的文件,因为它们正在构建整体输出.

项目参考:一个新的隔离单元

与实际项目交互的一些重要观察:

  • 在检查低于 50,000 LOC 的实现(非 .d.ts)代码的项目时,TypeScript 通常是“快速的”(< 5-10 秒)
  • .d.ts 文件,尤其是在skipLibCheck ,在类型检查和内存成本方面几乎是“免费的”
  • 几乎所有的软件都可以细分为小于 50,000 LOC 的组件
  • 几乎所有大型项目都已经通过目录对其文件进行了一些结构化,以产生中等大小的子组件
  • 大多数编辑发生在不需要重新检查或重新发送整个解决方案的叶节点或近叶节点组件中

把这些放在一起,如果有可能一次只对一个 50,000 LOC 的实现代码块进行类型检查,那么在交互式场景中几乎不会有“慢”交互,而且我们几乎永远不会耗尽内存。

我们引入了一个新概念,即项目引用,它在两个 TypeScript 编译单元之间声明了一种新的依赖关系,其中检查依赖单元的实现代码; 相反,我们只是从一个确定的位置加载它的.d.ts输出。

句法

一个新的references选项(TODO:Bikeshed!)被添加到tsconfig.json

{
  "extends": "../tsproject.json",
  "compilerOptions": {
    "outDir": "../bin",
    "references": [
      { "path": "../otherProject" }
    ]
  }
}

references数组指定要从此项目引用的一组其他项目。
每个references对象的path指向一个tsconfig.json文件或包含tsconfig.json文件的文件夹。
当我们发现他们的需要时,可能会向这个对象添加其他选项。

语义

项目引用改变了以下行为:

  • 当模块解析将解析为项目rootDir子目录中的.ts文件时,它解析为该项目outDir.d.ts文件

    • 如果此解决方案失败,我们可能会检测到它并发出更智能的错误,例如Referenced project "../otherProject" is not built而不是简单的“找不到文件”

  • 没有别的(TODO:到目前为止?)

参考项目的执行限制

为了有意义地提高构建性能,我们需要确保在看到项目引用时限制 TypeScript 的行为。

具体来说,以下几点应该是正确的:

  • 永远不要读取或解析引​​用项目的输入 .ts 文件
  • 应从磁盘读取引用项目的tsconfig.json
  • 截至到最新的检查应该要求违反上述约束

为了遵守这些承诺,我们需要对您引用的项目施加一些限制。

  • declaration自动设置为true 。 尝试覆盖此设置是错误的
  • rootDir默认为"." (包含tsconfig.json文件的目录),而不是从输入文件集推断
  • 如果提供了files数组,则必须提供所有输入文件的名称

    • 例外:作为类型引用的一部分包含的文件(例如node_modules/@types )不需要指定

  • 任何被引用的项目本身都必须有一个references数组(可能为空)。

为什么是"declaration": true

项目引用通过使用声明文件 (.d.ts) 代替其实现文件 (.ts) 来提高构建速度。
因此,很自然地,任何引用的项目都必须启用declaration设置。
这由"project": true暗示

为什么要修改rootDir

rootDir控制输入​​文件如何映射到输出文件名。 TypeScript 的默认行为是计算输入文件完整图的公共源目录。 例如,输入文件集["src/a.ts", "src/b.ts"]将产生输出文件["a.js", "b.js"]
但是输入文件集["src/a.ts", "b.ts"]将产生输出文件["src/a.js", "b.js"]

计算输入文件集需要递归解析每个根文件及其所有引用,
这在大型项目中是昂贵的。 但是我们今天无法改变这种行为,而不会以一种糟糕的方式破坏现有项目,所以这种改变只会在提供references数组时发生。

无圆度

自然,项目可能不会形成具有任何循环性的图形。 (待办事项:除了构建编排噩梦之外,这实际上会导致什么问题?)如果发生这种情况,您将看到一条错误消息,指示已形成的循环路径:

TS6187: Project references may not form a circular graph. Cycle detected:
    C:/github/project-references-demo/core/tsconfig.json ->
    C:/github/project-references-demo/zoo/tsconfig.json ->
    C:/github/project-references-demo/animals/tsconfig.json ->
    C:/github/project-references-demo/core/tsconfig.json

tsbuild

该提案有意在“真实”构建系统中使用它时含糊其辞。 很少有项目超过 50,000 LOC“快速”边界而不引入tsc以外的东西来编译 .ts 代码。

“你不能构建foo因为bar还没有构建”的用户场景是一个明显的“去取那个”类型的任务,计算机应该处理,而不是而不是开发者的心理负担。

我们希望像gulpwebpack等工具(或它们各自的 TS 插件)能够理解项目引用并正确处理这些构建依赖项,包括最新检查。

为了确保这是可能的,我们将为 TypeScript 构建编排工具提供一个参考实现,该工具演示以下行为:

  • 快速更新检查
  • 项目图排序
  • 构建并行化
  • (待办事项:其他人?)

此工具应仅使用公共 API,并有详细记录以帮助构建工具作者了解实现项目引用的正确方法。

去做

填写部分以完全完成此提案

  • 您将如何转换现有项目

    • 基本上只需放入tsconfig.json文件,然后添加修复构建错误所需的引用

  • baseUrl

    • 使实施变得困难,但实际上对最终用户没有影响

  • 嵌套项目的简要讨论(TL;DR 它需要被允许)
  • 在不破坏消费者的情况下,将“太大”的项目细分为更小的项目的大纲场景
  • 找出勒纳场景

    • 可用数据点 (N = 1) 表示他们不需要这个,因为他们的构建已经以这种方式有效构建

    • 查找更多示例或反例以更好地了解人们是如何做到这一点的

  • 对于通过 webpack/babel/rollup 等管道传输 JS 的人,我们是否需要dtsEmitOnly设置?

    • 也许references + noEmit暗示了这一点

极好的!

找出勒纳场景

  • 可用数据点 (N = 1) 表示他们不需要这个,因为他们的构建已经以这种方式有效构建

“this”是指提案还是参考构建实现? 虽然您可以使用 lerna 进行构建(我的团队这样做),但如果 TS(或根据此提案构建的工具)自行处理,它会变得粗糙并且效率更高。

TODO 部分是整个提案的 TODO

好的!

任何被引用的项目本身都必须有一个引用数组(可能为空)。

这真的有必要吗? 如果这样的包有.d.ts文件还不够吗?
(在这种情况下,甚至可能不需要tsconfig.json ?)

我的用例:考虑一个不使用outDir的(例如第三方)项目,所以.ts.js.d.ts将在旁边彼此,并且 TS 当前将尝试编译.ts而不是使用.d.ts

我不使用outDir的原因是更容易允许import "package/subthing"样式的导入,否则必须是import "package/dist/subthing"outDir: "dist"
并且能够直接使用 NPM 包或其源存储库(例如使用npm link )。

(如果package.json允许在main指定目录会很有帮助,但是唉......)

对于通过 webpack/babel/rollup 等管道传输 JS 的人,我们是否需要一个 dtsEmitOnly 设置?

绝对地! 这是目前一个很大的缺失部分。 目前,您可以在使用outFile时获得单个 d.ts 文件,但是当您切换到模块并使用捆绑器时,您会丢失它。 能够为模块的入口点(使用export as namespace MyLib )发出单个 d.ts 文件将是惊人的。 我知道外部工具可以做到这一点,但如果它集成到发射器和语言服务中,那就太好了。

这真的有必要吗? 如果这样的包有 .d.ts 文件还不够吗?

我们需要目标 tsconfig 中的一些东西告诉我们期望输出文件的位置。 此提案的先前版本具有“您必须明确指定rootDir ”,这相当麻烦(您必须在每个 tsconfig 中写入"rootDir": "." )。 因为我们想要翻转这个世界上的各种行为,如果你有一个引用数组并且它是关键的东西,那么只说你得到“项目”行为更有意义,而不是指定一堆您必须明确声明的标志。

该提案将与我们已经构建的 TypeScript 项目的方式密切相关。 我们细分为更小的单元,每个单元都有一个 tsconfig.json 并通过 gulp 独立构建。 项目通过引用 d.ts 文件相互引用。

在理想情况下,不需要预先构建引用的项目。 IE。 TypeScript 对引用的项目进行“构建”,并在语言服务的内存中维护“d.ts”等效项。 这将允许在“源”项目中所做的更改显示在“依赖”项目中,而无需重建。

我们需要目标 tsconfig 中的一些东西告诉我们期望输出文件的位置。

只有在使用outDir时才如此,不是吗?

如:如果我有一个 tsconfig :

  • 不使用outDir (但确实有declaration: true ,当然),那么我们不需要rootDir ,也不需要references
  • 确实有outDir ,那么您需要设置references和/或rootDir (和declaration: true

询问的原因是我可以通过引用它来为任何 TS 包启用“项目模式”,即它在我的控制之下。

在这种情况下,如果它在找到它正在寻找的 .d.ts 文件时也能正常工作(即,如果没有 .ts 文件或 tsconfig 文件,则不会抱怨)也很好。 因为这将启用另一种情况,即在必要时用其源版本“替换”NPM 版本(可能只有 .d.ts 文件)。

例如,考虑 NPM 包 MyApp 和 SomeLib。
SomeLib 可以有 tsconfig: declaration: true

存储库如:

package.json
tsconfig.json
index.ts
sub.ts

编译后,变成:

package.json
tsconfig.json
index.ts
index.d.ts
index.js
sub.ts
sub.d.ts
sub.js

这种结构使例如

// somewhere in MyApp
import something from "SomeLib/sub";

在已发布的 NPM 包中,我目前总是必须剥离 .ts 文件,否则如果 MyApp 使用 SomeLib,所有源都将被 TS 重新编译:

所以,在 NPM 上,这变成了:

package.json
index.d.ts
index.js
sub.d.ts
sub.js

现在,如果我将references: ["SomeLib"]放在 MyApp 的 tsconfig 中,如果它对NPM 版本和 SomeLib 的源版本都“按原样”工作会很好,即它不会抱怨例如缺少 tsconfig,只要它确实在正确的位置找到了sub.d.ts


相关但不同的问题:

我现在意识到,如果SomeLibreferences放在他的 tsconfig 中,这将允许将来发布带有 .ts 文件的 NPM 包。 但是,我想当任何依赖包没有明确地将references: ["SomeLib"]放在它们的 tsconfig 中时,TS 仍然会总是重新编译这些。

或者当import 'ing 它(即不是references 'ing 它)时,MyLib 中的references也会自动引入“项目边界”?

IIRC,最初的想法之一是,如果一个模块例如通过node_modules ,那么.d.ts文件将优先于.ts文件,但后来又改回来了,因为启发式(“通过node_modules ”)一般来说问题太大。 可能有一个明确的“项目边界”可以解决这个问题(例如projectRoot: true ,而不是references ,或者除了

对于 lerna 案例,我希望有一个更简单的解决方案。

  1. 在单个包的目录中,包应该知道的关于monorepo任何结构。 个人tsconfig JSON文件不应包含任何引用。

    • 这允许您将单个包拆分到单独的存储库中,并且只需使用主存储库工具克隆它们,例如 ProseMirror: https :

  2. 在根“工作区”存储库(可以包含所有代码,但也可以简单地克隆其他存储库)中,在其 tsconfig.json 中有引用

    • 这样做只是为了让工具可以识别并跟踪对源的引用,而不是进入 .d.ts 文件。

我的全部担忧是,一旦您使用相对降序"../xx"路径向各个项目配置文件添加引用,它们就不再可用作独立模块 - 它们必须位于特定的工作区结构中。

添加“工作区”的新概念 tsconfig.json 解决了这个问题。 这样,如果您例如“git clone”单个包,以正常方式安装其依赖项(例如使用 npm 或 yarn)应该让您单独处理它,因为编译的依赖项会引入它们的定义文件。 如果您克隆整个工作区并运行命令以引入所有包,则工作区配置将允许您浏览所有源。

请注意,工作区tsconfig.json也与 Yarn 的工作区package.json完美对齐https://yarnpkg.com/lang/en/docs/workspaces/

我在这里做了一点概念证明

https://github.com/spion/typescript-workspace-plugin

只需将插件添加到各个存储库的所有tsconfig.json文件中

{
  "plugins": [{"name": "typescript-workspace-plugin"}]
}

然后在顶层 package.json 和 yarn 的“workspaces”条目旁边,添加一个“workspace-sources”条目:

{
  "workspaces": ["packages/*"],
  "workspace-sources": {
    "*": ["packages/*/src"]
  }
}

该字段的工作原理与 tsconfig.json 中的“paths”字段非常相似,但它仅影响单个项目的语言服务,将它们指向包源。 恢复正确的“转到定义/类型”功能和类似功能。

仅当使用 outDir 时才如此,不是吗?

正确的。 我们假设几乎每个拥有大型项目的人都在使用outDir 。 我很想听听没有的项目

现在,如果我在 MyApp 的 tsconfig 中添加引用:["SomeLib"],如果它对 NPM 版本和 SomeLib 的源版本都“按原样”工作,那就太好了

大粉丝,我很喜欢这个主意。 我需要考虑它是否真的需要。

这里的一个警告是,我认为包作者需要 a) 在 TS 找到它们的地方同时发布 .ts 和 tsconfig 文件,或者 b)两者都不发布并且只有 .d.ts 文件可访问。 在 (a) 的情况下,我们会递归地跟踪项目引用,正确的事情就会发生,而在 (b) 中,我们不会爬到错误的地方。

或者,MyLib 中的引用是否还打算在导入时自动引入“项目边界”(即不引用它)?

@mhegazy 交谈,我们认为实际上有一个关于项目如何引用行为的简单模型:具有references数组的任何项目永远不会“看到”项目文件夹之外的.ts文件 - 这包括文件在exclude d 目录下。 仅此更改就使 lerna 场景以及其他场景开箱即用(“工作”意味着“模块引用始终解析为 .d.ts”)。

我需要更多地查看“工作区”模型。

仅当使用 outDir 时才如此,不是吗?

正确的。 我们假设几乎每个拥有大型项目的人都在使用 outDir。 我很想听听没有的项目

我们在 Visual Studio 解决方案中有 67 个 TS 项目,它们在编译时没有使用outdir和 postbuild grunttasks 来创建输出目录结构(以及 uglify 和其他后处理)。

大多数项目都有这样的 tsconfig.json

 "include": [
    "../baseProj/Lib/jquery.d.ts",
    "../baseProj/baseProj.d.ts"
  ]

我花了一些时间通读了参考建议,并更正了 - AFAICT lerna 和 yarn 工作区用户不需要这里提出的任何工作区功能:

  1. 我们已经有一个基于 package.json 的依赖图,所以我们知道运行构建的顺序。 事实上,lerna 有一个通用的 run 命令可以按顺序运行它,并且编写一个在适用的情况下也添加并行性的工具并不难。 skipLibCheck应该使性能影响可以忽略不计,但我还没有检查过。
  2. Lerna 和 Yarn 已经在适当的 node_modules 位置创建了指向其他模块的符号链接。 因此,所有依赖项都能够跟随另一个模块的 package.json,读取 types/typings 字段并找到引用的 module.d.ts 类型定义文件。

我们没有的,以及我编写的插件提供的,是一种同时加载所有源的方法。 当我需要同时对两个或更多模块进行更改时,我不希望“转到定义”和“转到类型定义”将我发送到 .d.ts 文件。 我希望它将我发送到原始源代码位置,以便我可以对其进行编辑。 否则,我只会加载单个项目目录,而由 lerna/yarn 创建的 node_modules 符号链接将正常工作。

对我们来说也是一样。 我们没有使用 Lerna,而是使用Rush来计算我们的依赖图,但效果是一样的。 当我们构建项目时, tsc只是需要运行的众多任务之一。 我们的编译器选项是由更大的构建系统计算的,我们正在转向一个模型,其中tsconfig.json不是输入文件,而是生成的输出(主要是为了 VS Code)。

我们没有的,以及我编写的插件提供的,是一种同时加载所有源的方法。 当我需要同时对两个或更多模块进行更改时,我不希望“转到定义”和“转到类型定义”将我发送到 .d.ts 文件。 我希望它将我发送到原始源代码位置,以便我可以对其进行编辑。

+1 这太棒了。

如果我们梦想更好的多项目支持,我的第一个请求将是编译器服务,类似于 VS Code IntelliSense 的工作原理。 如果 Rush 可以调用tsc 100 次而不必启动编译器引擎 100 次,我们的构建速度会明显加快。 编译器是我们最昂贵的构建步骤之一。 在 monorepo 中,构建时间非常重要。

@iclanton @nickpape-msft @qz2017

是的,请!

我认为项目系统最有用的结果之一是如果
“转到定义”转到源文件而不是 d.ts 文件,并且
通过项目引用树向下搜索“查找所有引用”。

据推测,这也将解锁“全局重命名”类型重构。

2017 年 11 月 9 日星期四晚上 9:30 Salvatore Previti通知@github.com
写道:

是的,请!


您收到此消息是因为您订阅了此线程。
直接回复本邮件,在GitHub上查看
https://github.com/Microsoft/TypeScript/issues/3469#issuecomment-343356868
或静音线程
https://github.com/notifications/unsubscribe-auth/AANX6d19Zz7TCd_GsP7Kzb-9XJAisG6Hks5s07VXgaJpZM4E-oPT
.

@mhegazy 交谈,我们认为实际上有一个关于项目引用行为的简单模型:任何具有引用数组的项目永远不会“看到”项目文件夹之外的 .ts 文件 - 这包括排除目录下的文件。

很好,在那种情况下,为什么必须指定任何特定的引用?
看起来有一个标志就足够了(比如projectRoot: true )。
例如, references: ["foo"]references: []之间的区别是什么?
因为如果我import "foo"import "bar" ,两者都会忽略任何.ts文件。

因此,在这种情况下,提案变为:

鉴于此 tsconfig.json( projectRoot上的 TODO 自行车棚):

{
  "extends": "../tsproject.json",
  "compilerOptions": {
    "projectRoot": true
   }
}

tsc然后需要解析项目文件夹之外的内容(包括排除目录下的文件)时,它只会查看.d.ts文件(或者,它可能只是 _prefer_ .d.ts文件,并回退到tsconfig和/或.ts如果它只看到那个)。

这使得编译过程中的解析快速而简单。
它适用于 monorepo 引用(即import "../foo" )和基于包的引用(即import "foo" )。
它适用于 NPM 包及其源代码表示。
它消除了在编译期间解析tsconfig.json机器的需要,尽管如果找不到.d.ts的错误消息将不太有用。

这听起来好得令人难以置信,如果确实如此简单,那么我可能忽略了一些重要的事情:)


正如其他人也指出的那样,“IntelliSense”确实继续使用 .ts 文件仍然非常重要。

因此,如果随后发出“转到定义”、“查找引用”等的请求,它应该使用一些反向映射从它使用的.d.ts文件中定位相应的.ts文件,以便远的。

这种映射可以使用例如:

  • .d.ts使用嵌入的注释字符串,如//# sourceURL = ../src/foo.ts .d.ts

    • 可以使用更精细的源映射将“卷起”的.d.ts映射回原始.ts

  • 解析.js文件,并使用其源映射定位`.ts

这确实引入了在.ts更改时重建.d.ts的主题,但我不确定该提案是否应该解决此问题。 例如,今天已经是这样,需要一些过程来重建.js文件,即使是从根项目本身。 所以我想可以安全地假设,如果存在,也会有一些东西来重建依赖关系。 可能是每个包的一堆并行tsc ,可能是tsbuild ,可能是具有类似compileOnSave行为的智能 IDE,等等。

@pgonzal @michaelaird https://github.com/spion/typescript-workspace-plugin仅此而已 - 它恢复定义并找到多 tsconfig yarn/lerna/etc 工作区项目的所有引用。

有趣...我想知道我们是否可以使用 Rush 来完成这项工作。 我们来看看。

仅供参考,作为这项工作的一部分,我们构建的另一个简单工具是wsrun

lerna run类似,它为工作区中的所有包运行一个命令。

由于需要按顺序编译打字稿依赖项,wsrun 能够根据其package.json依赖项以拓扑顺序运行包命令。 它支持构建期间的并行性。 它不是最佳并行,但可以稍后改进。

请注意, oao是另一个用于纱线的 monorepo 工具。 它最近还增加了对命令的“拓扑”排序的支持。

我只想在这里发布“重构”这个词,因为它对我们来说是一个重要的目标,尽管可能与当前的提案(https://github.com/Microsoft/TypeScript/issues/3469#issuecomment-341317069)和在这个问题中没有经常提到。

我们的用例是一个包含多个 TS 项目的 monorepo,它基本上可以简化为common-library加上几个应用程序: app1app2 。 在 IDE 中工作时,应将它们视为单个项目,例如,重命名重构应适用于所有三个模块,但app1app2也是两个独立的构建目标。 虽然我同意构建通常是一个单独的问题,但事实是我们的应用程序非常小,执行cd app1 && tsc事情对我们来说完全没问题。

如果 TypeScript 提出了一个很好的方法来支持这一点,那就太棒了。

对于 monorepo 跨项目重构/引用,如果您在 vscode 中工作,我发现此设置对我有用:

根 tsconfig:

"compilerOptions": {
   "baseUrl": ".",
   // global types are different per project
   "types": [],
   "paths": {
      "lib": ["packages/lib/src"],
      "xyz1": ["packages/xyz1/src"],
      "xyz2": ["packages/xyz2/src"],
   }
},
"include": ["./stub.ts"], // empty file with export {} to stop vscode complaining about no input files
"exclude": ["node_modules"]

包/xyz1/tsconfig.json

{
  "extends": "../../tsconfig",
   "compilerOptions": {
      "types": ["node"],
   },
   "include": ["src/**/*"]
}

包/xyz2/tsconfig.json

{
  "extends": "../../tsconfig",
   "compilerOptions": {
      "types": ["webpack-env"]
   },
  "include": ["src/**/*"]
} 

包/lib/tsconfig.json

{
   "extends": "../../tsconfig",
    "compilerOptions": { ... },
    "include": ["src/**/*"],
    // special file to load referenced projects when inside in lib package, without it they won't be 
    // visible until you open some file in these projects 
    "files": ["./references.ts"],
}

包/lib/tsconfig-build.json

{
   "extends": "./tsconfig",
    // exclude referenced projects when building
   "files": []
}

包/lib/references.ts

import "xyz1";
import "xyz2";

export {};

您需要正确的main属性在包的package.json ,即使对于没有 lib 的包,也可能是types ,例如:

  "main": "src/main.tsx",
  "types": "src/main.tsx",

通过这种方式重构/重命名lib某些内容也会重构xyz1xyz2引用。 通过这种方式,项目也可以有不同的全局变量/目标库。

在 Gradle,他们简单地称之为 -复合构建

顺便说一句,如果我想为 TypeScript 编译器做出贡献,我可以开始吗? 我已经克隆了 repo,这不是一件小事(我过去常常阅读源代码:angular、ionic、express,当我无法从文档中获取它或远离互联网时......)我真的需要一个人的帮助请指点我要走的路。

谢谢!
TypeScript 的美好未来。

我有一个原型,如果人们使用相对模块路径,我希望他们尝试。 https://github.com/RyanCavanaugh/project-references-demo上有一个示例存储库,其中概述了基本场景并展示了它的工作原理。

在本地尝试:

git clone https://github.com/RyanCavanaugh/TypeScript
git checkout pr-lkg
npm install
npm run build
npm link

然后在你的另一个文件夹中:

npm link typescript

随着事情的变化,我将保持pr-lkg标签指向最新的工作提交。 接下来是我的待办事项:

  • [x] 未构建依赖项目时添加更好的错误消息
  • [ ] 将重定向行为应用于非相关模块路径
  • [] 公开一个 API 供构建工具使用

@RyanCavanaugh由于缺少del模块或Error: Cannot find module 'C:\github\TypeScript\built\local\tsc.js' (可能是您的本地路径?)等错误,我无法真正构建它,但总的来说,结构看起来很棒。

这是tsc -only 还是它还会考虑使用语言服务器在 VSCode 中进行项目范围的重构?

...也缺少pr-lkg标签。

标签在那里(它不是一个分支)。 见https://github.com/RyanCavanaugh/TypeScript/tree/pr-lkg

tsconfig.jsonreferences是选择加入每个依赖项,将它应用于rootDir之外解析的所有内容不是更有意义吗?

我正在想象一个类似于sandbox属性的东西,它看起来像:

# tsconfig.json
{
  "compilerOptions": {
    "outDir": "lib",
    "sandbox": "."
  },
  "include": ["src/index.ts"]
}

Sandbox 也会将rootDir设置为相同的值。 不是显式提供包含tsconfig.json目录的路径,而是应用正常的模块解析,您可以搜索 FS 树以自动找到tsconfig.json

# package.json
{
  "name": "animals",
  "module": "src",
  "typings": "lib",
  "dependencies": {
    "core": "*"
  }
}

这边走:

  • 不必维护两个同步列表( referencesdependencies )。
  • 包对自身以外的文件系统一无所知。
  • 使用节点模块解析策略而不是自定义路径。

@RyanCavanaugh ,据我所知,我只能在本地进行这些更改,我将无法发送此类项目进行测试,例如,在 travis-ci.org 上。 对?

今天与@billti / @mhegazy 开会的笔记


  • Find References / Rename 必须工作,至少对于确定项目关闭的必要编译上下文适合内存的情况
  • 解决方案范围的重命名将寻找“解决方案”文件,向后工作以查找引用项目,然后实现加载图形中符号可见的部分。 这可能是一个迭代过程,以找到真正的原始声明
  • tsbuild 需要处理 -w 模式
  • 解决方案可能没有源自解决方案文件夹之外的子项目
  • 需要清晰的文档,例如 gulp、webpack

侧边栏:此重命名今天不起作用

function f() {
  if (Math.random() > 0.5) {
    return { foo: 10 };
  } else {
    return { foo: 20 };
}
// rename foo here doesn't rename *both* instances in the function body
f().foo;

感谢瑞安、穆罕默德和比尔。 当我最初抱怨 TypeScript 不支持中型项目时,让 Find References/Rename 场景跨项目工作是我想到的核心用例之一。 中型项目是模块化的,但不是很大。 到目前为止,我在这里看到的提案和工作感觉更像是可扩展性游戏。 这对于 TypeScript 的长期健康非常重要,但它主要有利于大型项目,而不是中型项目。 我在 Ryan 的评论中听到的内容更符合改进 TypeScript 开发中型项目的人体工程学所需的内容。

一如既往,非常感谢整个 TypeScript 团队的努力! 你在做很棒的工作。

lerna/yarn 工作区有一个技巧,可以让您的生活更轻松。
将子项目的 package.json 中的maintypes条目指向您的 src/index。 ts文件,查找引用/重命名方案将简单地工作。
您将能够通过运行单个 tsc 来编译整个项目。
您可以对某些包裹执行此操作,也可以对所有包裹执行此操作。 您的来电。

有一些缺点和陷阱(如果你在一个包中进行了扩充,或者导入了任何全局符号,它会污染你的整个程序),但总的来说它工作得很好。
当您想发布到 NPM 时,您只需将 main 和 types 设置为适当的值(作为构建的一部分,左右)

通过上述设置,我或多或少获得了所有预期功能

这是 lerna/yarn 工作区的一个技巧,可以让您的生活更轻松。
将子项目的 package.json 中的 main 和 types 条目指向您的 src/index.ts 文件,Find References/Rename 方案将简单地工作。

根据我的经验,该设置的问题在于 TypeScript 将开始将 _external_ 包的 ts 文件视为需要它们的包的 _sources_,而不是外部库。 这会导致许多问题。

  • 外部包被编译多次,每次都使用 _requiring_ 包的 tsconfig。 如果需要的包有不同的 tsconfigs(例如不同的库),这可能会导致在需要的包上出现错误的编译错误,直到它被再次编译。

  • 需要的包也会编译得更慢,因为它们包含的文件比必要的多。

  • 所有包的rootDir成为顶级目录,可能允许直接包含来自任何包的任何 TS 文件,而不是仅包含来自index 。 如果开发人员不小心,他们可能会绕过所需的包 API。 此外,如果构建过程不健壮,则需要的包可能最终包含来自本应是外部的所需包的重复代码。

在我们的项目中,我们已经排除了依赖 TS 文件的缺点。 所有包间依赖都在index.d.ts文件上,因此编译器将它们视为外部文件,一切都很好。

当然,依赖于.d.ts存在需要多步构建的问题(不可能使用 webpack 等工具开箱即用)和 IDE 体验不佳的问题(重命名、引用不跨越包边界) )。

我同意其他一些观点——打字稿是一个编译器,而不是一个构建系统。 我们需要我们的工具来支持更好的多项目构建。 我知道社区中有一些选择开始这样做。 例如,C# 有一个名为 Roslyn 的编译器和一个名为 MSBuild 的构建工具。

今天与@mhegazy讨论如何以尽可能少的痛苦使重命名工作。

重命名的非退化最坏情况如下所示:

// alpha.ts
const v = { a: 1 };
export function f() { return v; }
export function g() { return v; }

// alpha.d.ts (generated)
export function f(): { a: number };
export function g(): { a: number };

// beta.ts (in another project)
import { f } from '../etc/alpha';
f().a;

// gamma.ts (in yet another project)
import { g } from '../etc/alpha';
g().a;

关键观察是不可能知道重命名f().a应该重命名g().a除非您可以看到alpha.ts将两者关联起来。

实施计划的粗略草图:

  • 具有 .d.ts 文件的“丰富”和“精简”内存 SourceFile 表示。 “精益”文件从磁盘读取; “丰富的”是由于内存中的 .d.ts 生成而产生的
  • “丰富的”.d.ts SourceFiles 引用了从它们的标识符节点到原始源文件中的原始名称 [s]
  • 在重命名期间,我们首先像往常一样对相关符号进行定义。 如果这源于项目引用的 .d.ts,我们实现加载它并创建它的“丰富”.d.ts 文件

    • 注意:这个过程是迭代的,即它可能需要回溯几个级别,直到它进入一个真正的实现文件(由于项目引用而不是.d.ts 重定向)

  • 现在使用“rich”指针找出项目的 .d.ts 中的哪些其他标识符来自相同的源文件标识符
  • 在每个下游项目中,对重命名的标识符进行文本扫描,并查看其“go-to-def”结果是否是 .d.ts 中“从相同符号开始”的任何位置
  • 在实现文件中执行重命名

@RyanCavanaugh会去定义/找到所有引用都适用于这个模型吗?

早期与 Anders 和 Mohamed 围绕大量开放性问题进行的讨论中的笔记

  • prepend也适用于.d.ts ? 是的
  • 我们如何处理 dogfood 分支中的@internal ? 我们需要将内部声明保留在本地 .d.ts 文件中,但不希望它们出现在输出版本中

    • 删除--stripInternal

    • 但不要弃用它(还……?)

    • Ryan 编写remove-internal工具(完成)

    • 内置 -> LKG 过程删除@internal声明

  • 如果您从@types更改 .d.ts 文件会发生什么?

    • 如果你想看到可能的新错误,需要手动强制重建☹️

    • 如果这真的有问题,最终可以将“我看过的文件.txt”文件写入输出文件夹

  • noEmitOnError强制性的吗? 是的。

    • 否则重建会隐藏错误!

  • referenceTarget -> composable ✨ 🚲 🏡 ✨
  • 不想发出声明但想要快速重建的叶节点项目呢?

    • tsbuild或等价物可以检查他们是否符合composable的非上游相关要求

  • 循环引用,(如何)它们是如何工作的?

    • 默认情况下,没有

    • 如果你想这样做,你可以指定例如{ path: "../blah", circular: true }

    • 如果你这样做了,你就需要确保你的构建是确定性的并且总是达到一个固定点(可能不是?!)

    • 循环的重新映射可选的(但优先)重新映射

杂项

  • @weswigham有一个重命名的替代想法,我们需要与@mhegazy讨论

我已经输了。 我主要只是想将源映射的解释排除在编译器之外(单独负责单独的工具),但是当您不在时,我仍然努力添加它(因为显然无缝转到 def 是可取的)。

@瑞安卡瓦诺
合并后是否应该重命名/查找所有引用在引用的项目中工作 #23944 ? 如果只需要语言服务(而不是 tsbuild),我们还应该使用composite: trueprojectReferences: []吗?

合并后是否应该重命名/查找所有引用在引用的项目中工作 #23944 ?

还没有。 但我们接下来正在研究这个。

如果只需要语言服务(而不是 tsbuild),我们还应该使用复合:true 和 projectReferences:[] 吗?

不确定我理解这个问题..你是什么意思“语言服务”而不是“构建”?

不确定我理解这个问题..你是什么意思“语言服务”而不是“构建”?

我只对 monorepo 中多个项目的编辑器支持(重命名/查找所有引用/等)感兴趣,而不对新的构建工具(又名build mode )(#22997)感兴趣,因为我正在使用babel 用于我的编译。

那应该只是工作。 build 是一个选择加入的功能,如果你不想使用它,你不需要使用它......类似于tsc不是你在 VSCode 中的语言服务体验所必需的。

但是,您可能需要使用声明和声明映射进行构建,以生成跨项目引用运行所需的元数据。

我不确定我是否正确理解了提案的所有方面,但是是否可以不让单个项目通过路径而是通过名称来引用其他项目? 工作区项目应该有一种方法来指定每个项目路径,类似于通过 globs 的纱线工作区,或者可能通过列出每个单独的项目名称:

基本上,而不是:

"dependencies": [
    "../common", 
    "../util"
],

我们可以请

"dependencies": [
    "common", 
    "util"
],

并有一个工作区 tsconfig.json

"workspace": {
  "common": "packages/common",
  "util": "packages/util"
}

或者更好的是,路径语法:

"workspace": {
  "*":"packages/*"
}

好处:

  • 能够根据模块系统指定不同的查找规则
  • 例如,在工作区之外下载包时回退到 node_modules 的能力
  • 能够通过克隆多个存储库并为路径设置不同的配置来自由聚合多个工作区以进行工作,以便您可以并行处理更多包。

或者至少,我们是否可以保留非路径名称(那些不以“./”或“../”开头的名称)以供将来使用...

我不确定这有多少相关性,但 Yarn 1.7 最近引入了“聚焦工作区”的概念,请参阅此博客文章

这里有没有人对工作区和@RyanCavanaugh围绕 TypeScript 项目引用/构建模式所做的工作足够熟悉,可能会发表评论来解释它们是否相关? 我的直觉是 Yarn 工作区(今年 npm 也将获得它们)和未来的 TypeScript 版本之间的_somewhere_很好地支持具有多个项目和共享库的 monorepos。 (我们目前感到痛苦。)

我真的很想了解此功能的进展情况。 我们计划在下个月左右将 Aurelia vNext 迁移到 monorepo。 我们的新版本是 100% TypeScript,如果可以的话,我们很乐意使用官方的 TS 项目系统而不是 Lerna。 我们也很高兴成为新功能的早期采用者/测试者 :)

核心支持使用源映射支持的用于构建支持的tsc --b已经在并绑定到 TS 3.0。 打字稿代码库已转移到使用它。 我们目前正在使用 typescript repo 测试此支持。

此时还需要做的是: 1. 找到所有引用/重命名以处理多项目场景。 2. 解决编辑器后台更新.d.ts 文件的问题, 3. --watch支持多项目场景。 还有很多很多的测试。

这张票在他的书上已经有 3 年了。 还有 3/6 未完成的项目?

@claudeduguay这是对 TypeScript 支持的项目的根本性改变,是时候庆祝一下了,你不觉得吗? 我为此感到非常高兴!

@mhegazy这是个好消息。 我很高兴听到 TS 团队也在他们自己的项目中测试它。 期待最后几件事完成并将其作为 Aurelia 的一个选项 :) 一旦有一些关于设置的文档,我们就会将我们的 vNext 移至新的项目系统。 等不及了!

@mhegazy您能否阐明这一切如何与基于 ES2015 模块的 package.json 文件和项目一起使用? 例如,在 Aurelia vNext 中,我们有@aurelia/kernel@aurelia/runtime@aurelia/jit等包。这些是将在import使用的模块名称在各个项目中的陈述。 TS 编译器将如何理解这些模块名称映射到各种引用文件夹? 它会选择放置在每个引用文件夹中的 package.json 文件吗? 这与 Lerna 或 Yarn 工作区有何不同? 我对上面链接的初步研究让我觉得我需要将 TS 项目(构建)与 Lerna(dep 链接和发布)结合使用以获得有效的解决方案,但我没有看到 TS 将如何正确构建如果它不能与 package.json 和 node_modules 集成。 TS repo 源与您的普通 Lerna 项目完全不同(与实际完全不同),所以我开始怀疑这是否能够满足我们的需求。 您可以提供更多信息,尤其是。 与我在此处描述的类似的工作演示解决方案设置将非常有帮助。 谢谢!

我和@EisenbergEffect 有同样的问题。 特别是我也希望这能很好地与lerna管理的 monorepo 配合使用。

需要考虑的两种 monorepo 方案

让我们从一个设置开始,在这个设置中,您已经用 lerna 符号链接了所有内容:

/packages
  /a
    /node_modules
      /b -> symlink to b with package.json "types" pointing to dist/index.d.ts
  /b
    /dist
      /index.d.ts -> built output of entry point declaration file

我们想要在这里发生的关键是重建b我们构建的a如果a已过时。 所以我们将"references": [ { "path": "../b" } ]atsconfig.json并在tsc --build中运行a以获得正确的上游构建b 。 在这个世界中,项目引用只是作为构建图的表示,并允许更智能的增量重建。 理想情况下,lerna 和 TS 可以在这里合作,并在适当的情况下将package.json依赖项镜像到tsconfig.json中。

另一种情况(可能不太常见)是,如果您没有进行符号链接,但仍想“表现得好像”您正在处理一组实时软件包。 今天你可以通过一些相当乏味的路径映射来做到这一点,有些人会这样做。 此处的项目引用同样有助于构建排序,但非常需要支持引用 tsconfig 文件中的属性,以便在引用时自动创建路径映射; 例如,如果你有

{
  "compilerOptions": { "outDir": "bin" },
  "package": "@RyanCavanaugh/coolstuff"
}

然后添加"references": [{ "path": "../cool" }]将自动添加从@RyanCavanaugh/coolstuff -> ../cool/bin/的路径映射。 我们还没有添加这个,但如果结果证明它是一个更常见的场景,可以研究它。

理想情况下,lerna 和 TS 可以在这里合作,并在适当的情况下将 package.json 依赖项镜像到 tsconfig.json

而不是依靠外部工具,我们可以选择读你的package.json (只要它旁边的tsconfig)作为潜在的引用,如果composite: true是集(查看一下,如果每个解决封装具有tsconfig.json ,如果有,则将其视为可构建的项目节点并重复出现)。 由于一切都被符号链接到位,我们甚至不需要更改分辨率(很多?任何?)来处理工作区。 Lerna 实际上并没有像 afaik 那样设置任何特定于 ts(或构建特定)的东西,它只是将所有内容符号链接到位并管理版本控制。 这将是对我们今天所做的优化,即加载ts文件(因为我们更喜欢那些而不是声明)并重新编译所有内容而不管是否过时。

@RyanCavanaugh这听起来很令人兴奋。 知道它是否适用于Rush 的符号链接策略吗? 简而言之,Rush 创建了一个合成包common/temp/package.json ,其中包含 repo 中所有包的所有依赖项的超集。 然后我们使用pnpm为这个合成包执行单个安装操作。 (PNPM 使用符号链接来创建有向无环图而不是 NPM 的树结构,从而消除了重复的库实例)。 然后 Rush 为 repo 中的每个项目创建一个node_modules文件夹,由指向common/temp下相应文件夹的符号链接组成。 结果与 TypeScript 和标准 NodeJS 解析算法完全兼容。 它非常快,因为整个 repo 有一个 shrinkwrap 文件和一个版本控制方程,同时仍然允许每个包指定自己的依赖项。

对于此模型,我们没有在tsconfig.json任何特殊内容。 如果 goto-definition 功能需要一些特殊的 TypeScript 配置,我们最好在安装过程中自动生成它,而不是将它存储在 Git 中。

@pgonzal感谢您提供 Rush 的链接! 我还没有看到。 我今晚去看看。

@RyanCavanaugh感谢您的解释。 您与 lerna 的第一个场景与我们所拥有的最接近。 这是我们带有 TS 和 lerna 的 UX 存储库,作为我们希望使用https://github.com/aurelia/ux上的新项目支持的示例

@weswigham听起来您所描述的内容也适合我们的场景。 上面的示例回购。

请注意,在纱线工作区的情况下,模块不会在每个单独的包目录中进行符号链接,而是在顶级工作区node_modules中进行符号链接。

这也是为什么我认为不以点('./' 或 '../')开头的引用应该保留给将来使用。 希望这些最终会成为“命名引用”,通过活动模块解析策略处理,而不是被视为相对路径。

@spion如果需要,我们将只使用path以外的属性名称(例如"references": [ { "module": "@foo/baz" } ] ); 我不想引起混淆,其中"bar""./bar"files含义相同,但references含义不同

下面的文档/博客文章正在进行中(将根据反馈进行编辑)

我鼓励任何关注此线程的人尝试一下。 我现在正在研究 monorepo 场景,以解决那里的任何最后的错误/功能,并且应该很快就会有一些指导


项目参考

项目引用是 TypeScript 3.0 中的一项新功能,它允许您将 TypeScript 程序组织成更小的部分。

通过这样做,您可以大大缩短构建时间,强制执行组件之间的逻辑分离,并以新的更好的方式组织您的代码。

我们还为tsc引入了一种新模式,即--build标志,它与项目引用协同工作以实现更快的 TypeScript 构建。

示例项目

让我们看一个相当普通的程序,看看项目引用如何帮助我们更好地组织它。
假设您有一个包含两个模块converterunits ,以及每个模块对应的测试文件:

/src/converter.ts
/src/units.ts
/test/converter-tests.ts
/test/units-tests.ts
/tsconfig.json

测试文件导入实现文件并进行一些测试:

// converter-tests.ts
import * as converter from "../converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

以前,如果您使用单个 tsconfig 文件,则使用此结构会很尴尬:

  • 实现文件可以导入测试文件
  • 不可能同时构建testsrc而没有src出现在输出文件夹名称中,这是您可能不想要的
  • 在实现文件更改只是内部需要再次类型检查测试,尽管这不会造成过新的错误
  • 仅更改测试需要再次对实现进行类型检查,即使没有任何更改

您可以使用多个 tsconfig 文件来解决其中一些问题,但会出现新的问题:

  • 没有内置的最新检查,所以你最终总是运行tsc两次
  • 两次调用tsc会导致更多的启动时间开销
  • tsc -w不能同时在多个配置文件上运行

项目参考可以解决所有这些问题,甚至更多。

什么是项目参考?

tsconfig.json文件有一个新的顶级属性references 。 它是一个对象数组,用于指定要引用的项目:

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../src" }
    ]
}

每个引用的path属性可以指向包含tsconfig.json文件的目录,或指向配置文件本身(可以有任何名称)。

当您引用一个项目时,会发生新的事情:

  • 从引用的项目导入模块将改为加载其输出声明文件 ( .d.ts )
  • 如果引用的项目生成outFile ,则输出文件.d.ts文件的声明将在此项目中可见
  • 如果需要,构建模式(见下文)将自动构建引用的项目

通过分成多个项目,您可以大大提高类型检查和编译的速度,减少使用编辑器时的内存使用量,并提高程序逻辑分组的执行情况。

composite

引用的项目必须启用新的composite设置。
需要此设置以确保 TypeScript 可以快速确定在哪里可以找到引用项目的输出。
启用composite标志会改变一些事情:

  • rootDir设置,如果没有明确设置,默认为包含tsconfig文件的目录
  • 所有实现文件必须与include模式匹配或列在files数组中。 如果违反此约束, tsc将通知您未指定哪些文件
  • declaration必须开启

declarationMaps

我们还添加了对声明源映射的支持。
如果您启用--declarationMap ,您将能够使用“转到定义”和重命名等编辑器功能在受支持的编辑器中跨项目边界透明地导航和编辑代码。

prependoutFile

您还可以在引用中使用prepend选项启用依赖项的输出:

   "references": [
       { "path": "../utils", "prepend": true }
   ]

预先准备一个项目将包括该项目的输出高于当前项目的输出。
这适用于.js文件和.d.ts文件,并且源映射文件也将正确发出。

tsc将只使用磁盘上的现有文件来执行此过程,因此可能会创建一个无法生成正确输出文件的项目,因为某些项目的输出将在结果文件中多次出现.
例如:

  ^ ^ 
 /   \
B     C
 ^   ^
  \ /
   D

在这种情况下,重要的是不要在每个引用前添加,因为您最终会在D的输出中得到A两个副本 - 这可能会导致意外结果。

项目参考注意事项

项目引用有一些您应该注意的权衡。

由于依赖项目使用从其依赖项构建的.d.ts文件,因此您必须签入某些构建输出在克隆后构建项目,然后才能在编辑器中导航项目而不会看到虚假错误。
我们正在开发一个应该能够缓解这种情况的幕后 .d.ts 生成过程,但现在我们建议通知开发人员他们应该在克隆后进行构建。

此外,为了保持与现有构建工作流程的兼容性,除非使用--build开关调用,否则tsc不会自动构建依赖项。
让我们了解更多关于--build

TypeScript 的构建模式

期待已久的功能是针对 TypeScript 项目的智能增量构建。
在 3.0 中,您可以将--build标志与tsc
这实际上是tsc一个新入口点,它的行为更像是一个构建协调器,而不是一个简单的编译器。

运行tsc --build (简称tsc -b )将执行以下操作:

  • 查找所有引用的项目
  • 检测它们是否是最新的
  • 以正确的顺序构建过时的项目

您可以为tsc -b提供多个配置文件路径(例如tsc -b src test )。
就像tsc -p ,如果名为tsconfig.json则无需指定配置文件名本身。

tsc -b命令行

您可以指定任意数量的配置文件:

 > tsc -b                                # Build the tsconfig.json in the current directory
 > tsc -b src                            # Build src/tsconfig.json
 > tsc -b foo/release.tsconfig.json bar  # Build foo/release.tsconfig.json and bar/tsconfig.json

不要担心对命令行上传递的文件进行排序 - tsc会在需要时重新排序它们,以便始终首先构建依赖项。

还有一些特定于tsc -b标志:

  • --verbose :打印详细日志以解释正在发生的事情(可以与任何其他标志结合使用)
  • --dry :显示将要完成的操作,但实际上并未构建任何内容
  • --clean :删除指定项目的输出(可以与--dry结合使用)
  • --force :就好像所有项目都过时一样
  • --watch :监视模式(不能与除--verbose之外的任何标志结合使用)

注意事项

通常, tsc会在存在语法或类型错误的情况下产生输出( .js.d.ts ),除非打开noEmitOnError
在增量构建系统中执行此操作将非常糟糕 - 如果您的过时依赖项之一出现新错误,您只会看到一次,因为后续构建将跳过构建现在最新的项目。
出于这个原因, tsc -b作用就像为所有项目启用了noEmitOnError

如果您签入任何构建输出( .js.d.ts.d.ts.map等),您可能需要在某些源代码控制后运行--force构建操作取决于您的源代码控制工具是否保留本地副本和远程副本之间的时间映射。

msbuild

如果您有 msbuild 项目,则可以通过添加来开启启用构建模式

    <TypeScriptBuildMode>true</TypeScriptBuildMode>

到您的项目文件。 这将启用自动增量构建和清理。

请注意,与tsconfig.json / -p ,不会考虑现有的 TypeScript 项目属性 - 所有设置都应使用您的 tsconfig 文件进行管理。

一些团队已经设置了基于 msbuild 的工作流,其中 tsconfig 文件与它们配对的托管项目具有相同的隐式图形排序。
如果您的解决方案是这样,您可以继续使用msbuildtsc -p以及项目引用; 这些是完全可互操作的。

指导

整体结构

有了更多的tsconfig.json文件,您通常会希望使用配置文件继承来集中您的常用编译器选项。
通过这种方式,您可以更改一个文件中的设置,而不必编辑多个文件。

另一个好的做法是拥有一个“解决方案” tsconfig.json文件,该文件仅包含所有叶节点项目的references
这提供了一个简单的入口点; 例如,在 TypeScript 存储库中,我们只需运行tsc -b src来构建所有端点,因为我们列出了src/tsconfig.json中的所有子项目
请注意,从 3.0 开始,如果tsconfig.json文件中至少有一个reference ,则files数组为空不再是错误。

您可以在 TypeScript 存储库中看到这些模式 - 请参阅src/tsconfig_base.jsonsrc/tsconfig.jsonsrc/tsc/tsconfig.json作为关键示例。

构建相关模块

通常,使用相关模块转换 repo 不需要太多。
只需在给定父文件夹的每个子目录中放置一个tsconfig.json文件,然后将reference添加到这些配置文件中以匹配程序的预期分层。
您需要将outDir设置为输出文件夹的显式子文件夹,或者将rootDir设置为所有项目文件夹的公共根目录。

构造 outFiles

使用outFile编译的布局更加灵活,因为相对路径并不重要。
要记住的一件事是,您通常希望在“最后一个”项目之前不要使用prepend - 这将缩短构建时间并减少任何给定构建所需的 I/O 量。
TypeScript 存储库本身就是一个很好的参考——我们有一些“库”项目和一些“端点”项目; “端点”项目尽可能小,并且只引入他们需要的库。

Monorepos 的结构化

TODO:进行更多实验并弄清楚这一点。 Rush 和 Lerna 似乎有不同的模型,这意味着我们的结局不同

也在寻找关于 #25164 的反馈

@RyanCavanaugh非常好的文章和出色的功能,非常适合尝试一下,尤其是。 在我花了几天时间将我们的大项目组织成带有配置文件引用的子项目之后。

我有几个注意事项:

  1. 什么是 monorepo? 多描述一下这个用例会很好。
  2. 在大多数情况下(尤其是大型项目),在构建过程中会生成许多额外的工件。 在我们的例子中,这些是 CSS、HTML、图像文件等,通过 gulp。 我想知道如何使用这种构建工具来适应这种新的做事方式。 说,我想不仅在 *.ts 文件上运行 watch,而且在我们所有的其他文件(样式、标记等)上运行。 怎么做? 需要并行运行,比如gulp watchtsc -b -w

@vvs monorepo 是 NPM 包的集合,通常由 Rush 或 Lerna 等工具管理

如果你正在使用 gulp,你会想要使用一个能够理解项目引用的加载器来获得最佳体验。 @rbuckton在这里做了一些工作,因为我们确实有一些开发人员在内部使用 gulpfile; 也许他可以权衡那里的好的模式是什么样的

@RyanCavanaugh这看起来不错。 我对 Lerna 的指导很感兴趣 :)

@RyanCavanaugh这看起来很棒,我目前正在尝试使用我们的 lerna monorepo。

在你的文章中,我唯一不清楚的是prepend选项。 我不太明白它要解决什么问题,在什么情况下你想使用它,如果你不使用它会发生什么。

这太棒了! 我从事ts-loader和相关项目。 在使用 TypeScript 的LanguageServiceHost / WatchHost项目中,是否可能需要进行更改以支持此功能?

(有关我的意思的示例,请参阅 https://github.com/TypeStrong/ts-loader/blob/master/src/servicesHost.ts。)

如果是这样,非常感谢所有指导/ PR! 事实上,如果您希望在 webpack 世界中对此进行测试,我很乐意帮助推出支持此功能的 ts-loader 版本。

当然,如果它“有效”那就更好了 :smile:

做得好!

@yortus @EisenbergEffect我已经在https://github.com/RyanCavanaugh/learn-a上设置了一个示例 lerna 存储库,其中包含一个 README,其中概述了我为使其工作所采取的步骤。

如果我理解正确,如果所有内容(X 及其所有依赖项和传递依赖项)都是最新的, tsc -b X将不执行任何操作? 想知道即使没有针对没有引用的单个项目的-b标志,我们是否也能做到这一点? (当然,在这种情况下减去依赖性)

这很酷。 我倾向于使用 Lerna 这样的配置(按功能分离单声道回购)。 我认为这也会起作用。

{
"lerna": "2.11.0",
“包”:[
"包/组件/ ","包/库/ ",
"包/框架/ ","包/应用程序/ ",
“包/工具/*”
],
“版本”:“0.0.0”
}

所以这在typescript@next上可用?

我将使用我们的纱线工作区存储库对此进行测试。 我们必须将nohoist用于一些尚不支持工作区的模块,因此很高兴看到它如何处理它。

@RyanCavanaugh我今晚拿了 repo 进行了试运行。 我在 repo 上打开了一个问题来报告我遇到的一些问题。 再次感谢你把它放在一起。 我期待着尽快使用它。

十分有趣! 目前在我公司,我们使用自己的工具mtsc来同时支持多个项目的监视模式。 我们有大约 5 个项目需要在同一个 repo 中编译和观看。

项目具有不同的配置,例如; ECMA 目标(es5、es6)、类型(node、jest、DOM 等)、emit(有些使用 webpack,有些自己编译为 js)。 他们都共享一件事,那就是tslint 插件,其余的都可以不同。 我的工具还在项目编译后运行 tslint(每个项目,如果项目在 tslint 完成之前重新编译则停止)。

我对当前提案的主要担忧是您无法说出哪些项目共享哪些资源。 我们有一个服务器和一个客户端项目,它们都使用一个特殊的实用程序文件夹,但我们不想看到两次编译错误。 但这可以用过滤器解决,所以没什么大不了的:)

我已经用我们的 lerna monorepo 尝试了新的--build模式,它目前由 17 个相互依赖的包组成。 让一切正常工作需要一段时间,但现在一切正常,并且能够逐步构建对我们来说是一个巨大的进步。

我确实遇到了一些我在下面描述的问题。 我希望这是对 TS 团队有用的反馈,并可能帮助其他人让--build模式为他们的项目工作。

tsc --build模式的反馈

1. 伪“\已过期,因为输出文件 '2.map' 不存在"

我在每次构建的每个包中都收到了这条消息,因此即使没有任何更改,每次构建都变成了完整的重新构建。 我注意到@RyanCavanaugh已经在 #25281 中修复了这个问题,所以如果你更新到20180628或每晚更新,这不再是问题。 以下问题假设您至少每晚更新到20180628

2. "\是最新的来自其依赖项的 .d.ts 文件”,当它不是

编辑:在#25337 报告。

要重现此问题,请按照他的说明设置@RyanCavanaughlearn-a示例存储库。 运行tsc -b packages --verbose以查看第一次构建的所有内容。 现在将pkg1/src/index.ts 1 行更改为import {} from "./foo";并保存。 再次运行tsc -b packages --verbose 。 跳过pkg2的构建,即使pkg1更改方式破坏了pkg2的源代码。 您现在可以在pkg2/src/index.ts看到红色波浪线。 使用tsc -b packages --force再次构建并显示构建错误。 以下问题假设使用--force构建以解决此问题。

3. 生成的.d.ts文件导致下游包中的“重复标识符”构建错误

编辑:在#25338 报告。

要重现此问题,请按照他的说明设置@RyanCavanaughlearn-a示例存储库。 现在运行lerna add @types/node将 Node.js 类型添加到所有三个包中。 运行tsc -b packages --force以确认它仍然可以正常构建。 现在将以下代码添加到pkg1/src/index.ts

// CASE1 - no build errors in pkg1, but 'duplicate identifier' build errors in pkg2
// import {parse} from 'url';
// export const bar = () => parse('bar');

// CASE2 - no build errors in pkg1 or in downstream packages
// import {parse, UrlWithStringQuery} from 'url';
// export const bar = (): UrlWithStringQuery => parse('bar');

// CASE3 - no build errors in pkg1 or in downstream packages
// export declare const bar: () => import("url").UrlWithStringQuery;

// CASE4 - no build errors in pkg1, but 'duplicate identifier' build errors in pkg2
// import {parse} from 'url';
// type UrlWithStringQuery = import("url").UrlWithStringQuery;
// export const bar = (): UrlWithStringQuery => parse('bar');

一次取消注释一个案例并运行tsc -b packages --force 。 情况 1 和 4 会导致pkg2出现大量构建错误。 对于情况 2 和 3,没有构建错误。 案例 1 和案例 4 的重要区别似乎是生成的pkg1/lib/index.d.ts的第一行:

/// <reference path="../node_modules/@types/node/index.d.ts" />

情况 2 和 3 不会生成此行。 在案例 1 和案例 4 中构建pkg2 ,它在不同路径中包含@types/node声明的两个相同副本,这会导致“重复标识符”错误。

也许这是设计使然,因为案例 2 和案例 3 有效。 然而,它似乎很混乱。 对于这 4 种情况中的任何一种,在pkg1都没有构建错误或警告,但下游构建行为对导出声明的确切样式非常敏感。 我认为(a) pkg1应该在案例 1 和 4 中出错,或者(b)所有四个案例都应该具有相同的下游构建行为,或者(c) TS 团队应该有一些明确的指导如何编写声明来避免这个问题。

4.使用yarn工作区时生成的.d.ts文件中import类型的问题

在尝试使用我们的 17 个包 monorepo 使构建模式工作时,我解决了由生成的.d.ts文件中的import类型中的相对路径引起的许多构建错误。 我终于弄清楚了与模块提升有关的问题。 也就是说,当使用 yarn 工作区时,所有安装的模块都被提升到 monorepo-root node_modules目录,包括 monorepo 中所有包的符号链接。 我改变了monorepo在使用packages财产lerna.json ,这将导致lerna使用其自己的非冲顶引导算法,并解决了这个问题。 无论如何,这是一种更好/更安全的方法,尽管速度较慢。

我不确定 TS 是否打算支持模块提升设置,所以我没有为我遇到的问题开发一个重现,但如果有兴趣,我可以尝试制作一个。 我认为问题可能是某些构建通过顶级packages目录(根据 tsconfig 设置)和顶级node_modules目录(根据import在生成的.d.ts文件中输入)。 这有时会由于结构类型而起作用,但对于导出的唯一符号之类的东西会失败。

5.重复配置

设置一个 monorepo 来使用 lerna 基本上只需要在lerna.json放入"packages": ["packages/*"]类的东西。 Lerna 通过展开 globstars 计算出确切的包列表,然后通过查看每个包的package.json声明的依赖关系来计算出确切的依赖关系图。 您可以随意添加和删除包和依赖项,并且lerna无需更改其配置即可跟上。

TypeScript --build模式涉及更多的仪式。 全局模式不被识别,因此所有包必须在@RyanCavanaughlearn-a示例存储库中明确列出和维护(例如在packages/tsconfig.json )。 构建模式不查看package.json依赖项,因此每个包都必须在其package.json文件(在"dependencies" )以及它是tsconfig.json文件(在"references" )。

这是一个小小的不便,但我把它包括在这里,因为与lerna's方法相比,我发现繁琐明显。

6. tsc因全局模块扩充而崩溃

编辑:在#25339 报告。

要重现此问题,建立@RyanCavanaughlearn-a样品回购按他的指示。 现在运行lerna add @types/multermulter添加到所有三个包中。 运行tsc -b packages --force以确认它仍然可以正常构建。 现在pkg1/src/index.ts下行添加到

export {Options} from 'multer';

再次运行tsc -b packages --force 。 编译器因违反断言而崩溃。 我简要地查看了堆栈跟踪和断言,这似乎与Express命名空间的全局扩充有关。

感谢@yortus的反馈。 靠欣赏。 对于 3,我认为是https://github.com/Microsoft/TypeScript/issues/25278。

对于4,我不熟悉模块提升作为一个概念。 你能详细说明和/或分享一个再现吗?

@mhegazy许多使用 lerna 和 yarn 的人都使用工作区(包括我自己)。 更多信息在这里: https :

我目前正在使用 yarn 工作区、lerna、扩展 tsconfigs,其中基础 tsconfig 声明paths为所有在root/node_modules下找到的带有提升模块的包共享。 当我听到yarnmonorepo ,我会想到workspaces因为这是该功能的真正意图 - 简化使用并减少重复。 我原以为这种更改会简单地删除我在基本 tsconfig.xml 文件中声明的冗长/痛苦的paths

这是我们根 monorepo tsconfig 的示例(如果有帮助的话):

{
  "extends": "./packages/build/tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
      "@alienfast/build/*": ["./build/src/*"],
      "@alienfast/common-node/*": ["./common-node/src/*"],
      "@alienfast/common/*": ["./common/src/*"],
      "@alienfast/concepts/*": ["./concepts/src/*"],
      "@alienfast/faas/*": ["./faas/src/*"],
      "@alienfast/math/*": ["./math/src/*"],
      "@alienfast/notifications/*": ["./notifications/src/*"],
      "@alienfast/ui/*": ["./ui/src/*"],
      "@alienfast/build": ["./build/src"],
      "@alienfast/common-node": ["./common-node/src"],
      "@alienfast/common": ["./common/src"],
      "@alienfast/concepts": ["./concepts/src"],
      "@alienfast/faas": ["./faas/src"],
      "@alienfast/math": ["./math/src"],
      "@alienfast/notifications": ["./notifications/src"],
      "@alienfast/ui": ["./ui/src"],
    }
  },
  "include": ["./typings/**/*", "./packages/*/src/**/*"],
  "exclude": ["node_modules", "./packages/*/node_modules"]
}

我将尝试分叉以获取示例:
https://github.com/RyanCavanaugh/learn-a

这是@RyanCavanaugh带有纱线工作区的 repo 的非合并 PR:
https://github.com/RyanCavanaugh/learn-a/pull/3/files

我们还在Jupyterlab 中使用了模块提升,以及 lerna 和 yarn。 它允许我们基本上在所有包之间共享我们安装的依赖项,因此它们在文件系统中只存在一次,在根项目中。

我认为工作区比在所有包之间使用link命令更简洁,以便它们可以相互访问(或至少访问它们的依赖项)。

如上所述,模块提升将所有依赖项移动到根node_modules目录。 这利用了这样一个事实,即节点模块解析将始终向上遍历目录树并搜索所有node_modules目录,直到找到所需的模块。 monorepo 中的各个模块然后在此根node_modules中进行符号链接,一切正常。 纱线博客文章可能比我能更好地解释它。

不保证会发生吊装。 如果您有相同包的不匹配版本,它们将不会被提升。 此外,许多现有工具不支持提升,因为它们假设node_modules将在哪里,或者它们没有正确遵循节点模块解析。 因此,有一个nohoist设置可以禁用特定模块或依赖项的提升。

我在之前的反馈中添加了第六项。 tsc在那里描述的场景中崩溃。

@mhegazy我不确定第 3 项是否与 #25278 相关。 #25278 描述了无效的声明发出。 我生成的声明文件在语法和语义上都是有效的,但导致下游项目使用节点类型的两个副本构建,从而导致“重复标识符”错误。

如上所述,模块提升将所有依赖项移动到根 node_modules 目录。 这利用了节点模块解析将始终向上遍历目录树并搜索所有 node_modules 目录直到找到所需模块的事实。

顺便说一句,这种模型有一个缺点,它会导致“幻影依赖”,其中项目可以导入未在其 package.json 文件中明确声明的依赖项。 当您发布库时,这可能会导致问题,例如 (1) 安装的依赖项版本与测试/预期的版本不同,或 (2) 依赖项完全丢失,因为它是从未安装的不相关项目中提升的在这种情况下。 PNPM 和 Rush 都有旨在防止幻影依赖的架构选择。

我有一个关于tsc --build的一般性问题:TypeScript 编译器是否正在寻求接管在 monorepo 中为项目编排构建的角色? 通常,工具链将拥有一整套任务,例如:

  • 预处理
  • 生成本地化字符串
  • 将资产转换为 JavaScript(css、图像等)
  • 编译(类型检查/转译)
  • 汇总 .js 文件(例如 webpack)
  • 汇总 .d.ts 文件(例如 API Extractor)
  • 后处理包括单元测试和文档生成

通常像 Gulp 或 Webpack 这样的系统会管理这个管道,而编译器只是这条链中间的一个步骤。 有时,辅助工具也会以另一种方式运行构建,例如 Jest+ts-jest 用于jest --watch

tsc目的是自己管理这些东西吗? 如果没有,传统的构建协调器是否有办法解决依赖图本身,例如以正确的顺序(在更新预处理之后)在每个项目文件夹中重复调用 tsc ?

或者,如果设计是单次处理整个 monorepo(而今天我们在单独的 NodeJs 进程中构建每个项目),我也很好奇其他构建任务将如何参与:例如,我们是否会在一次所有项目? (过去会导致内存不足问题。)我们会失去利用多进程并发的能力吗?

顺便说一句,这些不是批评。 我只是想了解大局和预期用途。

@pgonzal是的,构建真实世界的 monorepo 有许多非 tsc 部分。 对于我们的 lerna monorepo,我采用了以下方法:

  • monorepo 中的每个包都可以选择在其package.json定义prebuild脚本和/或postbuild脚本。 这些包含构建的非 tsc 方面。
  • 在 monorepo 的package.json ,有这些脚本:
    "prebuild": "lerna run prebuild", "build": "tsc --build monorepo.tsconfig.json --verbose", "postbuild": "lerna run postbuild",
  • 就是这样。 运行yarn build在monorepo水平运行prebuild用于定义它们,然后将其运行的每个软件包脚本tsc --build步骤,则它运行所有postbuild脚本。 (按照 npm 和 yarn 的约定,执行npm run foonpm run prefoo && npm run foo && npm run postfoo大致相同。)

你如何处理jest --watchwebpack-dev-server ? 例如,当修改源文件时,是否再次运行预构建/后构建步骤?

这对ts-node和相关工作流程有任何影响吗? 我们的一些辅助应用程序“直接”从 TypeScript 运行,例如"start": "ts-node ./src/app.ts""start:debug": "node -r ts-node/register --inspect-brk ./src/app.ts"

在 #25355 报告了构建模式的另一个问题。

感谢您迄今为止提供的所有出色反馈和调查。 我真的很感谢所有花时间尝试和踢轮胎的人。

@yortus re https://github.com/Microsoft/TypeScript/issues/3469#issuecomment -400439520

伟大的写了,再次感谢提供这一点。 您的问题按顺序 -

  1. 固定的
  2. 公关在 #25370
  3. 讨论记录的问题 - 不是很明显正确的修复是什么,但我们会做一些事情
  4. 调查(下)
  5. 记录 #25376
  6. 技术上与--build AFAICT 无关。 这是我们最近添加的一个新断言; 内森的调查

@rosskevin 🎉 为learn-a回购的公关! 我将把它合并到一个分支中,这样我们就可以更好地进行比较和对比。

@pgonzal重新https://github.com/Microsoft/TypeScript/issues/3469#issuecomment -401577442

我有一个关于 tsc --build 的一般性问题:TypeScript 编译器是否正在寻求接管为 monorepo 中的项目编排构建的角色?

好问题; 我想非常明确地回答这个问题:绝对不是

如果您今天很高兴使用tsc来构建您的项目,我们希望您明天也可以使用tsc -b来构建您的多部分项目。 如果您今天很高兴使用gulp来构建您的项目,我们希望您明天也可以使用gulp来构建您的多部分项目。 我们可以控制第一个场景,但需要工具和插件作者来帮助我们处理第二个场景,这就是为什么即使tsc -b也只是对暴露的 API 的一个薄包装,工具作者可以使用它来帮助项目引用很好地发挥作用在他们的构建模型中。

更广泛的背景是,关于tsc -b是否应该存在,或者作为一个单独的工具/入口点存在相当激烈的内部争论——构建一个通用的构建协调器是一项艰巨的任务,而不是我们报名参加。 对于我们自己的存储库,我们将tsc与轻量级任务运行器框架一起使用,现在将tsc -b与相同的任务运行器一起使用,我希望其他迁移的人也能保持现有的构建链到位只有小的调整。

@borekb re https://github.com/Microsoft/TypeScript/issues/3469#issuecomment -401593804

这对 ts-node 和相关工作流程有什么影响吗? 我们的一些辅助应用程序“直接”从 TypeScript 运行

对于隐式不能具有项目引用的单文件脚本,影响为零。

@EisenbergEffectlearn-a存储库中有一些关于跨项目重命名和其他语言服务功能的问题。 这里最大的悬而未决的问题是,我们是否能够在 3.0 中使此功能处于可用状态。 如果是这样,那么跨项目重命名将“正常工作”,但需要注意的是,我们显然不可能最终找到所有下游项目并更新它们——这将是基于一些启发式寻找其他项目的“最大努力”项目。

如果我们认为跨项目重命名对于 3.0 来说不是可以接受的稳定+完整,我们可能只会在重命名的符号位于另一个项目的 .d.ts 输出文件中时阻止重命名操作 - 允许您这样做将是非常令人困惑,因为 .d.ts 文件会在上游项目被修改在上游项目的后续构建中得到更新,这意味着在您进行本地重命名和您意识到声明代码之间很容易相隔几天实际上并没有更新。

对于 Go to Definition 之类的功能,这些功能现在可以在 VS Code 中使用,并且将在 Visual Studio 的未来版本中开箱即用。 这些功能都需要启用 .d.ts.map 文件(打开declarationMap )。 有一些针对每个功能的工作来解决这个问题,所以如果你发现某些东西没有按预期工作,请记录一个错误,因为我们可能错过了一些情况。

我此时正在跟踪的未解决问题:

  • 跨项目重命名 - @andy-ms 正在实施
  • 需要分析提升模块设置并了解它们的影响 - 对我来说
  • 应该有一个使用yarn的示例learn-a repo 版本,另一个使用pnpm ,另一个使用提升模式中的一个

开放问题

  • 我们是否应该实现读取package.json的逻辑来推断项目引用? 记录 #25376
  • .d.ts.map是否应该为composite项目隐式发出?

@RyanCavanaugh添加到

我目前正在跟踪的未解决问题

我们还提到有一个增量输出缓存,与实际项目输出位置分开,以处理诸如在 LS 后台更新声明之类的事情(今天,更改不会在编辑器中跨项目边界传播,直到您构建), stripInternal和变异构建过程(我们的构建输出就地发生变异,因此不适合 LS 操作)。

抱歉问了一个愚蠢的问题,因为它已在路线图中检查,我该如何启用此功能?

@aleksey-bykov 你可以在 typescript @next 中使用它

我刚刚在我们的纱线工作区驱动的 monorepo 上尝试了这个,它运行良好。

我注意到的一件事是tsc --build --watch报告错误,但没有输出任何内容表明构建现已修复。 2.9 中的标准tsc监视模式已经开始给出错误计数,很高兴在那里看到零,这样您就知道构建已经完成。

我有一个装满 *.d.ts 的文件夹,我该怎么做:

  • 使它成为一个项目并引用它(尝试过,没有用)
  • 为它使用“包含”

@timfish反馈与我听说的其他反馈相匹配; 记录 #25562

@aleksey-bykov https://github.com/Microsoft/TypeScript/issues/3469#issuecomment -400439520 应该有助于解释一些概念

@RyanCavanaugh看起来项目引用仅适用于 commonjs 和节点模块解析,不是吗?

在你的例子中:

import * as p1 from "@ryancavanaugh/pkg1";
import * as p2 from "@ryancavanaugh/pkg2";

p1.fn();
p2.fn4();
  1. 什么是@ryancavanaugh模块,和TS解析模块的方式有关系吗?
  2. 此示例是否应该与 AMD(经典模块分辨率)一起使用?
  3. 是否需要outFile才能找到定义?
  4. d.ts 文件应该在哪里,以便引用项目找到它们(我还可以使用 outDir 吗?TS 会在那里找到它们吗?)

我有 2 个简单的项目essentialscommon并且共同点无法解决在 Essentials 中编译的内容:

image

@aleksey-bykov

  1. 它只是一个作用域模块名称,在通常的节点模块解析算法下解析
  2. 您可以将项目引用与包括经典在内的任何模块系统一起使用,但示例名称(范围模块)在节点之外使用不是很友好
  3. TypeScript 寻找 .d.ts 文件位于被引用项目构建它们的位置

如果您有示例回购或其他内容,我可以诊断出您收到该错误的原因

@RyanCavanaugh请做
例子.zip

@RyanCavanaugh ,看起来tsc --build --watch最初不会输出任何文件,直到它看到对源文件的修改。

线程太长(在时间和空间上); 让我们在幸运问题编号 100 * 2^8 处进行讨论:#25600

此页面是否有帮助?
0 / 5 - 0 等级