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 <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-20 22:57:31 +00:00
1 parent 2a0546cd66
commit 2ebe726cc0
6 files changed
+23 -3

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -0,0 +1,4 @@
export function createAddon(sdk) {
const h = sdk.React.createElement
return { Component: () => <p className="fx">fixture</p>, dispose() {} }
}
@@ -0,0 +1 @@
.fx { color: inherit; }
@@ -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')
+5 -1
View File
@@ -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]}`)
+5 -2
View File
@@ -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