diff --git a/bun.lock b/bun.lock index 5f6c955..84fdbaa 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@cloudflare/workers-types": "4.20250424.0", "@eslint/js": "9.25.1", "@nordcraft/runtime": "1.0.1", + "@types/node": "22.14.1", "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", "eslint": "9.25.1", @@ -183,6 +184,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.31.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/type-utils": "8.31.0", "@typescript-eslint/utils": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.31.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", "@typescript-eslint/typescript-estree": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw=="], @@ -437,6 +440,8 @@ "undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], diff --git a/eslint.config.mjs b/eslint.config.mjs index c050c78..be7d99a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,7 +41,7 @@ export default [ sourceType: 'script', parserOptions: { - project: ['./tsconfig.json'], + project: ['./tsconfig.eslint.json'], tsconfigRootDir: __dirname, }, }, diff --git a/hono.d.ts b/hono.d.ts index 2f26601..9b154c4 100644 --- a/hono.d.ts +++ b/hono.d.ts @@ -1,7 +1,41 @@ -import type { ProjectFiles, ToddleProject } from '@nordcraft/ssr/dist/ssr.types' +import type { + Component, + PageComponent, +} from '@nordcraft/core/dist/component/component.types' +import type { + ProjectFiles, + Route, + ToddleProject, +} from '@nordcraft/ssr/dist/ssr.types' +import type { Routes } from './src/middleware/routesLoader' -export interface HonoEnv { - Variables: { - project: { files: ProjectFiles; project: ToddleProject } - } +export interface HonoEnv { + Variables: T +} + +export interface HonoProject { + // Holds project info such as sitemap, robots and icon + project: ToddleProject + config: ProjectFiles['config'] +} + +export interface HonoRoutes { + // Holds routes for the project + routes: Routes +} + +export interface HonoRoute { + route?: Route +} + +export interface HonoComponent { + // Holds all relevant files for a given component + files: ProjectFiles & { customCode: boolean } + component: Component +} + +export interface HonoPage { + // Holds all relevant files for a given component + files: ProjectFiles & { customCode: boolean } + page: PageComponent } diff --git a/package.json b/package.json index 4e3ac61..3caa561 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "nordcraft-cloudflare-worker", "scripts": { - "predev": "bun scripts/syncStaticAssets.js && bunx esbuild --bundle --outdir=dist --platform=node --format=esm src/index.ts", - "dev": "wrangler dev --no-bundle", - "build": "bun scripts/syncStaticAssets.js && bunx esbuild --bundle --outdir=dist --platform=node --format=esm src/index.ts", + "predev": "bun scripts/syncStaticAssets.ts && bunx esbuild --bundle --outdir=dist --platform=node --format=esm src/index.ts", + "dev": "wrangler dev --no-bundle --port 8989", + "build": "bun scripts/syncStaticAssets.ts && bunx esbuild --bundle --outdir=dist --platform=node --format=esm src/index.ts", "deploy": "wrangler deploy --no-bundle", "typecheck": "tsc --noEmit", "watch": "tsc --noEmit -w", @@ -22,6 +22,7 @@ "@nordcraft/runtime": "1.0.1", "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", + "@types/node": "22.14.1", "eslint-plugin-inclusive-language": "2.2.1", "eslint": "9.25.1", "prettier-plugin-organize-imports": "4.1.0", diff --git a/scripts/routes.ts b/scripts/routes.ts new file mode 100644 index 0000000..9c533f7 --- /dev/null +++ b/scripts/routes.ts @@ -0,0 +1,117 @@ +import type { + Component, + RouteDeclaration, +} from '@toddledev/core/dist/component/component.types' +import { isPageComponent } from '@toddledev/core/dist/component/isPageComponent' +import { createStylesheet } from '@toddledev/core/dist/styling/style.css' +import { theme as defaultTheme } from '@toddledev/core/dist/styling/theme.const' +import { takeIncludedComponents } from '@toddledev/ssr/dist/components/utils' +import type { + ProjectFiles, + Route, + ToddleProject, +} from '@toddledev/ssr/dist/ssr.types' +import { + generateCustomCodeFile, + hasCustomCode, + takeReferencedFormulasAndActions, +} from '@toddledev/ssr/src/custom-code/codeRefs' +import { removeTestData } from '@toddledev/ssr/src/rendering/testData' + +interface Routes { + pages: Record + routes: Record +} + +type Files = Record< + string, + { component: Component; files: ProjectFiles & { customCode: boolean } } +> + +export const splitRoutes = (json: { + files: ProjectFiles + project: ToddleProject +}): { + project: { project: ToddleProject; config: ProjectFiles['config'] } + routes: Routes + files: Files + styles: Record + code: Record + components: Partial> +} => { + const filesMap: Files = {} + const stylesMap: Record = {} + const codeMap: Record = {} + const { files } = json + + const routes: Routes = { + routes: { ...(files.routes ?? {}) }, + pages: {}, + } + Object.entries(files.components).forEach(([name, component]) => { + if (component) { + if (isPageComponent(component)) { + routes.pages[name] = { + name, + route: { + path: component.route.path, + query: component.route.query, + }, + } + const components = takeIncludedComponents({ + root: component, + projectComponents: files.components, + packages: files.packages, + includeRoot: true, + }) + const theme = + (files.themes + ? Object.values(files.themes)[0] + : files.config?.theme) ?? defaultTheme + const styles = createStylesheet(component, components, theme, { + // The reset stylesheet is loaded separately + includeResetStyle: false, + // Font faces are created from a stylesheet referenced in the head + createFontFaces: false, + }) + stylesMap[name] = styles + let customCode = false + if (hasCustomCode(component, files)) { + customCode = true + const code = takeReferencedFormulasAndActions({ + component, + files, + }) + const output = generateCustomCodeFile({ + code, + componentName: component.name, + projectId: 'toddle', + }) + codeMap[name] = output + } + filesMap[name] = { + component: removeTestData(component), + files: { + customCode, + config: files.config, + themes: files.themes, + components: Object.fromEntries( + components.map((c) => [c.name, removeTestData(c)]), + ), + // Routes are not necessary in output files for components + routes: undefined, + }, + } + } + } + }) + + return { + routes, + components: files.components, + files: filesMap, + styles: stylesMap, + code: codeMap, + project: { project: json.project, config: files.config }, + } +} diff --git a/scripts/syncStaticAssets.js b/scripts/syncStaticAssets.js deleted file mode 100644 index 0657f16..0000000 --- a/scripts/syncStaticAssets.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copy files from the static-assets directory to the dist directory using fs -// This script is executed by the build process -const fs = require('fs') -import { RESET_STYLES } from '@nordcraft/core/dist/styling/theme.const' - -// assets/_static/ folder -fs.mkdirSync(`${__dirname}/../assets/_static`, { recursive: true }) -;[ - 'page.main.esm.js', - 'page.main.esm.js.map', - 'custom-element.main.esm.js', -].forEach((f) => - fs.copyFileSync( - `${__dirname}/../node_modules/@nordcraft/runtime/dist/${f}`, - `${__dirname}/../assets/_static/${f}`, - ), -) -fs.writeFileSync(`${__dirname}/../assets/_static/reset.css`, RESET_STYLES) - -// dist/ folder -fs.mkdirSync(`${__dirname}/../dist`, { recursive: true }) -fs.copyFileSync( - `${__dirname}/../__project__/project.json`, - `${__dirname}/../dist/project.json`, -) diff --git a/scripts/syncStaticAssets.ts b/scripts/syncStaticAssets.ts new file mode 100644 index 0000000..6844ca9 --- /dev/null +++ b/scripts/syncStaticAssets.ts @@ -0,0 +1,62 @@ +// Copy files from the static-assets directory to the dist directory using fs +// This script is executed by the build process +import { RESET_STYLES } from '@nordcraft/core/dist/styling/theme.const' +import * as fs from 'fs' +import { splitRoutes } from './routes' + +// assets/_static/ folder +fs.rmdirSync(`${__dirname}/../assets/_static`, { recursive: true }) +fs.mkdirSync(`${__dirname}/../assets/_static`, { recursive: true }) +;[ + 'page.main.esm.js', + 'page.main.esm.js.map', + 'custom-element.main.esm.js', +].forEach((f) => + fs.copyFileSync( + `${__dirname}/../node_modules/@nordcraft/runtime/dist/${f}`, + `${__dirname}/../assets/_static/${f}`, + ), +) +fs.writeFileSync(`${__dirname}/../assets/_static/reset.css`, RESET_STYLES) + +// dist/ folder +fs.rmdirSync(`${__dirname}/../dist`, { recursive: true }) +fs.mkdirSync(`${__dirname}/../dist`, { recursive: true }) +const projectFile = fs.readFileSync(`${__dirname}/../__project__/project.json`) +const json = JSON.parse(projectFile.toString()) +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { project, routes, components, files, styles, code } = splitRoutes(json) +// Create a stylesheet for each component +Object.entries(styles).forEach(([name, style]) => { + fs.writeFileSync( + `${__dirname}/../assets/_static/${name.toLowerCase()}.css`, + style, + ) +}) +// Create a js file with custom code for each component +Object.entries(code).forEach(([name, c]) => { + fs.writeFileSync( + `${__dirname}/../assets/_static/cc_${name.toLowerCase()}.js`, + c, + ) +}) +const jsiFy = (obj: any) => `export default ${JSON.stringify(obj)}` +fs.writeFileSync(`${__dirname}/../dist/project.js`, jsiFy(project)) +fs.writeFileSync(`${__dirname}/../dist/routes.js`, jsiFy(routes)) +// fs.writeFileSync( +// `${__dirname}/../dist/components.js`, +// JSON.stringify(Object.entries(components).map(([name]) => name)), +// ) +fs.mkdirSync(`${__dirname}/../dist/components`, { recursive: true }) +Object.entries(files).forEach(([name, file]) => { + fs.writeFileSync( + `${__dirname}/../dist/components/${name.toLowerCase()}.js`, + jsiFy(file.files), + ) +}) +// Object.entries(components).forEach(([name, file]) => { +// fs.writeFileSync( +// `${__dirname}/../dist/components/${name.toLowerCase()}.js`, +// JSON.stringify(file), +// ) +// }) diff --git a/src/index.ts b/src/index.ts index fe97292..39bf6ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,11 @@ import { initIsEqual } from '@nordcraft/ssr/dist/rendering/equals' -import type { ProjectFiles, ToddleProject } from '@nordcraft/ssr/dist/ssr.types' import { Hono } from 'hono' +import { createMiddleware } from 'hono/factory' import type { HonoEnv } from '../hono' +import { componentLoader } from './middleware/componentLoader' +import { pageLoader } from './middleware/pageLoader' +import { loadProjectInfo } from './middleware/projectInfo' +import { routesLoader } from './middleware/routesLoader' import { proxyRequestHandler } from './routes/apiProxy' import { customCode } from './routes/customCode' import { customElement } from './routes/customElement' @@ -20,49 +24,146 @@ initIsEqual() const app = new Hono() -// Keep the project reference in memory for future requests -let project: { files: ProjectFiles; project: ToddleProject } -// Load the project onto context to make it easier to use for other routes -app.use(async (c, next) => { - if (!project) { - const path = `./project.json` - try { - const content = await import(path) - project = JSON.parse(content.default) as { - files: ProjectFiles - project: ToddleProject - } - } catch (e) { - console.error( - 'Unable to load project.json', - e instanceof Error ? e.message : e, - ) - } - if (!project) { - return c.text('Project not found', { status: 404 }) - } - } - c.set('project', project) - return next() -}) +// // Keep the project reference in memory for future requests +// let project: { files: ProjectFiles; project: ToddleProject } +// // Load the project onto context to make it easier to use for other routes +// app.use(async (c, next) => { +// if (!project) { +// const path = `./project.json` +// try { +// const content = await import(path) +// project = JSON.parse(content.default) as { +// files: ProjectFiles +// project: ToddleProject +// } +// } catch (e) { +// console.error( +// 'Unable to load project.json', +// e instanceof Error ? e.message : e, +// ) +// } +// if (!project) { +// return c.text('Project not found', { status: 404 }) +// } +// } +// c.set('project', project) +// return next() +// }) -app.get('/sitemap.xml', sitemap) -app.get('/robots.txt', robots) -app.get('/manifest.json', manifest) -app.get('/favicon.ico', favicon) -app.get('/serviceWorker.js', serviceWorker) +app.get('/sitemap.xml', loadProjectInfo, routesLoader, sitemap) +app.get('/robots.txt', loadProjectInfo, robots) +app.get('/manifest.json', loadProjectInfo, manifest) +app.get('/favicon.ico', loadProjectInfo, favicon) +app.get('/serviceWorker.js', loadProjectInfo, serviceWorker) // Nordcraft specific endpoints/services on /.toddle/ subpath 👇 app.route('/.toddle/fonts', fontRouter) -app.get('/.toddle/stylesheet/:pageName{.+.css}', stylesheetHandler) -app.get('/.toddle/custom-code.js', customCode) +app.get( + '/.toddle/stylesheet/:pageName{.+.css}', + createMiddleware((c, next) => { + let pageName = c.req.param('pageName') + // Remove the .css extension + pageName = pageName.slice(0, '.css'.length * -1) + return componentLoader(pageName)(c, next) + }), + stylesheetHandler, +) // single page +app.get('/.toddle/custom-code.js', customCode) // Single (or all) component app.all( '/.toddle/omvej/components/:componentName/apis/:apiName', proxyRequestHandler, ) -app.get('/.toddle/custom-element/:filename{.+.js}', customElement) +app.get( + '/.toddle/custom-element/:filename{.+.js}', + loadProjectInfo, + customElement, +) // project infor + single component -// Treat all other requests as page requests -app.get('/*', toddlePage) +// Treat all other requests as route or page requests +// const rest = createFactory().createHandlers( +// routesLoader, +// // First we try loading a route if it exists +// routeLoader, +// (ctx, next) => { +// const route = ctx.var.route +// if (route) { +// return routeHandler(ctx, route) +// } +// return next() +// }, +// pageLoader, +// toddlePage, +// ) + +// app.get('*', async (ctx) => { +// console.log('Got request', new URL(ctx.req.url).href) +// const countInput = Number(ctx.req.query('count')) +// const parallel = ctx.req.query('parallel') === 'true' ? true : false +// const count = isNaN(countInput) || countInput <= 0 ? 100 : countInput +// const now = new Date() +// console.time('loadComponents') +// const components = await loadJsonFile(`./components.json`) +// if (!components) { +// return ctx.text('Components not found', { status: 404 }) +// } +// const componentFileTime = new Date().getTime() - now.getTime() +// console.timeEnd('loadComponents') +// const loadComponentsStart = new Date() +// const componentLoadTimes: Record = {} +// console.time('load each component') +// if (!parallel) { +// for (let i = 0; i < Math.min(count, components.length); i++) { +// const componentName = components[i] +// const componentLoadStart = new Date() +// console.time(`load ${componentName}`) +// const component = await loadJsonFile(`./components/${componentName}.json`) +// console.timeEnd(`load ${componentName}`) +// if (!component) { +// break +// } +// const componentLoadEnd = new Date() +// componentLoadTimes[componentName] = +// componentLoadEnd.getTime() - componentLoadStart.getTime() +// } +// } else { +// await Promise.all( +// components +// .slice(0, Math.min(count, components.length)) +// .map(async (name) => { +// const componentLoadStart = new Date() +// await loadJsonFile(`./components/${name}.json`) +// const componentLoadEnd = new Date() +// componentLoadTimes[name] = +// componentLoadEnd.getTime() - componentLoadStart.getTime() +// }), +// ) +// } + +// console.timeEnd('load each component') +// const loadComponentsEnd = new Date() +// return ctx.json({ +// parallel, +// componentFileTime, +// loadComponentsTime: +// loadComponentsEnd.getTime() - loadComponentsStart.getTime(), +// componentLoadTimes, +// }) +// }) // TODO: remove this +app.get( + '/*', + routesLoader, + loadProjectInfo, + // First we try loading a route if it exists + // routeLoader, + // createMiddleware>((ctx, next) => { + // const route = ctx.var.route + // if (route) { + // return routeHandler(ctx, route) + // } + // return next() + // }), + pageLoader, + toddlePage, +) // routes + single page export default app diff --git a/src/middleware/componentLoader.ts b/src/middleware/componentLoader.ts new file mode 100644 index 0000000..aab1cfc --- /dev/null +++ b/src/middleware/componentLoader.ts @@ -0,0 +1,19 @@ +import type { ProjectFiles } from '@toddledev/ssr/dist/ssr.types' +import type { MiddlewareHandler } from 'hono' +import type { HonoComponent, HonoEnv } from '../../hono' +import { loadJsFile } from './jsLoader' + +export const componentLoader = + (name: string): MiddlewareHandler> => + async (ctx, next) => { + const componentFile = await loadJsFile< + ProjectFiles & { customCode: boolean } + >(`./components/${name}.js`) + const component = componentFile?.components?.[name] + if (!component) { + return ctx.text('Component not found', { status: 404 }) + } + ctx.set('component', component) + ctx.set('files', componentFile) + return next() + } diff --git a/src/middleware/jsLoader.ts b/src/middleware/jsLoader.ts new file mode 100644 index 0000000..523fa00 --- /dev/null +++ b/src/middleware/jsLoader.ts @@ -0,0 +1,15 @@ +const fileCache = new Map() + +export const loadJsFile = async (path: string): Promise => { + if (fileCache.has(path)) { + return fileCache.get(path) as T + } + try { + const content = await import(path.toLowerCase()) + const parsed = content.default as T + fileCache.set(path, parsed) + return parsed + } catch (e) { + console.error(`Unable to load ${path}`, e instanceof Error ? e.message : e) + } +} diff --git a/src/middleware/pageLoader.ts b/src/middleware/pageLoader.ts new file mode 100644 index 0000000..d2ada27 --- /dev/null +++ b/src/middleware/pageLoader.ts @@ -0,0 +1,29 @@ +import { isPageComponent } from '@nordcraft/core/dist/component/isPageComponent' +import { matchPageForUrl } from '@nordcraft/ssr/dist/routing/routing' +import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types' +import type { MiddlewareHandler } from 'hono' +import type { HonoEnv, HonoPage, HonoRoutes } from '../../hono' +import { loadJsFile } from './jsLoader' + +export const pageLoader: MiddlewareHandler< + HonoEnv +> = async (ctx, next) => { + const url = new URL(ctx.req.url) + const page = matchPageForUrl({ + url, + components: ctx.var.routes.pages, + }) + if (page) { + const pageContent = await loadJsFile< + ProjectFiles & { customCode: boolean } + >(`./components/${page.name}.js`) + const component = pageContent?.components?.[page.name] + if (!component || !isPageComponent(component)) { + return ctx.text('Page content not found', { status: 404 }) + } + ctx.set('page', component) + ctx.set('files', pageContent) + return next() + } + return ctx.text('Page not found', { status: 404 }) +} diff --git a/src/middleware/projectInfo.ts b/src/middleware/projectInfo.ts new file mode 100644 index 0000000..aa6c14c --- /dev/null +++ b/src/middleware/projectInfo.ts @@ -0,0 +1,15 @@ +import { createMiddleware } from 'hono/factory' +import type { HonoEnv, HonoProject } from '../../hono' +import { loadJsFile } from './jsLoader' + +export const loadProjectInfo = createMiddleware>( + async (ctx, next) => { + const project = await loadJsFile('./project.js') + if (!project) { + return ctx.text('Project configuration not found', { status: 404 }) + } + ctx.set('project', project.project) + ctx.set('config', project.config) + return next() + }, +) diff --git a/src/middleware/routeLoader.ts b/src/middleware/routeLoader.ts new file mode 100644 index 0000000..4a2df82 --- /dev/null +++ b/src/middleware/routeLoader.ts @@ -0,0 +1,28 @@ +import { matchRouteForUrl } from '@toddledev/ssr/dist/routing/routing' +import type { Route } from '@toddledev/ssr/dist/ssr.types' +import { createMiddleware } from 'hono/factory' +import type { HonoEnv, HonoRoute, HonoRoutes } from '../../hono' + +const routes: Partial> = {} + +export const routeLoader = createMiddleware>( + async (ctx, next) => { + const url = new URL(ctx.req.url) + let route: Route | undefined = routes[url.pathname] + if (route) { + ctx.set('route', route) + routes[url.pathname] = route + return next() + } + route = matchRouteForUrl({ + url, + routes: ctx.var.routes?.routes ?? {}, + }) + if (!route) { + return next() + } + routes[url.pathname] = route + ctx.set('route', route) + return next() + }, +) diff --git a/src/middleware/routesLoader.ts b/src/middleware/routesLoader.ts new file mode 100644 index 0000000..a308834 --- /dev/null +++ b/src/middleware/routesLoader.ts @@ -0,0 +1,27 @@ +import type { RouteDeclaration } from '@toddledev/core/dist/component/component.types' +import type { Route } from '@toddledev/ssr/dist/ssr.types' +import { createMiddleware } from 'hono/factory' +import type { HonoEnv, HonoRoutes } from '../../hono' +import { loadJsFile } from './jsLoader' + +export interface Routes { + pages: Record + routes: Record +} + +let routes: Routes | undefined + +export const routesLoader = createMiddleware>( + async (ctx, next) => { + if (!routes) { + routes = await loadJsFile('routes.js') + if (!routes) { + return ctx.text('Route declarations for project not found', { + status: 404, + }) + } + } + ctx.set('routes', routes) + return next() + }, +) diff --git a/src/routes/customCode.ts b/src/routes/customCode.ts index a279b58..402db59 100644 --- a/src/routes/customCode.ts +++ b/src/routes/customCode.ts @@ -6,14 +6,16 @@ import { } from '@nordcraft/ssr/src/custom-code/codeRefs' import { escapeSearchParameter } from '@nordcraft/ssr/src/rendering/request' import type { Context } from 'hono' -import type { HonoEnv } from '../../hono' +import type { HonoComponent, HonoEnv, HonoProject } from '../../hono' -export const customCode = async (c: Context) => { +export const customCode = async ( + c: Context>, +) => { const url = new URL(c.req.url) const entry = escapeSearchParameter(url.searchParams.get('entry')) let component: Component | undefined if (isDefined(entry)) { - component = c.var.project.files.components[entry] + component = c.var.component if (!isDefined(component)) { return c.text(`Component "${entry}" not found in project`, { status: 404, @@ -23,12 +25,12 @@ export const customCode = async (c: Context) => { const code = takeReferencedFormulasAndActions({ component, - files: c.var.project.files, + files: c.var.files, }) const output = generateCustomCodeFile({ code, componentName: component?.name ?? entry ?? undefined, - projectId: c.var.project.project.short_id, + projectId: c.var.project.short_id, }) const headers: Record = { 'content-type': 'text/javascript', diff --git a/src/routes/customElement.ts b/src/routes/customElement.ts index 0629606..02c6eb0 100644 --- a/src/routes/customElement.ts +++ b/src/routes/customElement.ts @@ -4,18 +4,21 @@ import { takeIncludedComponents } from '@nordcraft/ssr/dist/components/utils' import { removeTestData } from '@nordcraft/ssr/dist/rendering/testData' import { replaceTagInNodes } from '@nordcraft/ssr/dist/utils/tags' import { getFontCssUrl } from '@nordcraft/ssr/src/rendering/fonts' -import { escapeSearchParameter } from '@nordcraft/ssr/src/rendering/request' import { transformRelativePaths } from '@nordcraft/ssr/src/utils/media' import type { Context } from 'hono' -import type { HonoEnv } from '../../hono' +import type { HonoComponent, HonoEnv, HonoProject } from '../../hono' export const customElement = async ( - ctx: Context, + ctx: Context< + HonoEnv, + '/.toddle/custom-element/:filename{.+.js}' + >, ) => { const url = new URL(ctx.req.url) + const project = ctx.var.project // Get name of the component from the URL path (e.g. https://toddle.dev/.toddle/custom-element/MyComponent.js) -> MyComponent // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const name = ctx.req.param('filename')?.replace('.js', '') + // const name = ctx.req.param('filename')?.replace('.js', '') const errorResponse = (error: string, status: 403 | 404) => ctx.json( { @@ -31,17 +34,18 @@ export const customElement = async ( status, }, ) - const { files, project } = ctx.var.project + const files = ctx.var.files try { - const component = Object.values(files.components).find( - (c) => c!.name === name, - ) - if (!component) { - return errorResponse( - `Unable to find component ${escapeSearchParameter(name)}`, - 404, - ) - } + const component = ctx.var.component + // const component = Object.values(files.components).find( + // (c) => c!.name === name, + // ) + // if (!component) { + // return errorResponse( + // `Unable to find component ${escapeSearchParameter(name)}`, + // 404, + // ) + // } if (component.route) { return errorResponse( diff --git a/src/routes/favicon.ts b/src/routes/favicon.ts index be213a0..1b4a522 100644 --- a/src/routes/favicon.ts +++ b/src/routes/favicon.ts @@ -3,12 +3,12 @@ import { validateUrl } from '@nordcraft/core/dist/utils/url' import { isDefined } from '@nordcraft/core/dist/utils/util' import type { Context } from 'hono' import { stream } from 'hono/streaming' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoProject } from '../../hono' -export const favicon = async (c: Context) => { +export const favicon = async (c: Context>) => { try { const iconUrl = applyFormula( - c.var.project.files.config?.meta?.icon?.formula, + c.var.config?.meta?.icon?.formula, undefined as any, ) const validIconUrl = validateUrl(iconUrl) diff --git a/src/routes/manifest.ts b/src/routes/manifest.ts index 014530d..462801b 100644 --- a/src/routes/manifest.ts +++ b/src/routes/manifest.ts @@ -2,14 +2,14 @@ import { applyFormula } from '@nordcraft/core/dist/formula/formula' import { validateUrl } from '@nordcraft/core/dist/utils/url' import type { Context } from 'hono' import { stream } from 'hono/streaming' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoProject } from '../../hono' const MANIFEST_CONTENT_TYPE = 'application/manifest+json' -export const manifest = async (c: Context) => { +export const manifest = async (c: Context>) => { try { const manifestUrl = applyFormula( - c.var.project.files.config?.meta?.manifest?.formula, + c.var.config?.meta?.manifest?.formula, undefined as any, ) const validManifestUrl = validateUrl(manifestUrl) diff --git a/src/routes/robots.ts b/src/routes/robots.ts index a87d5e1..501f6f0 100644 --- a/src/routes/robots.ts +++ b/src/routes/robots.ts @@ -2,13 +2,13 @@ import { applyFormula } from '@nordcraft/core/dist/formula/formula' import { validateUrl } from '@nordcraft/core/dist/utils/url' import type { Context } from 'hono' import { stream } from 'hono/streaming' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoProject } from '../../hono' const ROBOTS_CONTENT_TYPE = 'text/plain' -export const robots = async (c: Context) => { +export const robots = async (c: Context>) => { try { - const robots = c.var.project.files.config?.meta?.robots + const robots = c.var.config?.meta?.robots // we don't provide a context below, as the formula should just be a value formula const robotsUrl = applyFormula(robots?.formula, undefined as any) const validatedRobotsUrl = validateUrl(robotsUrl) diff --git a/src/routes/routeHandler.ts b/src/routes/routeHandler.ts index 509398a..3168266 100644 --- a/src/routes/routeHandler.ts +++ b/src/routes/routeHandler.ts @@ -3,11 +3,14 @@ import { REWRITE_HEADER } from '@nordcraft/core/dist/utils/url' import { getRouteDestination } from '@nordcraft/ssr/dist/routing/routing' import type { Route } from '@nordcraft/ssr/dist/ssr.types' import type { Context } from 'hono' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoRoute, HonoRoutes } from '../../hono' -export const routeHandler = async (c: Context, route: Route) => { +export const routeHandler = async ( + c: Context>, + route: Route, +) => { const destination = getRouteDestination({ - files: c.var.project.files, + files: {} as any, // c.var.project.files, req: c.req.raw, route, }) diff --git a/src/routes/serviceWorker.ts b/src/routes/serviceWorker.ts index de5dea1..129612d 100644 --- a/src/routes/serviceWorker.ts +++ b/src/routes/serviceWorker.ts @@ -3,11 +3,11 @@ import { validateUrl } from '@nordcraft/core/dist/utils/url' import { isDefined } from '@nordcraft/core/dist/utils/util' import type { Context } from 'hono' import { stream } from 'hono/streaming' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoProject } from '../../hono' -export const serviceWorker = async (c: Context) => { +export const serviceWorker = async (c: Context>) => { try { - const config = c.var.project.files.config + const config = c.var.config const serviceWorkerUrl = isDefined(config?.meta?.serviceWorker) ? // We don't need to provide a context for applyFormula, as the formula should just be a value formula applyFormula(config.meta.serviceWorker.formula, undefined as any) diff --git a/src/routes/sitemap.ts b/src/routes/sitemap.ts index c243a43..4014b04 100644 --- a/src/routes/sitemap.ts +++ b/src/routes/sitemap.ts @@ -1,19 +1,21 @@ import type { PageComponent } from '@nordcraft/core/dist/component/component.types' -import { isPageComponent } from '@nordcraft/core/dist/component/isPageComponent' import { applyFormula } from '@nordcraft/core/dist/formula/formula' import { validateUrl } from '@nordcraft/core/dist/utils/url' import { isDefined } from '@nordcraft/core/dist/utils/util' import type { Context } from 'hono' import { stream } from 'hono/streaming' -import type { HonoEnv } from '../../hono' +import type { HonoEnv, HonoProject, HonoRoutes } from '../../hono' const SITEMAP_CONTENT_TYPE = 'application/xml' -export const sitemap = async (c: Context) => { +export const sitemap = async ( + c: Context>, +) => { try { const url = new URL(c.req.url) - const project = c.var.project - const sitemapFormula = project.files.config?.meta?.sitemap?.formula + const config = c.var.config + const routes = c.var.routes + const sitemapFormula = config?.meta?.sitemap?.formula if (isDefined(sitemapFormula)) { const sitemapUrl = validateUrl( // we don't provide a context for applyFormula, as the formula should just be a value formula @@ -35,10 +37,8 @@ export const sitemap = async (c: Context) => { const content = `\ - ${Object.values(project.files.components) + ${Object.values(routes.pages) .filter((component, i): component is PageComponent => - component && - isPageComponent(component) && // only include static routes component.route?.path.every((path) => path.type === 'static') && // limit to 1000 pages for now to keep performance reasonable diff --git a/src/routes/stylesheetHandler.ts b/src/routes/stylesheetHandler.ts index 73ddfdb..d9900f9 100644 --- a/src/routes/stylesheetHandler.ts +++ b/src/routes/stylesheetHandler.ts @@ -1,35 +1,30 @@ -import { isPageComponent } from '@nordcraft/core/dist/component/isPageComponent' import { createStylesheet } from '@nordcraft/core/dist/styling/style.css' import { theme as defaultTheme } from '@nordcraft/core/dist/styling/theme.const' import { takeIncludedComponents } from '@nordcraft/ssr/dist/components/utils' import type { Context } from 'hono' -import type { HonoEnv } from '../../hono' +import type { HonoComponent, HonoEnv, HonoRoutes } from '../../hono' export const stylesheetHandler = async ( - c: Context, + c: Context< + HonoEnv, + '/.toddle/stylesheet/:pageName{.+.css}' + >, ) => { - const project = c.var.project - let pageName = c.req.param('pageName') - // Remove the .css extension - pageName = pageName.slice(0, '.css'.length * -1) - const page = project.files.components[pageName] - if (!page || !isPageComponent(page)) { - return new Response(null, { - headers: { 'content-type': 'text/css' }, - status: 404, - }) + const files = c.var.files + const page = c.var.component + if (!page.route) { + return c.text('Page not found', { status: 404 }) } // Find the theme to use for the page const theme = - (project.files.themes - ? Object.values(project.files.themes)[0] - : project.files.config?.theme) ?? defaultTheme + (files.themes ? Object.values(files.themes)[0] : files.config?.theme) ?? + defaultTheme // Get all included components on the page const includedComponents = takeIncludedComponents({ root: page, - projectComponents: project.files.components, - packages: project.files.packages, + projectComponents: files.components, + packages: files.packages, includeRoot: true, }) diff --git a/src/routes/toddlePage.ts b/src/routes/toddlePage.ts index 50b6d89..62c627b 100644 --- a/src/routes/toddlePage.ts +++ b/src/routes/toddlePage.ts @@ -11,42 +11,24 @@ import { renderHeadItems, } from '@nordcraft/ssr/dist/rendering/head' import { getCharset, getHtmlLanguage } from '@nordcraft/ssr/dist/rendering/html' -import { - get404Page, - matchPageForUrl, - matchRouteForUrl, -} from '@nordcraft/ssr/dist/routing/routing' -import { hasCustomCode } from '@nordcraft/ssr/src/custom-code/codeRefs' import { removeTestData } from '@nordcraft/ssr/src/rendering/testData' import type { Context } from 'hono' import { html, raw } from 'hono/html' -import type { HonoEnv } from '../../hono' -import { routeHandler } from './routeHandler' +import type { HonoEnv, HonoPage, HonoProject, HonoRoutes } from '../../hono' -export const toddlePage = async (c: Context) => { +export const toddlePage = async ( + c: Context>, +) => { const project = c.var.project + const files = c.var.files + const page = c.var.page const url = new URL(c.req.url) - // Prefer routes over pages in case of conflicts - const route = matchRouteForUrl({ url, routes: project.files.routes }) - if (route) { - return routeHandler(c, route) - } - let page = matchPageForUrl({ - url, - components: project.files.components, - }) - if (!page) { - page = get404Page(project.files.components) - if (!page) { - return c.html('Page not found', { status: 404 }) - } - } const formulaContext = getPageFormulaContext({ component: page, branchName: 'main', req: c.req.raw, logErrors: true, - files: project.files, + files, }) const language = getHtmlLanguage({ pageInfo: page.route.info, @@ -56,15 +38,14 @@ export const toddlePage = async (c: Context) => { // Find the theme to use for the page const theme = - (project.files.themes - ? Object.values(project.files.themes)[0] - : project.files.config?.theme) ?? defaultTheme + (files.themes ? Object.values(files.themes)[0] : files.config?.theme) ?? + defaultTheme // Get all included components on the page const includedComponents = takeIncludedComponents({ root: page, - projectComponents: project.files.components, - packages: project.files.packages, + projectComponents: files.components, + packages: files.packages, includeRoot: true, }) @@ -73,8 +54,8 @@ export const toddlePage = async (c: Context) => { getComponent: (name, packageName) => { const nodeLookupKey = [packageName, name].filter(isDefined).join('/') const component = packageName - ? project.files.packages?.[packageName]?.components[name] - : project.files.components[name] + ? files.packages?.[packageName]?.components[name] + : files.components[name] if (!component) { console.warn(`Unable to find component ${nodeLookupKey} in files`) return undefined @@ -84,8 +65,8 @@ export const toddlePage = async (c: Context) => { }, packageName: undefined, globalFormulas: { - formulas: project.files.formulas, - packages: project.files.packages, + formulas: files.formulas, + packages: files.packages, }, }) const head = renderHeadItems({ @@ -96,10 +77,10 @@ export const toddlePage = async (c: Context) => { // Just to be explicit about where to grab the reset stylesheet from resetStylesheetPath: '/_static/reset.css', // This refers to the stylesheet endpoint declared in index.ts - pageStylesheetPath: `/.toddle/stylesheet/${page.name}.css`, + pageStylesheetPath: `/_static/${page.name.toLowerCase()}.css`, page: toddleComponent, - files: project.files, - project: project.project, + files: files, + project, context: formulaContext, theme, }), @@ -109,7 +90,7 @@ export const toddlePage = async (c: Context) => { formulaContext, env: formulaContext.env as ToddleServerEnv, req: c.req.raw, - files: project.files, + files: files, includedComponents, evaluateComponentApis: async (_) => ({ // TODO: Show an example of how to evaluate APIs - potentially using an adapter @@ -123,7 +104,7 @@ export const toddlePage = async (c: Context) => { // Prepare the data to be passed to the client for hydration const toddleInternals: ToddleInternals = { - project: c.var.project.project.short_id, + project: project.short_id, branch: 'main', commit: 'unknown', pageState: formulaContext.data, @@ -132,16 +113,13 @@ export const toddlePage = async (c: Context) => { isPageLoaded: false, cookies: Object.keys(formulaContext.env.request.cookies), } - const usesCustomCode = hasCustomCode(toddleComponent, c.var.project.files) + const usesCustomCode = files.customCode let codeImport = '' if (usesCustomCode) { - const customCodeSearchParams = new URLSearchParams([ - ['entry', toddleComponent.name], - ]) codeImport = ` ', diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..eee51b0 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "noEmit": true, + "strict": true, + "allowJs": false + }, + "include": ["src", "scripts"] +} diff --git a/tsconfig.json b/tsconfig.json index 006929d..c326377 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Enable latest features - "lib": ["DOM", "DOM.Iterable", "ESNext"], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", @@ -22,7 +22,8 @@ // Some stricter flags "noUnusedLocals": true, "noUnusedParameters": true, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + "types": ["@cloudflare/workers-types"] }, "include": ["src"], "exclude": ["node_modules"] diff --git a/wrangler.toml b/wrangler.toml index 015d047..62a302d 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,8 +6,11 @@ compatibility_date = "2025-02-05" rules = [ # Include all json files in the worker (should only be the actual project) - { type = "Text", globs = ["**/*.json"], fallthrough = true } + { type = "ESModule", globs = ["**/*.js"], fallthrough = true } ] +[observability.logs] +enabled = true + [assets] directory = "./assets" \ No newline at end of file