@siddharatha/adapter-node-rolldown 1.0.0

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/files/index.js ADDED
@@ -0,0 +1,217 @@
1
+ import { createServer } from 'node:http';
2
+ import polka from 'polka';
3
+ import { config } from 'ENV';
4
+ import { initTelemetry, shutdownTelemetry } from 'TELEMETRY';
5
+ import { createCompressionMiddleware, createStaticMiddleware, createBodyParser } from 'MIDDLEWARES';
6
+ import { handler } from 'HANDLER';
7
+
8
+ // WebSocket support
9
+ let wss = null;
10
+ if (config.websocket.enabled) {
11
+ const { WebSocketServer } = await import('ws');
12
+ wss = WebSocketServer;
13
+ }
14
+
15
+ // Track server state
16
+ let isShuttingDown = false;
17
+ let server = null;
18
+ let wsServer = null;
19
+
20
+ // Export for instrumentation support
21
+ export const path = '/';
22
+ export let port = config.port;
23
+ export let host = config.host;
24
+ export { server };
25
+
26
+ /**
27
+ * Initialize and start the server
28
+ */
29
+ async function start() {
30
+ console.log('Starting SvelteKit high-performance server...');
31
+
32
+ // Initialize OpenTelemetry first (for request tracing)
33
+ await initTelemetry();
34
+
35
+ // Create HTTP server
36
+ server = createServer();
37
+
38
+ // Configure server performance settings
39
+ server.keepAliveTimeout = config.keepAliveTimeout;
40
+ server.headersTimeout = config.headersTimeout;
41
+ if (config.maxRequestsPerSocket) {
42
+ server.maxRequestsPerSocket = config.maxRequestsPerSocket;
43
+ }
44
+
45
+ // Create Polka app
46
+ const app = polka({ server });
47
+
48
+ // Health check endpoints (before other middleware)
49
+ if (config.healthCheck.enabled) {
50
+ app.get('/health', (req, res) => {
51
+ if (isShuttingDown) {
52
+ res.writeHead(503, { 'Content-Type': 'text/plain' });
53
+ res.end('Shutting down');
54
+ return;
55
+ }
56
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
57
+ res.end('OK');
58
+ });
59
+
60
+ app.get('/readiness', async (req, res) => {
61
+ if (isShuttingDown) {
62
+ res.writeHead(503, { 'Content-Type': 'application/json' });
63
+ res.end(JSON.stringify({ status: 'not ready', reason: 'shutting down' }));
64
+ return;
65
+ }
66
+
67
+ // Add custom readiness checks here (database, cache, etc.)
68
+ res.writeHead(200, { 'Content-Type': 'application/json' });
69
+ res.end(JSON.stringify({ status: 'ready' }));
70
+ });
71
+ }
72
+
73
+ // Apply compression middleware
74
+ app.use(createCompressionMiddleware());
75
+
76
+ // Apply body parser
77
+ app.use(createBodyParser());
78
+
79
+ // Serve prerendered pages and static assets
80
+ app.use(createStaticMiddleware('client', true));
81
+ app.use(createStaticMiddleware('prerendered', false));
82
+
83
+ // Handle all other requests with SvelteKit
84
+ app.use((req, res) => {
85
+ // Convert Polka request to format expected by SvelteKit
86
+ handler(req, res);
87
+ });
88
+
89
+ // Initialize WebSocket server if enabled
90
+ if (config.websocket.enabled && wss) {
91
+ wsServer = new wss({
92
+ server,
93
+ path: config.websocket.path
94
+ });
95
+
96
+ wsServer.on('connection', (ws, req) => {
97
+ console.log(`WebSocket connection established: ${req.url}`);
98
+
99
+ ws.on('message', (data) => {
100
+ // Handle WebSocket messages
101
+ // You can add custom logic here or expose via hooks
102
+ try {
103
+ const message = JSON.parse(data.toString());
104
+ console.log('WebSocket message:', message);
105
+
106
+ // Echo back for now (customize as needed)
107
+ ws.send(JSON.stringify({ type: 'echo', data: message }));
108
+ } catch (error) {
109
+ console.error('WebSocket message error:', error);
110
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
111
+ }
112
+ });
113
+
114
+ ws.on('close', () => {
115
+ console.log('WebSocket connection closed');
116
+ });
117
+
118
+ ws.on('error', (error) => {
119
+ console.error('WebSocket error:', error);
120
+ });
121
+
122
+ // Send welcome message
123
+ ws.send(JSON.stringify({ type: 'connected', message: 'WebSocket connected' }));
124
+ });
125
+
126
+ console.log(`WebSocket server enabled on path: ${config.websocket.path}`);
127
+ }
128
+
129
+ // Start listening
130
+ server.listen(config.port, config.host, () => {
131
+ console.log(`\n✓ Server running on http://${config.host}:${config.port}`);
132
+ console.log(` - Compression: ${config.compression ? 'enabled' : 'disabled'}`);
133
+ console.log(` - WebSocket: ${config.websocket.enabled ? `enabled (${config.websocket.path})` : 'disabled'}`);
134
+ console.log(` - OpenTelemetry: ${config.telemetry.enabled ? 'enabled' : 'disabled'}`);
135
+ console.log(` - Health checks: ${config.healthCheck.enabled ? 'enabled (/health, /readiness)' : 'disabled'}`);
136
+ console.log(` - Body limit: ${config.bodyLimit}`);
137
+ console.log('');
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Graceful shutdown handler
143
+ */
144
+ async function gracefulShutdown(signal) {
145
+ if (isShuttingDown) {
146
+ console.log('Shutdown already in progress...');
147
+ return;
148
+ }
149
+
150
+ isShuttingDown = true;
151
+ console.log(`\n${signal} received, starting graceful shutdown...`);
152
+
153
+ // Set a timeout to force shutdown
154
+ const forceShutdownTimer = setTimeout(() => {
155
+ console.error('Forced shutdown after timeout');
156
+ process.exit(1);
157
+ }, config.gracefulShutdownTimeout);
158
+
159
+ try {
160
+ // Stop accepting new connections
161
+ if (server) {
162
+ await new Promise((resolve) => {
163
+ server.close(() => {
164
+ console.log('✓ HTTP server closed');
165
+ resolve();
166
+ });
167
+ });
168
+ }
169
+
170
+ // Close WebSocket connections
171
+ if (wsServer) {
172
+ console.log('Closing WebSocket connections...');
173
+ wsServer.clients.forEach((ws) => {
174
+ ws.close(1001, 'Server shutting down');
175
+ });
176
+
177
+ await new Promise((resolve) => {
178
+ wsServer.close(() => {
179
+ console.log('✓ WebSocket server closed');
180
+ resolve();
181
+ });
182
+ });
183
+ }
184
+
185
+ // Shutdown OpenTelemetry and flush traces
186
+ await shutdownTelemetry();
187
+
188
+ clearTimeout(forceShutdownTimer);
189
+ console.log('✓ Graceful shutdown complete');
190
+ process.exit(0);
191
+ } catch (error) {
192
+ console.error('Error during shutdown:', error);
193
+ clearTimeout(forceShutdownTimer);
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ // Register shutdown handlers
199
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
200
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
201
+
202
+ // Handle uncaught errors
203
+ process.on('uncaughtException', (error) => {
204
+ console.error('Uncaught exception:', error);
205
+ gracefulShutdown('UNCAUGHT_EXCEPTION');
206
+ });
207
+
208
+ process.on('unhandledRejection', (reason, promise) => {
209
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
210
+ gracefulShutdown('UNHANDLED_REJECTION');
211
+ });
212
+
213
+ // Start the server
214
+ start().catch((error) => {
215
+ console.error('Failed to start server:', error);
216
+ process.exit(1);
217
+ });
@@ -0,0 +1,162 @@
1
+ import compression from 'compression';
2
+ import sirv from 'sirv';
3
+ import { config } from 'ENV';
4
+
5
+ /**
6
+ * Create compression middleware with optimized settings
7
+ */
8
+ export function createCompressionMiddleware() {
9
+ if (!config.compression) {
10
+ return (req, res, next) => next();
11
+ }
12
+
13
+ return compression({
14
+ level: config.compressionLevel,
15
+ threshold: 1024, // Only compress responses > 1KB
16
+ memLevel: 8,
17
+ filter: (req, res) => {
18
+ const contentType = res.getHeader('Content-Type');
19
+
20
+ // Don't compress images, videos, or already compressed content
21
+ if (contentType && (
22
+ contentType.includes('image/') ||
23
+ contentType.includes('video/') ||
24
+ contentType.includes('audio/') ||
25
+ contentType.includes('font/')
26
+ )) {
27
+ return false;
28
+ }
29
+
30
+ // Use default compression filter for everything else
31
+ return compression.filter(req, res);
32
+ }
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Create static file serving middleware with optimized settings
38
+ */
39
+ export function createStaticMiddleware(dir, precompressed = true) {
40
+ return sirv(dir, {
41
+ dev: false,
42
+ etag: true,
43
+ maxAge: 31536000, // 1 year for immutable assets
44
+ immutable: true,
45
+ gzip: precompressed, // Serve .gz files if available
46
+ brotli: precompressed, // Serve .br files if available
47
+ setHeaders: (res, pathname) => {
48
+ // Cache control based on file type
49
+ if (pathname.includes('immutable')) {
50
+ res.setHeader('Cache-Control', 'public, immutable, max-age=31536000');
51
+ } else if (pathname.endsWith('.html')) {
52
+ res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
53
+ } else {
54
+ res.setHeader('Cache-Control', 'public, max-age=3600');
55
+ }
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Simple body parser for JSON and URL-encoded data with configurable limits
62
+ */
63
+ export function createBodyParser() {
64
+ const limitBytes = parseLimit(config.bodyLimit);
65
+
66
+ return (req, res, next) => {
67
+ // Skip if not a body method
68
+ if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') {
69
+ return next();
70
+ }
71
+
72
+ // Skip if body already parsed
73
+ if (req.body) {
74
+ return next();
75
+ }
76
+
77
+ const contentType = req.headers['content-type'] || '';
78
+
79
+ // Skip if not JSON or form data (SvelteKit will handle FormData/multipart)
80
+ if (!contentType.includes('application/json') &&
81
+ !contentType.includes('application/x-www-form-urlencoded')) {
82
+ return next();
83
+ }
84
+
85
+ let data = '';
86
+ let size = 0;
87
+
88
+ req.on('data', chunk => {
89
+ size += chunk.length;
90
+ if (size > limitBytes) {
91
+ req.removeAllListeners('data');
92
+ req.removeAllListeners('end');
93
+ res.writeHead(413, { 'Content-Type': 'application/json' });
94
+ res.end(JSON.stringify({ error: 'Request body too large' }));
95
+ return;
96
+ }
97
+ data += chunk.toString();
98
+ });
99
+
100
+ req.on('end', () => {
101
+ try {
102
+ if (contentType.includes('application/json')) {
103
+ req.body = data ? JSON.parse(data) : {};
104
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
105
+ req.body = parseUrlEncoded(data);
106
+ }
107
+ } catch (error) {
108
+ res.writeHead(400, { 'Content-Type': 'application/json' });
109
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
110
+ return;
111
+ }
112
+ next();
113
+ });
114
+
115
+ req.on('error', (error) => {
116
+ console.error('Body parse error:', error);
117
+ res.writeHead(400, { 'Content-Type': 'application/json' });
118
+ res.end(JSON.stringify({ error: 'Request error' }));
119
+ });
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Parse size limit string to bytes
125
+ */
126
+ function parseLimit(limit) {
127
+ if (typeof limit === 'number') return limit;
128
+
129
+ const match = limit.match(/^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i);
130
+ if (!match) return 10 * 1024 * 1024; // Default 10MB
131
+
132
+ const value = parseFloat(match[1]);
133
+ const unit = (match[2] || 'b').toLowerCase();
134
+
135
+ const multipliers = {
136
+ b: 1,
137
+ kb: 1024,
138
+ mb: 1024 * 1024,
139
+ gb: 1024 * 1024 * 1024
140
+ };
141
+
142
+ return value * (multipliers[unit] || 1);
143
+ }
144
+
145
+ /**
146
+ * Parse URL-encoded form data
147
+ */
148
+ function parseUrlEncoded(str) {
149
+ const obj = {};
150
+ const pairs = str.split('&');
151
+
152
+ for (const pair of pairs) {
153
+ const [key, value] = pair.split('=');
154
+ if (key) {
155
+ const decodedKey = decodeURIComponent(key);
156
+ const decodedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
157
+ obj[decodedKey] = decodedValue;
158
+ }
159
+ }
160
+
161
+ return obj;
162
+ }
package/files/shims.js ADDED
@@ -0,0 +1,6 @@
1
+ // Polyfills for Node.js environment
2
+ import { installPolyfills } from '@sveltejs/kit/node/polyfills';
3
+
4
+ if (JSON.parse('POLYFILL')) {
5
+ installPolyfills();
6
+ }
@@ -0,0 +1,126 @@
1
+ import { config } from 'ENV';
2
+
3
+ let sdk = null;
4
+
5
+ /**
6
+ * Initialize OpenTelemetry SDK for distributed tracing
7
+ */
8
+ export async function initTelemetry() {
9
+ if (!config.telemetry.enabled) {
10
+ console.log('OpenTelemetry is disabled');
11
+ return null;
12
+ }
13
+
14
+ try {
15
+ // Dynamic imports to avoid loading if telemetry is disabled
16
+ const { NodeSDK } = await import('@opentelemetry/sdk-node');
17
+ const { getNodeAutoInstrumentations } = await import('@opentelemetry/auto-instrumentations-node');
18
+ const { Resource } = await import('@opentelemetry/resources');
19
+ const { SemanticResourceAttributes } = await import('@opentelemetry/semantic-conventions');
20
+
21
+ // Choose exporter based on protocol
22
+ let TraceExporter;
23
+ if (config.telemetry.protocol === 'grpc') {
24
+ const module = await import('@opentelemetry/exporter-trace-otlp-grpc');
25
+ TraceExporter = module.OTLPTraceExporter;
26
+ } else {
27
+ const module = await import('@opentelemetry/exporter-trace-otlp-http');
28
+ TraceExporter = module.OTLPTraceExporter;
29
+ }
30
+
31
+ const exporterConfig = {
32
+ url: config.telemetry.endpoint,
33
+ headers: {}
34
+ };
35
+
36
+ // Add Dynatrace API token if provided
37
+ if (config.telemetry.dynatraceToken) {
38
+ exporterConfig.headers['Authorization'] = `Api-Token ${config.telemetry.dynatraceToken}`;
39
+ }
40
+
41
+ // Merge custom headers from config
42
+ if (config.telemetry.customConfig.headers) {
43
+ Object.assign(exporterConfig.headers, config.telemetry.customConfig.headers);
44
+ }
45
+
46
+ sdk = new NodeSDK({
47
+ resource: new Resource({
48
+ [SemanticResourceAttributes.SERVICE_NAME]: config.telemetry.serviceName,
49
+ [SemanticResourceAttributes.SERVICE_VERSION]: config.telemetry.serviceVersion,
50
+ ...config.telemetry.customConfig.resourceAttributes
51
+ }),
52
+ traceExporter: new TraceExporter(exporterConfig),
53
+ instrumentations: [
54
+ getNodeAutoInstrumentations({
55
+ // Disable noisy instrumentations
56
+ '@opentelemetry/instrumentation-fs': {
57
+ enabled: false
58
+ },
59
+ '@opentelemetry/instrumentation-http': {
60
+ enabled: true,
61
+ ignoreIncomingPaths: ['/health', '/readiness'],
62
+ ignoreOutgoingUrls: [/\/health$/, /\/readiness$/]
63
+ },
64
+ '@opentelemetry/instrumentation-dns': {
65
+ enabled: false
66
+ },
67
+ // Enable all others by default
68
+ ...config.telemetry.customConfig.instrumentations
69
+ })
70
+ ],
71
+ // Sampling configuration
72
+ ...(config.telemetry.sampleRate < 1.0 && {
73
+ sampler: {
74
+ shouldSample: () => {
75
+ return Math.random() < config.telemetry.sampleRate
76
+ ? { decision: 1 } // RECORD_AND_SAMPLED
77
+ : { decision: 0 }; // NOT_RECORD
78
+ }
79
+ }
80
+ })
81
+ });
82
+
83
+ await sdk.start();
84
+ console.log('OpenTelemetry SDK initialized successfully');
85
+ console.log(` Service: ${config.telemetry.serviceName}`);
86
+ console.log(` Endpoint: ${config.telemetry.endpoint || 'default'}`);
87
+ console.log(` Protocol: ${config.telemetry.protocol}`);
88
+ console.log(` Sample Rate: ${(config.telemetry.sampleRate * 100).toFixed(1)}%`);
89
+
90
+ return sdk;
91
+ } catch (error) {
92
+ console.error('Failed to initialize OpenTelemetry:', error.message);
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Shutdown telemetry SDK and flush remaining spans
99
+ */
100
+ export async function shutdownTelemetry() {
101
+ if (sdk) {
102
+ console.log('Shutting down OpenTelemetry SDK...');
103
+ try {
104
+ await sdk.shutdown();
105
+ console.log('OpenTelemetry SDK shutdown complete');
106
+ } catch (error) {
107
+ console.error('Error shutting down OpenTelemetry:', error.message);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get tracer for custom instrumentation
114
+ */
115
+ export function getTracer(name = 'sveltekit-adapter') {
116
+ if (!config.telemetry.enabled) {
117
+ return null;
118
+ }
119
+
120
+ try {
121
+ const { trace } = require('@opentelemetry/api');
122
+ return trace.getTracer(name);
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
package/index.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ export interface AdapterOptions {
2
+ /**
3
+ * Output directory for the build
4
+ * @default 'build'
5
+ */
6
+ out?: string;
7
+
8
+ /**
9
+ * Pre-compress static assets with gzip and brotli
10
+ * @default true
11
+ */
12
+ precompress?: boolean;
13
+
14
+ /**
15
+ * Prefix for environment variables
16
+ * @default ''
17
+ */
18
+ envPrefix?: string;
19
+
20
+ /**
21
+ * Enable runtime compression middleware
22
+ * @default true
23
+ */
24
+ compression?: boolean;
25
+
26
+ /**
27
+ * Compression level (1-9, where 9 is maximum compression)
28
+ * @default 6
29
+ */
30
+ compressionLevel?: number;
31
+
32
+ /**
33
+ * Maximum request body size
34
+ * @default '10mb'
35
+ */
36
+ bodyLimit?: string | number;
37
+
38
+ /**
39
+ * Enable WebSocket support
40
+ * @default true
41
+ */
42
+ websocket?: boolean;
43
+
44
+ /**
45
+ * WebSocket endpoint path
46
+ * @default '/ws'
47
+ */
48
+ websocketPath?: string;
49
+
50
+ /**
51
+ * Enable OpenTelemetry tracing
52
+ * @default true
53
+ */
54
+ telemetry?: boolean;
55
+
56
+ /**
57
+ * Additional OpenTelemetry configuration
58
+ */
59
+ telemetryConfig?: {
60
+ /**
61
+ * Custom resource attributes
62
+ */
63
+ resourceAttributes?: Record<string, string>;
64
+
65
+ /**
66
+ * Custom HTTP headers for exporter
67
+ */
68
+ headers?: Record<string, string>;
69
+
70
+ /**
71
+ * Instrumentation-specific configuration
72
+ */
73
+ instrumentations?: Record<string, any>;
74
+ };
75
+
76
+ /**
77
+ * Telemetry sampling rate (0.0 to 1.0)
78
+ * @default 1.0
79
+ */
80
+ telemetrySampleRate?: number;
81
+
82
+ /**
83
+ * Enable health check endpoints (/health, /readiness)
84
+ * @default true
85
+ */
86
+ healthCheck?: boolean;
87
+
88
+ /**
89
+ * Graceful shutdown timeout in milliseconds
90
+ * @default 30000
91
+ */
92
+ gracefulShutdownTimeout?: number;
93
+
94
+ /**
95
+ * Inject global polyfills (fetch, Headers, etc.)
96
+ * @default true
97
+ */
98
+ polyfill?: boolean;
99
+
100
+ /**
101
+ * External packages to exclude from bundle.
102
+ * Can be an array of package names or a function that receives package.json and returns an array.
103
+ * If not specified, uses package.json dependencies by default.
104
+ * @default undefined
105
+ * @example
106
+ * // Array of package names
107
+ * external: ['polka', 'sirv']
108
+ *
109
+ * // Function to dynamically determine externals
110
+ * external: (pkg) => [...Object.keys(pkg.dependencies), 'some-other-package']
111
+ */
112
+ external?: string[] | ((pkg: any) => string[]);
113
+
114
+ /**
115
+ * Bundle all dependencies (ignore package.json dependencies).
116
+ * When true, all code including node_modules will be bundled.
117
+ * @default false
118
+ */
119
+ bundleAll?: boolean;
120
+
121
+ /**
122
+ * Additional rolldown configuration options.
123
+ * These will be merged with the default rolldown config.
124
+ */
125
+ rolldownOptions?: Record<string, any>;