Build shared Client bundles independently for Node and browser

This commit is contained in:
imccyu committed 2026-10-14 14:43:59 +08:00
1 parent b2ea1d1b71
commit e137ebaaa1
17 files changed
+208 -42

No files matched your search

+3 -1
View File
@@ -1090,7 +1090,9 @@ export class ClientModuleRegistry extends Service {
const requested = requestUrl.pathname.slice(prefix.length)
const sourceMap = requested.endsWith('.map')
const fileName = sourceMap ? requested.slice(0, -'.map'.length) : requested
if (!CLIENT_CHUNK.test(fileName)) return undefined
// The Node Client entry and its split chunks share this directory but
// must never be served as browser factory scripts.
if (!CLIENT_CHUNK.test(fileName) || fileName === 'client.node.js' || fileName.startsWith('client.node.')) return undefined
if (resourceUrl !== chunkUrl(record.entry.id, fileName, record.entry.rev, sourceMap)) return undefined
return { record, fileName, sourceMap, resourceUrl }
}
@@ -915,6 +915,36 @@ describe('client bundle activation', () => {
expect((await routeRequest(route, url.replace(`rev=${row.rev}`, 'rev=stale'))).status).toBe(404)
})
it('loads a renamed browser export at the logical URL without exposing Node bundles as chunks', async () => {
const packageName = '@fixture/dual-client'
const legacyPath = writePackage(packageName, {
dsh: { client: { platform: 'web' } },
exports: {
'./client': {
types: './lib/types/client/index.d.ts',
node: './lib/client.node.js',
browser: './lib/client.browser.js',
default: './lib/client.browser.js',
},
'./package.json': './package.json',
},
})
mkdirSync(dirname(legacyPath), { recursive: true })
writeFileSync(join(dirname(legacyPath), 'client.browser.js'), 'module.exports = { browser: true }\n')
for (const name of ['client.node.js', 'client.node.helper.js']) {
writeFileSync(join(dirname(legacyPath), name), 'throw new Error("Node code must not reach a browser")\n')
}
const { service, route } = constructWithRoute([packageName])
const row = service.graph().entries[0]!
expect(row.url).toContain(`${packageName}/client.js`)
expect((await routeRequest(route, row.url)).body.toString('utf8')).toContain('browser: true')
for (const name of ['client.node.js', 'client.node.helper.js']) {
for (const suffix of ['', '.map']) {
expect((await routeRequest(route, chunkReference(packageName, name + suffix, row.rev))).status).toBe(404)
}
}
})
it('publishes a new chunk revision when a completed build rewrites only the entry timestamp', async () => {
const packageName = '@fixture/chunk-only-rebuild'
const clientPath = writePackage(packageName)
+37 -10
View File
@@ -104,7 +104,7 @@ function browserSourcePath(source: string, sourcemapPath: string): string {
* `import.meta.dirname`. Its `package.json` name is the plugin id stamped into
* the __ModuleLoader__.load handoff and onto the injected style tags.
* @param libEntry - node-half entries.
* @param options - phase placement, lib overrides, companion Node configs, and optional per-file Client banner.
* @param options - phase placement, lib overrides, optional independently bundled Node Client, companion configs, and per-file Client banner.
* @returns ENV-selected tsdown config for the current build face.
* @throws {Error} when `<packageDir>/package.json` is unreadable or declares no name.
*/
@@ -118,13 +118,16 @@ export function clientBundle(
return ({ env }) => {
const face = buildFace(env?.DSH_BUILD_FACE)
const clientEntry = face === undefined ? 'src/client/index.ts' : 'lib/types/client/index.js'
const client = clientConfig(manifest, clientEntry, options.clientBanner)
const client = clientConfig(manifest, clientEntry, options.clientBanner, options.dualClient === true)
const node = [lib, ...(options.companions ?? [])]
const clientFaces = options.dualClient === true
? [nodeClientConfig(manifest, clientEntry), client]
: [client]
if (face === 'host') return options.hostPhase === true ? node : [SKIP_WORKSPACE_BUILD]
if (face === 'client') {
return options.hostPhase === true ? [client] : [...node, client]
return options.hostPhase === true ? clientFaces : [...node, ...clientFaces]
}
return [...node, client]
return [...node, ...clientFaces]
}
}
@@ -208,6 +211,8 @@ export function clientOnly(configs: readonly UserConfig[]): BuildFaceConfig {
interface ClientBundleOptions {
/** Emit the Node-side artifacts during the Host pass instead of the Client pass. */
readonly hostPhase?: boolean
/** Emit independent ESM `client.node.js` and browser-factory `client.browser.js` from the same Client entry. */
readonly dualClient?: boolean
/** Additional Node-side configs emitted alongside the package library. */
readonly companions?: readonly UserConfig[]
/** Overrides for the package's primary Node-side library config. */
@@ -467,7 +472,30 @@ function asyncChunkRequirePlugin(): TsdownPlugin {
}
}
function clientConfig(manifest: PackageManifest, entry: string, clientBanner?: (fileName: string) => string | undefined): UserConfig {
/** Bundle the Client entry for plain Node independently of the Host entry and browser factory. */
function nodeClientConfig(manifest: PackageManifest, entry: string): UserConfig {
const isolation = clientInputIsolation(manifest.name)
return clientLibraryConfig(manifest, [entry], {
name: `${manifest.name}/client-node`,
entry: { 'client.node': entry },
sourcemap: true,
inputOptions: { resolve: { conditionNames: ['node', 'import', 'module', 'default'] } },
plugins: [tscSourceMapPlugin(), isolation.plugin],
outputOptions: {
entryFileNames: 'client.node.js',
chunkFileNames: 'client.node.[name].js',
sourcemapExcludeSources: false,
sourcemapPathTransform: isolation.sourcePath,
},
})
}
function clientConfig(
manifest: PackageManifest,
entry: string,
clientBanner?: (fileName: string) => string | undefined,
dualClient = false,
): UserConfig {
const id = manifest.name
let requested: ReadonlySet<string> | undefined
const isRequested = (specifier: string): boolean => (requested ??= clientExternals(manifest)).has(specifier)
@@ -475,9 +503,8 @@ function clientConfig(manifest: PackageManifest, entry: string, clientBanner?: (
return {
name: `${id}/client`,
entry: { client: entry },
// Browser bundle lands next to the node half (single lib/ artifact dir;
// the entryFileNames pin keeps it exactly lib/client.js). clean must stay
// off — a default clean would wipe the node-half output emitted above.
// Browser bundle lands beside the Node artifacts. clean must stay off:
// cleaning this invocation would wipe the independently emitted Node entry.
outDir: 'lib',
format: 'cjs',
platform: 'browser',
@@ -601,10 +628,10 @@ function clientConfig(manifest: PackageManifest, entry: string, clientBanner?: (
},
}],
outputOptions: {
entryFileNames: 'client.js',
entryFileNames: dualClient ? 'client.browser.js' : 'client.js',
// The imported source basename becomes the published chunk name; package
// files lists and artifact tests pin every intentional chunk.
chunkFileNames: 'client.[name].js',
chunkFileNames: dualClient ? 'client.browser.[name].js' : 'client.[name].js',
sourcemapExcludeSources: false,
// The map is served from /plugins/<scoped-package>/client.js.map. The
// browser resolves its local sources back into URLs that mirror the
+4 -4
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-typert-registry/use-this-package:
en: d885ccd8f13315f1
zh: cea7c258f5f57e2d
en: eeeaecf7800bcb6b
zh: 2562380c15cb2428
/deepseek-ai-dsh-typert-registry/use-this-package/minimal-setup:
en: 1ec4b304a024bb3c
zh: f17e796c26faabab
@@ -24,8 +24,8 @@
en: 254d265a3e5b1638
zh: bfa325ec79afbc72
/deepseek-ai-dsh-typert-registry/use-this-package/registering-a-contribution:
en: 2b2f750a1b145d85
zh: d15c55c01af1755a
en: 9ca425515904d5c9
zh: b251226ecfa32002
/deepseek-ai-dsh-typert-registry/use-this-package/lookup-and-context-providers:
en: 937dfd14d9e18a5c
zh: 6cf6aa352c8825c6
+3 -1
View File
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
<a id="use-this-package"></a>
## Use this package
Mount the registry in any Host or Client composition that stores or consumes generated Typert artifacts; it provides `ctx.typert`. There is no configuration.
Mount the registry in any Host or Client composition that stores or consumes generated Typert artifacts; it provides `ctx.typert`. There is no configuration. Node resolves `/client` to the standalone ESM `lib/client.node.js` bundle for programmatic Cordis composition; browsers use the `lib/client.browser.js` loader factory through the logical `/plugins/.../client.js` URL. Both bundles use the same registry implementation, separate from the Host package root.
### Minimal setup
@@ -45,6 +45,8 @@ Invocation information is available through existing `ctx.typert.local.get/list`
Generated artifacts register through the [loader](../loader/README.md) in Loader compositions; any other owner calls `ctx.typert.register(contribution)` directly and receives the exact disposer that withdraws it. Duplicate package-face identities, schema keys, invocation ids, or endpoints reject the whole batch before anything is committed.
Harness offline integrations register an ordinary `TYPERT_REMOTE` contribution with `ctx.typert.remotes.register(contribution)` in their profile Context. `$mount(contribution)` already performs that registration, so registering and mounting the same contribution in the same registry duplicates it and fails. Keep offline metadata ownership separate from the live connection-and-mount Context. Independent offline consumers may instead read `TYPERT_REMOTE.methods` without any Registry. Registry state belongs to its Context and process, is not shared across processes, and neither registration nor querying opens a connection.
### Lookup and Context providers
Remote calls resolve Host objects and scoped Contexts through `ctx.typert.lookups` and `ctx.typert.contexts`. `registerHost()` installs the Host wire declaration and its wire-to-Context resolver, while `configureHost()` replaces only that resolver. `registerClient()` installs the bidirectional Client adapter for the same merge-declared kind. Host-to-Client event sources carry their domain identity explicitly instead of projecting it back from a Context.
+3 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
在存储或消费生成 Typert 产物的任何 Host 或 Client 组合中挂载本注册表;它提供 `ctx.typert`。没有配置。
在存储或消费生成 Typert 产物的任何 Host 或 Client 组合中挂载本注册表;它提供 `ctx.typert`。没有配置。Node 将 `/client` 解析为供程序化 Cordis 组合使用的独立 ESM bundle `lib/client.node.js`;浏览器通过逻辑 URL `/plugins/.../client.js` 使用 `lib/client.browser.js` loader factory。两个 bundle 使用相同的注册表实现,且与 Host 包根入口分离。
### 最小设置
@@ -45,6 +45,8 @@ kind: "package-reference"
生成产物在 Loader 组合中通过 [loader](../loader/README.zh.md) 注册;其他所有者直接调用 `ctx.typert.register(contribution)`,并获得撤销它的同一资源释放函数。重复的包与 face 组合键、schema 键、调用 id 或端点会在提交任何内容之前拒绝整个批次。
Harness 离线集成在各自的 profile Context 中通过 `ctx.typert.remotes.register(contribution)` 注册普通 `TYPERT_REMOTE` 贡献。`$mount(contribution)` 已执行该注册,因此在同一个注册表中先注册再挂载同一贡献会因重复而失败。离线元数据所有权应与建立实际连接及挂载的 Context 分离。独立离线消费方也可以直接读取 `TYPERT_REMOTE.methods`,无需任何 Registry。注册表状态归所属 Context 与进程所有,不跨进程共享;注册和查询都不会打开连接。
### Lookup 与 Context 提供方
Remote 调用通过 `ctx.typert.lookups` 与 `ctx.typert.contexts` 解析 Host 对象与作用域 Context。`registerHost()` 安装 Host wire 声明及其 wire 到 Context 的 resolver,`configureHost()` 只替换该 resolver。`registerClient()` 为同一个 merge-declared kind 安装双向 Client adapter。Host 到 Client 的事件源显式携带领域 identity,而不从 Context 反向投影。
+5 -2
View File
@@ -20,7 +20,9 @@
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.js"
"node": "./lib/client.node.js",
"browser": "./lib/client.browser.js",
"default": "./lib/client.browser.js"
},
"./types": {
"types": "./lib/types/types.d.ts",
@@ -44,7 +46,8 @@
"files": [
"locale/*.json",
"lib/index.js",
"lib/client.js",
"lib/client.browser.js",
"lib/client.node.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
+1 -1
View File
@@ -1,3 +1,3 @@
import { clientBundle } from '../../client/tsdown.client.ts'
export default clientBundle(import.meta.dirname, ['lib/types/index.js'])
export default clientBundle(import.meta.dirname, ['lib/types/index.js'], { dualClient: true })
@@ -442,6 +442,17 @@ it('requires the Inspector Worker, Client chunks, and mirrored DevTools resource
}
})
it('requires both physical Client entries only for opted-in packages', () => {
const dir = 'packages/typert/registry'
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
expect(checkWorkspaceManifest({ dir, manifest })).toEqual([])
for (const omitted of ['lib/client.node.js', 'lib/client.browser.js']) {
const files = manifest.files!.filter(file => file !== omitted)
expect(checkWorkspaceManifest({ dir, manifest: { ...manifest, files } }))
.toEqual([expect.stringContaining('package.json files must be')])
}
})
it('requires the standalone shortcut protocol and rejects unrelated runtime files', () => {
const dir = 'packages/client/shortcuts'
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
+12 -5
View File
@@ -249,6 +249,7 @@ export function expectedDshPackageFiles(manifest: PackageManifest): readonly str
const target = exportDefault(manifest, resource)
if (target?.startsWith('./') && target.endsWith('.json')) localeFiles.add(target.slice(2))
}
const browserClient = exportDefault(manifest, './client')
const bundle = manifest.dsh?.bundle
const bundleFiles = bundle === undefined ? [] : bundlePatchFiles(bundle).map(file => file.replace(/^\.\//, ''))
const extras = [
@@ -272,11 +273,11 @@ export function expectedDshPackageFiles(manifest: PackageManifest): readonly str
// Keyed on the artifact path, like ./client below.
...exportDefault(manifest, './worker') === './lib/worker.cjs' ? ['lib/worker.cjs'] : [],
...exportDefault(manifest, './worker') === './lib/worker.js' ? ['lib/worker.js'] : [],
// UI plugin packages ship their browser bundle beside the node lib
// (single-artifact ruling: dist/ retired, ./client resolves lib/client.js).
// Keyed on the artifact path, not the subpath name: a package's ./client is
// a browser-safe source channel, not a bundle.
...exportDefault(manifest, './client') === './lib/client.js' ? ['lib/client.js'] : [],
// Only physical browser bundles need a publication entry; a ./client
// subpath may instead expose a browser-safe emitted source tree.
...browserClient === './lib/client.js' ? ['lib/client.js'] : [],
...browserClient === './lib/client.browser.js' ? ['lib/client.browser.js'] : [],
...exportCondition(manifest, './client', 'node') === './lib/client.node.js' ? ['lib/client.node.js'] : [],
// runtime's shell-held loader subpath ships as its own bundle beside the client half.
...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [],
// A store subpath ships its own bundle (single-entry builds; no shared chunk).
@@ -331,6 +332,12 @@ function exportDefault(manifest: PackageManifest, subpath: string): string | und
return typeof target === 'string' ? target : undefined
}
/** Exact named condition target of an export entry. */
function exportCondition(manifest: PackageManifest, subpath: string, condition: string): string | undefined {
const target = exportFields(manifest.exports?.[subpath])?.[condition]
return typeof target === 'string' ? target : undefined
}
/** Whether any export's runtime default points into the tsc-emitted lib/types tree. */
function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
return Object.keys(manifest.exports ?? {}).some(subpath =>
@@ -278,6 +278,13 @@ describe('client build environment', () => {
const chunked = buildFixture(officialEnvironment)
write(join(chunked, 'packages/client/example/lib/client.pdf.js'), 'module.exports = {}\n')
expect(() => { readClientBuildRecord(chunked) }).toThrow(/artifacts differ/)
const dual = buildFixture(officialEnvironment)
write(join(dual, 'packages/client/example/lib/client.node.js'), 'export const node = true\n')
write(join(dual, 'packages/client/example/lib/client.node.helper.js'), 'export const helper = true\n')
expect(() => { readClientBuildRecord(dual) }).not.toThrow()
write(join(dual, 'packages/client/example/lib/client.browser.js'), 'module.exports = { browser: true }\n')
expect(() => { readClientBuildRecord(dual) }).toThrow(/artifacts differ/)
})
it('keeps public client values out of workflow-wide environments', () => {
+1 -1
View File
@@ -331,7 +331,7 @@ export function readClientBuildRecord(
function clientArtifactDigest(root: string): ClientArtifactDigest {
const paths = globSync([...CLIENT_ARTIFACT_PATTERNS], { cwd: root })
.map(path => path.replaceAll('\\', '/'))
.filter(path => statSync(resolve(root, path)).isFile())
.filter(path => !/\/lib\/client\.node(?:\.|$)/.test(path) && statSync(resolve(root, path)).isFile())
.sort()
if (paths.length === 0) throw new Error('complete client build produced no Vite or dynamic client artifacts')
+51
View File
@@ -75,6 +75,29 @@ describe('client bundle build faces', () => {
expect(() => staticLinked(root, ['lib/types/index.js'])).toThrow(/declares no string package name/)
})
it('opts shared Clients into independent Node ESM and browser factory entries', () => {
for (const dir of ['client/connection', 'api/gateway', 'typert/registry']) {
const configs = clientBundle(packageDir(dir), ['lib/types/index.js'], { dualClient: true })(
{ env: { DSH_BUILD_FACE: 'client' } },
)
const node = configs.find(config => config.name?.endsWith('/client-node'))
const browser = configs.find(config => config.name?.endsWith('/client'))
expect(node?.entry).toEqual({ 'client.node': 'lib/types/client/index.js' })
expect(node).toMatchObject({ platform: 'node', format: ['esm'], sourcemap: true })
expect(node?.inputOptions).toMatchObject({ resolve: { conditionNames: ['node', 'import', 'module', 'default'] } })
expect(node?.outputOptions).toMatchObject({ entryFileNames: 'client.node.js' })
expect(browser?.platform).toBe('browser')
expect(browser?.outputOptions).toMatchObject({ entryFileNames: 'client.browser.js' })
expect(configs.find(config => config.platform === 'node' && config.name?.endsWith('/client'))).toBeUndefined()
}
const ordinary = clientBundle(REQUESTING_PACKAGE_DIR, ['lib/types/index.js'])(
{ env: { DSH_BUILD_FACE: 'client' } },
)
expect(ordinary.some(config => config.name?.endsWith('/client-node'))).toBe(false)
expect(ordinary.find(config => config.platform === 'browser')?.outputOptions)
.toMatchObject({ entryFileNames: 'client.js' })
})
it('watches source in development and consumes emitted JavaScript in the Client build', () => {
const bundle = clientBundle(REQUESTING_PACKAGE_DIR, ['lib/types/index.js'])
const development = bundle({ env: {} }).find(config => config.platform === 'browser')
@@ -87,6 +110,34 @@ describe('client bundle build faces', () => {
})
describe('client bundle dynamic imports', () => {
it('keeps a dual Client browser chunk disjoint from its Node output', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-dual-client-chunk-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
const entry = join(root, 'lib/types/client/index.js')
mkdirSync(dirname(entry), { recursive: true })
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: REQUESTING_PACKAGE, type: 'module' }))
writeFileSync(entry, 'export const load = () => import("./terminal.js")\n')
writeFileSync(join(dirname(entry), 'terminal.js'), 'export const marker = "terminal"\n')
const configs = clientBundle(root, ['lib/types/index.js'], { dualClient: true })(
{ env: { DSH_BUILD_FACE: 'client' } },
)
for (const platform of ['node', 'browser'] as const) {
const config = configs.find(config => config.name?.endsWith(platform === 'node' ? '/client-node' : '/client'))
if (config === undefined) throw new Error(`${platform} config missing`)
const builds = await build({
...config, cwd: root, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
try {
const names = builds.flatMap(bundle => bundle.chunks).map(chunk => chunk.fileName)
expect(names).toContain(platform === 'node' ? 'client.node.js' : 'client.browser.js')
expect(names).toContain(platform === 'node' ? 'client.node.terminal.js' : 'client.browser.terminal.js')
} finally {
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
}
})
it('compiles import() to the module loader asynchronous operation', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-dynamic-import-'))
onTestFinished(() => { rmSync(root, { recursive: true, force: true }) })
+16
View File
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url'
import { once } from 'node:events'
import { execa } from 'execa'
import { afterEach, describe, expect, it } from 'vitest'
import type { Message } from 'publint'
import { isBrowserBundleFormatFalsePositive } from './publint-browser-format.ts'
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const runner = fileURLToPath(new URL('./publint-all.ts', import.meta.url))
@@ -91,6 +93,20 @@ async function run(root: string, signal: AbortSignal) {
return completed
}
describe('publint browser format filtering', () => {
it('exempts browser factories but not the sibling Node ESM artifact', () => {
const format = (file: string): Message => ({
code: 'FILE_INVALID_FORMAT',
args: { actualFilePath: `package/lib/${file}` },
path: ['./client'],
}) as Message
expect(isBrowserBundleFormatFalsePositive(format('client.browser.js'))).toBe(true)
expect(isBrowserBundleFormatFalsePositive(format('client.js'))).toBe(true)
expect(isBrowserBundleFormatFalsePositive(format('client.node.js'))).toBe(false)
expect(isBrowserBundleFormatFalsePositive(format('client.node.helper.js'))).toBe(false)
})
})
describe('publint package runner', () => {
it('reports deadline cancellation after every owned child closes', async ({ signal }) => {
const deadline = new AbortController()
+1 -16
View File
@@ -12,6 +12,7 @@ import { publint, type Message, type PackFile } from 'publint'
import { formatMessage } from 'publint/utils'
import ts from 'typescript'
import { defaultParallelism } from './parallelism.ts'
import { isBrowserBundleFormatFalsePositive } from './publint-browser-format.ts'
const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
const repositoryRoot = resolve(import.meta.dirname, '..')
@@ -177,22 +178,6 @@ function relativeImports(file: string, sourceText: string): RelativeImport[] {
return imports
}
/**
* publint reads the CommonJS interop preamble inside the prebuilt browser
* bundles and reports CJS-written-as-ESM. Node never resolves those files:
* `lib/client.js` is evaluated by the page module system as a classic script,
* and `lib/worker.js` by `new Worker(url, { type: 'module' })` — both outside
* the Node resolution publint models. Exactly that verdict on exactly those
* files is suppressed; every other publint error stays fatal.
*/
function isBrowserBundleFormatFalsePositive(message: Message): boolean {
if (message.code !== 'FILE_INVALID_FORMAT') return false
const filePath = (message.args as { actualFilePath?: string }).actualFilePath ?? ''
const exportKey = Array.isArray(message.path) ? message.path.join('/') : ''
return /(^|\/)lib\/(client|worker)\.js$/.test(filePath)
|| /(^|\/)\.\/(client|worker)$/.test(exportKey)
}
async function runPublint(target: PackageTarget): Promise<PublintResult> {
try {
const files = publicationFiles(target)
+16
View File
@@ -0,0 +1,16 @@
import type { Message } from 'publint'
/**
* Browser Client and Worker artifacts run outside Node's ESM loader, so their
* CommonJS-format verdict is false; a Node Client entry must never be exempt.
* Other publint diagnostics remain fatal.
* @param message - one publint diagnostic for a published file.
* @returns true only for the known browser script format verdict.
*/
export function isBrowserBundleFormatFalsePositive(message: Message): boolean {
if (message.code !== 'FILE_INVALID_FORMAT') return false
const filePath = (message.args as { actualFilePath?: string }).actualFilePath ?? ''
const exportKey = Array.isArray(message.path) ? message.path.join('/') : ''
return /(^|\/)lib\/(?:client(?:\.browser)?|worker)\.js$/.test(filePath)
|| filePath === '' && /(^|\/)\.\/worker$/.test(exportKey)
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2024", "ESNext.Disposable"],
"types": ["node"]
}
}