@edgeone/nuxt-pages 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # EdgeOne Nuxt Deploy
2
+
3
+ A professional deployment package that seamlessly deploys your Nuxt 3/4 applications to Tencent Cloud EdgeOne platform with optimized performance and intelligent caching.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🚀 **One-Click Deployment** - Automated build and deployment process for EdgeOne
8
+ - 🏗️ **Nitro Integration** - Full compatibility with Nuxt 3's Nitro engine
9
+ - 📦 **Monorepo Support** - Optimized templates for complex project structures
10
+ - 🎯 **Smart Caching** - Multi-layer caching with memory, regional blobs, and tag invalidation
11
+ - ⚡ **Performance Optimized** - Static asset handling, lazy loading, and OpenTelemetry tracing
12
+ - 🔧 **Auto Configuration** - Intelligent Nuxt config detection and modification
13
+ - 🌐 **SSR Ready** - Full server-side rendering support on EdgeOne
14
+
15
+ ## 📋 Requirements
16
+
17
+ - **Nuxt**: 3+
18
+ - **Node.js**: 18.x or higher
19
+ - **EdgeOne**: Tencent Cloud EdgeOne account
20
+
21
+ ## 🚀 Quick Start
22
+
23
+ ### Installation
24
+
25
+ ```bash
26
+ npm install @edgeone/nuxt-pages
27
+ ```
28
+
29
+ ### Basic Usage
30
+
31
+ 1. **Add to your build process:**
32
+
33
+ ```javascript
34
+ // In your build script or CI/CD pipeline
35
+ import { onPreBuild, onBuild, onPostBuild } from '@edgeone/nuxt-pages'
36
+
37
+ const buildOptions = {
38
+ cwd: process.cwd(),
39
+ env: process.env,
40
+ meta: {},
41
+ functions: {},
42
+ constants: {
43
+ PUBLISH_DIR: 'dist'
44
+ }
45
+ }
46
+
47
+ // Execute build phases
48
+ await onPreBuild(buildOptions)
49
+ await onBuild(buildOptions)
50
+ await onPostBuild(buildOptions)
51
+ ```
52
+
53
+ 2. **Your Nuxt project will be automatically configured:**
54
+
55
+ The package will create or modify your `nuxt.config.ts`:
56
+
57
+ ```typescript
58
+ export default defineNuxtConfig({
59
+ srcDir: 'app',
60
+ nitro: {
61
+ preset: 'node-server',
62
+ output: {
63
+ dir: '.edgeone',
64
+ publicDir: '.edgeone/assets',
65
+ serverDir: '.edgeone/server-handler',
66
+ },
67
+ },
68
+ devtools: { enabled: true },
69
+ })
70
+ ```
71
+
72
+ ## 🏗️ Architecture
73
+
74
+ ### Build Process
75
+
76
+ The deployment follows a three-phase approach:
77
+
78
+ 1. **PreBuild Phase** (`onPreBuild`)
79
+ - Validates Nuxt version compatibility
80
+ - Configures Nitro build output
81
+ - Sets up EdgeOne-specific configurations
82
+
83
+ 2. **Build Phase** (`onBuild`)
84
+ - Creates server handlers
85
+ - Generates route metadata for pages and API routes
86
+ - Patches Nitro handlers for EdgeOne compatibility
87
+
88
+ 3. **PostBuild Phase** (`onPostBuild`)
89
+ - Restores original configurations
90
+ - Cleanup and optimization
91
+
92
+ ### Caching Strategy
93
+
94
+ - **Memory Cache**: LRU cache for frequently accessed data
95
+ - **Regional Blobs**: Distributed storage for static assets
96
+ - **Tag Invalidation**: Smart cache invalidation based on content tags
97
+ - **Stale-While-Revalidate**: Background revalidation for optimal performance
98
+
99
+ ## 📁 Project Structure
100
+
101
+ After deployment, your project will have:
102
+
103
+ ```
104
+ your-project/
105
+ ├── .edgeone/
106
+ │ ├── assets/ # Static assets
107
+ │ ├── server-handler/ # Server-side code
108
+ │ │ ├── chunks/ # Nitro chunks
109
+ │ │ ├── handler.js # EdgeOne handler
110
+ │ │ └── index.mjs # Server entry point
111
+ │ └── dist/ # Runtime modules
112
+ ├── app/ # Your Nuxt app (if using srcDir)
113
+ ├── nuxt.config.ts # Auto-generated/modified config
114
+ └── package.json
115
+ ```
116
+
117
+ ## ⚙️ Configuration
118
+
119
+ ### Advanced Options
120
+
121
+ You can customize the deployment behavior:
122
+
123
+ ```typescript
124
+ // Custom build options
125
+ const buildOptions = {
126
+ cwd: process.cwd(),
127
+ env: {
128
+ ...process.env,
129
+ USE_REGIONAL_BLOBS: 'true',
130
+ NITRO_PORT: '9000'
131
+ },
132
+ meta: {
133
+ // Custom metadata
134
+ },
135
+ functions: {
136
+ // Function-specific settings
137
+ },
138
+ constants: {
139
+ PUBLISH_DIR: 'dist'
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Environment Variables
145
+
146
+ - `USE_REGIONAL_BLOBS`: Enable regional blob storage (default: true)
147
+ - `NITRO_PORT`: Development server port (default: 9000)
148
+ - `NITRO_PUBLIC_DIR`: Static assets directory
149
+
150
+ ## 🎯 Monorepo Support
151
+
152
+ For monorepo projects, the package automatically detects the structure and uses optimized templates:
153
+
154
+ ```javascript
155
+ // Automatic detection of monorepo structure
156
+ // Uses nuxt-handler-monorepo.tmpl.js for complex setups
157
+ // Handles working directory changes and path resolution
158
+ ```
159
+
160
+ ## 🔧 Development
161
+
162
+ ### Local Testing
163
+
164
+ The package includes a development server for local testing:
165
+
166
+ ```bash
167
+ # Start development server
168
+ npm run start
169
+ ```
170
+
171
+ Your Nuxt app will be available at `http://localhost:9000`
172
+
173
+ ### Build Scripts
174
+
175
+ ```json
176
+ {
177
+ "scripts": {
178
+ "build": "node ./tools/build.js",
179
+ "build:watch": "node ./tools/build.js --watch",
180
+ "start": "node dist/index.js",
181
+ "test": "ts-node src/test.ts"
182
+ }
183
+ }
184
+ ```
185
+
186
+ ## 📊 Performance Features
187
+
188
+ - **Static Asset Optimization**: 1-year cache headers for static files
189
+ - **Lazy Loading**: Nitro app initialization on first request
190
+ - **OpenTelemetry Tracing**: Built-in performance monitoring
191
+ - **Error Handling**: Graceful fallbacks and error recovery
192
+ - **Memory Management**: Efficient memory usage with LRU caching
193
+
194
+ ## 🚨 Compatibility
195
+
196
+ ### Supported Features
197
+ - ✅ Nuxt 3+
198
+ - ✅ Server-Side Rendering (SSR)
199
+ - ✅ Static Site Generation (SSG)
200
+ - ✅ API Routes
201
+ - ✅ Middleware
202
+ - ✅ Plugins
203
+ - ✅ Monorepo structures
204
+
205
+ ### Known Limitations
206
+ - ❌ `@nuxt/image` module (under development)
207
+ - ⚠️ Nuxt versions below 3 (compatibility in progress)
208
+
209
+ ## 🛠️ Troubleshooting
210
+
211
+ ### Common Issues
212
+
213
+ 1. **Build Fails**: Ensure Nuxt version is 3+
214
+ 2. **Module Conflicts**: Check for unsupported modules like `@nuxt/image`
215
+ 3. **Path Issues**: Verify your project structure matches expected layout
216
+
217
+ ### Debug Mode
218
+
219
+ Enable detailed logging:
220
+
221
+ ```bash
222
+ DEBUG=edgeone:* npm run build
223
+ ```
224
+
225
+ ## 📝 API Reference
226
+
227
+ ### Core Functions
228
+
229
+ #### `onPreBuild(options: BuildOptions)`
230
+ Prepares the project for EdgeOne deployment.
231
+
232
+ #### `onBuild(options: BuildOptions)`
233
+ Executes the main build process.
234
+
235
+ #### `onPostBuild(options: BuildOptions)`
236
+ Cleanup and finalization.
237
+
238
+ ### Types
239
+
240
+ ```typescript
241
+ interface BuildOptions {
242
+ cwd: string
243
+ env: any
244
+ meta: any
245
+ functions: any
246
+ constants: {
247
+ PUBLISH_DIR: string
248
+ }
249
+ }
250
+ ```
251
+
252
+ ## 📄 License
253
+
254
+ ISC License - see [LICENSE](LICENSE) file for details.
255
+
@@ -0,0 +1,18 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ RUN_CONFIG_FILE,
9
+ copyNuxtServerCode,
10
+ verifyNuxtHandlerDirStructure
11
+ } from "../../esm-chunks/chunk-TP3RAVPL.js";
12
+ import "../../esm-chunks/chunk-V2LFVP3C.js";
13
+ import "../../esm-chunks/chunk-6BT4RYQJ.js";
14
+ export {
15
+ RUN_CONFIG_FILE,
16
+ copyNuxtServerCode,
17
+ verifyNuxtHandlerDirStructure
18
+ };
@@ -0,0 +1,17 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ addNitroBuildOutputConfig,
9
+ resetNitroConfig
10
+ } from "../../esm-chunks/chunk-7RNB5RB6.js";
11
+ import "../../esm-chunks/chunk-5JK44IEA.js";
12
+ import "../../esm-chunks/chunk-V2LFVP3C.js";
13
+ import "../../esm-chunks/chunk-6BT4RYQJ.js";
14
+ export {
15
+ addNitroBuildOutputConfig,
16
+ resetNitroConfig
17
+ };
@@ -0,0 +1,17 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ createServerHandler,
9
+ patchNitroHandler
10
+ } from "../../esm-chunks/chunk-L23O2KDO.js";
11
+ import "../../esm-chunks/chunk-TP3RAVPL.js";
12
+ import "../../esm-chunks/chunk-V2LFVP3C.js";
13
+ import "../../esm-chunks/chunk-6BT4RYQJ.js";
14
+ export {
15
+ createServerHandler,
16
+ patchNitroHandler
17
+ };
@@ -0,0 +1,16 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ PluginContext,
9
+ SERVER_HANDLER_NAME
10
+ } from "../esm-chunks/chunk-Y3YAV6NZ.js";
11
+ import "../esm-chunks/chunk-5JK44IEA.js";
12
+ import "../esm-chunks/chunk-6BT4RYQJ.js";
13
+ export {
14
+ PluginContext,
15
+ SERVER_HANDLER_NAME
16
+ };
@@ -0,0 +1,18 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ createNuxtApiRoutesMeta,
9
+ createNuxtPagesRouteMeta,
10
+ createNuxtRoutesMeta
11
+ } from "../esm-chunks/chunk-UFRAZNP3.js";
12
+ import "../esm-chunks/chunk-5JK44IEA.js";
13
+ import "../esm-chunks/chunk-6BT4RYQJ.js";
14
+ export {
15
+ createNuxtApiRoutesMeta,
16
+ createNuxtPagesRouteMeta,
17
+ createNuxtRoutesMeta
18
+ };
@@ -0,0 +1,260 @@
1
+ import { resolve, dirname } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { readFileSync, existsSync, statSync } from 'fs';
4
+ import { extname } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Static assets directory
10
+ const ASSET_DIR = resolve(__dirname, '../assets');
11
+
12
+ // MIME type mapping
13
+ const MIME_TYPES = {
14
+ '.html': 'text/html; charset=utf-8',
15
+ '.js': 'application/javascript',
16
+ '.css': 'text/css',
17
+ '.json': 'application/json',
18
+ '.png': 'image/png',
19
+ '.jpg': 'image/jpeg',
20
+ '.jpeg': 'image/jpeg',
21
+ '.gif': 'image/gif',
22
+ '.svg': 'image/svg+xml',
23
+ '.ico': 'image/x-icon',
24
+ '.txt': 'text/plain',
25
+ '.xml': 'application/xml'
26
+ };
27
+
28
+ /**
29
+ * Get the MIME type of a file
30
+ */
31
+ function getMimeType(filePath) {
32
+ const ext = extname(filePath).toLowerCase();
33
+ return MIME_TYPES[ext] || 'application/octet-stream';
34
+ }
35
+
36
+ /**
37
+ * Handle static file requests
38
+ */
39
+ function handleStaticFile(url) {
40
+ try {
41
+ // Remove query parameters
42
+ let cleanUrl = url.split('?')[0];
43
+
44
+ // Handle IPX image processing paths from @nuxt/image
45
+ // Convert /_ipx/s_800x600/hero.png to /hero.png
46
+ // Also handles nested paths: /_ipx/s_800x600/images/hero.png -> /images/hero.png
47
+ if (cleanUrl.startsWith('/_ipx/')) {
48
+ let newUrl = '';
49
+
50
+ // Remove /_ipx/ prefix
51
+ const ipxPath = cleanUrl.slice(6); // Remove '/_ipx/'
52
+
53
+ // IPX format: /_ipx/[params]/[original_path]
54
+ // Parameters are typically the first segment(s) and contain underscores/commas
55
+ // The original file path starts after the params
56
+ const pathSegments = ipxPath.split('/').filter(s => s); // Remove empty segments
57
+
58
+ if (pathSegments.length === 0) {
59
+ // Empty path after /_ipx/, skip
60
+ return null;
61
+ }
62
+
63
+ // Check each segment for file extension
64
+ for (let i = 0; i < pathSegments.length; i++) {
65
+ const segment = pathSegments[i];
66
+ if(segment.startsWith('s_') || segment.startsWith('w_') || segment.startsWith('h_') || segment.startsWith('q_') || segment.startsWith('f_') || segment.startsWith('c_') || segment.startsWith('bg_') || segment.startsWith('blur_') || segment.startsWith('_')) {
67
+ continue;
68
+ }
69
+ // Check if segment ends with a known image extension
70
+ newUrl += '/' + segment;
71
+ }
72
+
73
+ if(newUrl.startsWith('/http')) newUrl = newUrl.slice(1);
74
+
75
+ if(newUrl.includes('http:/') && !newUrl.includes('http://')) {
76
+ newUrl = newUrl.replace('http:/', 'http://');
77
+ } else if(newUrl.includes('https:/') && !newUrl.includes('https://')) {
78
+ newUrl = newUrl.replace('https:/', 'https://');
79
+ }
80
+
81
+ return {
82
+ statusCode: 302,
83
+ headers: {
84
+ 'from-server': 'true',
85
+ location: newUrl
86
+ }
87
+ }
88
+ }
89
+
90
+ // 本地调试寻找文件路径
91
+ const possiblePaths = [];
92
+
93
+ // Direct file path
94
+ const directPath = resolve(ASSET_DIR, cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl);
95
+ possiblePaths.push(directPath);
96
+
97
+ // Try each possible path
98
+ for (const filePath of possiblePaths) {
99
+ // Security check: ensure file is within asset directory
100
+ if (!filePath.startsWith(ASSET_DIR)) {
101
+ continue;
102
+ }
103
+
104
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
105
+ const content = readFileSync(filePath);
106
+ const mimeType = getMimeType(filePath);
107
+
108
+ return {
109
+ statusCode: 200,
110
+ headers: {
111
+ 'Content-Type': mimeType,
112
+ 'Content-Length': content.length.toString(),
113
+ 'Cache-Control': 'public, max-age=31536000' // 1 year cache
114
+ },
115
+ body: content
116
+ };
117
+ }
118
+ }
119
+ } catch (error) {
120
+ console.error('Static file error:', error);
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Lazy load Nitro application
128
+ */
129
+ let nitroApp = null;
130
+ async function getNitroApp() {
131
+ if (!nitroApp) {
132
+ // Set environment variables to prevent automatic server startup
133
+ process.env.NITRO_PORT = '';
134
+ process.env.PORT = '';
135
+
136
+ // Set correct static assets path
137
+ process.env.NITRO_PUBLIC_DIR = ASSET_DIR;
138
+
139
+ const nitroModule = await (async () => {
140
+ try {
141
+ return await import('./chunks/nitro/nitro.mjs')
142
+ } catch {
143
+ return await import('./chunks/_/nitro.mjs')
144
+ }
145
+ })()
146
+
147
+ const { {{USE_NITRO_APP_SYMBOL}}: useNitroApp } = nitroModule
148
+ nitroApp = useNitroApp();
149
+ }
150
+ return nitroApp;
151
+ }
152
+
153
+ async function getBody(req) {
154
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
155
+ const chunks = [];
156
+ for await (const chunk of req) {
157
+ chunks.push(chunk);
158
+ }
159
+ const body = Buffer.concat(chunks).toString();
160
+ return body;
161
+ }
162
+ return '';
163
+ }
164
+
165
+ /**
166
+ * EdgeOne function handler
167
+ */
168
+ export default async function handler(req, res, context) {
169
+ try {
170
+ const url = req.url || '/';
171
+ const method = req.method || 'GET';
172
+ const headers = req.headers || new Headers();
173
+ const body = await getBody(req);
174
+
175
+ // First try to handle static assets
176
+ if (method === 'GET') {
177
+ const staticResponse = handleStaticFile(url);
178
+ if (staticResponse) {
179
+ return new Response(staticResponse.body, {
180
+ status: staticResponse.statusCode,
181
+ headers: staticResponse.headers
182
+ });
183
+ }
184
+ }
185
+
186
+ // Handle dynamic requests
187
+ const app = await getNitroApp();
188
+
189
+ try {
190
+ const response = await app.localCall({
191
+ url,
192
+ method,
193
+ headers,
194
+ body
195
+ });
196
+
197
+ // 正确处理 headers,特别是多个 set-cookie 的情况
198
+ const responseHeaders = new Headers();
199
+
200
+ // 如果 response.headers 是 Headers 对象,直接复制
201
+ if (response.headers instanceof Headers) {
202
+ response.headers.forEach((value, key) => {
203
+ responseHeaders.append(key, value);
204
+ });
205
+ } else {
206
+ // 如果是普通对象,需要特殊处理 set-cookie 数组
207
+ for (const [key, value] of Object.entries(response.headers)) {
208
+ const lowerKey = key.toLowerCase();
209
+
210
+ // set-cookie 是特殊 header,可以有多个值
211
+ if (lowerKey === 'set-cookie' && Array.isArray(value)) {
212
+ // 为每个 cookie 单独添加
213
+ value.forEach(cookie => {
214
+ responseHeaders.append('Set-Cookie', cookie);
215
+ });
216
+ } else {
217
+ // 其他 header 直接设置
218
+ responseHeaders.set(key, Array.isArray(value) ? value.join(', ') : value);
219
+ }
220
+ }
221
+ }
222
+
223
+ // console.log('responseHeaders.getSetCookie() --->', responseHeaders.getSetCookie());
224
+ return new Response(response.body, {
225
+ status: response.status || response.statusCode,
226
+ headers: responseHeaders
227
+ });
228
+ } catch (nitroError) {
229
+ // Handle Nitro static file read errors (prerender files not found)
230
+ // Check error and its cause property (H3Error may wrap actual error in cause)
231
+ const actualError = nitroError?.cause || nitroError;
232
+ const errorPath = actualError?.path || nitroError?.path;
233
+ const errorCode = actualError?.code || nitroError?.code;
234
+
235
+ // If error is due to prerender static file not found, try dynamic rendering
236
+ if (errorCode === 'ENOENT' &&
237
+ errorPath &&
238
+ (errorPath.includes('/assets/') || errorPath.includes('assets/')) &&
239
+ (errorPath.includes('/index.html') || errorPath.includes('index.html'))) {
240
+ console.warn(`Prerender file not found: ${errorPath}, falling back to dynamic rendering for ${url}`);
241
+
242
+ // If static file handling has been tried but file not found, should perform dynamic rendering
243
+ // Nitro should be able to handle dynamic routes, but if it still tries to read static files,
244
+ // it may be due to configuration issues. We throw an error directly to let user know to build or check configuration
245
+ throw new Error(`Prerender route ${url} not found. Make sure to run build first or configure routeRules correctly. Original error: ${actualError?.message || nitroError?.message}`);
246
+ }
247
+
248
+ // Other errors are thrown directly
249
+ throw nitroError;
250
+ }
251
+ } catch (error) {
252
+ return new Response(`Server Error: ${error.message}`, {
253
+ status: 500,
254
+ headers: {
255
+ 'Content-Type': 'text/plain'
256
+ }
257
+ });
258
+ }
259
+ }
260
+