From 3445f4bd1aaf54c03ec813e10d3d77ef4c108ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 28 May 2026 15:13:35 -0300 Subject: [PATCH 1/9] feat(presets): add nitro preset for azion add support for Nitro-based applications to deploy on Azion. Includes custom runtime module, build configuration, storage connectors, and edge caching rules for optimal edge deployment. --- packages/presets/package.json | 6 +- packages/presets/src/index.ts | 1 + packages/presets/src/presets/nitro/config.ts | 206 ++++++++++++++++++ .../presets/src/presets/nitro/custom/index.js | 87 ++++++++ .../nitro/custom/runtime/azion-module.js | 39 ++++ packages/presets/src/presets/nitro/index.ts | 6 + .../presets/src/presets/nitro/metadata.ts | 7 + .../presets/src/presets/nitro/prebuild.ts | 19 ++ 8 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 packages/presets/src/presets/nitro/config.ts create mode 100644 packages/presets/src/presets/nitro/custom/index.js create mode 100644 packages/presets/src/presets/nitro/custom/runtime/azion-module.js create mode 100644 packages/presets/src/presets/nitro/index.ts create mode 100644 packages/presets/src/presets/nitro/metadata.ts create mode 100644 packages/presets/src/presets/nitro/prebuild.ts 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..9bf5e50c --- /dev/null +++ b/packages/presets/src/presets/nitro/custom/index.js @@ -0,0 +1,87 @@ +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) { + // Fix: seroval's switch(a) { case Object: } uses strict === which fails across V8 realms. + // EdgeVM creates its own realm, so objects from the outer Node.js context have a + // different Object constructor identity. Normalise it to the local Object before the switch. + // const ssrBundlePath = resolve( + // nitro.options.output.serverDir, + // '_ssr', + // 'ssr.mjs', + // ) + // try { + // let ssrContent = await readFile(ssrBundlePath, 'utf-8') + // const pattern = /switch \((\w+)\) \{\n(\s+)case Object:/g + // let count = 0 + // ssrContent = ssrContent.replace(pattern, (full, switchVar, indent) => { + // count++ + // const normalizer = + // `if (${switchVar} != null && ${switchVar} !== Object && ${switchVar}.name === "Object") ${switchVar} = Object;\n` + + // `${indent}` + // return normalizer + full + // }) + // if (count > 0) { + // await writeFile(ssrBundlePath, ssrContent) + // console.log( + // `[azion preset] Applied seroval cross-realm Object fix to _ssr/ssr.mjs (${count} patch(es))`, + // ) + // } else { + // console.warn( + // '[azion preset] seroval switch (...) { case Object: pattern not found in _ssr/ssr.mjs', + // ) + // } + // } catch (e) { + // console.warn('[azion preset] Could not patch _ssr/ssr.mjs:', e.message) + // } + + 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..7d412282 --- /dev/null +++ b/packages/presets/src/presets/nitro/custom/runtime/azion-module.js @@ -0,0 +1,39 @@ +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; + attachRuntimeContext(request, { 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; From a50d2e3914bcda3cf1385ed7cbbce7f9cd06d833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 28 May 2026 15:14:54 -0300 Subject: [PATCH 2/9] feat(polyfills): add import.meta.url polyfill support --- .../esbuild/plugins/node-polyfills/node-polyfills.ts | 11 ++++++++++- packages/unenv-preset/src/index.ts | 1 + .../src/polyfills/node/globals/import-meta-url.js | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/unenv-preset/src/polyfills/node/globals/import-meta-url.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/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/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; From ce9a1a92ce5bac612c209d14bf24862082875400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Fri, 29 May 2026 14:45:50 -0300 Subject: [PATCH 3/9] feat(presets): extract __name injection into dedicated patch module --- .../presets/src/presets/nitro/custom/index.js | 39 ++++--------------- .../custom/patches/inject-name-helper.js | 20 ++++++++++ 2 files changed, 27 insertions(+), 32 deletions(-) create mode 100644 packages/presets/src/presets/nitro/custom/patches/inject-name-helper.js diff --git a/packages/presets/src/presets/nitro/custom/index.js b/packages/presets/src/presets/nitro/custom/index.js index 9bf5e50c..cfde568e 100644 --- a/packages/presets/src/presets/nitro/custom/index.js +++ b/packages/presets/src/presets/nitro/custom/index.js @@ -2,6 +2,7 @@ import { writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { injectNameHelper } from './patches/inject-name-helper.js'; export default { extends: 'base-worker', @@ -41,38 +42,12 @@ export default { }, hooks: { async compiled(nitro) { - // Fix: seroval's switch(a) { case Object: } uses strict === which fails across V8 realms. - // EdgeVM creates its own realm, so objects from the outer Node.js context have a - // different Object constructor identity. Normalise it to the local Object before the switch. - // const ssrBundlePath = resolve( - // nitro.options.output.serverDir, - // '_ssr', - // 'ssr.mjs', - // ) - // try { - // let ssrContent = await readFile(ssrBundlePath, 'utf-8') - // const pattern = /switch \((\w+)\) \{\n(\s+)case Object:/g - // let count = 0 - // ssrContent = ssrContent.replace(pattern, (full, switchVar, indent) => { - // count++ - // const normalizer = - // `if (${switchVar} != null && ${switchVar} !== Object && ${switchVar}.name === "Object") ${switchVar} = Object;\n` + - // `${indent}` - // return normalizer + full - // }) - // if (count > 0) { - // await writeFile(ssrBundlePath, ssrContent) - // console.log( - // `[azion preset] Applied seroval cross-realm Object fix to _ssr/ssr.mjs (${count} patch(es))`, - // ) - // } else { - // console.warn( - // '[azion preset] seroval switch (...) { case Object: pattern not found in _ssr/ssr.mjs', - // ) - // } - // } catch (e) { - // console.warn('[azion preset] Could not patch _ssr/ssr.mjs:', e.message) - // } + const ssrBundlePath = resolve(nitro.options.output.serverDir, '_ssr', 'ssr.mjs'); + try { + await injectNameHelper(ssrBundlePath); + } catch (e) { + console.warn('[azion preset] Could not patch _ssr/ssr.mjs for __name:', e.message); + } await writeFile( resolve(nitro.options.output.dir, 'package.json'), diff --git a/packages/presets/src/presets/nitro/custom/patches/inject-name-helper.js b/packages/presets/src/presets/nitro/custom/patches/inject-name-helper.js new file mode 100644 index 00000000..c7d1b1ec --- /dev/null +++ b/packages/presets/src/presets/nitro/custom/patches/inject-name-helper.js @@ -0,0 +1,20 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +// Fix: EdgeVM transforms add the __name helper to function bodies. When seroval +// calls .toString() on those functions to embed them in inline