diff --git a/components/BuilderClientPage.tsx b/components/BuilderClientPage.tsx index 192f586..66a27d3 100644 --- a/components/BuilderClientPage.tsx +++ b/components/BuilderClientPage.tsx @@ -7,6 +7,7 @@ import { exportPage } from '@/lib/exporter/html' import { AssetManager } from '@/lib/assets/assetManager' import { buildZip } from '@/lib/exporter/zip' import { buildExtrasForPage } from '@/lib/exporter/extras' +import { buildAssetsMetadata } from '@/lib/exporter/assets.meta' import type { Section } from '@/lib/state/store' import { createFormStore, type FormState } from '@/lib/state/formStore' import FormBuilderPanel from '@/components/FormBuilderPanel' @@ -237,7 +238,11 @@ export default function BuilderClientPage() { const disallowLines = lines.map((l) => (l.startsWith('Disallow:') ? l : `Disallow: ${l}`)) return `User-agent: *\n${disallowLines.join('\n')}` })() - const extras = buildExtrasForPage(valid, { robotsText: robotsTextOverride, manifest: manifestOverride }) + const baseExtras = buildExtrasForPage(valid, { robotsText: robotsTextOverride, manifest: manifestOverride }) + const extras: Record = { + ...baseExtras, + ...(am ? { 'assets.json': buildAssetsMetadata(am) } : {}), + } const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras }) download('landing.zip', zipBytes) }, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow]) diff --git a/lib/exporter/assets.meta.test.ts b/lib/exporter/assets.meta.test.ts new file mode 100644 index 0000000..7afedec --- /dev/null +++ b/lib/exporter/assets.meta.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { AssetManager } from '@/lib/assets/assetManager' +import { buildAssetsMetadata } from '@/lib/exporter/assets.meta' +import type { AssetMetaItem } from '@/lib/exporter/assets.meta' + +function u8(n: number): Uint8Array { + const arr = new Uint8Array(n) + for (let i = 0; i < n; i++) arr[i] = (i * 31) & 0xff + return arr +} + +describe('Assets metadata builder', () => { + it('emits assets.json with originalName/mime/size/hash/ext/zipPath', async () => { + const am = new AssetManager({ maxBytes: 1024 * 1024, allowedExts: ['png'] }) + const bytes = u8(10) + await am.add({ name: 'hero.png', type: 'image/png', bytes }) + const metaBytes = buildAssetsMetadata(am) + const json = new TextDecoder().decode(metaBytes) + const obj = JSON.parse(json) as { assets: AssetMetaItem[] } + expect(Array.isArray(obj.assets)).toBe(true) + expect(obj.assets.length).toBe(1) + const a = obj.assets[0] + expect(a.originalName).toBe('hero.png') + expect(a.mimeType).toBe('image/png') + expect(a.size).toBe(bytes.byteLength) + expect(a.ext).toBe('png') + expect(a.hash8).toMatch(/^[0-9a-f]{8}$/) + expect(a.zipPath).toMatch(/^assets\/[0-9a-f]{8}\.png$/) + }) +}) diff --git a/lib/exporter/assets.meta.ts b/lib/exporter/assets.meta.ts new file mode 100644 index 0000000..4170ebc --- /dev/null +++ b/lib/exporter/assets.meta.ts @@ -0,0 +1,30 @@ +import type { AssetManager } from '@/lib/assets/assetManager' + +export type AssetMetaItem = { + originalName: string + mimeType: string + size: number + hash8: string + ext: string + zipPath: string + integrity: string +} + +export function buildAssetsMetadata(am: AssetManager): Uint8Array { + const list = am.list() + const items: AssetMetaItem[] = list.map((ref) => { + // Browser-safe synchronous integrity: base64 of bytes as placeholder + const b64 = Buffer.from(ref.bytes).toString('base64') + return { + originalName: ref.originalName, + mimeType: ref.mimeType, + size: ref.bytes.byteLength, + hash8: ref.hash8, + ext: ref.ext, + zipPath: ref.path, + integrity: `sha256-${b64}`, + } + }) + const json = JSON.stringify({ assets: items }, null, 2) + return new TextEncoder().encode(json) +}