mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(sidebar): phone feed list leftovers before the flip (dark) Bump the feed's semantics version for P11's desktop facts; mark switch-on phone page bundles so packaging and orcad staging refuse them; show the page's last-listed rows before the first feed read; draw the host's pending creations; pass the listed host's own label into host sections instead of the running device's platform. * fix(sidebar): review fixes for the phone feed leftovers (dark) A dark orcad may stage the dark page (only a marker of an off switch is refused); the bundle tree writer leaves the marker; the pre-feed list keeps the view's filters; a creation names its project only outside that project's header and survives search when its project matches; the feed view and row labels read one resolved desktop platform. * fix(sidebar): second review fixes for the phone feed leftovers (dark) Test the off-switch marker check across switches; resolve the desktop platform once for rows and headers; a section search narrows keeps only matching rows.
156 lines
5.6 KiB
JavaScript
156 lines
5.6 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFile, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
import { createRequire } from 'node:module'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const { darkBuildMarkers } = createRequire(import.meta.url)('../dark-build-switches.cjs')
|
|
|
|
const projectDir = fileURLToPath(new URL('../..', import.meta.url))
|
|
|
|
export const MOBILE_WEB_BUNDLE_SCHEMA_VERSION = 1
|
|
export const MOBILE_WEB_BUNDLE_ENTRYPOINT = 'index.html'
|
|
|
|
const CONTENT_TYPE_BY_EXTENSION = {
|
|
css: 'text/css; charset=utf-8',
|
|
html: 'text/html; charset=utf-8',
|
|
js: 'text/javascript; charset=utf-8',
|
|
png: 'image/png',
|
|
// The app bundle emits images as same-origin assets rather than data: URLs, so each one is
|
|
// content-hashed and served from here. Fonts are absent by design: the policy sets
|
|
// font-src 'none'.
|
|
jpg: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
gif: 'image/gif',
|
|
webp: 'image/webp',
|
|
svg: 'image/svg+xml'
|
|
}
|
|
|
|
/**
|
|
* Canonical serialization the buildId hashes. Key order is fixed and the list is sorted by path,
|
|
* so the id is a pure function of content. Must stay byte-identical to the contract module's
|
|
* serializer in src/shared/mobile-web-bundle/.
|
|
*/
|
|
export function serializeMobileWebBundleAssets(assets) {
|
|
return JSON.stringify(
|
|
[...assets]
|
|
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
.map(({ path, sha256, byteLength, contentType }) => ({
|
|
path,
|
|
sha256,
|
|
byteLength,
|
|
contentType
|
|
}))
|
|
)
|
|
}
|
|
|
|
export function computeMobileWebBundleBuildId(assets) {
|
|
return createHash('sha256').update(serializeMobileWebBundleAssets(assets), 'utf8').digest('hex')
|
|
}
|
|
|
|
export function sha256Hex(bytes) {
|
|
return createHash('sha256').update(bytes).digest('hex')
|
|
}
|
|
|
|
export function contentTypeForExtension(extension) {
|
|
const contentType = CONTENT_TYPE_BY_EXTENSION[extension]
|
|
if (!contentType) {
|
|
throw new Error(`[mobile-web-bundle-manifest] no content type registered for .${extension}`)
|
|
}
|
|
return contentType
|
|
}
|
|
|
|
export function hashedAsset(bytes, extension) {
|
|
const sha256 = sha256Hex(bytes)
|
|
return {
|
|
bytes,
|
|
path: `assets/${sha256}.${extension}`,
|
|
sha256,
|
|
byteLength: bytes.byteLength,
|
|
contentType: contentTypeForExtension(extension)
|
|
}
|
|
}
|
|
|
|
function readIntegerConstant(source, name, file = 'src/shared/protocol-version.ts') {
|
|
const match = new RegExp(`export const ${name} = (\\d+)`).exec(source)
|
|
if (!match) {
|
|
throw new Error(`[mobile-web-bundle-manifest] ${name} not found in ${file}`)
|
|
}
|
|
return Number.parseInt(match[1], 10)
|
|
}
|
|
|
|
/**
|
|
* Parsed rather than imported because protocol-version.ts is TypeScript and this script runs on
|
|
* bare node during packaging, before any build output exists.
|
|
*/
|
|
export async function readProtocolWindow() {
|
|
const source = await readFile(join(projectDir, 'src', 'shared', 'protocol-version.ts'), 'utf8')
|
|
return {
|
|
runtimeProtocolVersion: readIntegerConstant(source, 'RUNTIME_PROTOCOL_VERSION'),
|
|
// The bundle is a client: the floor it cares about is the oldest host protocol it can talk to.
|
|
minCompatibleRuntimeProtocolVersion: readIntegerConstant(
|
|
source,
|
|
'MIN_COMPATIBLE_RUNTIME_SERVER_VERSION'
|
|
)
|
|
}
|
|
}
|
|
|
|
/** The page's build number, parsed for the same reason the protocol window is. */
|
|
export async function readPageVersion() {
|
|
const file = join('src', 'shared', 'mobile-web-bundle', 'manifest-contract.ts')
|
|
const source = await readFile(join(projectDir, file), 'utf8')
|
|
return readIntegerConstant(source, 'MOBILE_WEB_PAGE_VERSION', file)
|
|
}
|
|
|
|
export async function readDesktopVersion() {
|
|
const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'))
|
|
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
|
throw new Error('[mobile-web-bundle-manifest] root package.json has no version')
|
|
}
|
|
return packageJson.version
|
|
}
|
|
|
|
/**
|
|
* Manifest assembly and the on-disk write, kept apart from any one builder so the manifest shape
|
|
* the contract module and the packaging guard read has a single producer.
|
|
*/
|
|
export async function writeMobileWebBundleTree({
|
|
outDir,
|
|
written,
|
|
desktopVersion,
|
|
protocolWindow,
|
|
// Empty for a tree with no route list at all: a shell reading it finds no screen listed and
|
|
// renders every route natively.
|
|
routes = [],
|
|
env = process.env
|
|
}) {
|
|
const assets = written
|
|
.map(({ path, sha256, byteLength, contentType }) => ({ path, sha256, byteLength, contentType }))
|
|
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
const manifest = {
|
|
schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION,
|
|
buildId: computeMobileWebBundleBuildId(assets),
|
|
desktopVersion,
|
|
minCompatibleRuntimeProtocolVersion: protocolWindow.minCompatibleRuntimeProtocolVersion,
|
|
runtimeProtocolVersion: protocolWindow.runtimeProtocolVersion,
|
|
pageVersion: await readPageVersion(),
|
|
entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT,
|
|
totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0),
|
|
assets,
|
|
routes
|
|
}
|
|
|
|
// Why a full clear: a stale asset left from an earlier build would ship unreferenced inside asar.
|
|
await rm(outDir, { recursive: true, force: true })
|
|
await mkdir(join(outDir, 'assets'), { recursive: true })
|
|
for (const asset of written) {
|
|
await writeFile(join(outDir, asset.path), asset.bytes)
|
|
}
|
|
await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
// Outside the manifest, so the page's bytes and buildId stay what the switch compiled.
|
|
for (const marker of darkBuildMarkers(env)) {
|
|
await writeFile(join(outDir, marker.fileName), marker.text, 'utf8')
|
|
}
|
|
return { manifest, outDir }
|
|
}
|