polen
Version:
A framework for delightful GraphQL developer portals
141 lines (116 loc) • 4.44 kB
text/typescript
import { TinyGlobby } from '#dep/tiny-globby/index'
import { Fs, Path } from '@wollybeard/kit'
import { Err } from '@wollybeard/kit'
import { buildManifest, type PolenBuildManifest } from './manifest.js'
export type RebasePlan = RebaseOverwritePlan | RebaseCopyPlan
export interface RebaseOverwritePlan {
changeMode: `mutate`
newBasePath: string
sourcePath: string
}
export interface RebaseCopyPlan {
changeMode: `copy`
newBasePath: string
sourcePath: string
targetPath: string
}
export const rebase = async (plan: RebasePlan): Promise<void> => {
// 1. Validate source is a Polen build
const manifestResult = await buildManifest.read(plan.sourcePath)
if (Err.is(manifestResult)) {
throw new Error(`Polen build manifest not found at: ${Path.join(plan.sourcePath, `.polen`, `build.json`)}`)
}
const manifest = manifestResult
// 2. Validate newBasePath is valid URL path
if (!isValidUrlPath(plan.newBasePath)) {
throw new Error(`Invalid base path: ${plan.newBasePath}`)
}
// 3. Handle copy vs mutate
let workingPath: string
if (plan.changeMode === `copy`) {
if (await Fs.exists(plan.targetPath)) {
const isEmpty = await Fs.isEmptyDir(plan.targetPath)
if (!isEmpty) {
throw new Error(`Target path already exists and is not empty: ${plan.targetPath}`)
}
}
await Fs.copyDir({ from: plan.sourcePath, to: plan.targetPath })
workingPath = plan.targetPath
} else {
workingPath = plan.sourcePath
}
// 4. Update HTML files with new base path
await updateHtmlFiles(workingPath, manifest.basePath, plan.newBasePath)
// 5. Update manifest
await updateManifest(workingPath, { basePath: plan.newBasePath })
}
//
//
//
//
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Local Helpers
//
//
// TODO: this is very generic, factor out to kit-temp
const isValidUrlPath = (path: string): boolean => {
// URL path should start with / and not contain invalid characters
if (!path.startsWith(`/`)) return false
if (!path.endsWith(`/`)) return false
// Basic validation - no spaces, proper URL characters
const urlPathRegex = /^\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]*\/$/
return urlPathRegex.test(path)
}
const updateHtmlFiles = async (buildPath: string, oldBasePath: string, newBasePath: string): Promise<void> => {
// Find all HTML files recursively
const htmlFiles = await findHtmlFiles(buildPath)
for (const htmlFile of htmlFiles) {
await updateHtmlFile(htmlFile, oldBasePath, newBasePath)
}
}
const findHtmlFiles = async (dir: string): Promise<string[]> => {
return await TinyGlobby.glob(`**/*.html`, {
absolute: true,
cwd: dir,
onlyFiles: true,
})
}
const updateHtmlFile = async (filePath: string, oldBasePath: string, newBasePath: string): Promise<void> => {
const content = await Fs.read(filePath)
if (content === null) {
throw new Error(`Could not read HTML file: ${filePath}`)
}
// Simple regex-based approach to update base tag
// Look for existing base tag first
const baseTagRegex = /<base\s+href\s*=\s*["']([^"']*)["'][^>]*>/i
let updatedContent: string
if (baseTagRegex.test(content)) {
// Update existing base tag
updatedContent = content.replace(baseTagRegex, `<base href="${newBasePath}">`)
} else {
// Insert new base tag in head
const headRegex = /<head[^>]*>/i
const headMatch = headRegex.exec(content)
if (headMatch) {
const insertPosition = headMatch.index + headMatch[0].length
updatedContent = content.slice(0, insertPosition)
+ `\n <base href="${newBasePath}">`
+ content.slice(insertPosition)
} else {
throw new Error(`Could not find <head> tag in HTML file: ${filePath}`)
}
}
await Fs.write({ path: filePath, content: updatedContent })
}
const updateManifest = async (buildPath: string, updates: Partial<PolenBuildManifest>): Promise<void> => {
const manifestPath = Path.join(buildPath, `.polen`, `build.json`)
const manifestResult = await buildManifest.read(buildPath)
if (Err.is(manifestResult)) {
throw new Error(`Polen build manifest not found at: ${manifestPath}`)
}
const currentManifest = manifestResult
const updatedManifest = { ...currentManifest, ...updates }
await Fs.write({
path: manifestPath,
content: JSON.stringify(updatedManifest, null, 2),
})
}