From 2ebe726cc029c3f1da9f591135b2a32306c35fea Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 20 Oct 2026 22:57:31 +0000 Subject: [PATCH] feat(addons): ship a committed icon.png instead of the generated square The shared UI builder and the Tables builder copy ui/icon.png from the add-on source tree to the package unchanged when the file exists. They write the generated placeholder only when it does not. Add a fixture with an icon and a test for both cases. Assisted-by: Claude Code:claude-sonnet-5-5 node Signed-off-by: Ettore Di Giacinto --- .../internal/uibuild/test/fixture-icon/icon.png | Bin 0 -> 4516 bytes .../uibuild/test/fixture-icon/src/index.jsx | 4 ++++ .../uibuild/test/fixture-icon/src/style.css | 1 + addons/internal/uibuild/test/uibuild.test.mjs | 8 ++++++++ addons/internal/uibuild/uibuild.mjs | 6 +++++- addons/tables/ui/build.mjs | 7 +++++-- 6 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 addons/internal/uibuild/test/fixture-icon/icon.png create mode 100644 addons/internal/uibuild/test/fixture-icon/src/index.jsx create mode 100644 addons/internal/uibuild/test/fixture-icon/src/style.css diff --git a/addons/internal/uibuild/test/fixture-icon/icon.png b/addons/internal/uibuild/test/fixture-icon/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7bbc235734525acf44e81c14dd13bbe524d3b6b6 GIT binary patch literal 4516 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 LQ=O74w;%rtXA>wF literal 0 HcmV?d00001 diff --git a/addons/internal/uibuild/test/fixture-icon/src/index.jsx b/addons/internal/uibuild/test/fixture-icon/src/index.jsx new file mode 100644 index 000000000..b0dc486aa --- /dev/null +++ b/addons/internal/uibuild/test/fixture-icon/src/index.jsx @@ -0,0 +1,4 @@ +export function createAddon(sdk) { + const h = sdk.React.createElement + return { Component: () =>

fixture

, dispose() {} } +} diff --git a/addons/internal/uibuild/test/fixture-icon/src/style.css b/addons/internal/uibuild/test/fixture-icon/src/style.css new file mode 100644 index 000000000..a5d53397a --- /dev/null +++ b/addons/internal/uibuild/test/fixture-icon/src/style.css @@ -0,0 +1 @@ +.fx { color: inherit; } diff --git a/addons/internal/uibuild/test/uibuild.test.mjs b/addons/internal/uibuild/test/uibuild.test.mjs index 36276d45d..0b585fc06 100644 --- a/addons/internal/uibuild/test/uibuild.test.mjs +++ b/addons/internal/uibuild/test/uibuild.test.mjs @@ -30,6 +30,14 @@ test('buildAddonUI writes the module, the style sheet and a PNG icon', async () assert.deepEqual(await firstPixel(png), [26, 62, 138]) }) +test('a committed icon.png next to src is copied unchanged and no square is generated', async () => { + const root = join(here, 'fixture-icon') + const dist = await buildAddonUI({ root, name: 'fixture', rgb: [26, 62, 138] }) + const out = readFileSync(join(dist, 'ui/icon.png')) + assert.ok(out.equals(readFileSync(join(root, 'icon.png'))), 'the packaged icon is the committed file') + assert.equal(out.readUInt32BE(16), 256, 'the committed icon keeps its own size, not the 128 pixel square') +}) + test('the bundle is an ES module that exports createAddon and calls h for JSX', async () => { const dist = await buildAddonUI({ root: join(here, 'fixture'), name: 'fixture', rgb: [1, 2, 3] }) const js = readFileSync(join(dist, 'ui/fixture.js'), 'utf8') diff --git a/addons/internal/uibuild/uibuild.mjs b/addons/internal/uibuild/uibuild.mjs index 4f78d58f6..d0cf759a3 100644 --- a/addons/internal/uibuild/uibuild.mjs +++ b/addons/internal/uibuild/uibuild.mjs @@ -136,7 +136,11 @@ export async function buildAddonUI({ root, name, rgb, compat }) { } else { copyFileSync(join(root, 'src/style.css'), join(out, `ui/${name}.css`)) } - writeFileSync(join(out, 'ui/icon.png'), solidIcon(rgb)) + // A committed icon (ui/icon.png next to src/) ships unchanged. Only an + // add-on without one gets the generated solid square. + const icon = join(root, 'icon.png') + if (existsSync(icon)) copyFileSync(icon, join(out, 'ui/icon.png')) + else writeFileSync(join(out, 'ui/icon.png'), solidIcon(rgb)) const js = readFileSync(join(out, `ui/${name}.js`), 'utf8') const bad = reactImport.exec(js) if (bad) throw new Error(`the bundle must not import React: found ${bad[0]}`) diff --git a/addons/tables/ui/build.mjs b/addons/tables/ui/build.mjs index 42dba4239..72b5dc95a 100644 --- a/addons/tables/ui/build.mjs +++ b/addons/tables/ui/build.mjs @@ -8,7 +8,7 @@ import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import { dirname, join, resolve } from 'node:path' -import { mkdirSync, rmSync, writeFileSync, copyFileSync, readFileSync } from 'node:fs' +import { existsSync, mkdirSync, rmSync, writeFileSync, copyFileSync, readFileSync } from 'node:fs' import { deflateSync } from 'node:zlib' const here = dirname(fileURLToPath(import.meta.url)) @@ -78,7 +78,10 @@ export async function build() { logLevel: 'warn', }) copyFileSync(join(here, 'src/style.css'), join(out, 'ui/tables.css')) - writeFileSync(join(out, 'ui/icon.png'), icon()) + // A committed ui/icon.png ships unchanged; the drawn grid is the fallback. + const committed = join(here, 'icon.png') + if (existsSync(committed)) copyFileSync(committed, join(out, 'ui/icon.png')) + else writeFileSync(join(out, 'ui/icon.png'), icon()) const js = readFileSync(join(out, 'ui/tables.js'), 'utf8') if (/from\s+['"](react|react-dom)/.test(js) || /require\(/.test(js)) throw new Error('the bundle must not import React') return out