Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content about nodejs loader #349

Merged
merged 12 commits into from
Nov 15, 2024
17 changes: 13 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"files": {
"include": ["**/*.{ts,tsx,js,jsx,mts,cts,css}"],
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.css", "**/*.json"],
"ignore": [
"./node_modules",
"./.pnp",
Expand Down Expand Up @@ -41,8 +41,7 @@
"useSortedClasses": "error",
"noCommonJs": "error",
"noEnum": "error",
"noUnknownTypeSelector": "error",
"noDescendingSpecificity": "error"
"noUnknownTypeSelector": "error"
},
"recommended": false,
"complexity": {
Expand All @@ -69,7 +68,8 @@
"noUnsafeDeclarationMerging": "error",
"useNamespaceKeyword": "error",
"noEmptyBlock": "error",
"noDuplicateAtImportRules": "error"
"noDuplicateAtImportRules": "error",
"noDuplicateObjectKeys": "error"
}
}
},
Expand Down Expand Up @@ -139,5 +139,14 @@
"parser": {
"cssModules": true
}
},
"json": {
"formatter": {
"enabled": true,
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"indentStyle": "space"
}
}
}
4 changes: 3 additions & 1 deletion components/Common/Codebox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const Codebox: FC<CodeboxProps> = async props => {
if (!isValidElement(props.children)) return null;

const code = props.children.props.children.trim();
const lang = props.children.props.className.replace('language-', '').replace('mjs', 'js');
const lang = props.children.props.className
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems wrong to use classname (but if that's how some tool works, sure)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have test it manually 😇
"I am the test runner"

.replace('language-', '')
.replace('mjs', 'js');

const html = await codeToHtml(code, {
theme: 'vitesse-light',
Expand Down
148 changes: 77 additions & 71 deletions content/blog/how-to-use-nodejs-loader.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ authors: AugustinMauroy

A loader in the context of Node.js is a mechanism that allows developers to customize the behavior of module resolution and loading. It provides hooks that can be used to intercept and modify how Node.js handles `import` statements and `require` calls. This customization can be particularly useful for tasks such as:

- Loading non-JavaScript files (e.g., TypeScript, CoffeeScript)
- Loading non-JavaScript files (e.g., TypeScript, CoffeeScript, CSS, yaml)
- Transpiling code on-the-fly
- Implementing custom module formats
- Overriding or extending the default module resolution logic
Expand All @@ -22,7 +22,7 @@ A loader in Node.js is essentially a set of custom functions that allow you to c

#### The `resolve` Hook

The `resolve` hook is responsible for determining the location of a module given a specifier. A specifier is the string or URL that you pass to `import` or `require`. For example, when you write `import 'some-module'`, the `resolve` hook helps Node.js figure out where `some-module` is located.
The `resolve` hook is responsible for determining the location of a module, given a specifier. A specifier is the string or URL that you pass to `import` or `require`. For example, when you write `import 'some-module'`, the `resolve` hook helps Node.js figure out where `some-module` is located.

Here's a breakdown of what the `resolve` hook does:

Expand Down Expand Up @@ -54,13 +54,13 @@ The `load` hook allows you to customize how Node.js loads modules. For example,

Let's look at a simple example of a loader that handles `.coffee` files by transpiling them to JavaScript:

```mjs
// coffee-loader.mjs
```mjs fileName="coffee-loader.mjs"
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import coffeescript from 'coffeescript';
import { findPackageJSON } from 'node:module';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

Expand All @@ -81,16 +81,16 @@ export async function load(url, context, nextLoad) {
}

async function getPackageType(url) {
const isFilePath = !!extname(url);
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
const packagePath = resolvePath(dir, 'package.json');
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});

return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
const packagePath = findPackageJSON('..', import.meta.url);
if (packagePath) {
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
return type;
}
return 'module'; // Default to 'module' if no package.json is found
}
```

Expand All @@ -107,30 +107,40 @@ By defining these hooks, the loader can customize how Node.js handles different

Using the Node.js ESM loader involves creating custom hooks that intercept and modify the default behavior of module resolution and loading. These hooks allow you to handle non-JavaScript files, transpile code on-the-fly, and implement custom module formats. Here's a step-by-step guide on how to use the Node.js ESM loader:

#### Step 1: Create a Loader Module
### Step 1: Create the Loader Module

First, you need to create a module that exports the custom hooks. This module will contain the `resolve` and `load` hooks that define how modules are resolved and loaded.
First, create the loader module that handles `.coffee` files by transpiling them to JavaScript:
AugustinMauroy marked this conversation as resolved.
Show resolved Hide resolved

Here's an example of a loader module that handles `.coffee` files by transpiling them to JavaScript:

```mjs
// coffee-loader.mjs
```mjs fileName="coffee-loader.mjs"
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import coffeescript from 'coffeescript';
import { findPackageJSON } from 'node:module';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

export async function resolve(specifier, context, nextResolve) {
const resolvedUrl = new URL(specifier, context.parentURL).href;
if (extensionsRegex.test(resolvedUrl)) {
const format = await getPackageType(resolvedUrl);
return {
url: resolvedUrl,
format,
shortCircuit: true,
};
}
return nextResolve(specifier, context);
AugustinMauroy marked this conversation as resolved.
Show resolved Hide resolved
}

export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
const format = await getPackageType(url);
const { source: rawSource } = await nextLoad(url, { ...context, format });
const { source: rawSource } = await nextLoad(url, context);
const transformedSource = coffeescript.compile(rawSource.toString(), url);

return {
format,
format: context.format,
shortCircuit: true,
source: transformedSource,
};
Expand All @@ -140,73 +150,69 @@ export async function load(url, context, nextLoad) {
}

async function getPackageType(url) {
const isFilePath = !!extname(url);
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
const packagePath = resolvePath(dir, 'package.json');
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});

return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
const packagePath = findPackageJSON('..', import.meta.url);
if (packagePath) {
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
return type;
}
return 'module'; // Default to 'module' if no package.json is found
}
```

#### Step 2: Register the Loader
### Step 2: Create the Registration Module

Next, you need to register the loader module using the `register` method from `node:module`. This can be done using the `--import` flag when running your Node.js application.
Next, create a separate registration module that registers the loader:

Here's an example of how to register the loader module:
```mjs fileName="register-coffee-loader.mjs"
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';

```bash
node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffee-loader.mjs"));' ./main.coffee
register(pathToFileURL(new URL('./coffee-loader.mjs', import.meta.url)));
```

In this example, the `register` method is called with the path to the `coffee-loader.mjs` module. This ensures that the custom hooks are registered before any application files are imported.
### Step 3: Use the Registration Module with `--import`

Now, you can use the `--import` flag to import the registration module when running your Node.js application:

```bash
node --import ./register-coffee-loader.mjs ./main.coffee
```

#### Step 3: Use the Loader in Your Application
### Step 4: Create the Main Application File

Once the loader is registered, you can use it in your application to handle non-JavaScript files. For example, you can import `.coffee` files directly in your JavaScript code:
Create the main application file that imports `.coffee` files:

```coffee
# main.coffee
```coffee fileName="main.coffee"
import { scream } from './scream.coffee'
console.log scream 'hello, world'

import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
```

```coffee
# scream.coffee
```coffee fileName="scream.coffee"
export scream = (str) -> str.toUpperCase()
```

When you run the application with the registered loader, the `.coffee` files will be transpiled to JavaScript on-the-fly, and the transpiled code will be executed by Node.js.
### Step 5: Run the Application

#### Step 4: Chain Multiple Loaders (Optional)
Finally, run the application with the registered loader:

You can also chain multiple loaders to create more complex module resolution and loading behaviors. To do this, simply call the `register` method multiple times with different loader modules:

```mjs
// entrypoint.mjs
import { register } from 'node:module';

register('./foo-loader.mjs', import.meta.url);
register('./bar-loader.mjs', import.meta.url);
await import('./my-app.mjs');
```bash
node --import ./register-coffee-loader.mjs ./main.coffee
```

In this example, the `foo-loader.mjs` and `bar-loader.mjs` modules are registered in sequence. The registered hooks will form chains that run last-in, first-out (LIFO). This means that the hooks in `bar-loader.mjs` will be called first, followed by the hooks in `foo-loader.mjs`, and finally the default Node.js hooks.

### Conclusion

Using the Node.js ESM loader involves creating custom hooks that intercept and modify the default behavior of module resolution and loading. By registering these hooks, you can handle non-JavaScript files, transpile code on-the-fly, and implement custom module formats. This customization can be particularly useful for tasks such as loading TypeScript or CoffeeScript files, testing React components, and more.

By following these steps, you can leverage the power of the Node.js ESM loader to tailor the module system to better suit your specific needs, making it easier to work with different file types and module formats.

### What a loader can do?
### What can a loader do?

A loader in Node.js can significantly enhance the capabilities of the module system by allowing developers to customize how modules are resolved and loaded. This customization can be particularly useful for a variety of tasks, including:

Expand Down Expand Up @@ -242,13 +248,13 @@ Loaders can be integrated with build tools like Webpack, Rollup, or Parcel. This

Here's an example of a loader that transpiles CoffeeScript files to JavaScript:

```mjs
// coffee-loader.mjs
```mjs fileName="coffee-loader.mjs"
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import coffeescript from 'coffeescript';
import { findPackageJSON } from 'node:module';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

Expand All @@ -269,16 +275,16 @@ export async function load(url, context, nextLoad) {
}

async function getPackageType(url) {
const isFilePath = !!extname(url);
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
const packagePath = resolvePath(dir, 'package.json');
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});

return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
const packagePath = findPackageJSON('..', import.meta.url);
if (packagePath) {
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
return type;
}
return 'module'; // Default to 'module' if no package.json is found
}
```

Expand Down
Loading