Skip to content

Commit 5800398

Browse files
sxzzsxzzbot
andauthored
feat(cjs): warn when platform is ignored for the CJS format (#1111)
Co-authored-by: Kevin Bot <gh-bot@sxzz.moe>
1 parent 5d55836 commit 5800398

13 files changed

Lines changed: 172 additions & 34 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Each feature is self-contained and modular:
177177

178178
Plugins follow Rolldown's interface. Internal plugins are added based on config, user plugins append last. User plugins may additionally implement tsdown-specific hooks (`tsdownConfig` to modify user config before resolution, `tsdownConfigResolved` to read the resolved config) — see `src/features/plugin.ts`. The build supports dual-format output (ESM + CJS) with a second pass for CJS type declarations (`cjsDts`).
179179

180-
Public plugin exports in `src/plugins.ts`: `DepsPlugin`, `NodeProtocolPlugin`, `ReportPlugin`, `ShebangPlugin`, `WatchPlugin`
180+
Public plugin exports in `src/plugins.ts`: `DepsPlugin`, `NodeProtocolPlugin`, `ReportPlugin`, `ShebangPlugin`, `ShimsPlugin`, `WatchPlugin`
181181

182182
### Key Architectural Patterns
183183

‎__snapshots__/tsnapi/plugins.snapshot.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export declare function DepsPlugin({ pkg, deps: { neverBundle, alwaysBundle: jsA
66
export declare function NodeProtocolPlugin(_: "strip" | true): Plugin;
77
export declare function ReportPlugin(_: ResolvedConfig, _?: boolean, _?: boolean): Plugin;
88
export declare function ShebangPlugin(_: Logger, _: string, _?: string, _?: boolean): Plugin;
9+
export declare function ShimsPlugin(_: ResolvedConfig, _: NormalizedFormat): Plugin;
910
export declare function WatchPlugin(_: Set<string>, { config, chunks }: TsdownBundle): Plugin;
1011
// #endregion

‎__snapshots__/tsnapi/plugins.snapshot.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export function DepsPlugin(_, _) {}
66
export function NodeProtocolPlugin(_) {}
77
export function ReportPlugin(_, _, _) {}
88
export function ShebangPlugin(_, _, _, _) {}
9+
export function ShimsPlugin(_, _) {}
910
export function WatchPlugin(_, _) {}
1011
// #endregion

‎docs/options/platform.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tsdown --platform neutral
1717
- **`neutral`:** A platform-agnostic target with no specific runtime assumptions. Use this if your code is intended to run in multiple environments or you want full control over runtime behavior. This is particularly useful for libraries or shared code that may be used in both Node.js and browser environments.
1818

1919
> [!NOTE]
20-
> For the CJS format, the platform is always set to `'node'` and cannot be changed. [Why?](https://github.com/rolldown/rolldown/pull/4693#issuecomment-2912229545)
20+
> For the CJS format, the platform is always set to `'node'`, and tsdown warns if you set `'browser'`. [Why?](https://github.com/rolldown/rolldown/pull/4693#issuecomment-2912229545) Use `platform: 'neutral'` if your code isn't browser-only, or set `inputOptions: { platform: 'browser' }` to build CJS for the browser anyway.
2121
2222
### Example
2323

‎docs/zh-CN/options/platform.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tsdown --platform neutral
1717
- **`neutral`:** 与平台无关的目标,不对特定运行时环境做假设。如果您的代码需要在多个环境中运行,或者您希望完全控制运行时行为,可以选择此选项。特别适合用于 Node.js 和浏览器环境的库或共享代码。
1818

1919
> [!NOTE]
20-
> 对于 CJS 格式,平台始终为 `'node'`,无法更改。 [为什么?](https://github.com/rolldown/rolldown/pull/4693#issuecomment-2912229545)
20+
> 对于 CJS 格式,平台始终为 `'node'`,设置为 `'browser'` 时 tsdown 会发出警告。 [为什么?](https://github.com/rolldown/rolldown/pull/4693#issuecomment-2912229545) 如果代码不只用于浏览器,请使用 `platform: 'neutral'`;如果仍需要为浏览器构建 CJS,可以设置 `inputOptions: { platform: 'browser' }`。
2121
2222
### 示例
2323

‎skills/tsdown/references/option-platform.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,23 @@ export default defineConfig({
9191

9292
## CJS Format Limitation
9393

94-
**CJS format always uses `node` platform** and cannot be changed.
94+
**CJS format always uses `node` platform**, and tsdown warns when `platform: 'browser'` is set.
9595

9696
```ts
9797
export default defineConfig({
9898
entry: ['src/index.ts'],
9999
format: ['cjs'],
100-
platform: 'browser', // Ignored for CJS
100+
platform: 'browser', // Ignored for CJS, with a warning
101+
})
102+
```
103+
104+
Use `platform: 'neutral'` if the code isn't browser-only. To force the browser platform for CJS, override it via `inputOptions`:
105+
106+
```ts
107+
export default defineConfig({
108+
format: ['cjs'],
109+
platform: 'browser',
110+
inputOptions: { platform: 'browser' },
101111
})
102112
```
103113

‎src/features/cjs.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, test, vi } from 'vitest'
2+
import { warnCjsPlatform } from './cjs.ts'
3+
import type { ResolvedConfig } from '../config/types.ts'
4+
import type { InputOptions } from 'rolldown'
5+
6+
function run(config: Partial<ResolvedConfig>, inputOptions: InputOptions) {
7+
const warnOnce = vi.fn()
8+
warnCjsPlatform(
9+
{ logger: { warnOnce }, ...config } as unknown as ResolvedConfig,
10+
inputOptions,
11+
)
12+
return warnOnce
13+
}
14+
15+
describe('warnCjsPlatform', () => {
16+
test('warns when cjs is built for a non-node platform', () => {
17+
const warnOnce = run(
18+
{ format: 'cjs', platform: 'browser' },
19+
{ platform: 'node' },
20+
)
21+
expect(warnOnce).toHaveBeenCalledOnce()
22+
expect(warnOnce.mock.calls[0].join(' ')).toContain(
23+
"`platform: 'browser'` is ignored",
24+
)
25+
})
26+
27+
test('does not warn when the platform is forced via inputOptions', () => {
28+
const warnOnce = run(
29+
{ format: 'cjs', platform: 'browser' },
30+
{ platform: 'browser' },
31+
)
32+
expect(warnOnce).not.toHaveBeenCalled()
33+
})
34+
35+
test('does not warn for the neutral platform', () => {
36+
const warnOnce = run(
37+
{ format: 'cjs', platform: 'neutral' },
38+
{ platform: 'node' },
39+
)
40+
expect(warnOnce).not.toHaveBeenCalled()
41+
})
42+
43+
test('does not warn for the node platform', () => {
44+
const warnOnce = run(
45+
{ format: 'cjs', platform: 'node' },
46+
{ platform: 'node' },
47+
)
48+
expect(warnOnce).not.toHaveBeenCalled()
49+
})
50+
51+
test('does not warn for other formats', () => {
52+
const warnOnce = run(
53+
{ format: 'es', platform: 'browser' },
54+
{ platform: 'browser' },
55+
)
56+
expect(warnOnce).not.toHaveBeenCalled()
57+
})
58+
})

‎src/features/cjs.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { coerce, parseRange, satisfies } from 'verkit'
22
import type { ResolvedConfig } from '../config/index.ts'
3+
import type { InputOptions } from 'rolldown'
34

45
const RANGE_REQUIRING_ESM = parseRange('^20.19.0 || >=22.12.0')
56

@@ -27,3 +28,25 @@ export function warnLegacyCJS(config: ResolvedConfig): void {
2728
)
2829
}
2930
}
31+
32+
export function warnCjsPlatform(
33+
config: ResolvedConfig,
34+
inputOptions: InputOptions,
35+
): void {
36+
if (
37+
config.format !== 'cjs' ||
38+
// `neutral` makes no runtime assumptions, so building CJS for Node.js is expected
39+
config.platform !== 'browser' ||
40+
// the user forced another platform via `inputOptions`
41+
inputOptions.platform !== 'node'
42+
) {
43+
return
44+
}
45+
46+
config.logger.warnOnce(
47+
config.nameLabel,
48+
"The `cjs` format always targets Node.js, so `platform: 'browser'` is ignored for it.\n" +
49+
"Use `platform: 'neutral'` if the code isn't browser-only, " +
50+
"or set `inputOptions: { platform: 'browser' }` to build CJS for the browser anyway.",
51+
)
52+
}

‎src/features/rolldown.ts‎

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import pkg from '../../package.json' with { type: 'json' }
2121
import { mergeUserOptions } from '../config/options.ts'
2222
import { importWithError, pkgExists, toArray } from '../utils/general.ts'
2323
import { createSuppressWarnings, LogLevels } from '../utils/logger.ts'
24+
import { warnCjsPlatform } from './cjs.ts'
2425
import { DepsPlugin } from './deps.ts'
2526
import { NodeProtocolPlugin } from './node-protocol.ts'
2627
import { resolveChunkAddon, resolveChunkFilename } from './output.ts'
2728
import { ReportPlugin } from './report.ts'
2829
import { ShebangPlugin } from './shebang.ts'
29-
import { getShims } from './shims.ts'
30+
import { ShimsPlugin } from './shims.ts'
3031
import { WatchPlugin } from './watch.ts'
3132
import type {
3233
DtsOptions,
@@ -53,6 +54,7 @@ export async function getBuildOptions(
5354
cjsDts,
5455
isDualFormat,
5556
)
57+
if (!cjsDts) warnCjsPlatform(config, inputOptions)
5658

5759
const outputOptions: OutputOptions = await resolveOutputOptions(
5860
inputOptions,
@@ -102,7 +104,6 @@ async function resolveInputOptions(
102104
platform,
103105
plugins: userPlugins,
104106
report,
105-
shims,
106107
target,
107108
treeshake,
108109
tsconfig,
@@ -194,7 +195,7 @@ async function resolveInputOptions(
194195
plugins.push(...cssPostPlugins)
195196
}
196197

197-
let define: TransformOptions['define'] = {
198+
const define: TransformOptions['define'] = {
198199
...config.define,
199200
...Object.keys(env).reduce((acc, key) => {
200201
const value = JSON.stringify(env[key])
@@ -204,14 +205,8 @@ async function resolveInputOptions(
204205
}, Object.create(null)),
205206
}
206207

207-
let inject: TransformOptions['inject']
208-
if (shims && !cjsDts) {
209-
const shims = getShims(config)
210-
inject = shims.inject
211-
define = { ...define, ...shims.define }
212-
if (shims.plugin) {
213-
plugins.push(shims.plugin)
214-
}
208+
if (!cjsDts) {
209+
plugins.push(ShimsPlugin(config, format))
215210
}
216211

217212
// `neverBundle: true` is handled by DepsPlugin instead of rolldown's
@@ -261,7 +256,6 @@ async function resolveInputOptions(
261256
transform: {
262257
target,
263258
define,
264-
inject,
265259
},
266260
plugins,
267261
moduleTypes: {

‎src/features/shims.ts‎

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path'
22
import type { ResolvedConfig } from '../config/index.ts'
3+
import type { NormalizedFormat } from '../config/types.ts'
34
import type { Plugin } from 'rolldown'
45

56
export const shimFile: string = path.resolve(
@@ -26,24 +27,44 @@ const __TSDOWN_SHIM_FILENAME__ = /* @__PURE__ */ __tsdown_shims_url.fileURLToPat
2627
const __TSDOWN_SHIM_DIRNAME__ = /* @__PURE__ */ __tsdown_shims_path.dirname(__TSDOWN_SHIM_FILENAME__)
2728
`
2829

29-
export const shimsPlugin: Plugin = {
30-
name: 'tsdown:shims-banner',
31-
banner: shimsInjectCode,
30+
const cjsImportMetaDefine: Record<string, string> = {
31+
'import.meta.url': 'require("url").pathToFileURL(__filename).href',
32+
'import.meta.filename': '__filename',
33+
'import.meta.dirname': '__dirname',
3234
}
3335

34-
export function getShims(config: ResolvedConfig): {
35-
define?: Record<string, string>
36-
inject?: Record<string, [string, string]>
37-
plugin?: Plugin
38-
} {
39-
if (config.format !== 'es' || config.platform !== 'node') return {}
40-
41-
if (config.unbundle) {
42-
return {
43-
define: shimsDefine,
44-
plugin: shimsPlugin,
45-
}
46-
}
36+
/**
37+
* Shims are decided in the `options` hook, from the final `platform` after the
38+
* user's `inputOptions` are merged.
39+
*/
40+
export function ShimsPlugin(
41+
config: ResolvedConfig,
42+
format: NormalizedFormat,
43+
): Plugin {
44+
let banner = false
45+
46+
return {
47+
name: 'tsdown:shims',
48+
options(options) {
49+
const isNode = options.platform === 'node'
50+
const transform = (options.transform ||= {})
4751

48-
return { inject: shimsInject }
52+
// Rolldown only rewrites `import.meta` for CJS on the node platform
53+
if (format === 'cjs') {
54+
if (!isNode) {
55+
transform.define = { ...cjsImportMetaDefine, ...transform.define }
56+
}
57+
return
58+
}
59+
60+
if (format !== 'es' || !config.shims || !isNode) return
61+
if (config.unbundle) {
62+
transform.define = { ...transform.define, ...shimsDefine }
63+
banner = true
64+
} else {
65+
transform.inject = { ...transform.inject, ...shimsInject }
66+
}
67+
},
68+
banner: () => (banner ? shimsInjectCode : ''),
69+
}
4970
}

0 commit comments

Comments
 (0)