0.1.7 reads `package.json#icon` plus `locale/<lang>.json` to illustrate and label a plugin in the Plugins page and the `plugin_manager` list. The plugin had neither, so it rendered as a bare row (and every other third-party entry in this profile still does — ours is now the only one with a real mark). The icon follows the two icons DSH ships for this feature (packages/experimental/*/icon.svg): a 36x36, fill-only geometric mark with a blue gradient and no stroked outlines or text. It reads as a panel with a docked right rail — the sidebar this plugin is. The first cut used two blocks separated by a gap, but the rail shared the panel's light tone at the top, so the split leaned on a 3px gap that shrinks away at list size; a darker rail flush inside one panel survives any downscale. `locale/en.json` is the discovery baseline English needs to be present for the whole directory to be read — and `locale/zh.json` carries the Chinese title and blurb. The manifest's own `description` still advertised the terminal and the browser, which this plugin handed to DSH in 0.20.0; it is now user-visible as the fallback label, so it was corrected too. Every failure in this feature is silent: bad metadata only ever degrades to a plain text row. `tests/plugin-meta.spec.ts` therefore pins both ends — the manifest wiring and the packed tarball — plus the house style of the artwork, and was red/green verified (removing `icon.svg` from `files` fails it twice, once against the real tarball). Verified on the live 3080 instance: `readPluginMeta` resolves the icon to a `data:image/svg+xml` URI and the page renders it (`ourIconExact: 1`, no page errors).
121 lines
6.0 KiB
TypeScript
121 lines
6.0 KiB
TypeScript
/**
|
|
* Plugin presentation-metadata guard for DSH 0.1.7+.
|
|
*
|
|
* 0.1.7 added `readPluginMeta` (packages/boot/app-boot/src/package-meta.ts):
|
|
* the Plugins page and `plugin_manager` list read `package.json#icon` plus
|
|
* `<pkg>/locale/<lang>.json` to label and illustrate a plugin. DSH's own gate
|
|
* (`scripts/verify-package-meta.ts`) is explicit that this is all-or-nothing —
|
|
* a declared icon whose locale resources do not resolve is itself an error —
|
|
* and the whole feature fails SILENTLY: a metadata problem only ever degrades
|
|
* to a plain text row, so nothing tells you the icon stopped shipping.
|
|
*
|
|
* These assertions pin the wiring at both ends: the manifest, and the tarball
|
|
* npm would actually publish (an icon listed in `files` but absent on disk, or
|
|
* present on disk but not listed, both look fine in the source tree).
|
|
*/
|
|
import { execFileSync } from 'node:child_process'
|
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
|
|
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as {
|
|
name: string
|
|
version: string
|
|
icon?: string
|
|
files?: string[]
|
|
exports?: Record<string, unknown>
|
|
}
|
|
|
|
/** The icon and locale directory names this feature is built on. */
|
|
const ICON = 'icon.svg'
|
|
const LOCALE_DIR = 'locale'
|
|
|
|
/** Every locale document, as `[filename, parsed]` pairs. */
|
|
function locales(): Array<[string, { meta?: { title?: unknown; description?: unknown } }]> {
|
|
const dir = resolve(ROOT, LOCALE_DIR)
|
|
return readdirSync(dir).filter(name => name.endsWith('.json')).sort()
|
|
.map(name => [name, JSON.parse(readFileSync(join(dir, name), 'utf8')) as { meta?: { title?: unknown; description?: unknown } }])
|
|
}
|
|
|
|
/** `package.json#icon`, resolved against the manifest directory. */
|
|
const iconPath = (): string => resolve(ROOT, pkg.icon!.replace(/^\.\//u, ''))
|
|
|
|
describe('DSH plugin presentation metadata', () => {
|
|
it('declares a relative icon that exists inside the package', () => {
|
|
expect(pkg.icon, 'package.json#icon').toBeDefined()
|
|
// readPluginMeta rejects an absolute path or a `scheme:` outright.
|
|
expect(pkg.icon!.startsWith('./'), 'icon must be a relative path').toBe(true)
|
|
expect(pkg.icon!.includes('..'), 'icon must stay inside the package').toBe(false)
|
|
expect(pkg.icon!, 'only the raster/vector types DSH accepts').toMatch(/\.(svg|png|jpe?g|webp)$/u)
|
|
expect(existsSync(iconPath()), `${pkg.icon} must exist`).toBe(true)
|
|
})
|
|
|
|
it('publishes the icon and every locale document through files', () => {
|
|
expect(pkg.files, 'package.json#files').toBeDefined()
|
|
// `files` is npm's allow-list: an entry missing here never reaches the
|
|
// tarball, and DSH's gate rejects a declared-but-unpublished icon.
|
|
expect(pkg.files!).toContain(ICON)
|
|
expect(pkg.files!).toContain(`${LOCALE_DIR}/*.json`)
|
|
})
|
|
|
|
it('exposes the locale documents and the manifest through exports', () => {
|
|
// `${name}/locale/<file>` is how readPluginMeta resolves a translation,
|
|
// and `${name}/package.json` is how it reads the icon declaration.
|
|
expect(pkg.exports?.['./locale/*.json'], 'exports["./locale/*.json"]').toBe('./locale/*.json')
|
|
expect(pkg.exports?.['./package.json'], 'exports["./package.json"]').toBe('./package.json')
|
|
})
|
|
|
|
it('ships an English baseline and complete translations beside it', () => {
|
|
const documents = locales()
|
|
const baseline = documents.find(([name]) => name === 'en.json')
|
|
// English is the discovery baseline: without it readPluginMeta cannot find
|
|
// the directory at all, so every other locale would go unread too.
|
|
expect(baseline, `${LOCALE_DIR}/en.json must exist`).toBeDefined()
|
|
|
|
const shapeOf = (value: { meta?: { title?: unknown; description?: unknown } }): string[] =>
|
|
Object.keys(value.meta ?? {}).sort()
|
|
|
|
for (const [name, document] of documents) {
|
|
expect(shapeOf(document), `${name} must mirror en.json`).toEqual(shapeOf(baseline![1]))
|
|
expect(typeof document.meta?.title, `${name} meta.title`).toBe('string')
|
|
expect(document.meta!.title, `${name} meta.title must not be empty`).not.toBe('')
|
|
expect(typeof document.meta?.description, `${name} meta.description`).toBe('string')
|
|
expect(document.meta!.description, `${name} meta.description must not be empty`).not.toBe('')
|
|
}
|
|
})
|
|
|
|
it('keeps the icon in the built-in house style', () => {
|
|
const svg = readFileSync(iconPath(), 'utf8')
|
|
// Every icon DSH ships for this feature is a 36x36, fill-only geometric
|
|
// mark (see packages/experimental/*/icon.svg). Pinning the frame keeps a
|
|
// later redraw from silently changing how the mark scales in the list.
|
|
expect(svg).toMatch(/viewBox="0 0 36 36"/u)
|
|
expect(svg).toMatch(/width="36"/u)
|
|
expect(svg).toMatch(/height="36"/u)
|
|
expect(svg, 'the root must not paint a background').toMatch(/fill="none"/u)
|
|
// No glyph outlines and no text: the built-ins are pure filled shapes, and
|
|
// a <text> node would also render in whatever font the browser picks.
|
|
expect(svg, 'no stroked outlines in the built-in style').not.toMatch(/stroke=/u)
|
|
expect(svg, 'no <text> in an icon').not.toMatch(/<text[\s>]/u)
|
|
})
|
|
|
|
it('actually packs the icon and locales into the tarball', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'plugin-meta-pack-'))
|
|
try {
|
|
execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe', shell: process.platform === 'win32' })
|
|
const contents = execFileSync('tar', ['-tzf', `${pkg.name}-${pkg.version}.tgz`], { cwd: dir, encoding: 'utf8' })
|
|
const files = contents.split('\n')
|
|
expect(files, `${ICON} must be published`).toContain(`package/${ICON}`)
|
|
for (const [name] of locales()) {
|
|
expect(files, `${LOCALE_DIR}/${name} must be published`).toContain(`package/${LOCALE_DIR}/${name}`)
|
|
}
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
}, 120_000)
|
|
})
|