pulse-js-framework 1.9.0 → 1.9.3
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/cli/build.js +38 -0
- package/cli/index.js +12 -1
- package/cli/ssg.js +490 -0
- package/compiler/parser.js +20 -0
- package/compiler/transformer/view.js +80 -1
- package/package.json +17 -1
- package/runtime/ssr-mismatch.js +388 -0
- package/runtime/ssr-preload.js +228 -0
- package/runtime/ssr-serializer.js +3 -1
- package/runtime/ssr-stream.js +388 -0
- package/runtime/ssr.js +103 -3
- package/server/express.js +108 -0
- package/server/fastify.js +119 -0
- package/server/hono.js +107 -0
- package/server/index.js +208 -0
- package/server/utils.js +254 -0
package/server/utils.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Server Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for server framework adapters.
|
|
5
|
+
* Provides HTML injection, asset serving helpers, and request context.
|
|
6
|
+
*
|
|
7
|
+
* @module pulse-js-framework/server/utils
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
11
|
+
import { extname, resolve, sep } from 'path';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// MIME Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const MIME_TYPES = {
|
|
18
|
+
'.html': 'text/html; charset=utf-8',
|
|
19
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
20
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
21
|
+
'.css': 'text/css; charset=utf-8',
|
|
22
|
+
'.json': 'application/json; charset=utf-8',
|
|
23
|
+
'.png': 'image/png',
|
|
24
|
+
'.jpg': 'image/jpeg',
|
|
25
|
+
'.jpeg': 'image/jpeg',
|
|
26
|
+
'.gif': 'image/gif',
|
|
27
|
+
'.svg': 'image/svg+xml',
|
|
28
|
+
'.ico': 'image/x-icon',
|
|
29
|
+
'.webp': 'image/webp',
|
|
30
|
+
'.avif': 'image/avif',
|
|
31
|
+
'.woff': 'font/woff',
|
|
32
|
+
'.woff2': 'font/woff2',
|
|
33
|
+
'.ttf': 'font/ttf',
|
|
34
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
35
|
+
'.otf': 'font/otf',
|
|
36
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
37
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
38
|
+
'.wasm': 'application/wasm',
|
|
39
|
+
'.map': 'application/json; charset=utf-8',
|
|
40
|
+
'.mp4': 'video/mp4',
|
|
41
|
+
'.webm': 'video/webm',
|
|
42
|
+
'.mp3': 'audio/mpeg',
|
|
43
|
+
'.ogg': 'audio/ogg'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get MIME type for a file extension.
|
|
48
|
+
* @param {string} filePath - File path or extension
|
|
49
|
+
* @returns {string} MIME type
|
|
50
|
+
*/
|
|
51
|
+
export function getMimeType(filePath) {
|
|
52
|
+
const ext = extname(filePath).toLowerCase();
|
|
53
|
+
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// HTML Template Injection
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Inject rendered HTML and state into an HTML template.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} template - HTML template string
|
|
64
|
+
* @param {Object} options - Injection options
|
|
65
|
+
* @param {string} [options.html] - Rendered app HTML
|
|
66
|
+
* @param {string} [options.state] - Serialized state script tag
|
|
67
|
+
* @param {string} [options.head] - Extra head content (preload hints, meta)
|
|
68
|
+
* @param {string} [options.bodyAttrs] - Extra body attributes
|
|
69
|
+
* @returns {string} Complete HTML page
|
|
70
|
+
*/
|
|
71
|
+
export function injectIntoTemplate(template, options = {}) {
|
|
72
|
+
const { html = '', state = '', head = '', bodyAttrs = '' } = options;
|
|
73
|
+
|
|
74
|
+
let result = template;
|
|
75
|
+
|
|
76
|
+
// Inject app HTML
|
|
77
|
+
result = result.replace('<!--app-html-->', html);
|
|
78
|
+
|
|
79
|
+
// Inject state
|
|
80
|
+
result = result.replace('<!--app-state-->', state);
|
|
81
|
+
|
|
82
|
+
// Inject head content (preload hints, meta tags)
|
|
83
|
+
if (head) {
|
|
84
|
+
result = result.replace('</head>', `${head}\n</head>`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Inject body attributes
|
|
88
|
+
if (bodyAttrs) {
|
|
89
|
+
result = result.replace('<body', `<body ${bodyAttrs}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read an HTML template from disk with caching.
|
|
97
|
+
* @param {string} templatePath - Path to HTML template
|
|
98
|
+
* @returns {string} Template content
|
|
99
|
+
*/
|
|
100
|
+
const templateCache = new Map();
|
|
101
|
+
|
|
102
|
+
export function readTemplate(templatePath) {
|
|
103
|
+
if (templateCache.has(templatePath)) {
|
|
104
|
+
return templateCache.get(templatePath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!existsSync(templatePath)) {
|
|
108
|
+
throw new Error(`[Pulse Server] Template not found: ${templatePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const content = readFileSync(templatePath, 'utf-8');
|
|
112
|
+
templateCache.set(templatePath, content);
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Clear template cache (useful in development).
|
|
118
|
+
*/
|
|
119
|
+
export function clearTemplateCache() {
|
|
120
|
+
templateCache.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Static Asset Serving
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @typedef {Object} StaticAssetResult
|
|
129
|
+
* @property {Buffer|null} content - File content
|
|
130
|
+
* @property {string} mimeType - MIME type
|
|
131
|
+
* @property {number} [status] - HTTP status code
|
|
132
|
+
* @property {Object} [headers] - Response headers
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a static asset from the dist directory.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} pathname - Request pathname
|
|
139
|
+
* @param {string} distDir - Distribution directory
|
|
140
|
+
* @param {Object} [options] - Options
|
|
141
|
+
* @param {number} [options.maxAge=31536000] - Cache max-age for assets
|
|
142
|
+
* @param {boolean} [options.immutable=true] - Set immutable cache for hashed assets
|
|
143
|
+
* @returns {StaticAssetResult|null} Asset result or null if not found
|
|
144
|
+
*/
|
|
145
|
+
export function resolveStaticAsset(pathname, distDir, options = {}) {
|
|
146
|
+
const { maxAge = 31536000, immutable = true } = options;
|
|
147
|
+
|
|
148
|
+
// Security: prevent directory traversal
|
|
149
|
+
const normalized = pathname.replace(/\.\./g, '').replace(/\/+/g, '/');
|
|
150
|
+
const resolvedDist = resolve(distDir) + sep;
|
|
151
|
+
const filePath = resolve(distDir, normalized);
|
|
152
|
+
|
|
153
|
+
// Must be within distDir (trailing sep prevents prefix attacks like dist-evil/)
|
|
154
|
+
if (!filePath.startsWith(resolvedDist) && filePath !== resolve(distDir)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const content = readFileSync(filePath);
|
|
163
|
+
const mimeType = getMimeType(filePath);
|
|
164
|
+
|
|
165
|
+
// Hashed assets get long cache
|
|
166
|
+
const isHashed = /\.[a-f0-9]{8,}\.\w+$/.test(pathname);
|
|
167
|
+
const cacheControl = isHashed && immutable
|
|
168
|
+
? `public, max-age=${maxAge}, immutable`
|
|
169
|
+
: `public, max-age=${Math.min(maxAge, 3600)}`;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
content,
|
|
173
|
+
mimeType,
|
|
174
|
+
status: 200,
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': mimeType,
|
|
177
|
+
'Content-Length': String(content.length),
|
|
178
|
+
'Cache-Control': cacheControl
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Request Context
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @typedef {Object} PulseRequestContext
|
|
189
|
+
* @property {string} url - Full URL
|
|
190
|
+
* @property {string} pathname - URL pathname
|
|
191
|
+
* @property {Object} query - Query parameters
|
|
192
|
+
* @property {string} method - HTTP method
|
|
193
|
+
* @property {Object} headers - Request headers
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a Pulse request context from various framework request objects.
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} req - Framework-specific request object
|
|
200
|
+
* @param {string} [type='generic'] - Framework type
|
|
201
|
+
* @returns {PulseRequestContext}
|
|
202
|
+
*/
|
|
203
|
+
export function createRequestContext(req, type = 'generic') {
|
|
204
|
+
switch (type) {
|
|
205
|
+
case 'express':
|
|
206
|
+
return {
|
|
207
|
+
url: req.originalUrl || req.url,
|
|
208
|
+
pathname: req.path || new URL(req.url, 'http://localhost').pathname,
|
|
209
|
+
query: req.query || {},
|
|
210
|
+
method: req.method,
|
|
211
|
+
headers: req.headers || {}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
case 'hono':
|
|
215
|
+
return {
|
|
216
|
+
url: req.url || '',
|
|
217
|
+
pathname: new URL(req.url, 'http://localhost').pathname,
|
|
218
|
+
query: Object.fromEntries(new URL(req.url, 'http://localhost').searchParams),
|
|
219
|
+
method: req.method,
|
|
220
|
+
headers: Object.fromEntries(req.headers || [])
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
case 'fastify':
|
|
224
|
+
return {
|
|
225
|
+
url: req.url,
|
|
226
|
+
pathname: req.routeOptions?.url || new URL(req.url, 'http://localhost').pathname,
|
|
227
|
+
query: req.query || {},
|
|
228
|
+
method: req.method,
|
|
229
|
+
headers: req.headers || {}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
default:
|
|
233
|
+
return {
|
|
234
|
+
url: typeof req.url === 'string' ? req.url : '',
|
|
235
|
+
pathname: new URL(typeof req.url === 'string' ? req.url : '/', 'http://localhost').pathname,
|
|
236
|
+
query: {},
|
|
237
|
+
method: req.method || 'GET',
|
|
238
|
+
headers: req.headers || {}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Exports
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
export default {
|
|
248
|
+
getMimeType,
|
|
249
|
+
injectIntoTemplate,
|
|
250
|
+
readTemplate,
|
|
251
|
+
clearTemplateCache,
|
|
252
|
+
resolveStaticAsset,
|
|
253
|
+
createRequestContext
|
|
254
|
+
};
|