diff --git a/CHANGELOG.md b/CHANGELOG.md index b285a67..2644bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/client.js b/lib/client.js index a6545b9..0d022a1 100755 --- a/lib/client.js +++ b/lib/client.js @@ -1,5 +1,5 @@ window.__ModuleLoader__.load({ - id: "dsh-github", + id: "@perrylink/dsh-github", factory: (require) => { var module = { exports: {} }; var exports = module.exports; diff --git a/scripts/bundle-client.mjs b/scripts/bundle-client.mjs index 7fa2c63..dc71fe8 100755 --- a/scripts/bundle-client.mjs +++ b/scripts/bundle-client.mjs @@ -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) @@ -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; diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index 93face6..d5d0434 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -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', ] @@ -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') diff --git a/test/client-bundle.test.ts b/test/client-bundle.test.ts index 5a358d7..fedacab 100755 --- a/test/client-bundle.test.ts +++ b/test/client-bundle.test.ts @@ -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') }) })