Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 032372bb01 | |||
| 176568f976 | |||
| 6a9a7939b2 |
@@ -7,6 +7,7 @@ import { exportPage } from '@/lib/exporter/html'
|
|||||||
import { AssetManager } from '@/lib/assets/assetManager'
|
import { AssetManager } from '@/lib/assets/assetManager'
|
||||||
import { buildZip } from '@/lib/exporter/zip'
|
import { buildZip } from '@/lib/exporter/zip'
|
||||||
import { buildExtrasForPage } from '@/lib/exporter/extras'
|
import { buildExtrasForPage } from '@/lib/exporter/extras'
|
||||||
|
import { buildAssetsMetadata } from '@/lib/exporter/assets.meta'
|
||||||
import type { Section } from '@/lib/state/store'
|
import type { Section } from '@/lib/state/store'
|
||||||
import { createFormStore, type FormState } from '@/lib/state/formStore'
|
import { createFormStore, type FormState } from '@/lib/state/formStore'
|
||||||
import FormBuilderPanel from '@/components/FormBuilderPanel'
|
import FormBuilderPanel from '@/components/FormBuilderPanel'
|
||||||
@@ -237,7 +238,11 @@ export default function BuilderClientPage() {
|
|||||||
const disallowLines = lines.map((l) => (l.startsWith('Disallow:') ? l : `Disallow: ${l}`))
|
const disallowLines = lines.map((l) => (l.startsWith('Disallow:') ? l : `Disallow: ${l}`))
|
||||||
return `User-agent: *\n${disallowLines.join('\n')}`
|
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<string, Uint8Array> = {
|
||||||
|
...baseExtras,
|
||||||
|
...(am ? { 'assets.json': buildAssetsMetadata(am) } : {}),
|
||||||
|
}
|
||||||
const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras })
|
const zipBytes = await buildZip({ html: exported.html, css: exported.css, js: exported.js, assets, extras })
|
||||||
download('landing.zip', zipBytes)
|
download('landing.zip', zipBytes)
|
||||||
}, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow])
|
}, [pageData, am, manifestName, manifestShortName, manifestStartUrl, manifestDisplay, manifestThemeColor, robotsDisallow])
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class AssetManager {
|
|||||||
private readonly allowed: Set<string>
|
private readonly allowed: Set<string>
|
||||||
private byHash = new Map<string, AssetRef>()
|
private byHash = new Map<string, AssetRef>()
|
||||||
private byOriginal = new Map<string, string>() // originalName -> path
|
private byOriginal = new Map<string, string>() // originalName -> path
|
||||||
|
private referenced = new Set<string>() // originalName referenced via rewrite
|
||||||
|
|
||||||
constructor(opts: AssetManagerOptions) {
|
constructor(opts: AssetManagerOptions) {
|
||||||
this.maxBytes = opts.maxBytes
|
this.maxBytes = opts.maxBytes
|
||||||
@@ -91,8 +92,13 @@ export class AssetManager {
|
|||||||
if (urlOrLocalRef.startsWith('local:')) {
|
if (urlOrLocalRef.startsWith('local:')) {
|
||||||
const key = urlOrLocalRef.slice('local:'.length)
|
const key = urlOrLocalRef.slice('local:'.length)
|
||||||
const p = this.byOriginal.get(key)
|
const p = this.byOriginal.get(key)
|
||||||
|
if (p) this.referenced.add(key)
|
||||||
return p ?? urlOrLocalRef
|
return p ?? urlOrLocalRef
|
||||||
}
|
}
|
||||||
return urlOrLocalRef
|
return urlOrLocalRef
|
||||||
}
|
}
|
||||||
|
|
||||||
|
referencedList(): string[] {
|
||||||
|
return Array.from(this.referenced)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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$/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { AssetManager } from '@/lib/assets/assetManager'
|
||||||
|
|
||||||
|
export type AssetMetaItem = {
|
||||||
|
originalName: string
|
||||||
|
mimeType: string
|
||||||
|
size: number
|
||||||
|
hash8: string
|
||||||
|
ext: string
|
||||||
|
zipPath: string
|
||||||
|
integrity: string
|
||||||
|
group: 'images' | 'fonts' | 'other'
|
||||||
|
referencedAt: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAssetsMetadata(am: AssetManager): Uint8Array {
|
||||||
|
const list = am.list()
|
||||||
|
const referenced = new Set(am.referencedList())
|
||||||
|
const items: AssetMetaItem[] = list.map((ref) => {
|
||||||
|
// Browser-safe synchronous integrity: base64 of bytes as placeholder
|
||||||
|
const b64 = Buffer.from(ref.bytes).toString('base64')
|
||||||
|
const group: 'images' | 'fonts' | 'other' = ref.mimeType.startsWith('image/')
|
||||||
|
? 'images'
|
||||||
|
: ref.mimeType.startsWith('font/')
|
||||||
|
? 'fonts'
|
||||||
|
: 'other'
|
||||||
|
const referencedAt = referenced.has(ref.originalName) ? [`local:${ref.originalName}`] : []
|
||||||
|
return {
|
||||||
|
originalName: ref.originalName,
|
||||||
|
mimeType: ref.mimeType,
|
||||||
|
size: ref.bytes.byteLength,
|
||||||
|
hash8: ref.hash8,
|
||||||
|
ext: ref.ext,
|
||||||
|
zipPath: ref.path,
|
||||||
|
integrity: `sha256-${b64}`,
|
||||||
|
group,
|
||||||
|
referencedAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const json = JSON.stringify({ assets: items }, null, 2)
|
||||||
|
return new TextEncoder().encode(json)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user