diff --git a/.changeset/silly-colts-doubt.md b/.changeset/silly-colts-doubt.md new file mode 100644 index 00000000..7f0e13b0 --- /dev/null +++ b/.changeset/silly-colts-doubt.md @@ -0,0 +1,7 @@ +--- +'@aziontech/presets': minor +'@aziontech/unenv-preset': patch +'@aziontech/builder': patch +--- + +feat: add nitro preset diff --git a/packages/builder/src/bundlers/esbuild/esbuild.config.ts b/packages/builder/src/bundlers/esbuild/esbuild.config.ts index 5c03279e..7ec2f38c 100644 --- a/packages/builder/src/bundlers/esbuild/esbuild.config.ts +++ b/packages/builder/src/bundlers/esbuild/esbuild.config.ts @@ -6,7 +6,6 @@ export default { platform: 'browser', mainFields: ['browser', 'module', 'main'], target: 'es2022', - keepNames: true, allowOverwrite: true, loader: { '.js': 'js', diff --git a/packages/builder/src/bundlers/esbuild/plugins/node-polyfills/node-polyfills.ts b/packages/builder/src/bundlers/esbuild/plugins/node-polyfills/node-polyfills.ts index 566c2e7f..ee39f20f 100644 --- a/packages/builder/src/bundlers/esbuild/plugins/node-polyfills/node-polyfills.ts +++ b/packages/builder/src/bundlers/esbuild/plugins/node-polyfills/node-polyfills.ts @@ -178,9 +178,18 @@ function handleNodeJSGlobals(build: PluginBuild, getAbsolutePath: (moving: strin const BUNDLER_POLYFILL_RE = /^@aziontech\/builder\/polyfills\/.+/; const prefix = path.resolve(getAbsolutePath('../'), '_global_polyfill-'); + const dotNotationKeys = new Set(['import.meta.url']); + + if (inject['import.meta.url']) { + build.initialOptions.define = build.initialOptions.define ?? {}; + build.initialOptions.define['import.meta.url'] = JSON.stringify('file://' + process.cwd() + '/index.js'); + } + build.initialOptions.inject = [ ...(build.initialOptions.inject ?? []), - ...Object.keys(inject).map((globalName) => `${prefix}${globalName}.js`), + ...Object.keys(inject) + .filter((globalName) => !dotNotationKeys.has(globalName)) + .map((globalName) => `${prefix}${globalName}.js`), ]; // Resolve polyfills from @aziontech/unenv-preset diff --git a/packages/presets/docs/preset-nitro.md b/packages/presets/docs/preset-nitro.md new file mode 100644 index 00000000..2a61d973 --- /dev/null +++ b/packages/presets/docs/preset-nitro.md @@ -0,0 +1,191 @@ +# Nitro Preset + +This preset enables server-side rendering for Nitro-based applications on the Azion Platform. + +## Prerequisites + +- Node.js 18+ +- A Nitro-based project (e.g. TanStack Start, Analog, or a custom Nitro app) +- Azion CLI installed globally + +## Installation + +Install the Azion presets package in your project: + +```bash +npm install @aziontech/presets +``` + +## Configuration + +### TanStack Start + Vite + +Configure your `vite.config.ts` to use the Azion Nitro preset: + +```typescript +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import { defineConfig } from 'vite'; +import viteReact from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +import { nitro } from 'nitro/vite'; + +// import the preset using createRequire to ensure correct path resolution +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +export default defineConfig(() => { + return { + server: { + port: 3000, + }, + resolve: { + tsconfigPaths: true, + }, + plugins: [ + tailwindcss(), + nitro({ + // Use require.resolve to ensure the preset path is correctly resolved + preset: require.resolve('@aziontech/presets/nitro/preset'), + }), + tanstackStart(), + viteReact(), + ], + }; +}); +``` + +### Generic Nitro App + +For any Nitro-based project, configure your `vite.config.ts`: + +```typescript +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +export default defineNitroConfig({ + preset: require.resolve('@aziontech/presets/nitro/preset'), +}); +``` + +Or directly with the node_modules path: + +```typescript +export default defineNitroConfig({ + preset: './node_modules/@aziontech/presets/src/presets/nitro/custom/index.js', +}); +``` + +## Project Setup + +### 1. Link Your Project + +Connect your project to Azion and select the Nitro preset: + +```bash +azion link +``` + +When prompted, choose the **Nitro preset** from the available options. + +## Development Workflow + +### Preview Your Application + +#### Build and Preview + +Build your application and preview it locally: + +```bash +azion build +azion dev +``` + +#### Skip Framework Build (Optional) + +If you want to skip the framework build process and use existing build artifacts: + +```bash +azion dev --skip-framework-build +``` + +This is useful when you've already built your application and want to quickly test the edge function behavior. + +## Deployment + +### Deploy to Azion Edge + +Deploy your application directly from your local environment: + +```bash +azion -t +azion deploy --local +``` + +This command will: + +1. Build your application with the Nitro preset +2. Package the edge function +3. Deploy to Azion's edge network +4. Provide you with the deployment URL + +## How It Works + +After a successful build, Nitro outputs two directories: + +- `.output/server/` — the server-side bundle (`index.mjs`) deployed as an Azion edge function +- `.output/public/` — static assets uploaded to Azion's storage bucket + +At runtime, the Azion Nitro module: + +1. Attaches the Azion runtime context (`env`, `ctx`) to each incoming request +2. Checks if the request targets a static asset +3. Serves static assets directly from storage +4. Forwards all other requests to Nitro's native fetch handler + +## Features + +The Nitro preset provides: + +- **Server-Side Rendering**: Full SSR support via Nitro's native server +- **Edge Runtime**: Optimized for Azion's edge computing platform +- **Static Asset Handling**: Efficient static file serving from Azion storage with cache policy +- **API Routes**: Support for Nitro server API routes +- **WASM Support**: WebAssembly modules supported out of the box + +## Troubleshooting + +### Common Issues + +**Build Errors**: Ensure the preset path in your config resolves correctly. Use `require.resolve` when possible to guarantee the path is valid. + +**Deployment Failures**: Verify that the Azion CLI is authenticated and your project is properly linked. + +**Runtime Errors**: Check that your application is compatible with edge runtime constraints (no Node.js-only APIs). + +### Getting Help + +For additional support: + +- Check the [Azion Documentation](https://www.azion.com/en/documentation/) +- Contact Azion Support for platform-specific issues + +## Example Project Structure + +``` +my-nitro-app/ +├── vite.config.ts # Azion preset configuration (TanStack Start) +├── package.json # Include @aziontech/presets dependency +├── app/ # Application source +├── server/ # Nitro server routes and middleware +└── public/ # Static assets +``` + +## Next Steps + +After successful deployment: + +1. Test your application on the provided edge URL +2. Configure custom domains if needed + +> **Note**: We are currently working on a Pull Request to the official Nitro repository to include an Azion preset natively. This will simplify the configuration process in future versions. diff --git a/packages/presets/package.json b/packages/presets/package.json index 03263dc7..bfdac37a 100644 --- a/packages/presets/package.json +++ b/packages/presets/package.json @@ -22,7 +22,8 @@ "./nuxt/*": "./src/presets/nuxt/nitro/*/index.js", "./sveltekit": "./src/presets/svelte/kit/index.js", "./sveltekit/cache": "./src/presets/svelte/kit/cache/index.js", - "./preset/*": "./dist/presets/*" + "./preset/*": "./dist/presets/*", + "./nitro/preset": "./src/presets/nitro/custom/index.js" }, "author": "aziontech", "license": "MIT", @@ -32,7 +33,8 @@ "README.md", "src/presets/next/*", "src/presets/nuxt/nitro/*", - "src/presets/svelte/kit/*" + "src/presets/svelte/kit/*", + "src/presets/nitro/custom/*" ], "dependencies": { "@aziontech/config": "workspace:*", diff --git a/packages/presets/src/index.ts b/packages/presets/src/index.ts index 5bc2023e..47c1fe2f 100644 --- a/packages/presets/src/index.ts +++ b/packages/presets/src/index.ts @@ -10,6 +10,7 @@ export * from './presets/hugo'; export * from './presets/javascript'; export * from './presets/jekyll'; export * from './presets/next'; +export * from './presets/nitro'; export * from './presets/nuxt'; export * from './presets/opennextjs'; export * from './presets/preact'; diff --git a/packages/presets/src/presets/nitro/config.ts b/packages/presets/src/presets/nitro/config.ts new file mode 100644 index 00000000..b54384f6 --- /dev/null +++ b/packages/presets/src/presets/nitro/config.ts @@ -0,0 +1,206 @@ +import type { AzionBuild, AzionConfig } from '@aziontech/config'; + +const config: AzionConfig = { + build: { + entry: '.output/server/index.mjs', + polyfills: true, + bundler: 'esbuild', + preset: 'nitro', + } as AzionBuild, + storage: [ + { + name: '$BUCKET_NAME', + prefix: '$BUCKET_PREFIX', + dir: './.output/public', + workloadsAccess: 'read_write', + }, + ], + connectors: [ + { + name: '$CONNECTOR_NAME', + active: true, + type: 'storage', + attributes: { + bucket: '$BUCKET_NAME', + prefix: '$BUCKET_PREFIX', + }, + }, + ], + functions: [ + { + name: '$FUNCTION_NAME', + path: './functions/index.js', + bindings: { + storage: { + bucket: '$BUCKET_NAME', + prefix: '$BUCKET_PREFIX', + }, + }, + }, + ], + applications: [ + { + name: '$APPLICATION_NAME', + cache: [ + { + name: '$APPLICATION_NAME', + browser: { + maxAgeSeconds: 7200, + }, + edge: { + maxAgeSeconds: 7200, + }, + }, + ], + rules: { + request: [ + { + name: 'Redirect to index.html for Subpaths', + description: 'Handle subpath requests by rewriting to index.html', + active: true, + criteria: [ + [ + { + variable: '${uri}', + conditional: 'if', + operator: 'matches', + argument: '^(?!.*/$)(?![sS]*.[a-zA-Z0-9]+$).*', + }, + ], + ], + behaviors: [ + { + type: 'set_connector', + attributes: { + value: '$CONNECTOR_NAME', + }, + }, + { + type: 'rewrite_request', + attributes: { + value: '${uri}/index.html', + }, + }, + ], + }, + { + name: 'Redirect to index.html', + description: 'Handle directory requests by rewriting to index.html', + active: true, + criteria: [ + [ + { + variable: '${uri}', + conditional: 'if', + operator: 'matches', + argument: '.*/$', + }, + ], + ], + behaviors: [ + { + type: 'set_connector', + attributes: { + value: '$CONNECTOR_NAME', + }, + }, + { + type: 'rewrite_request', + attributes: { + value: '${uri}index.html', + }, + }, + ], + }, + { + name: 'Deliver Static Assets and set cache policy', + description: 'Deliver static assets directly from storage and set cache policy', + active: true, + criteria: [ + [ + { + variable: '${uri}', + conditional: 'if', + operator: 'matches', + argument: + '.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|otf|woff|woff2|eot|pdf|doc|docx|xls|xlsx|ppt|pptx|mp4|webm|mp3|wav|ogg|css|js|json|xml|html|txt|csv|zip|rar|7z|tar|gz|webmanifest|map|md|yaml|yml)$', + }, + ], + ], + behaviors: [ + { + type: 'set_connector', + attributes: { + value: '$CONNECTOR_NAME', + }, + }, + { + type: 'set_cache_policy', + attributes: { + value: '$APPLICATION_NAME', + }, + }, + { + type: 'deliver', + }, + ], + }, + { + name: 'Execute Nitro Function', + description: 'Execute Nitro function for all requests', + active: true, + criteria: [ + [ + { + variable: '${uri}', + conditional: 'if', + operator: 'matches', + argument: '^/', + }, + ], + ], + behaviors: [ + { + type: 'run_function', + attributes: { + value: '$FUNCTION_NAME', + }, + }, + { + type: 'forward_cookies', + }, + ], + }, + ], + }, + functionsInstances: [ + { + name: '$FUNCTION_INSTANCE_NAME', + ref: '$FUNCTION_NAME', + }, + ], + }, + ], + workloads: [ + { + name: '$WORKLOAD_NAME', + active: true, + infrastructure: 1, + deployments: [ + { + name: '$DEPLOYMENT_NAME', + current: true, + active: true, + strategy: { + type: 'default', + attributes: { + application: '$APPLICATION_NAME', + }, + }, + }, + ], + }, + ], +}; + +export default config; diff --git a/packages/presets/src/presets/nitro/custom/index.js b/packages/presets/src/presets/nitro/custom/index.js new file mode 100644 index 00000000..aacbf21a --- /dev/null +++ b/packages/presets/src/presets/nitro/custom/index.js @@ -0,0 +1,54 @@ +import { writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export default { + extends: 'base-worker', + entry: fileURLToPath(new URL('./runtime/azion-module.js', import.meta.url)), + output: { + publicDir: '{{ output.dir }}/public/{{ baseURL }}', + }, + exportConditions: ['workerd', 'worker'], + minify: false, + commands: { + preview: 'azion dev -p 3000', + deploy: 'azion deploy --local', + }, + rollupConfig: { + output: { + format: 'esm', + exports: 'named', + inlineDynamicImports: false, + }, + plugins: [ + { + name: 'azion-resolve-from-project', + resolveId(id) { + if (id.startsWith('nitro/')) { + try { + return createRequire(resolve(process.cwd(), 'package.json')).resolve(id); + // eslint-disable-next-line no-empty + } catch {} + } + }, + }, + ], + }, + wasm: { + lazy: false, + esmImport: true, + }, + hooks: { + async compiled(nitro) { + await writeFile( + resolve(nitro.options.output.dir, 'package.json'), + JSON.stringify({ private: true, main: './server/index.mjs' }, null, 2), + ); + await writeFile( + resolve(nitro.options.output.dir, 'package-lock.json'), + JSON.stringify({ lockfileVersion: 1 }, null, 2), + ); + }, + }, +}; diff --git a/packages/presets/src/presets/nitro/custom/runtime/azion-module.js b/packages/presets/src/presets/nitro/custom/runtime/azion-module.js new file mode 100644 index 00000000..b19387e7 --- /dev/null +++ b/packages/presets/src/presets/nitro/custom/runtime/azion-module.js @@ -0,0 +1,43 @@ +import '#nitro/virtual/polyfills'; +import { isPublicAssetURL } from '#nitro/virtual/public-assets'; +import { useNitroApp } from 'nitro/app'; + +function attachRuntimeContext(request, ctx) { + request.runtime ??= { name: 'azion' }; + request.runtime.azion = { + ...request.runtime.azion, + ...ctx, + }; + request.waitUntil = ctx.context?.waitUntil.bind(ctx.context); +} + +async function fetchStaticAsset(url) { + try { + const pathname = decodeURIComponent(url.pathname); + const assetUrl = new URL(pathname === '/' ? 'index.html' : pathname, 'file://'); + return fetch(assetUrl); + } catch (e) { + return new Response(e.message || e.toString(), { status: 404 }); + } +} + +const nitroApp = useNitroApp(); + +export default { + async fetch(request, env, context) { + globalThis.__env__ = { + ...env, + // Fallback to process.env + ...process.env, + }; + attachRuntimeContext(request, { env: globalThis.__env__, context }); + + const url = new URL(request.url); + + if (isPublicAssetURL(url.pathname)) { + return await fetchStaticAsset(url); + } + + return await nitroApp.fetch(request); + }, +}; diff --git a/packages/presets/src/presets/nitro/index.ts b/packages/presets/src/presets/nitro/index.ts new file mode 100644 index 00000000..339a2cee --- /dev/null +++ b/packages/presets/src/presets/nitro/index.ts @@ -0,0 +1,6 @@ +import type { AzionBuildPreset } from '@aziontech/config'; +import config from './config'; +import metadata from './metadata'; +import prebuild from './prebuild'; + +export const nitro: AzionBuildPreset = { config, metadata, prebuild }; diff --git a/packages/presets/src/presets/nitro/metadata.ts b/packages/presets/src/presets/nitro/metadata.ts new file mode 100644 index 00000000..dfe5b5ae --- /dev/null +++ b/packages/presets/src/presets/nitro/metadata.ts @@ -0,0 +1,7 @@ +import type { PresetMetadata } from '@aziontech/config'; + +const metadata: PresetMetadata = { + name: 'nitro', +}; + +export default metadata; diff --git a/packages/presets/src/presets/nitro/prebuild.ts b/packages/presets/src/presets/nitro/prebuild.ts new file mode 100644 index 00000000..f8ddd259 --- /dev/null +++ b/packages/presets/src/presets/nitro/prebuild.ts @@ -0,0 +1,19 @@ +import { BuildConfiguration, BuildContext } from '@aziontech/config'; +import { exec } from '@aziontech/utils/node'; + +/** + * Runs custom prebuild actions for Nitro + */ +async function prebuild(_: BuildConfiguration, ctx: BuildContext): Promise { + const skipBuild = ctx.skipFrameworkBuild; + + if (!skipBuild) { + await exec(`npm run build`, { + scope: 'Nitro', + verbose: true, + interactive: true, + }); + } +} + +export default prebuild; diff --git a/packages/unenv-preset/src/index.ts b/packages/unenv-preset/src/index.ts index 518ef8fe..bf884748 100644 --- a/packages/unenv-preset/src/index.ts +++ b/packages/unenv-preset/src/index.ts @@ -5,6 +5,7 @@ export default { inject: { __dirname: `${polyfillsPath}/node/globals/path-dirname.js`, __filename: `${polyfillsPath}/node/globals/path-filename.js`, + 'import.meta.url': `${polyfillsPath}/node/globals/import-meta-url.js`, process: `${polyfillsPath}/node/globals/process.cjs`, performance: `unenv/polyfill/performance`, setInterval: `${polyfillsPath}/node/globals/set-interval.js`, diff --git a/packages/unenv-preset/src/polyfills/node/globals/async-storage.js b/packages/unenv-preset/src/polyfills/node/globals/async-storage.js index cbeae840..0ba886a9 100644 --- a/packages/unenv-preset/src/polyfills/node/globals/async-storage.js +++ b/packages/unenv-preset/src/polyfills/node/globals/async-storage.js @@ -19,4 +19,6 @@ if (async_hooks.AsyncLocalStorage && !async_hooks.AsyncLocalStorage.snapshot) { }; } +globalThis.AsyncLocalStorage = async_hooks.AsyncLocalStorage; + export default async_hooks.AsyncLocalStorage; diff --git a/packages/unenv-preset/src/polyfills/node/globals/import-meta-url.js b/packages/unenv-preset/src/polyfills/node/globals/import-meta-url.js new file mode 100644 index 00000000..ff1b1e9f --- /dev/null +++ b/packages/unenv-preset/src/polyfills/node/globals/import-meta-url.js @@ -0,0 +1,2 @@ +const importMetaUrl = 'file://' + __dirname; +export default importMetaUrl;