Skip to content

Commit db33bd5

Browse files
committed
build(cjs): support named exports
- https://2ality.com/2022/10/commonjs-named-exports.html Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
1 parent f8d6672 commit db33bd5

File tree

5 files changed

+191
-7
lines changed

5 files changed

+191
-7
lines changed

Diff for: build.config.ts

+177-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
* @see https://github.com/flex-development/mkbuild
55
*/
66

7-
import { defineBuildConfig, type Config } from '@flex-development/mkbuild'
7+
import { EXT_DTS_REGEX } from '@flex-development/ext-regex'
8+
import {
9+
defineBuildConfig,
10+
type Config,
11+
type OutputMetadata
12+
} from '@flex-development/mkbuild'
13+
import * as mlly from '@flex-development/mlly'
14+
import pathe from '@flex-development/pathe'
15+
import type { BuildResult, OutputFile, PluginBuild } from 'esbuild'
16+
import util from 'node:util'
817
import pkg from './package.json' assert { type: 'json' }
918

1019
/**
@@ -13,7 +22,173 @@ import pkg from './package.json' assert { type: 'json' }
1322
* @const {Config} config
1423
*/
1524
const config: Config = defineBuildConfig({
16-
entries: [{}, { format: 'cjs' }],
25+
entries: [
26+
{},
27+
{
28+
format: 'cjs',
29+
plugins: [
30+
{
31+
name: 'named-exports',
32+
setup({ initialOptions, onEnd }: PluginBuild): void {
33+
const { absWorkingDir = process.cwd(), format } = initialOptions
34+
35+
// do nothing if format is not commonjs
36+
if (format !== 'cjs') return void format
37+
38+
// add named exports
39+
return void onEnd(
40+
async (
41+
result: BuildResult<{ metafile: true; write: false }>
42+
): Promise<void> => {
43+
/**
44+
* Named exports.
45+
*
46+
* @const {Set<string>} names
47+
*/
48+
const names: Set<string> = new Set<string>()
49+
50+
/**
51+
* Output file objects.
52+
*
53+
* @const {OutputFile[]} outputFiles
54+
*/
55+
const outputFiles: OutputFile[] = []
56+
57+
/**
58+
* Adds named exports to the given `output` file content.
59+
*
60+
* @param {string} output - Output file content
61+
* @param {string[]} exports - Named exports
62+
* @return {string} Output file content with named exports
63+
*/
64+
const nameExports = (
65+
output: string,
66+
exports: string[]
67+
): string => {
68+
if (exports.length > 0) {
69+
// get sourceMappingURL comment
70+
const [sourcemap = ''] = /\/\/#.+\n/.exec(output) ?? []
71+
72+
/**
73+
* Output file content.
74+
*
75+
* @var {string} text
76+
*/
77+
let text: string = output.replace(sourcemap, '')
78+
79+
// add named exports
80+
for (const name of exports) {
81+
names.add(name)
82+
text += `exports.${name} = module.exports.${name};\n`
83+
}
84+
85+
// alias default export
86+
text += 'exports = module.exports;\n'
87+
88+
// re-add sourceMappingURL comment
89+
return (text += sourcemap)
90+
}
91+
92+
return output
93+
}
94+
95+
// add named exports to output file content
96+
for (const output of result.outputFiles) {
97+
// skip declaration files
98+
if (EXT_DTS_REGEX.test(output.path)) {
99+
outputFiles.push(output)
100+
continue
101+
}
102+
103+
// skip interface and type definition files
104+
if (/(?:interfaces|types)\/.*$/.test(output.path)) {
105+
outputFiles.push(output)
106+
continue
107+
}
108+
109+
/**
110+
* Relative path to output file.
111+
*
112+
* **Note**: Relative to {@linkcode absWorkingDir}.
113+
*
114+
* @const {string} outfile
115+
*/
116+
const outfile: string = output.path
117+
.replace(absWorkingDir, '')
118+
.replace(/^\//, '')
119+
120+
/**
121+
* {@linkcode output} metadata.
122+
*
123+
* @const {OutputMetadata} metadata
124+
*/
125+
const metadata: OutputMetadata =
126+
result.metafile.outputs[outfile]!
127+
128+
// skip output files without entry points
129+
if (!metadata.entryPoint) {
130+
outputFiles.push(output)
131+
continue
132+
}
133+
134+
/**
135+
* TypeScript source code for current output file.
136+
*
137+
* @const {string} code
138+
*/
139+
const code: string = (await mlly.getSource(
140+
pathe.resolve(absWorkingDir, metadata.entryPoint)
141+
)) as string
142+
143+
/**
144+
* Output file content.
145+
*
146+
* @const {string} text
147+
*/
148+
const text: string = nameExports(
149+
output.text,
150+
mlly
151+
.findExports(code)
152+
.filter(s => s.syntax === mlly.StatementSyntaxKind.NAMED)
153+
.flatMap(statement => statement.exports)
154+
.map(name => name.replace(/^default as /, ''))
155+
.filter(name => name !== 'default')
156+
)
157+
158+
// add output file with named exports
159+
outputFiles.push({
160+
...output,
161+
contents: new util.TextEncoder().encode(text),
162+
text
163+
})
164+
}
165+
166+
return void (result.outputFiles = outputFiles.map(output => {
167+
// add named exports to package entry point
168+
if (output.path.endsWith('dist/index.cjs')) {
169+
/**
170+
* Output file content.
171+
*
172+
* @const {string} text
173+
*/
174+
const text: string = nameExports(output.text, [...names])
175+
176+
return {
177+
...output,
178+
contents: new util.TextEncoder().encode(text),
179+
text
180+
}
181+
}
182+
183+
return output
184+
}))
185+
}
186+
)
187+
}
188+
}
189+
]
190+
}
191+
],
17192
sourcemap: true,
18193
sourcesContent: false,
19194
target: 'node' + pkg.engines.node.replace(/^\D+/, ''),

Diff for: loader.mjs

+8-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ export const load = async (url, context) => {
6161

6262
// transform typescript files
6363
if (/^\.(?:cts|mts|tsx?)$/.test(ext) && !/\.d\.(?:cts|mts|ts)$/.test(url)) {
64+
// push require condition for .cts files and update format
65+
if (ext === '.cts') {
66+
context.conditions = context.conditions ?? []
67+
context.conditions.unshift('require', 'node')
68+
context.format = mlly.Format.MODULE
69+
}
70+
6471
// resolve path aliases
6572
source = await mlly.resolveAliases(source, {
6673
aliases: tsconfig.compilerOptions.paths,
@@ -77,7 +84,7 @@ export const load = async (url, context) => {
7784

7885
// transpile source code
7986
const { code } = await esbuild.transform(source, {
80-
format: ext === '.cts' ? 'cjs' : 'esm',
87+
format: 'esm',
8188
loader: ext.slice(/^\.[cm]/.test(ext) ? 2 : 1),
8289
minify: false,
8390
sourcefile: fileURLToPath(url),

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"devDependencies": {
8686
"@commitlint/cli": "17.4.4",
8787
"@commitlint/lint": "17.4.4",
88+
"@flex-development/ext-regex": "1.0.0",
8889
"@flex-development/mkbuild": "1.0.0-alpha.14",
8990
"@flex-development/mlly": "1.0.0-alpha.13",
9091
"@flex-development/pathe": "1.0.3",

Diff for: typings/node/loader.d.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ declare global {
99
/**
1010
* Export conditions of relevant `package.json`.
1111
*/
12-
conditions: string[]
12+
conditions?: string[]
1313

1414
/**
1515
* Module format.
@@ -49,11 +49,11 @@ declare global {
4949
* Determines how `url` should be interpreted, retrieved, and parsed.
5050
*
5151
* @see {@linkcode LoadHookContext}
52-
* @see https://nodejs.org/docs/latest-v16.x/api/esm.html#loadurl-context-nextload
52+
* @see https://nodejs.org/api/esm.html#loadurl-context-nextload
5353
*
5454
* @async
5555
*
56-
* @param {string} url - Module URL
56+
* @param {string} url - Resolved module URL
5757
* @param {LoadHookContext} context - Hook context
5858
* @param {LoadHook} nextLoad - Subsequent `load` hook in the chain or default
5959
* Node.js `load` hook after last user-supplied `load` hook
@@ -116,7 +116,7 @@ declare global {
116116
* optionally its format (such as `'module'`) as a hint to the `load` hook.
117117
*
118118
* @see {@linkcode ResolveHookContext}
119-
* @see https://nodejs.org/docs/latest-v16.x/api/esm.html#resolvespecifier-context-nextresolve
119+
* @see https://nodejs.org/api/esm.html#resolvespecifier-context-nextresolve
120120
*
121121
* @async
122122
*

Diff for: yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,7 @@ __metadata:
11481148
"@commitlint/cli": "npm:17.4.4"
11491149
"@commitlint/lint": "npm:17.4.4"
11501150
"@commitlint/types": "npm:17.4.4"
1151+
"@flex-development/ext-regex": "npm:1.0.0"
11511152
"@flex-development/mkbuild": "npm:1.0.0-alpha.14"
11521153
"@flex-development/mlly": "npm:1.0.0-alpha.13"
11531154
"@flex-development/pathe": "npm:1.0.3"

0 commit comments

Comments
 (0)