DrillLab
第 07 / 09 节LESSON 07 / 09约 10 分钟~10 min

ESM:import / export 与那些莫名其妙的报错ESM: import / export, and the errors that look strange at first

为什么 subgraph 里 import 要写 .js 后缀,为什么 jest 要加一个实验性参数。Why an import in the subgraph needs the .js ending, and why jest needs an experimental flag.

1 个练习1 exercises地基 · 第 2 部分Foundations · Part 2
这一页有什么On this page6
学完这节你会After this lesson you can
  • 分清 default export 和 named export,以及各自怎么 importTell a default export from a named export, and import each one correctly
  • 知道 ESM 里相对路径必须带扩展名Know that a relative path in ESM must include the file ending
  • 看懂 import type 是干什么的Understand what import type is for
  • 认出「模块系统不匹配」这一类报错Recognise the errors that mean two module systems do not match
这在考试里考什么What the exam does with this

两个项目都是 ESM。subgraph 的 import 少一个 .js 就跑不起来;React 项目里 import type 用错会让构建失败。这类错误的报错信息通常很不友好。Both projects use ESM. One missing .js in a subgraph import and nothing runs. In the React project, a wrong import type makes the build fail. The messages these errors print are usually hard to read.

§01

default 和 named:一个模块只能有一个 defaultdefault and named: a module can have only one default

export default X 是「这个模块的主角是 X」, 一个文件只能有一个。import 它的时候名字随你起:

export const A / export function B具名导出,可以有很多个,import 时名字必须对上,而且要用花括号。

两个考试的组件都是 default 导出(export default NoteManager), 而 subgraph 的 resolver 是具名导出 (export const resolvers)—— 所以 import 的写法不同。

export default X says “X is the main character of this module”, and a file only gets one. When you import it, you pick whatever name you like:

export const A / export function B are named exports. There can be many of them, and on import the name has to line up and go inside curly braces.

Components in both exams are default exports (export default NoteManager), while the subgraph resolvers are named exports (export const resolvers) — so the import lines look different.

TSX源项目From source
1// 组件:default 导出
2export default NoteManager;
3
4// 使用方:名字可以自己起,不用花括号
5import NoteManager from "./components/NoteManager";
6import type { Note } from "../../types/Note"; // 具名 + 只要类型
1// the component: a default export
2export default NoteManager;
3
4// the caller: pick any name you like, no curly braces
5import NoteManager from "./components/NoteManager";
6import type { Note } from "../../types/Note"; // named, and types only
Source: react-notes-app/src/App.tsx 与各组件
JavaScript源项目From source
1// subgraph:具名导出,三个东西
2export const resolvers = { ... };
3export { createShippingInfoLoader, createOrderLoader };
4
5// 使用方:名字必须一字不差,要用花括号
6import {
7 resolvers,
8 createShippingInfoLoader,
9 createOrderLoader,
10} from './resolvers/orderResolvers.js';
1// the subgraph: named exports, three of them
2export const resolvers = { ... };
3export { createShippingInfoLoader, createOrderLoader };
4
5// the caller: every name must match exactly, and curly braces are required
6import {
7 resolvers,
8 createShippingInfoLoader,
9 createOrderLoader,
10} from './resolvers/orderResolvers.js';
Source: graphql-federation-practice/node-subgraph/src/index.js
§02

ESM 里相对路径必须带 .js —— 哪怕源文件是 .tsIn ESM a relative path must end in .js, even when the source file is .ts

这是 Node 原生 ESM 的硬规定,不是可选风格。This is a fixed rule of native ESM in Node, not a matter of style.

注意上面那行:from './resolvers/orderResolvers.js'带了 .js

在 CommonJS(require)时代,Node 会帮你猜: 你写 ./foo,它会依次试 ./foo.js./foo/index.js。原生 ESM 取消了这个猜测, 路径必须写完整。

漏了会得到这个报错 —— 而它长得像「文件不存在」, 容易让人去怀疑路径打错了:

而 React 那个项目里 import App from "./App"没写后缀却能跑,因为它经过 Vite —— 打包器有自己的解析规则,会帮你补。结论:走打包器可以省,直接给 Node 跑就必须写。

Look at that line above again: from './resolvers/orderResolvers.js'. It carries the .js.

Back in the CommonJS (require) era, Node guessed for you: write ./foo and it would try ./foo.js, then ./foo/index.js. Native ESM dropped the guessing, so the path has to be written out in full.

Leave it off and you get this error — which reads like “the file does not exist” and sends people off doubting their path:

Meanwhile import App from "./App" in the React project has no extension and runs fine, because it goes through Vite — a bundler has its own resolution rules and fills the extension in for you. Conclusion: with a bundler you can skip it; handing the file straight to Node, you must write it.

Terminal漏写 .js 的报错The error when .js is left off示意Illustrative
1Error [ERR_MODULE_NOT_FOUND]: Cannot find module
2 '/path/node-subgraph/src/resolvers/orderResolvers'
3 imported from /path/node-subgraph/src/index.js
4Did you mean to import "./resolvers/orderResolvers.js"?
好消息是 Node 现在会给出 Did you mean 提示。看到 ERR_MODULE_NOT_FOUND,第一反应应该是「后缀漏了」,而不是「路径写错了」。Node now prints a Did you mean hint, which helps. When you see ERR_MODULE_NOT_FOUND, your first guess should be a missing file extension, not a wrong path.
§03

import type:只要类型,不要运行时代码import type: take the type only, not any code that runs

import type { Note } from "../../types/Note"这个写法在告诉编译器:「我只需要 Note 这个类型, 编译完请把这行整个删掉」。

为什么要区分?因为 type Note = {...} 这种类型别名在运行时根本不存在 —— TypeScript 编译后类型全被擦掉了。 如果用普通 import,某些配置下打包器会保留这行 import, 然后在运行时去找一个不存在的导出。

实用规则:导入的是类型就写 import type, 导入的是能跑的东西(函数、组件、常量)就写普通 import真实项目里两种都有:

import type { Note } from "../../types/Note" tells the compiler: “all I need is Note as a type, so delete this whole line once you have compiled”.

Why draw the distinction? Because a type alias like type Note = {...} does not exist at runtime at all — TypeScript erases every type when it compiles. With a plain import, some setups keep that import line and then go looking at runtime for an export that is not there.

Practical rule: importing a type, write import type; importing something that runs (a function, a component, a constant), write a plain import. Real projects carry both:

TSX源项目From source
1import React, { useState, useEffect } from "react"; // 运行时要用
2import type { Note } from "../../types/Note"; // 只要类型
1import React, { useState, useEffect } from "react"; // needed at runtime
2import type { Note } from "../../types/Note"; // the type only
Source: react-notes-app/src/components/NoteForm/index.tsx
§04

为什么 subgraph 的 test script 那么长Why the test script of the subgraph is so long

回头看那条 script:

Jest 诞生于 CommonJS 时代,原生 ESM 支持至今还是实验性的。NODE_OPTIONS=--experimental-vm-modules 就是打开那个开关。 少了它,jest 一遇到 import 就报Cannot use import statement outside a module

另外 package.json 里内嵌的 jest 配置有"transform": {} —— 空对象, 意思是「不做任何转译」。因为源码本来就是标准 ESM, 不需要 Babel 把它转成 CommonJS。

这些不需要你去改。但需要你认出来 —— 看到这条 script 就知道「这个项目是 ESM,测试是 jest,别照 CommonJS 的思路改东西」。

Look at that script again:

Jest was born in the CommonJS era, and its native ESM support is still experimental to this day. NODE_OPTIONS=--experimental-vm-modules is the switch that turns it on. Without it, jest hits an import and reports Cannot use import statement outside a module.

The jest config embedded in package.json also has "transform": {} — an empty object, meaning “transpile nothing”. The source is already standard ESM, so Babel is not needed to turn it into CommonJS.

None of this needs changing by you. It needs recognising — see this script and you know “this project is ESM, the tests are jest, do not go editing things with a CommonJS mindset”.

JSON源项目From source
1"scripts": {
2 "start": "node src/index.js",
3 "test": "NODE_OPTIONS=--experimental-vm-modules jest"
4},
5"jest": {
6 "testEnvironment": "node",
7 "transform": {},
8 "testMatch": ["**/__tests__/**/*.test.js"]
9}
Source: graphql-federation-practice/node-subgraph/package.json
testMatch 说明测试文件必须放在 __tests__ 目录下、以 .test.js 结尾。放错位置 jest 就发现不了它 —— 「我写了测试但 jest 说 No tests found」多半是这个原因。testMatch says a test file must sit inside a __tests__ directory and end in .test.js. Put it anywhere else and jest will not find it. That is the usual reason for "I wrote a test but jest says No tests found".
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L2Debug LabDebug LabDebug Lab · ERR_MODULE_NOT_FOUNDDebug Lab · ERR_MODULE_NOT_FOUND

你在 node-subgraph/ 里跑 npm start, 服务器起不来。报错很长,但关键信息只有两行。

You run npm start inside node-subgraph/ and the server does not come up. The error is long, but only two lines of it matter.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npm start node:internal/modules/esm/resolve:274 Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/me/node-subgraph/src/dataSources/orderDataSource' imported from /Users/me/node-subgraph/src/index.js at finalizeResolution (node:internal/modules/esm/resolve:274:11) Did you mean to import "./dataSources/orderDataSource.js"?
JavaScriptsrc/index.js(第 2 行有问题)src/index.js (line 2 is the problem)示意Illustrative
1import { resolvers } from './resolvers/orderResolvers.js';
2import { OrderDataSource } from './dataSources/orderDataSource';
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

ERR_MODULE_NOT_FOUNDERR_MODULE_NOT_FOUND
相对路径漏了 .js 扩展名A relative path is missing the .js ending
Cannot use import statement outside a moduleCannot use import statement outside a module
缺 "type":"module" 或缺 --experimental-vm-modulesEither "type":"module" is missing, or --experimental-vm-modules is
jest 说 No tests foundjest says No tests found
对照 testMatch,看文件位置和命名Compare with testMatch: check where the file sits and what it is called
只用到某个类型You only use something as a type
写 import type,编译后整行消失Write import type. The whole line disappears after compiling
这节的要点What to take away
  1. default 导出一个文件只能有一个,import 时名字随意、不加花括号。A file can have only one default export. When you import it you may pick any name, and you use no curly braces.
  2. 具名导出可以多个,import 时名字必须一致、要加花括号。A file can have many named exports. When you import one, the name must match, and you use curly braces.
  3. 原生 ESM 里相对路径必须带 .js;走 Vite 这类打包器时可以省。In native ESM a relative path must end in .js. With a bundler such as Vite you may leave it out.
  4. import type 只借类型,编译后整行消失。import type borrows the type only. The whole line disappears after compiling.
  5. subgraph 那条长 test script 是为了让 jest 能跑 ESM,不需要改但要认得。That long test script in the subgraph is there so jest can run ESM. You do not need to change it, but you should recognise it.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises1 个,就在这一页上面 —— 别攒着最后一起做1 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson类型、type 与 interfaceTypes, type and interface
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 异步:Promise、await、all 和 allSettledAsync: Promise, await, all and allSettled