-
-
Notifications
You must be signed in to change notification settings - Fork 470
feat(web): add /new preview UI #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { describe, expect, it } from 'bun:test' | ||
| import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' | ||
| import { join } from 'node:path' | ||
| import { tmpdir } from 'node:os' | ||
| import { Hono } from 'hono' | ||
| import { mountMissingPreviewRoutes, mountPreviewStaticRoutes } from './server' | ||
|
|
||
| function createPreviewDist(): string { | ||
| const distDir = mkdtempSync(join(tmpdir(), 'hapi-preview-dist-')) | ||
| mkdirSync(join(distDir, 'assets'), { recursive: true }) | ||
| writeFileSync(join(distDir, 'index.html'), '<!doctype html><div id="root">preview</div>') | ||
| writeFileSync(join(distDir, 'assets', 'app.js'), 'console.log("preview")') | ||
| writeFileSync(join(distDir, 'manifest.webmanifest'), '{"name":"HAPI Preview"}') | ||
| return distDir | ||
| } | ||
|
|
||
| describe('preview static routes', () => { | ||
| it('serves preview assets and deep links under /new without shadowing root', async () => { | ||
| const app = new Hono() | ||
| mountPreviewStaticRoutes(app, createPreviewDist()) | ||
| app.get('/', (c) => c.text('root')) | ||
|
|
||
| const asset = await app.request('/new/assets/app.js') | ||
| expect(asset.status).toBe(200) | ||
| expect(await asset.text()).toBe('console.log("preview")') | ||
|
|
||
| const manifest = await app.request('/new/manifest.webmanifest') | ||
| expect(manifest.status).toBe(200) | ||
| expect(await manifest.text()).toContain('HAPI Preview') | ||
|
|
||
| const deepLink = await app.request('/new/sessions/session-1') | ||
| expect(deepLink.status).toBe(200) | ||
| expect(await deepLink.text()).toContain('preview') | ||
|
|
||
| const root = await app.request('/') | ||
| expect(root.status).toBe(200) | ||
| expect(await root.text()).toBe('root') | ||
| }) | ||
|
|
||
| it('does not mount preview routes when index.html is missing', async () => { | ||
| const distDir = mkdtempSync(join(tmpdir(), 'hapi-preview-missing-')) | ||
| const app = new Hono() | ||
| expect(mountPreviewStaticRoutes(app, distDir)).toBe(false) | ||
| }) | ||
|
|
||
| it('returns an explicit 503 for /new when the preview artifact is missing', async () => { | ||
| const app = new Hono() | ||
| mountMissingPreviewRoutes(app) | ||
| app.get('*', (c) => c.text('root fallback')) | ||
|
|
||
| const response = await app.request('/new/sessions/session-1') | ||
|
|
||
| expect(response.status).toBe(503) | ||
| expect(await response.text()).toContain('Preview app is not built') | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { Hono } from 'hono' | ||
| import { Hono, type Env, type Schema } from 'hono' | ||
| import { cors } from 'hono/cors' | ||
| import { logger } from 'hono/logger' | ||
| import { join } from 'node:path' | ||
|
|
@@ -190,6 +190,60 @@ function findWebappDistDir(): { distDir: string; indexHtmlPath: string } { | |
| return { distDir, indexHtmlPath: join(distDir, 'index.html') } | ||
| } | ||
|
|
||
| function findPreviewWebappDistDir(): string | null { | ||
| const configured = process.env.HAPI_WEB_PREVIEW_DIST_DIR?.trim() | ||
| const candidates = [ | ||
| ...(configured ? [configured] : []), | ||
| join(process.cwd(), '..', 'web', 'dist-new'), | ||
| join(import.meta.dir, '..', '..', '..', 'web', 'dist-new'), | ||
| join(process.cwd(), 'web', 'dist-new') | ||
| ] | ||
|
|
||
| for (const distDir of candidates) { | ||
| if (existsSync(join(distDir, 'index.html'))) { | ||
| return distDir | ||
| } | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| export function mountPreviewStaticRoutes<E extends Env, S extends Schema, BasePath extends string>( | ||
| app: Hono<E, S, BasePath>, | ||
| distDir: string | null | ||
| ): boolean { | ||
| if (!distDir || !existsSync(join(distDir, 'index.html'))) { | ||
| return false | ||
| } | ||
|
|
||
| app.use('/new/*', async (c, next) => { | ||
| if (c.req.method !== 'GET' && c.req.method !== 'HEAD') { | ||
| await next() | ||
| return | ||
| } | ||
|
|
||
| return await serveStatic({ | ||
| root: distDir, | ||
| rewriteRequestPath: (path) => path.replace(/^\/new\//, '/') | ||
| })(c, next) | ||
| }) | ||
| app.get('/new', async (c, next) => { | ||
| return await serveStatic({ root: distDir, path: 'index.html' })(c, next) | ||
| }) | ||
| app.get('/new/*', async (c, next) => { | ||
| return await serveStatic({ root: distDir, path: 'index.html' })(c, next) | ||
| }) | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| export function mountMissingPreviewRoutes<E extends Env, S extends Schema, BasePath extends string>( | ||
| app: Hono<E, S, BasePath> | ||
| ): void { | ||
| app.get('/new', (c) => c.text('Preview app is not built.\n\nRun:\n cd web\n bun run build:preview\n', 503)) | ||
| app.get('/new/*', (c) => c.text('Preview app is not built.\n\nRun:\n cd web\n bun run build:preview\n', 503)) | ||
| } | ||
|
|
||
| function serveEmbeddedAsset(asset: EmbeddedWebAsset): Response { | ||
| return new Response(Bun.file(asset.sourcePath), { | ||
| headers: { | ||
|
|
@@ -316,6 +370,10 @@ from GitHub Pages instead of through the relay tunnel. | |
| return app | ||
| } | ||
|
|
||
| if (!mountPreviewStaticRoutes(app, findPreviewWebappDistDir())) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MAJOR] This Suggested fix: const previewMounted = options.embeddedAssetMap
? false
: mountPreviewStaticRoutes(app, findPreviewWebappDistDir())
if (!previewMounted) {
mountMissingPreviewRoutes(app)
}
if (options.embeddedAssetMap) {
// existing embedded root app handling
} |
||
| mountMissingPreviewRoutes(app) | ||
| } | ||
|
|
||
| const { distDir, indexHtmlPath } = findWebappDistDir() | ||
|
|
||
| if (!existsSync(indexHtmlPath)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MAJOR] This
/newmount never runs for compiled/single-exe hubs because theoptions.embeddedAssetMapbranch above returns at line 365. The release pipeline embeds onlyweb/dist, so/newfalls through to the existing rootindex.htmlrather than the preview artifact/503.Suggested fix: