From 176568f976ad55709beeb6ce50a7772b4de1ac82 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Sat, 15 Nov 2025 13:38:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(assets):=20ZIP=EC=97=90=20assets.json=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20=EB=B0=8F=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0(integrity=20=ED=8F=AC=ED=95=A8)=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/BuilderClientPage.tsx | 7 ++++++- lib/exporter/assets.meta.test.ts | 30 ++++++++++++++++++++++++++++++ lib/exporter/assets.meta.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 lib/exporter/assets.meta.test.ts create mode 100644 lib/exporter/assets.meta.ts 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) +} -- 2.52.0