diff --git a/src/routes/drafts/typescript_module_resolution_explained.mdx b/src/routes/drafts/typescript_module_resolution_explained.mdx new file mode 100644 index 0000000..1e18c48 --- /dev/null +++ b/src/routes/drafts/typescript_module_resolution_explained.mdx @@ -0,0 +1,377 @@ +--- +meta: + title: I am the title + description: I am the description + dateCreated: 2022-11-28 +tags: + - "typescript" + +--- +import { InfoPanel } from "@/components/InfoPanel/InfoPanel" + +import { TextHighlight } from "@blacksheepcode/react-text-highlight"; + + +

This post very much falls into a true purpose of this blog, which is to serve as a reference for myself to look up later.

+
+ + + +The [`moduleResolution`](https://www.typescriptlang.org/tsconfig/#moduleResolution) property of TypeScript's tsconfig.json is likely one of the less understood options - one that people change randomly until their application works and then they don't touch again. + + +## First - forget TypeScript - let's understand how NodeJS behaves as it relates to modules + +Understanding the behaviour of NodeJS and modules is the key to understanding this whole thing. + + + +### CJS and ESM + +There are other module formats - but for our purposes we can ignore them. + +#### CommonJS (CJS) + +CJS (CommonJS) modules look like this: + +```js +module.exports = { + foo: () => console.log("hello world!"); +} +``` + +or + +```js +exports.foo = () => {} +``` + +and imported like + +```js +// package import +const {bar} = require("the-package"); + +// Relative module import +const {foo} = require("./the-module.js"); +``` + +CJS modules are the OG module format, developed specifically for node: + +> CommonJS modules are the original way to package JavaScript code for Node.js. + +[Link](https://nodejs.org/api/modules.html#modules-commonjs-modules) + +CJS modules _only_ work in a Node environment. Of course - they can be transpiled/bundled to create code that can run in a browser environment. + +#### EcmaScript Modules (ESM) + +EcmaScript Modules are the 'modern' module format that are specced by the [EcmaScript] standard itself](https://tc39.es/ecma262/#sec-modules). That is, anything that is said to be running JavaScript, should support ESM modules. + +**Node can run both ESM and CJS modules and [has done so since Node 14 (2020)](https://nodejs.org/en/blog/release/v14.0.0)**. + +Node supports ESM modules as a first class citizen. + + + + +### File extensions - the rules are different depending on whether you are using `require` or `import` + + +

For now, assume that you are running code without a package.json present.

+

The presence of a package.json changes Node's behaviour.

+
+ + +#### `require` - File extensions are optional + + +>If the exact filename is not found, then Node.js will attempt to load the required filename with the added extensions: .js, .json, and finally .node. +https://nodejs.org/api/modules.html#file-modules + +So something like this is valid + +``` +//main.js +const {foo} = require("./foo"); // Where the file is foo.js +``` + +so is this: + +``` +//main.js +const {foo} = require('./foo.js"); +``` + +But this: +``` +//main.js +const {foo} = require('./foo"); // Where the file is foo.cjs +``` + +Is not ok, they have mismatching file extensions so an explicit file extension is required: + +``` +//main.js +const {foo} = require('./foo.cjs"); +``` + + +#### `import` - File extensions are always required + +> A file extension must be provided when using the import keyword to resolve relative or absolute specifiers. Directory indexes (e.g. './startup/index.js') must also be fully specified. + +https://nodejs.org/api/esm.html#mandatory-file-extensions + +This is possibly the first trip up. As web developers we've been using bundlers for so long, we've just become used to dropping the file extensions - but when using `import` it is always required. + +We were using bundlers _before_ ESM modules were supported by Node - and so when it comes to running ESM in Node directly, all of a sudden there are these new constraints. + +### `.cjs` and `.mjs` files determine accepted module syntax. + +The file extensions `.cjs` (for CJS modules) and `.mjs` (for ESM modules) _are not mere convention_. + +Node uses these file extensions to determine which module format it expects the code to be written in. + +eg. + +``` +// alpha.cjs +export function foo() { + +} + +``` + +``` +node alpha.cjs +``` + +Will error with: + +``` +export function foo() {} +^^^^^^ + +SyntaxError: Unexpected token 'export' +``` + + +As will: + +``` +// bravo.mjs +module.exports = { + +} +``` + + +``` +node bravo.mjs +``` + +``` +module.exports = {}; +^ + +ReferenceError: module is not defined in ES module scope +``` + +So will: + +``` +//charlie.mjs +const path = require("node:path"); + +console.log(path); + +``` + +``` +node charlie.mjs +``` + + +``` +const path = require("node:path"); + ^ + +ReferenceError: require is not defined in ES module scope, you can use import instead +``` + + +### The `type` property of the `package.json` determines the expected module type of `.js` files + + + +

Now assume you are working with a package.json

+
+ +> Authors can tell Node.js to interpret JavaScript as an ES module via the .mjs file extension, the package.json "type" field with a value "module", or the --input-type flag with a value of "module". These are explicit markers of code being intended to run as an ES module. + +> Inversely, authors can explicitly tell Node.js to interpret JavaScript as CommonJS via the .cjs file extension, the package.json "type" field with a value "commonjs", or the --input-type flag with a value of "commonjs". + +> When code lacks explicit markers for either module system, Node.js will inspect the source code of a module to look for ES module syntax. + +https://nodejs.org/api/esm.html#enabling + +So in the absense of a package json, we can do + +``` +// delta.js +module.exports = () => {}; + +console.log("hello"); + +``` + +``` +node delta.js +``` + +And this be fine. + +Or + +``` +//echo.js +export default () => {}; + +console.log("hello"); + +``` + +``` +node echo.js +``` + +However, in the presences of a package.json with `"type": "module"` + +if we run + +``` +node delta.js +``` + +We will get +``` +module.exports = () => {}; +^ + +ReferenceError: module is not defined in ES module scope +This file is being treated as an ES module because it has a '.js' file extension and '/Users/davidjohnston/git-workspace/module-play/01_no_package_json copy/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension. +``` + +Similarly if we have a package.json with `"type": "commonjs"` and we run + +``` +node echo.js +``` + +We get: + +``` +(node:72515) Warning: Failed to load the ES module: /Users/davidjohnston/git-workspace/module-play/02_no_package_json copy 2/echo.js. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension. +(Use `node --trace-warnings ...` to show where the warning was created) +/Users/davidjohnston/git-workspace/module-play/02_no_package_json copy 2/echo.js:1 +export default () => {}; +^^^^^^ + +SyntaxError: Unexpected token 'export' +``` + + +### CJS and ESM Modules are generally interoperable + +Take an example where we do not have a package.json and we have files of the following format: + +| Function Name | Module Format | Extension | +| ------------- | ------------- | --------- | +| f1 | ESM | .js | +| f2 | ESM | .mjs | +| f3 | CJS | .js | +| f4 | CJS | .mjs | + +Now we run our script + +``` +//main_esm.js +import { f1 } from "./f1_esm.js"; +import { f2 } from "./f2_esm.mjs"; +import { f3 } from "./f3_cjs.js"; +import { f4 } from "./f4_cjs.cjs"; + +f1(); +f2(); +f3(); +f4(); + +``` + +``` +node main_esm.js +``` + +This runs fine. + +As does an equivilent CJS script. + +In the absence of otherwise being explicitly told via `type` property in the package json - Node inspects the format of the modules being imported and treats them accordingly. + + +On the other hand, if we are in an environment where we have `"type": "module"` in our `package.json`, Node will treat the `.js` files _only_ as ESM modules and the import of `f3_cjs.js` will fail: + +``` +node +``` + +``` +import { f3 } from "./f3_cjs.js"; + ^^ +SyntaxError: The requested module './f3_cjs.js' does not provide an export named 'f3' +``` + +We get a similar - and slightly more helpful error when we try run the equivilent CJS script + +``` +exports.f3 = () => { +^ + +ReferenceError: exports is not defined in ES module scope +This file is being treated as an ES module because it has a '.js' file extension and '/Users/davidjohnston/git-workspace/module-play/01_type_module/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension. +``` + + + + +### Dyanmic imports are allowed in CJS modules + +### Limits of ESM and CJS interoperability + +The main constraint on ESM and CJS interopability is that CJS modules can not import ESM modules that contain a top level `await` + +> require() only supports loading ECMAScript modules that meet the following requirements: + +> The module is fully synchronous (contains no top-level await); and + +https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require + + +## Summarising + +Both CJS and ESM are fully valid in Node. + +`.cjs` files will always be treated as CJS modules and `.mjs` files will always be treated as ESM modules. + +ESM modules always need to specify file extensions for relative imports. + +CJS modules can omit file extensions and Node will attempt to find files and index files + +Where some confusion can arise is with `.js` files - when it depends on whether Node has been explicitly instructed what module format to use. + +It might be a good convention to ban `.js` files from your codebase - remove any ambiguity. + + + + diff --git a/src/routes/drafts/typescript_module_resolution_explained_part_2.mdx b/src/routes/drafts/typescript_module_resolution_explained_part_2.mdx new file mode 100644 index 0000000..fc45fd6 --- /dev/null +++ b/src/routes/drafts/typescript_module_resolution_explained_part_2.mdx @@ -0,0 +1,82 @@ +--- +meta: + title: I am the title + description: I am the description + dateCreated: 2022-11-28 +tags: + - "typescript" + +--- +import { InfoPanel } from "@/components/InfoPanel/InfoPanel" + +import { TextHighlight } from "@blacksheepcode/react-text-highlight"; + + +Now that we understand some of the nuance of Node's module resolution behaviour - let's turn to TypeScript. + +[TypeScript has a good write up here.](https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution) + +>With or without modules involved, the compiler needs to know about the code’s intended runtime environment—what globals are available, for example. + +1. There maybe new(ish) syntax like optional chaining (`foo?.bar`) or constructs (eg. `Promise` has 'only' had widespread support since 2015). +2. Module syntax itself needs to be understood by the runtime executing it - eg. these days browsers can understand `import X from "./x"` syntax - but https://nodejs.org/api/packages.html#determining-module-system

}>Node will only respect this format within certain contexts.
. + +TypeScript uses the term 'host' - + +> When the output code (whether produced by tsc or a third-party transpiler) is run directly in a runtime like Node.js, the runtime is the host. + +e.g. You compile some code with `tsc` and then run the outputted file with `node`. + +> When there is no “output code” because a runtime consumes TypeScript files directly, the runtime is still the host. + +e.g. You run some code directly with [`tsx`](https://www.npmjs.com/package/tsx). + +> When a bundler consumes TypeScript inputs or outputs and produces a bundle, the bundler is the host, because it looked at the original set of imports/requires, looked up what files they referenced, and produced a new file or set of files where the original imports and requires are erased or transformed beyond recognition. (That bundle itself might comprise modules, and the runtime that runs it will be its host, but TypeScript doesn’t know about anything that happens post-bundler.) + +Hmmm. + +>When loading modules in a web browser, the behaviors TypeScript needs to model are actually split between the web server and the module system running in the browser. The browser’s JavaScript engine (or a script-based module-loading framework like RequireJS) controls what module formats are accepted, while the web server decides what file to send when one module triggers a request to load another. + +Hmmm. + + + +## The `module` option + +https://www.typescriptlang.org/tsconfig/#module + +This just changes the _outputted_ module format. + +One important thing to note here, is that while Node can understand ESM modules - it needs to be done right: + +- Files need to be named `.cjs` or `.mjs` + +or: + +- Package.json needs to have `type: "module"` + +TIL: If compiling code with one of the "node" module formats - TypeScript is still smart enough to compile ESM style modules - if they have file extensions `.mts` or `.cts`. + + +>CommonJS isn’t part of the ECMAScript specification, so runtimes, bundlers, and transpilers have been free to make up their own answers to these questions since ESM was standardized in 2015 + + + + +## `moduleResolution` + +Essentially 'how does TypeScript find the file you are talking about?'. + +The ones we need to care about are + +- Node16 (note that nodenext is identical to node16) +- Bundler + + + +Node16 - + +> In Node.js, module specifiers in import statements and dynamic import() calls are not allowed to omit file extensions or /index.js suffixes, while module specifiers in require calls are + +Bundler - + diff --git a/src/routes/drafts/typescript_module_resolution_explained_part_2_package_resolution.mdx b/src/routes/drafts/typescript_module_resolution_explained_part_2_package_resolution.mdx new file mode 100644 index 0000000..2b15bd1 --- /dev/null +++ b/src/routes/drafts/typescript_module_resolution_explained_part_2_package_resolution.mdx @@ -0,0 +1,49 @@ +--- +meta: + title: I am the title + description: I am the description + dateCreated: 2022-11-28 +tags: + - "typescript" + +--- + + + + +import { InfoPanel } from "@/components/InfoPanel/InfoPanel" + +import { TextHighlight } from "@blacksheepcode/react-text-highlight"; + + +OTHER USEFUL RESOURCES: +https://github.com/nodejs/package-examples + +TODO find that history of javascript repo + + + + + + + +In the last post we got into the nuance of module resolution for relative (e.g. `"./foo.js"`) imports. + +Now let's turn to package resolution - the behaviour of Node as it relates to resolving imports like: + +``` +import foo from "mypackage"; +``` + +or + +``` +const foo = require("mypackage"); +``` + +This section is, in my opinion, a lot simpler. There weren't any surprises here for me. + +The main reason we want to talk about this is that it's important to know about the `exports` property of the package.json. + + +## Package imports \ No newline at end of file diff --git a/src/routes/posts/adding_msw_bundler_to_remix_app_2.mdx b/src/routes/posts/adding_msw_bundler_to_remix_app_2.mdx index 32d65d9..6d6632d 100644 --- a/src/routes/posts/adding_msw_bundler_to_remix_app_2.mdx +++ b/src/routes/posts/adding_msw_bundler_to_remix_app_2.mdx @@ -14,7 +14,6 @@ tags: import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; -import { InfoPanel } from "@/components/InfoPanel/InfoPanel" import { TextHighlight } from "@blacksheepcode/react-text-highlight"; In the [previous post](./adding_msw_bundler_to_remix_app) I outlined how to make use of mdx-bundler to access your frontmatter metadata. This approach requires file system access as the files are parsed at run time.