polen
Version:
A framework for delightful GraphQL developer portals
234 lines (211 loc) • 8.88 kB
text/typescript
import type { Config } from '#api/config/index'
import { Content } from '#api/content/$'
import { createNavbar } from '#api/content/navbar'
import { VitePluginSelfContainedMode } from '#cli/_/self-contained-mode'
import type { ReactRouter } from '#dep/react-router/index'
import type { Vite } from '#dep/vite/index'
import { VitePluginJson } from '#lib/vite-plugin-json/index'
import { ViteVirtual } from '#lib/vite-virtual/index'
import { debugPolen } from '#singletons/debug'
import { superjson } from '#singletons/superjson'
import { Json, Str } from '@wollybeard/kit'
import type { ProjectData } from '../../../project-data.js'
import { SchemaAugmentation } from '../../schema-augmentation/index.js'
import { Schema } from '../../schema/index.js'
import { createLogger } from '../logger.js'
import { polenVirtual } from '../vi.js'
import { Pages } from './pages.js'
const viTemplateVariables = polenVirtual([`template`, `variables`])
const viTemplateSchemaAugmentations = polenVirtual([`template`, `schema-augmentations`])
export const viProjectData = polenVirtual([`project`, `data.jsonsuper`], { allowPluginProcessing: true })
export interface ProjectRoutesModule {
routes: ReactRouter.RouteObject[]
}
export const Core = (config: Config.Config): Vite.PluginOption[] => {
let schemaCache: Awaited<ReturnType<typeof Schema.readOrThrow>> | null = null
const readSchema = async () => {
if (schemaCache === null) {
const schema = await Schema.readOrThrow({
...config.schema,
projectRoot: config.paths.project.rootDir,
})
// todo: augmentations scoped to a version
schema?.versions.forEach(version => {
SchemaAugmentation.apply(version.after, config.schemaAugmentations)
})
schemaCache = schema
}
return schemaCache
}
const plugins: Vite.Plugin[] = []
// Note: The main use for this right now is to resolve the react imports
// from the mdx vite plugin which have to go through the Polen exports since Polen keeps those deps bundled.
//
// If we manage to get the mdx vite plugin that defers JSX transform to Rolldown then we can remove this!
//
if (config.advanced.isSelfContainedMode) {
plugins.push(VitePluginSelfContainedMode({
projectDirPathExp: config.paths.project.rootDir,
}))
}
const jsonsuper = VitePluginJson.create({
codec: {
validate: superjson,
importPath: import.meta.resolve(`#singletons/superjson`),
importExport: `superjson`,
},
filter: {
moduleTypes: [`jsonsuper`],
},
})
return [
...plugins,
/**
* If a `polen*` import is encountered from the user's project, resolve it to the currently
* running source code of Polen rather than the user's node_modules.
*
* Useful for the following cases:
*
* 1. Main: Using Polen CLI from the source code against some local example/development project.
*
* 2. Secondary: Using Polen CLI on a project that does not have Polen installed.
* (User would likely not want to do this because they would not be able to achieve type safety)
*/
{
name: `polen:internal-import-alias`,
enforce: `pre` as const,
resolveId(id, importer) {
// const debug = debugPolen.sub(`vite-plugin:internal-import-alias`)
const isPolenImporter = Boolean(
importer
&& (
importer.startsWith(config.paths.framework.sourceDir)
|| polenVirtual.includes(importer)
|| (importer.startsWith(config.paths.framework.rootDir) && importer.endsWith(`index.html`))
),
)
if (!isPolenImporter) return null
// debug(`check candidate`, { id, importer, isPolenImporter })
const find = Str.pattern<{ groups: [`path`] }>(/^#(?<path>.+)/)
const match = Str.match(id, find)
if (!match) return null
const to = `${config.paths.framework.sourceDir}/${match.groups.path}${config.paths.framework.sourceExtension}`
// debug(`did resolve`, { from: id, to })
return to
},
},
jsonsuper,
...Pages({
config,
}),
{
name: `polen:core`,
config(_, { command }) {
return {
root: config.paths.framework.rootDir,
// todo
// future: {
// removePluginHookHandleHotUpdate: 'warn',
// removePluginHookSsrArgument: 'warn',
// removeServerModuleGraph: 'warn',
// removeServerHot: 'warn',
// removeServerTransformRequest: 'warn',
// removeSsrLoadModule: 'warn',
// },
define: {
__BUILDING__: Json.encode(command === `build`),
__SERVING__: Json.encode(command === `serve`),
__COMMAND__: Json.encode(command),
__BUILD_ARCHITECTURE__: Json.encode(config.build.architecture),
__BUILD_ARCHITECTURE_SSG__: Json.encode(config.build.architecture === `ssg`),
'process.env.NODE_ENV': Json.encode(config.advanced.debug ? 'development' : 'production'),
},
customLogger: createLogger(config),
esbuild: false,
build: {
target: `esnext`,
assetsDir: config.paths.project.relative.build.relative.assets,
rollupOptions: {
treeshake: {
// Aggressive tree-shaking for smallest bundles
moduleSideEffects: false, // Only include code if an export is actually used
annotations: true, // Respect @__PURE__ annotations for better dead code elimination
unknownGlobalSideEffects: false, // Assume global functions don't have side effects
},
},
minify: !config.advanced.debug,
outDir: config.paths.project.absolute.build.root,
emptyOutDir: true, // disables warning that build dir not in root dir; expected b/c root dir = framework package
},
}
},
...ViteVirtual.IdentifiedLoader.toHooks(
{
identifier: viTemplateVariables,
loader() {
const s = `export const templateVariables = ${JSON.stringify(config.templateVariables)}`
return s
},
},
{
identifier: viTemplateSchemaAugmentations,
loader() {
const s = `export const schemaAugmentations = ${JSON.stringify(config.schemaAugmentations)}`
return s
},
},
{
identifier: viProjectData,
async loader() {
const debug = debugPolen.sub(`module-project-data`)
debug(`load`, { id: viProjectData.id })
const schema = await readSchema()
const navbar = []
// ━ Schema presence causes adding some navbar items
if (schema) {
// IMPORTANT: Always ensure paths start with '/' for React Router compatibility.
// Without the leading slash, React Router treats paths as relative, which causes
// hydration mismatches between SSR (where base path is prepended) and client
// (where basename is configured). This ensures consistent behavior.
navbar.push({ pathExp: `/reference`, title: `Reference` })
if (schema.versions.length > 1) {
navbar.push({ pathExp: `/changelog`, title: `Changelog` })
}
}
//
// ━━ Scan pages and add to navbar
//
const pagesDir = config.paths.project.absolute.pages
const scanResult = await Content.scan({ dir: pagesDir })
const data = createNavbar(scanResult.list)
navbar.push(...data)
//
// ━━ Put It All together
//
const projectData: ProjectData = {
schema,
basePath: config.build.base,
paths: config.paths.project,
navbar, // Complete navbar with schema and pages
server: {
port: config.server.port,
static: {
// todo
// relative from CWD of process that boots n1ode server
// can easily break! Use path relative in server??
directory: `./` + config.paths.project.relative.build.root,
// Uses Hono route syntax - includes base path
route: config.build.base.slice(0, -1) + `/` + config.paths.project.relative.build.relative.assets
+ `/*`,
},
},
warnings: config.warnings,
}
// Return just the JSON string - let the JSON plugin handle the transformation
return superjson.stringify(projectData)
},
},
),
},
]
}