Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ name: Publish package

on:
workflow_dispatch:
inputs:
release_tag:
description: Existing v-prefixed tag matching package.json
required: true
type: string
source_sha:
description: Exact 40-character main commit SHA to publish
required: true
type: string

permissions:
contents: read
Expand All @@ -11,7 +20,17 @@ jobs:
npm:
runs-on: ubuntu-latest
steps:
- name: Require an exact main source
env:
REQUESTED_SOURCE_SHA: ${{ inputs.source_sha }}
run: |
test "$GITHUB_REF" = "refs/heads/main"
test "$REQUESTED_SOURCE_SHA" = "$GITHUB_SHA"
test "$(printf '%s' "$REQUESTED_SOURCE_SHA" | wc -c)" -eq 40
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_sha }}
- uses: actions/setup-node@v4
with:
node-version: 22
Expand All @@ -20,6 +39,24 @@ jobs:
scope: '@velaros-ai'
- run: npm ci
- run: npm run check
- run: npm publish --access restricted
- id: release
name: Build the immutable release bundle
env:
RELEASE_TAG: ${{ inputs.release_tag }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
node scripts/prepare-release.mjs \
--tag "$RELEASE_TAG" \
--source-sha "$SOURCE_SHA" \
--out-dir release \
--require-tag
echo "tarball=$(node -p 'require("./release/release-manifest.json").tarball.fileName')" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: html-artifacts-${{ inputs.release_tag }}-${{ inputs.source_sha }}
path: release/
if-no-files-found: error
retention-days: 90
- run: npm publish "release/${{ steps.release.outputs.tarball }}" --access restricted
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion dist/browser.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 3 additions & 15 deletions dist/browser.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/browser.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@velaros-ai/html-artifacts",
"version": "0.1.0",
"version": "0.1.1",
"description": "A streaming HTML artifact protocol and sandboxed iframe runtime extracted from VelarOS.",
"keywords": [
"ai",
Expand Down Expand Up @@ -54,7 +54,9 @@
"test": "npm run build && node --test tests/*.test.mjs",
"demo": "npm run build && vite demo",
"demo:build": "npm run build && vite build demo --outDir ../demo-dist --emptyOutDir",
"check": "npm run typecheck && npm test && npm run demo:build && npm pack --dry-run",
"check:dist": "npm run build && node scripts/check-dist.mjs",
"check": "npm run typecheck && npm test && npm run demo:build && npm run check:dist && npm pack --dry-run",
"release:dry-run": "npm run check && node scripts/prepare-release.mjs --dry-run",
"prepack": "npm run build"
},
"devDependencies": {
Expand Down
27 changes: 27 additions & 0 deletions scripts/check-dist.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'

const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))

execFileSync('git', ['diff', '--exit-code', '--', 'dist'], {
cwd: repositoryRoot,
stdio: 'inherit',
})

const untrackedDistFiles = execFileSync(
'git',
['ls-files', '--others', '--exclude-standard', '--', 'dist'],
{
cwd: repositoryRoot,
encoding: 'utf8',
}
)
.trim()
.split('\n')
.filter(Boolean)

if (untrackedDistFiles.length > 0) {
console.error('Generated dist files are not tracked:')
for (const file of untrackedDistFiles) console.error(`- ${file}`)
process.exitCode = 1
}
95 changes: 95 additions & 0 deletions scripts/prepare-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { fileURLToPath } from 'node:url'

const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
const packageJson = JSON.parse(readFileSync(join(repositoryRoot, 'package.json'), 'utf8'))
const { values } = parseArgs({
options: {
'dry-run': { type: 'boolean', default: false },
'out-dir': { type: 'string' },
'require-tag': { type: 'boolean', default: false },
'source-sha': { type: 'string' },
tag: { type: 'string' },
},
})

function run(command, args) {
return execFileSync(command, args, {
cwd: repositoryRoot,
encoding: 'utf8',
}).trim()
}

const sourceSha = run('git', ['rev-parse', 'HEAD'])
if (!/^[0-9a-f]{40}$/.test(sourceSha)) {
throw new Error(`Expected a full source commit SHA, received: ${sourceSha}`)
}
if (values['source-sha'] && values['source-sha'] !== sourceSha) {
throw new Error(
`Requested source SHA ${values['source-sha']} does not match checkout ${sourceSha}`
)
}

const expectedTag = `v${packageJson.version}`
const releaseTag = values.tag ?? expectedTag
if (releaseTag !== expectedTag) {
throw new Error(`Release tag ${releaseTag} does not match package version ${expectedTag}`)
}

if (values['require-tag']) {
const taggedSourceSha = run('git', ['rev-parse', '--verify', `refs/tags/${releaseTag}^{commit}`])
if (taggedSourceSha !== sourceSha) {
throw new Error(
`Tag ${releaseTag} points to ${taggedSourceSha}, but the release source is ${sourceSha}`
)
}
}

const temporaryDirectory = values['dry-run']
? mkdtempSync(join(tmpdir(), 'velaros-html-artifacts-release-'))
: null
const outputDirectory = temporaryDirectory ?? resolve(repositoryRoot, values['out-dir'] ?? 'release')
mkdirSync(outputDirectory, { recursive: true })

try {
const packResult = JSON.parse(
run('npm', [
'pack',
'--json',
'--silent',
'--pack-destination',
outputDirectory,
])
)
if (!Array.isArray(packResult) || packResult.length !== 1 || !packResult[0]?.filename) {
throw new Error('npm pack did not return exactly one package archive')
}

const archivePath = resolve(outputDirectory, packResult[0].filename)
const archive = readFileSync(archivePath)
const manifest = {
packageName: packageJson.name,
version: packageJson.version,
tag: releaseTag,
sourceSha,
tarball: {
fileName: basename(archivePath),
sizeBytes: archive.byteLength,
sha256: createHash('sha256').update(archive).digest('hex'),
},
}
const serializedManifest = `${JSON.stringify(manifest, null, 2)}\n`

if (values['dry-run']) {
process.stdout.write(serializedManifest)
} else {
writeFileSync(join(outputDirectory, 'release-manifest.json'), serializedManifest)
}
} finally {
if (temporaryDirectory) rmSync(temporaryDirectory, { recursive: true, force: true })
}
20 changes: 3 additions & 17 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ export function mountHtmlArtifact(
Math.max(minHeight, normalizeDimension(options.initialHeight, DEFAULT_HTML_ARTIFACT_HEIGHT))
)
let bridgeMessages = createBridgeMessages()
let inboundBridgeTypes = createInboundBridgeTypes(bridgeMessages)
const iframe = document.createElement('iframe')
let shellDocument = createShellDocument(bridgeMessages)
let state: HtmlArtifactProtocolStreamState = createHtmlArtifactProtocolStreamState({
Expand All @@ -153,16 +152,6 @@ export function mountHtmlArtifact(
settleReady = resolve
})

function createInboundBridgeTypes(messages: HtmlArtifactBridgeMessages): Set<string> {
return new Set([
messages.resize,
messages.sendPrompt,
messages.openLink,
messages.generic,
messages.error,
])
}

function createShellDocument(messages: HtmlArtifactBridgeMessages): string {
return buildHtmlArtifactShellDocument({
bridgeMessages: messages,
Expand Down Expand Up @@ -262,10 +251,8 @@ export function mountHtmlArtifact(
const payload = readMessagePayload(event.data)
if (!payload) return

const sourceMatches = event.source === iframe.contentWindow
const isPrivateOpaqueMessage =
event.origin === 'null' && inboundBridgeTypes.has(payload.type)
if (!sourceMatches && !isPrivateOpaqueMessage) return
const frameWindow = iframe.contentWindow
if (!frameWindow || event.source !== frameWindow) return

if (payload.type === bridgeMessages.resize) {
applyReportedHeight(payload)
Expand All @@ -289,7 +276,7 @@ export function mountHtmlArtifact(
patchId: readString(payload.patchId) || undefined,
patchType: readString(payload.patchType) || undefined,
})
} else if (payload.type === HTML_ARTIFACT_WHEEL_MESSAGE_TYPE && sourceMatches) {
} else if (payload.type === HTML_ARTIFACT_WHEEL_MESSAGE_TYPE) {
const deltaX = Number(payload.deltaX) || 0
const deltaY = Number(payload.deltaY) || 0
if (options.onWheel) {
Expand Down Expand Up @@ -368,7 +355,6 @@ export function mountHtmlArtifact(
pendingMessages.length = 0
frameReady = false
bridgeMessages = createBridgeMessages()
inboundBridgeTypes = createInboundBridgeTypes(bridgeMessages)
shellDocument = createShellDocument(bridgeMessages)
iframe.style.height = `${initialHeight}px`
iframe.addEventListener('load', handleLoad, { once: true })
Expand Down
Loading
Loading