Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **The shipped browser bundle registers under the package name.** `scripts/bundle-client.mjs` hardcoded `id: "dsh-github"`; DSH 0.2.1 keys client rows by package name (`@perrylink/dsh-github`), so the loader fell back to the single-resource URL and re-executed the bundle, aborting web boot with `duplicate factory registration`. The wrapper now reads `package.json.name`, and `verify:artifacts` + `test/client-bundle.test.ts` assert the registration id.

## [0.7.18] - 2026-10-05

undefined
Expand Down
2 changes: 1 addition & 1 deletion lib/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
window.__ModuleLoader__.load({
id: "dsh-github",
id: "@perrylink/dsh-github",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Expand Down
6 changes: 5 additions & 1 deletion scripts/bundle-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { dirname, join } from 'node:path'

const root = dirname(dirname(fileURLToPath(import.meta.url)))
const file = join(root, 'lib', 'client.js')
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
if (typeof manifest.name !== 'string' || manifest.name.length === 0) {
throw new Error('bundle-client: package.json has no name')
}
let text = readFileSync(file, 'utf8')
if (text.startsWith('window.__ModuleLoader__.load(')) process.exit(0)

Expand Down Expand Up @@ -37,7 +41,7 @@ text = text.replace(/\/\/# sourceMappingURL=.*$/gm, '')

const tail = exports.map(name => `\t\texports.${name} = ${name};`).join('\n')
const wrapped = `window.__ModuleLoader__.load({
\tid: "dsh-github",
\tid: ${JSON.stringify(manifest.name)},
\tfactory: (require) => {
\t\tvar module = { exports: {} };
\t\tvar exports = module.exports;
Expand Down
19 changes: 17 additions & 2 deletions scripts/verify-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
// syntax leaking into shipped output and against a tarball missing the
// bundle patch or the Action contract.
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { runInNewContext } from 'node:vm'

const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))

const required = [
'lib/index.js',
'lib/index.d.ts',
'lib/client.js',
'action.yml',
'cordis.patch.yml',
]
Expand All @@ -23,10 +26,22 @@ for (const rel of required) {

execFileSync(process.execPath, ['--check', path.join(root, 'lib/index.js')], { stdio: 'inherit' })

const registrations = []
runInNewContext(readFileSync(path.join(root, 'lib/client.js'), 'utf8'), {
window: { __ModuleLoader__: { load: registration => registrations.push(registration) } },
})
if (registrations.length !== 1) {
throw new Error(`lib/client.js must register exactly one factory, registered ${String(registrations.length)}`)
}
if (registrations[0].id !== pkg.name) {
throw new Error(`lib/client.js registers ${JSON.stringify(registrations[0].id)}, expected ${JSON.stringify(pkg.name)}`)
}
if (typeof registrations[0].factory !== 'function') throw new Error('lib/client.js registered a non-function factory')

const index = await import(pathToFileURL(path.join(root, 'lib/index.js')).href)
if ('default' in index) throw new Error('lib/index.js must not carry a default export')
if (index.name !== 'dsh-github' || typeof index.apply !== 'function') {
throw new Error('lib/index.js exports an unexpected plugin face')
}

console.log('artifacts OK: syntax + ESM import + bundle patch and action contract present')
console.log('artifacts OK: host face + browser registration id + bundle patch and action contract present')
23 changes: 18 additions & 5 deletions test/client-bundle.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { readFileSync } from 'node:fs'
import { runInNewContext } from 'node:vm'
import { describe, expect, it } from 'vitest'

const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const bundle = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')

describe('shipped browser bundle', () => {
it('is a classic-script loader factory, not bare ESM', () => {
const source = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
expect(source.startsWith('window.__ModuleLoader__.load({')).toBe(true)
expect(source).toContain('id: "dsh-github"')
expect(/^import\s/m.test(source)).toBe(false)
expect(/^export\s/m.test(source)).toBe(false)
expect(bundle.startsWith('window.__ModuleLoader__.load({')).toBe(true)
expect(bundle).toContain(`id: ${JSON.stringify(pkg.name)}`)
expect(/^import\s/m.test(bundle)).toBe(false)
expect(/^export\s/m.test(bundle)).toBe(false)
})

it('registers exactly once under the package name', () => {
const registrations = []
runInNewContext(bundle, {
window: { __ModuleLoader__: { load: registration => registrations.push(registration) } },
})
expect(registrations).toHaveLength(1)
expect(registrations[0].id).toBe(pkg.name)
expect(typeof registrations[0].factory).toBe('function')
})
})