diff --git a/.github/frameworks.json b/.github/frameworks.json index 6ef9d50..277ad81 100644 --- a/.github/frameworks.json +++ b/.github/frameworks.json @@ -145,6 +145,12 @@ { "type": "build", "runFrequency": 5 }, { "type": "dependencies" } ] + }, + "app": { + "package": "app-mastro", + "buildScript": "build", + "buildOutputDir": "generated", + "measurements": [{ "type": "ssr" }] } }, { diff --git a/packages/app-mastro/.vscode/extensions.json b/packages/app-mastro/.vscode/extensions.json new file mode 100644 index 0000000..74a66e8 --- /dev/null +++ b/packages/app-mastro/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["ms-fast.fast-tagged-templates"] +} diff --git a/packages/app-mastro/README.md b/packages/app-mastro/README.md new file mode 100644 index 0000000..ecd9935 --- /dev/null +++ b/packages/app-mastro/README.md @@ -0,0 +1,40 @@ +# Mastro Template Basic for Node.js + +This is a basic TypeScript template for [Mastro](https://mastrojs.github.io) when using [Node.js](https://nodejs.org). + +Click the green **Use this template** button in the top right to create your own copy of this repository. Then clone the **Code** to your computer. + +## Run locally + +If you have multiple projects on your computer that require different Node.js versions, you should install a tool to manage those version for you; for example [Volta](https://volta.sh/) (see [pnpm Support](https://docs.volta.sh/advanced/pnpm)). + +Mastro requires Node.js >=24 (unless you want to install a [`URLPattern` polyfill](https://www.npmjs.com/package/urlpattern-polyfill)). + +[JSR recommends](https://jsr.io/docs/npm-compatibility#installing-and-using-jsr-packages) to use `pnpm`. + +The first time, you need to: + + pnpm install + +After that, to start the server: + + pnpm run start + +and open in your browser. + +To generate the whole static site (this will create a `generated` folder): + + pnpm run generate + +## Next steps + +To see how Mastro works, [follow the guide](https://mastrojs.github.io/guide/server-side-components-and-routing/). + +To make sure you're using the latest Mastro packages: + + pnpm update "@mastrojs/*" --latest + +## Deploy to production + +- [Deploy static site](https://mastrojs.github.io/guide/deploy/#deploy-static-site-with-ci%2Fcd) +- [Deploy server](https://mastrojs.github.io/guide/deploy/#deploy-server-to-production) diff --git a/packages/app-mastro/components/Layout.ts b/packages/app-mastro/components/Layout.ts new file mode 100644 index 0000000..f9ae3c2 --- /dev/null +++ b/packages/app-mastro/components/Layout.ts @@ -0,0 +1,21 @@ +import { type Html, html } from '@mastrojs/mastro' + +interface Props { + children: Html + title: string +} + +export const Layout = (props: Props) => html` + + + + + ${props.title} + + + +

${props.title}

+ ${props.children} + + +` diff --git a/packages/app-mastro/handlers/home.ts b/packages/app-mastro/handlers/home.ts new file mode 100644 index 0000000..727feb1 --- /dev/null +++ b/packages/app-mastro/handlers/home.ts @@ -0,0 +1,26 @@ +import { testData } from '../../testdata/src/ssr.ts' +import { html, htmlToResponse } from '@mastrojs/mastro' +import { Layout } from '../components/Layout.ts' + +export const GET = async () => { + const entries = await testData() + return htmlToResponse( + Layout({ + title: 'Test', + children: html` + + + ${entries.map( + (entry) => html` + + + + + `, + )} + +
${entry.id}${entry.name}
+ `, + }), + ) +} diff --git a/packages/app-mastro/package.json b/packages/app-mastro/package.json new file mode 100644 index 0000000..01bd333 --- /dev/null +++ b/packages/app-mastro/package.json @@ -0,0 +1,21 @@ +{ + "name": "app-mastro", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch server.ts", + "build": "node node_modules/@mastrojs/mastro/src/generator.js", + "type-check": "tsc" + }, + "dependencies": { + "@mastrojs/mastro": "jsr:^0", + "@remix-run/node-fetch-server": "^0.11" + }, + "devDependencies": { + "@types/node": "^24", + "typescript": "^5" + }, + "engines": { + "node": ">=24.12" + } +} diff --git a/packages/app-mastro/pnpm-lock.yaml b/packages/app-mastro/pnpm-lock.yaml new file mode 100644 index 0000000..f1f7be5 --- /dev/null +++ b/packages/app-mastro/pnpm-lock.yaml @@ -0,0 +1,156 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@mastrojs/mastro': + specifier: jsr:^0 + version: '@jsr/mastrojs__mastro@0.7.0' + '@remix-run/node-fetch-server': + specifier: ^0.11 + version: 0.11.0 + devDependencies: + '@types/node': + specifier: ^24 + version: 24.10.13 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@jsr/mastrojs__mastro@0.7.0': + resolution: {integrity: sha512-ZwaA6fDQ7+koRdUakGYCef0Dt05TLYEVpmYZGCwRWZSiSD8MGFenthS32xPoJRxZpGmWlGFCbBKZ/kJ03mcDvQ==, tarball: https://npm.jsr.io/~/11/@jsr/mastrojs__mastro/0.7.0.tgz} + + '@jsr/std__bytes@1.0.6': + resolution: {integrity: sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz} + + '@jsr/std__cli@1.0.27': + resolution: {integrity: sha512-aaY6VYkdv0qmAIiaYNfBH9Pd3Te5bJsEUQmNg/ak43AorET5+pBcI9RgqCgBXfkb2tBnLUlTBklQvirMz/CnAQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__cli/1.0.27.tgz} + + '@jsr/std__encoding@1.0.10': + resolution: {integrity: sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==, tarball: https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz} + + '@jsr/std__fmt@1.0.9': + resolution: {integrity: sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz} + + '@jsr/std__fs@1.0.22': + resolution: {integrity: sha512-PvDtgT25IqhFEX2LjQI0aTz/Wg61jCtJ8l19fE9MUSvSmtw57Kzr6sM7GcCsSrsZEdQ7wjLfXvvhy8irta4Zww==, tarball: https://npm.jsr.io/~/11/@jsr/std__fs/1.0.22.tgz} + + '@jsr/std__html@1.0.5': + resolution: {integrity: sha512-8ypLaw6ORY7jisEvsXOS/D631/pMCX78mV7fyromfzJXxqb35OUNCBC2E4Ca0goKQJW8I2XhEgoFu0ZXaIiGvA==, tarball: https://npm.jsr.io/~/11/@jsr/std__html/1.0.5.tgz} + + '@jsr/std__http@1.0.24': + resolution: {integrity: sha512-mfUI8vAkMVvf0wYxkZd9ZKfwFryLanHe+nbvxfFPkNO24B2IY6knkBJNN28cTZ8SITh8t8rv56Cx5uOAc0uGFg==, tarball: https://npm.jsr.io/~/11/@jsr/std__http/1.0.24.tgz} + + '@jsr/std__internal@1.0.12': + resolution: {integrity: sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz} + + '@jsr/std__media-types@1.1.0': + resolution: {integrity: sha512-dHvaxHL7ENWnltgL653uo3KnKFse3ZbopZop2gqsT7yrscx7irZEClu5Cba7gMPPRk4Lg1FbriNcaBViM2RSBw==, tarball: https://npm.jsr.io/~/11/@jsr/std__media-types/1.1.0.tgz} + + '@jsr/std__net@1.0.6': + resolution: {integrity: sha512-mh27Fw4UMCjGSIMoOhjia5cS5fNP9M9DZYhGB7EYSZNnzf/eguFiarii/W4oDwYMmnxCMouUzhc6Y7jFuwTzcg==, tarball: https://npm.jsr.io/~/11/@jsr/std__net/1.0.6.tgz} + + '@jsr/std__path@1.1.4': + resolution: {integrity: sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz} + + '@jsr/std__streams@1.0.17': + resolution: {integrity: sha512-LnPlWk20mDIV5/nqoUomAB8umOimfGEyWRApxLgekXFuqKGDsGpUAi58amieVU2XAGNclmUOtQOcQ/qOl3PNFg==, tarball: https://npm.jsr.io/~/11/@jsr/std__streams/1.0.17.tgz} + + '@remix-run/node-fetch-server@0.11.0': + resolution: {integrity: sha512-nCrFHVxDFioSHc0g/3m5ztwgjBt7g8qh/UwmYkDjuMePKFepMKfNGgH5S6L7iXKX+jUrf3ooVmhx3NGIoa9iYA==} + + '@types/node@24.10.13': + resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + + ts-blank-space@0.6.1: + resolution: {integrity: sha512-LcM3W5HEyzTaXUeQITV8ploUOGe+zuuoFYsCfPscFLhx3bZn2sSfHMKxsULVG/zA7an9UhReiHv4Kk/6QzlpXQ==} + engines: {node: '>=18.0.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + +snapshots: + + '@jsr/mastrojs__mastro@0.7.0': + dependencies: + '@jsr/std__http': 1.0.24 + '@jsr/std__media-types': 1.1.0 + '@jsr/std__path': 1.1.4 + ts-blank-space: 0.6.1 + + '@jsr/std__bytes@1.0.6': {} + + '@jsr/std__cli@1.0.27': + dependencies: + '@jsr/std__fmt': 1.0.9 + '@jsr/std__internal': 1.0.12 + + '@jsr/std__encoding@1.0.10': {} + + '@jsr/std__fmt@1.0.9': {} + + '@jsr/std__fs@1.0.22': + dependencies: + '@jsr/std__internal': 1.0.12 + '@jsr/std__path': 1.1.4 + + '@jsr/std__html@1.0.5': {} + + '@jsr/std__http@1.0.24': + dependencies: + '@jsr/std__cli': 1.0.27 + '@jsr/std__encoding': 1.0.10 + '@jsr/std__fmt': 1.0.9 + '@jsr/std__fs': 1.0.22 + '@jsr/std__html': 1.0.5 + '@jsr/std__media-types': 1.1.0 + '@jsr/std__net': 1.0.6 + '@jsr/std__path': 1.1.4 + '@jsr/std__streams': 1.0.17 + + '@jsr/std__internal@1.0.12': {} + + '@jsr/std__media-types@1.1.0': {} + + '@jsr/std__net@1.0.6': {} + + '@jsr/std__path@1.1.4': + dependencies: + '@jsr/std__internal': 1.0.12 + + '@jsr/std__streams@1.0.17': + dependencies: + '@jsr/std__bytes': 1.0.6 + + '@remix-run/node-fetch-server@0.11.0': {} + + '@types/node@24.10.13': + dependencies: + undici-types: 7.16.0 + + ts-blank-space@0.6.1: + dependencies: + typescript: 5.8.3 + + typescript@5.8.3: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} diff --git a/packages/app-mastro/routes/styles.css b/packages/app-mastro/routes/styles.css new file mode 100644 index 0000000..efa12c4 --- /dev/null +++ b/packages/app-mastro/routes/styles.css @@ -0,0 +1,26 @@ +html { + font-family: sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + word-break: break-word; + text-wrap-style: pretty; +} + +p { + hyphens: auto; + word-break: break-word; +} + +img { + max-width: 100%; +} + +@view-transition { + navigation: auto; +} diff --git a/packages/app-mastro/server.ts b/packages/app-mastro/server.ts new file mode 100644 index 0000000..ab268af --- /dev/null +++ b/packages/app-mastro/server.ts @@ -0,0 +1,27 @@ +import * as http from 'node:http' +import { createRequestListener } from '@remix-run/node-fetch-server' +import { Mastro } from '@mastrojs/mastro/server' +import { GET as getHome } from './handlers/home.ts' + +// This is using Mastro's programmatic (Express-like) router +// because the default file-based router requires the +// current working directory to be the project root, which isn't +// always the case in this pnpm monorepo. + +export const handler = new Mastro() + .get('/', getHome) + .createHandler() + +const port = 8000 + +if (import.meta.main) { + const server = http.createServer(createRequestListener(handler)) + + server.on('error', (e) => { + console.error(e) + }) + + server.listen(port, () => { + console.log(`Server running at http://localhost:${port}`) + }) +} diff --git a/packages/app-mastro/tsconfig.json b/packages/app-mastro/tsconfig.json new file mode 100644 index 0000000..826c1a4 --- /dev/null +++ b/packages/app-mastro/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "module": "NodeNext", + "moduleResolution": "nodenext", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "verbatimModuleSyntax": true, + + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true + } +} diff --git a/packages/stats-generator/src/ssr/handlers/mastro.ts b/packages/stats-generator/src/ssr/handlers/mastro.ts new file mode 100644 index 0000000..ed823ac --- /dev/null +++ b/packages/stats-generator/src/ssr/handlers/mastro.ts @@ -0,0 +1,11 @@ +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { packagesDir } from '../../constants.ts' +import type { SSRHandler } from '../types.ts' + +export async function buildMastroHandler(): Promise { + const entryPath = join(packagesDir, 'app-mastro', 'server.ts') + const entryUrl = pathToFileURL(entryPath).href + const { handler } = await import(entryUrl) + return { type: 'web', handler } +} diff --git a/packages/stats-generator/src/ssr/index.ts b/packages/stats-generator/src/ssr/index.ts index 4f10c03..cff8120 100644 --- a/packages/stats-generator/src/ssr/index.ts +++ b/packages/stats-generator/src/ssr/index.ts @@ -1,5 +1,6 @@ import { runBenchmark } from './run-benchmark.ts' import { buildAstroHandler } from './handlers/astro.ts' +import { buildMastroHandler } from './handlers/mastro.ts' import { buildNuxtHandler } from './handlers/nuxt.ts' import { buildSvelteKitHandler } from './handlers/sveltekit.ts' import { buildNextJSHandler } from './handlers/nextjs.ts' @@ -22,6 +23,12 @@ const SSR_FRAMEWORKS: SSRFrameworkConfig[] = [ package: 'app-astro', buildHandler: buildAstroHandler, }, + { + name: 'mastro-ssr', + displayName: 'Mastro SSR', + package: 'app-mastro', + buildHandler: buildMastroHandler, + }, { name: 'nuxt-ssr', displayName: 'Nuxt SSR',