@solcreek/adapter-creek 0.1.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/LICENSE +190 -0
- package/README.md +184 -0
- package/dist/build.d.ts +57 -0
- package/dist/build.js +1382 -0
- package/dist/bundler.d.ts +20 -0
- package/dist/bundler.js +991 -0
- package/dist/cache-handler.d.ts +32 -0
- package/dist/cache-handler.js +100 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +247 -0
- package/dist/manifest.d.ts +40 -0
- package/dist/manifest.js +27 -0
- package/dist/worker-entry.d.ts +133 -0
- package/dist/worker-entry.js +7734 -0
- package/package.json +64 -0
- package/src/shims/als-polyfill.js +7 -0
- package/src/shims/critters.js +7 -0
- package/src/shims/empty.js +2 -0
- package/src/shims/env.js +3 -0
- package/src/shims/fast-set-immediate.js +285 -0
- package/src/shims/fs.js +225 -0
- package/src/shims/http.js +240 -0
- package/src/shims/image-optimizer.js +18 -0
- package/src/shims/load-manifest.js +123 -0
- package/src/shims/opentelemetry.js +229 -0
- package/src/shims/sharp.js +12 -0
- package/src/shims/sqlite3-binding.js +517 -0
- package/src/shims/track-module-loading.js +68 -0
- package/src/shims/vm.js +49 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Minimal node:http shim for CF Workers.
|
|
2
|
+
// Plain objects with the minimum API surface that Next.js handler needs.
|
|
3
|
+
// Does NOT extend from node:stream (CF Workers stream compat may be incomplete).
|
|
4
|
+
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
|
|
7
|
+
export class IncomingMessage extends EventEmitter {
|
|
8
|
+
constructor(socket) {
|
|
9
|
+
super();
|
|
10
|
+
this.socket = socket || { encrypted: true, remoteAddress: "127.0.0.1", address: () => ({ port: 443 }), end() {}, destroy() {} };
|
|
11
|
+
this.connection = this.socket;
|
|
12
|
+
this.httpVersion = "1.1";
|
|
13
|
+
this.httpVersionMajor = 1;
|
|
14
|
+
this.httpVersionMinor = 1;
|
|
15
|
+
this.complete = true;
|
|
16
|
+
this.headers = {};
|
|
17
|
+
this.rawHeaders = [];
|
|
18
|
+
this.trailers = {};
|
|
19
|
+
this.rawTrailers = [];
|
|
20
|
+
this.method = "GET";
|
|
21
|
+
this.url = "/";
|
|
22
|
+
this.statusCode = null;
|
|
23
|
+
this.statusMessage = null;
|
|
24
|
+
this.aborted = false;
|
|
25
|
+
this.upgrade = false;
|
|
26
|
+
this.readable = true;
|
|
27
|
+
this._body = null;
|
|
28
|
+
this._bodyConsumed = false;
|
|
29
|
+
// Node.js Readable state — some Next.js code checks this directly.
|
|
30
|
+
this._readableState = { ended: false, endEmitted: false, flowing: null };
|
|
31
|
+
// Buffer chunks until a listener is attached.
|
|
32
|
+
// push() may be called before the handler adds "data" listeners.
|
|
33
|
+
this._bufferedChunks = [];
|
|
34
|
+
this._ended = false;
|
|
35
|
+
this._flowing = false;
|
|
36
|
+
}
|
|
37
|
+
// Readable stream interface (minimal)
|
|
38
|
+
read() {
|
|
39
|
+
if (this._bufferedChunks.length > 0) return this._bufferedChunks.shift();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
push(chunk) {
|
|
43
|
+
if (chunk === null) {
|
|
44
|
+
this._ended = true;
|
|
45
|
+
this.complete = true;
|
|
46
|
+
this._readableState.ended = true;
|
|
47
|
+
if (this._flowing) {
|
|
48
|
+
this._readableState.endEmitted = true;
|
|
49
|
+
this.emit("end");
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this._flowing) {
|
|
54
|
+
this.emit("data", chunk);
|
|
55
|
+
} else {
|
|
56
|
+
this._bufferedChunks.push(chunk);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Flush buffered data when listeners are ready
|
|
60
|
+
_startFlowing() {
|
|
61
|
+
if (this._flowing) return;
|
|
62
|
+
this._flowing = true;
|
|
63
|
+
this._readableState.flowing = true;
|
|
64
|
+
while (this._bufferedChunks.length > 0) {
|
|
65
|
+
this.emit("data", this._bufferedChunks.shift());
|
|
66
|
+
}
|
|
67
|
+
if (this._ended) {
|
|
68
|
+
this._readableState.endEmitted = true;
|
|
69
|
+
this.emit("end");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
on(event, fn) {
|
|
73
|
+
super.on(event, fn);
|
|
74
|
+
// When a "data" listener is attached, start flowing
|
|
75
|
+
if (event === "data" && !this._flowing) {
|
|
76
|
+
queueMicrotask(() => this._startFlowing());
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
addListener(event, fn) { return this.on(event, fn); }
|
|
81
|
+
pipe(dest) {
|
|
82
|
+
this.on("data", (chunk) => dest.write(chunk));
|
|
83
|
+
this.on("end", () => { if (dest.end) dest.end(); });
|
|
84
|
+
return dest;
|
|
85
|
+
}
|
|
86
|
+
unpipe() {}
|
|
87
|
+
resume() { this._startFlowing(); return this; }
|
|
88
|
+
pause() { return this; }
|
|
89
|
+
setEncoding() { return this; }
|
|
90
|
+
setTimeout() { return this; }
|
|
91
|
+
destroy() { this.emit("close"); return this; }
|
|
92
|
+
[Symbol.asyncIterator]() {
|
|
93
|
+
const self = this;
|
|
94
|
+
// Drain both currently-buffered chunks AND any chunks that arrive
|
|
95
|
+
// via subsequent push() calls. Use a pull-based queue with a
|
|
96
|
+
// resolver so we wake up the awaiting consumer exactly when new
|
|
97
|
+
// data arrives or end is reached. This replaces an older
|
|
98
|
+
// implementation that went through on("data"/"end") events with a
|
|
99
|
+
// queueMicrotask + _startFlowing dance — on workerd the microtask
|
|
100
|
+
// timing was unreliable for bodyParser:false handlers that iterate
|
|
101
|
+
// the request body directly (they would hang forever).
|
|
102
|
+
const queue = [];
|
|
103
|
+
let ended = false;
|
|
104
|
+
let resolver = null;
|
|
105
|
+
const notify = () => {
|
|
106
|
+
if (resolver) {
|
|
107
|
+
const r = resolver;
|
|
108
|
+
resolver = null;
|
|
109
|
+
r();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// Seed the queue with any pre-buffered chunks. Clear the shim's
|
|
113
|
+
// buffer so a later push() goes straight into our queue.
|
|
114
|
+
for (const chunk of self._bufferedChunks) queue.push(chunk);
|
|
115
|
+
self._bufferedChunks = [];
|
|
116
|
+
self._flowing = true;
|
|
117
|
+
self._readableState.flowing = true;
|
|
118
|
+
if (self._ended) {
|
|
119
|
+
ended = true;
|
|
120
|
+
self._readableState.endEmitted = true;
|
|
121
|
+
}
|
|
122
|
+
// Replace push() so later chunks land in our queue. Keep the null
|
|
123
|
+
// sentinel semantics for end-of-stream.
|
|
124
|
+
const origPush = self.push.bind(self);
|
|
125
|
+
self.push = (chunk) => {
|
|
126
|
+
if (chunk === null) {
|
|
127
|
+
ended = true;
|
|
128
|
+
self._ended = true;
|
|
129
|
+
self.complete = true;
|
|
130
|
+
self._readableState.ended = true;
|
|
131
|
+
self._readableState.endEmitted = true;
|
|
132
|
+
notify();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
queue.push(chunk);
|
|
136
|
+
notify();
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
async next() {
|
|
140
|
+
if (queue.length === 0 && !ended) {
|
|
141
|
+
await new Promise((r) => { resolver = r; });
|
|
142
|
+
}
|
|
143
|
+
if (queue.length > 0) return { done: false, value: queue.shift() };
|
|
144
|
+
return { done: true, value: undefined };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class ServerResponse extends EventEmitter {
|
|
151
|
+
constructor(req) {
|
|
152
|
+
super();
|
|
153
|
+
this.req = req;
|
|
154
|
+
this.statusCode = 200;
|
|
155
|
+
this.statusMessage = "";
|
|
156
|
+
this.headersSent = false;
|
|
157
|
+
this.finished = false;
|
|
158
|
+
this.writable = true;
|
|
159
|
+
this.sendDate = true;
|
|
160
|
+
this._headers = {};
|
|
161
|
+
this._headerNames = {};
|
|
162
|
+
this.socket = req?.socket || { encrypted: true, remoteAddress: "127.0.0.1" };
|
|
163
|
+
this.connection = this.socket;
|
|
164
|
+
}
|
|
165
|
+
setHeader(name, value) { this._headers[name.toLowerCase()] = value; this._headerNames[name.toLowerCase()] = name; }
|
|
166
|
+
appendHeader(name, value) {
|
|
167
|
+
const key = name.toLowerCase();
|
|
168
|
+
const existing = this._headers[key];
|
|
169
|
+
if (existing === undefined) {
|
|
170
|
+
this._headers[key] = value;
|
|
171
|
+
} else if (Array.isArray(existing)) {
|
|
172
|
+
existing.push(value);
|
|
173
|
+
} else {
|
|
174
|
+
this._headers[key] = [existing, value];
|
|
175
|
+
}
|
|
176
|
+
this._headerNames[key] = name;
|
|
177
|
+
}
|
|
178
|
+
getHeader(name) { return this._headers[name.toLowerCase()]; }
|
|
179
|
+
getHeaders() { return { ...this._headers }; }
|
|
180
|
+
getHeaderNames() { return Object.keys(this._headers); }
|
|
181
|
+
hasHeader(name) { return name.toLowerCase() in this._headers; }
|
|
182
|
+
removeHeader(name) { delete this._headers[name.toLowerCase()]; delete this._headerNames[name.toLowerCase()]; }
|
|
183
|
+
writeHead(code, msg, hdrs) {
|
|
184
|
+
this.statusCode = code;
|
|
185
|
+
if (typeof msg === "string") this.statusMessage = msg;
|
|
186
|
+
else if (typeof msg === "object") hdrs = msg;
|
|
187
|
+
if (hdrs) {
|
|
188
|
+
for (const [k, v] of Object.entries(hdrs)) {
|
|
189
|
+
// Array values (e.g., Set-Cookie) should be stored as-is
|
|
190
|
+
if (Array.isArray(v)) {
|
|
191
|
+
this._headers[k.toLowerCase()] = v;
|
|
192
|
+
this._headerNames[k.toLowerCase()] = k;
|
|
193
|
+
} else {
|
|
194
|
+
this.setHeader(k, v);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.headersSent = true;
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
write(chunk, encoding, cb) {
|
|
202
|
+
if (typeof encoding === "function") { cb = encoding; encoding = undefined; }
|
|
203
|
+
this.emit("data", chunk);
|
|
204
|
+
if (cb) cb();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
end(chunk, encoding, cb) {
|
|
208
|
+
if (typeof chunk === "function") { cb = chunk; chunk = null; }
|
|
209
|
+
if (typeof encoding === "function") { cb = encoding; encoding = null; }
|
|
210
|
+
if (chunk) this.emit("data", chunk);
|
|
211
|
+
this.finished = true;
|
|
212
|
+
this.writable = false;
|
|
213
|
+
this.emit("finish");
|
|
214
|
+
this.emit("close");
|
|
215
|
+
if (cb) cb();
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
get writableEnded() { return this.finished; }
|
|
219
|
+
get writableFinished() { return this.finished; }
|
|
220
|
+
flushHeaders() { this.headersSent = true; }
|
|
221
|
+
assignSocket() {}
|
|
222
|
+
detachSocket() {}
|
|
223
|
+
writeContinue() {}
|
|
224
|
+
writeProcessing() {}
|
|
225
|
+
setTimeout() { return this; }
|
|
226
|
+
addTrailers() {}
|
|
227
|
+
cork() {}
|
|
228
|
+
uncork() {}
|
|
229
|
+
// Writable interface stubs
|
|
230
|
+
destroy() { return this; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function createServer() { throw new Error("http.createServer not available in CF Workers"); }
|
|
234
|
+
export function request() { throw new Error("http.request not available in CF Workers"); }
|
|
235
|
+
export function get() { throw new Error("http.get not available in CF Workers"); }
|
|
236
|
+
|
|
237
|
+
export const METHODS = ["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"];
|
|
238
|
+
export const STATUS_CODES = { 200:"OK",201:"Created",204:"No Content",301:"Moved Permanently",302:"Found",304:"Not Modified",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error" };
|
|
239
|
+
|
|
240
|
+
export default { IncomingMessage, ServerResponse, createServer, request, get, METHODS, STATUS_CODES };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Image optimization shim for CF Workers.
|
|
2
|
+
// Delegates to Cloudflare Image Resizing instead of sharp.
|
|
3
|
+
// See: https://developers.cloudflare.com/images/transform-images/
|
|
4
|
+
|
|
5
|
+
export async function optimizeImage(buffer, { width, height, quality, contentType }) {
|
|
6
|
+
// CF Image Resizing is handled at the edge via cf.image options
|
|
7
|
+
// on fetch requests. For the adapter, we return the original image
|
|
8
|
+
// since optimization happens at the CDN layer, not in the worker.
|
|
9
|
+
//
|
|
10
|
+
// In production, the dispatch worker or CDN rules apply:
|
|
11
|
+
// fetch(imageUrl, { cf: { image: { width, height, quality, format } } })
|
|
12
|
+
return {
|
|
13
|
+
buffer,
|
|
14
|
+
contentType: contentType || "image/webp",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default { optimizeImage };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Shim for next/dist/server/load-manifest.external.js
|
|
2
|
+
// Reads manifests from globalThis.__MANIFESTS (embedded at build time by the adapter).
|
|
3
|
+
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
|
|
6
|
+
// Manifests that Next.js loads with `handleMissing: true` or that are
|
|
7
|
+
// conditionally generated and may not exist in every build. Returning {} for
|
|
8
|
+
// these matches the upstream Node fs behavior — without this, dynamic routes
|
|
9
|
+
// 500 when the build-time glob scan didn't pick them up.
|
|
10
|
+
//
|
|
11
|
+
// Source: next/dist/server/route-modules/route-module.ts
|
|
12
|
+
// Aligned with opennextjs-cloudflare#1151 + #1160.
|
|
13
|
+
const KNOWN_OPTIONAL_MANIFESTS = new Set([
|
|
14
|
+
"react-loadable-manifest", // Turbopack: only routes with dynamic imports
|
|
15
|
+
"subresource-integrity-manifest", // only when experimental.sri configured
|
|
16
|
+
"server-reference-manifest", // App Router only
|
|
17
|
+
"dynamic-css-manifest", // Pages Router + Webpack only
|
|
18
|
+
"fallback-build-manifest", // only for /_error
|
|
19
|
+
"prefetch-hints", // Next 16.2+
|
|
20
|
+
"_client-reference-manifest", // optional for static metadata routes (evalManifest)
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isKnownOptional(manifestPath) {
|
|
24
|
+
// Compare against the basename without trailing extension. Some Next.js
|
|
25
|
+
// constants omit the extension (e.g. SUBRESOURCE_INTEGRITY_MANIFEST), so we
|
|
26
|
+
// strip .json / .js before matching.
|
|
27
|
+
const base = manifestPath.split("/").pop() || manifestPath;
|
|
28
|
+
const stripped = base.replace(/\.(json|js)$/, "");
|
|
29
|
+
return KNOWN_OPTIONAL_MANIFESTS.has(stripped);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findInManifests(path) {
|
|
33
|
+
const manifests = globalThis.__MANIFESTS;
|
|
34
|
+
if (!manifests) return undefined;
|
|
35
|
+
|
|
36
|
+
// Direct match
|
|
37
|
+
if (manifests[path]) return manifests[path];
|
|
38
|
+
|
|
39
|
+
// Match by .next/ relative tail — handles path prefix differences
|
|
40
|
+
// Requested: //.next/routes-manifest.json
|
|
41
|
+
// Available: /Users/.../apps/www/.next/routes-manifest.json
|
|
42
|
+
const tail = path.includes(".next/")
|
|
43
|
+
? ".next/" + path.split(".next/").pop()
|
|
44
|
+
: path.split("/").pop();
|
|
45
|
+
|
|
46
|
+
for (const [key, val] of Object.entries(manifests)) {
|
|
47
|
+
const keyTail = key.includes(".next/")
|
|
48
|
+
? ".next/" + key.split(".next/").pop()
|
|
49
|
+
: key.split("/").pop();
|
|
50
|
+
if (tail === keyTail) return val;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadManifest(path, shouldCache = true, _cache = cache, skipParse = false, handleMissing) {
|
|
57
|
+
const cached = shouldCache && cache.get(path);
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
|
|
60
|
+
const content = findInManifests(path);
|
|
61
|
+
|
|
62
|
+
if (content === undefined) {
|
|
63
|
+
if (handleMissing || isKnownOptional(path)) {
|
|
64
|
+
const result = {};
|
|
65
|
+
if (shouldCache) cache.set(path, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`[Creek] Manifest not found: ${path}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let manifest = skipParse ? content : JSON.parse(content);
|
|
72
|
+
if (shouldCache) cache.set(path, manifest);
|
|
73
|
+
return manifest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function evalManifest(path, shouldCache = true, _cache = cache, handleMissing) {
|
|
77
|
+
const cached = shouldCache && cache.get(path);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
|
|
80
|
+
const content = findInManifests(path);
|
|
81
|
+
|
|
82
|
+
if (content === undefined) {
|
|
83
|
+
if (handleMissing || isKnownOptional(path)) {
|
|
84
|
+
const result = {};
|
|
85
|
+
if (shouldCache) cache.set(path, result);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`[Creek] Manifest not found for eval: ${path}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let contextObject = {};
|
|
92
|
+
try {
|
|
93
|
+
contextObject = JSON.parse(content);
|
|
94
|
+
} catch {
|
|
95
|
+
// JS manifests (e.g., _buildManifest.js) — extract JSON via regex.
|
|
96
|
+
// CF Workers blocks new Function() (CSP), so we parse the assignment
|
|
97
|
+
// pattern: self.__BUILD_MANIFEST = {...}
|
|
98
|
+
try {
|
|
99
|
+
const jsonMatch = content.match(/=\s*(\{[\s\S]*\})\s*[;\n]/);
|
|
100
|
+
if (jsonMatch) {
|
|
101
|
+
const data = JSON.parse(jsonMatch[1]);
|
|
102
|
+
for (const [key, val] of Object.entries(data)) {
|
|
103
|
+
contextObject[key] = val;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (shouldCache) cache.set(path, contextObject);
|
|
110
|
+
return contextObject;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadManifestFromRelativePath({ projectDir, distDir, manifest, shouldCache, cache: c, skipParse, handleMissing, useEval }) {
|
|
114
|
+
const manifestPath = (projectDir || "") + "/" + (distDir || ".next") + "/" + manifest;
|
|
115
|
+
if (useEval) return evalManifest(manifestPath, shouldCache, c, handleMissing);
|
|
116
|
+
return loadManifest(manifestPath, shouldCache, c, skipParse, handleMissing);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function clearManifestCache(path) {
|
|
120
|
+
return cache.delete(path);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { loadManifest, evalManifest, loadManifestFromRelativePath, clearManifestCache };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Minimal @opentelemetry/api shim for CF Workers
|
|
2
|
+
// Next.js uses it for tracing but it's not required for functionality.
|
|
3
|
+
|
|
4
|
+
const NOOP = () => {};
|
|
5
|
+
// NOOP_SPAN must be usable by Next.js (has methods like isRecording/end)
|
|
6
|
+
// but must NOT be serializable by React RSC. React only serializes
|
|
7
|
+
// enumerable own properties — so we use non-enumerable properties.
|
|
8
|
+
const NOOP_SPAN = Object.create(null);
|
|
9
|
+
Object.defineProperties(NOOP_SPAN, {
|
|
10
|
+
setAttribute: { value: () => NOOP_SPAN },
|
|
11
|
+
setAttributes: { value: () => NOOP_SPAN },
|
|
12
|
+
addEvent: { value: () => NOOP_SPAN },
|
|
13
|
+
setStatus: { value: () => NOOP_SPAN },
|
|
14
|
+
updateName: { value: () => NOOP_SPAN },
|
|
15
|
+
end: { value: NOOP },
|
|
16
|
+
isRecording: { value: () => false },
|
|
17
|
+
recordException: { value: NOOP },
|
|
18
|
+
spanContext: { value: () => ({ traceId: "", spanId: "", traceFlags: 0 }) },
|
|
19
|
+
});
|
|
20
|
+
const NOOP_TRACER = {
|
|
21
|
+
startSpan: () => NOOP_SPAN,
|
|
22
|
+
startActiveSpan: (name, ...args) => {
|
|
23
|
+
const fn = args[args.length - 1];
|
|
24
|
+
if (typeof fn === "function") return fn(NOOP_SPAN);
|
|
25
|
+
return NOOP_SPAN;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const NOOP_TRACER_PROVIDER = { getTracer: () => NOOP_TRACER };
|
|
29
|
+
const NOOP_CONTEXT_MANAGER = {
|
|
30
|
+
active: () => ROOT_CONTEXT,
|
|
31
|
+
with: (ctx, fn) => fn(),
|
|
32
|
+
bind: (ctx, target) => target,
|
|
33
|
+
enable: NOOP, disable: NOOP,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Context objects need getValue/setValue/deleteValue per OpenTelemetry API.
|
|
37
|
+
// setValue + deleteValue must return a NEW context with the key's value
|
|
38
|
+
// set/removed — not mutate in place, and not return the same empty ROOT_CONTEXT
|
|
39
|
+
// (or the tracer code calling \`trace.setSpan(ctx, span)\` would get a
|
|
40
|
+
// context that still doesn't contain the span).
|
|
41
|
+
function _makeContext(store) {
|
|
42
|
+
return {
|
|
43
|
+
getValue: (key) => store.get(key),
|
|
44
|
+
setValue: (key, value) => {
|
|
45
|
+
const next = new Map(store);
|
|
46
|
+
next.set(key, value);
|
|
47
|
+
return _makeContext(next);
|
|
48
|
+
},
|
|
49
|
+
deleteValue: (key) => {
|
|
50
|
+
const next = new Map(store);
|
|
51
|
+
next.delete(key);
|
|
52
|
+
return _makeContext(next);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export const ROOT_CONTEXT = _makeContext(new Map());
|
|
57
|
+
export const defaultTextMapGetter = { get: () => undefined, keys: () => [] };
|
|
58
|
+
export const defaultTextMapSetter = { set: NOOP };
|
|
59
|
+
export const INVALID_SPANID = "";
|
|
60
|
+
export const INVALID_TRACEID = "";
|
|
61
|
+
export const INVALID_SPAN_CONTEXT = { traceId: "", spanId: "", traceFlags: 0 };
|
|
62
|
+
|
|
63
|
+
// \`trace\` and \`context\` also have to cooperate across @opentelemetry/api
|
|
64
|
+
// copies via the same globalThis symbol that \`propagation\` uses below.
|
|
65
|
+
// Next.js's tracer.js imports the trace API through our shim; when the
|
|
66
|
+
// user's instrumentation runs \`NodeTracerProvider.register()\`, it writes
|
|
67
|
+
// the real tracer provider and context manager into
|
|
68
|
+
// \`globalThis[Symbol.for("opentelemetry.js.api.1")]\` via its own api
|
|
69
|
+
// copy. Without delegating here, Next.js's render path keeps calling
|
|
70
|
+
// NOOP_TRACER.startSpan + context.active()=ROOT_CONTEXT, so no real span
|
|
71
|
+
// is ever present in the context passed to \`propagation.inject\`. Result:
|
|
72
|
+
// the user's propagator sees no span and falls back to
|
|
73
|
+
// \`"invariant"\` for \`my-parent-span-id\`, failing the regex assertion
|
|
74
|
+
// \`/<meta name="my-parent-span-id" content="[a-f0-9]{16}">/\`. Reading
|
|
75
|
+
// from the shared global recovers the real tracer + context manager and
|
|
76
|
+
// gives the propagator a real span to read.
|
|
77
|
+
const CONTEXT_API_KEY = Symbol.for("opentelemetry.context");
|
|
78
|
+
const SPAN_KEY = Symbol.for("OpenTelemetry Context Key SPAN");
|
|
79
|
+
function _getGlobalTracerProvider() {
|
|
80
|
+
return _otelGlobal().trace ?? null;
|
|
81
|
+
}
|
|
82
|
+
function _getGlobalContextManager() {
|
|
83
|
+
return _otelGlobal().context ?? null;
|
|
84
|
+
}
|
|
85
|
+
export const trace = {
|
|
86
|
+
getTracer: (...args) => {
|
|
87
|
+
const g = _getGlobalTracerProvider();
|
|
88
|
+
if (g && typeof g.getTracer === "function") {
|
|
89
|
+
try { return g.getTracer(...args); } catch {}
|
|
90
|
+
}
|
|
91
|
+
return NOOP_TRACER;
|
|
92
|
+
},
|
|
93
|
+
getTracerProvider: () => _getGlobalTracerProvider() ?? NOOP_TRACER_PROVIDER,
|
|
94
|
+
setGlobalTracerProvider: (p) => { _otelGlobal().trace = p; return p; },
|
|
95
|
+
// getSpan(ctx): read the SPAN key from the context object — the same
|
|
96
|
+
// pattern sdk-trace-base uses. Falls back to undefined when the context
|
|
97
|
+
// is our empty ROOT_CONTEXT.
|
|
98
|
+
getSpan: (ctx) => {
|
|
99
|
+
if (ctx && typeof ctx.getValue === "function") {
|
|
100
|
+
const s = ctx.getValue(SPAN_KEY);
|
|
101
|
+
if (s) return s;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
},
|
|
105
|
+
getActiveSpan: () => {
|
|
106
|
+
const ctx = _callActiveContext();
|
|
107
|
+
if (ctx && typeof ctx.getValue === "function") return ctx.getValue(SPAN_KEY);
|
|
108
|
+
return undefined;
|
|
109
|
+
},
|
|
110
|
+
setSpan: (ctx, span) => (ctx || ROOT_CONTEXT).setValue(SPAN_KEY, span),
|
|
111
|
+
deleteSpan: (ctx) => (ctx || ROOT_CONTEXT).deleteValue(SPAN_KEY),
|
|
112
|
+
setSpanContext: (ctx) => ctx || ROOT_CONTEXT,
|
|
113
|
+
// getSpanContext: read span from context, return its spanContext() result.
|
|
114
|
+
// User propagators rely on \`trace.getSpanContext(ctx)?.spanId\` to fill
|
|
115
|
+
// their my-parent-span-id meta tag — without this, they fall back to
|
|
116
|
+
// the literal string "invariant".
|
|
117
|
+
getSpanContext: (ctx) => {
|
|
118
|
+
const span = trace.getSpan(ctx);
|
|
119
|
+
if (span && typeof span.spanContext === "function") {
|
|
120
|
+
try { return span.spanContext(); } catch {}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
},
|
|
124
|
+
isSpanContextValid: (sc) => !!sc && typeof sc.traceId === "string" && sc.traceId.length > 0,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function _callActiveContext() {
|
|
128
|
+
const mgr = _getGlobalContextManager();
|
|
129
|
+
if (mgr && typeof mgr.active === "function") {
|
|
130
|
+
try { return mgr.active(); } catch {}
|
|
131
|
+
}
|
|
132
|
+
return ROOT_CONTEXT;
|
|
133
|
+
}
|
|
134
|
+
export const context = {
|
|
135
|
+
active: () => _callActiveContext(),
|
|
136
|
+
with: (ctx, fn, thisArg, ...args) => {
|
|
137
|
+
const mgr = _getGlobalContextManager();
|
|
138
|
+
if (mgr && typeof mgr.with === "function") {
|
|
139
|
+
try { return mgr.with(ctx, fn, thisArg, ...args); } catch {}
|
|
140
|
+
}
|
|
141
|
+
return fn.call(thisArg, ...args);
|
|
142
|
+
},
|
|
143
|
+
bind: (ctx, target) => {
|
|
144
|
+
const mgr = _getGlobalContextManager();
|
|
145
|
+
if (mgr && typeof mgr.bind === "function") {
|
|
146
|
+
try { return mgr.bind(ctx, target); } catch {}
|
|
147
|
+
}
|
|
148
|
+
return target;
|
|
149
|
+
},
|
|
150
|
+
setGlobalContextManager: (mgr) => { _otelGlobal().context = mgr; return mgr; },
|
|
151
|
+
disable: () => { delete _otelGlobal().context; },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Propagation must be functional (not NOOP) for user instrumentation to
|
|
155
|
+
// work — Next.js's server render calls \`propagation.inject(ctx, carrier,
|
|
156
|
+
// setter)\` when \`experimental.clientTraceMetadata\` is configured, and
|
|
157
|
+
// expects the user's propagator (set by \`NodeTracerProvider.register({
|
|
158
|
+
// propagator })\`) to emit the \`<meta name="...">\` tags.
|
|
159
|
+
//
|
|
160
|
+
// Delicate: \`@opentelemetry/api\` is loaded as multiple copies in the
|
|
161
|
+
// bundle — one compiled into \`next/\`, one Turbopack-bundled from the
|
|
162
|
+
// user's \`node_modules\`, and this shim (aliased for bare-specifier
|
|
163
|
+
// imports at wrangler time). Their code agrees on a shared singleton by
|
|
164
|
+
// writing propagator/context/tracer onto
|
|
165
|
+
// \`globalThis[Symbol.for("opentelemetry.js.api.<major>")].propagation\`.
|
|
166
|
+
// If this shim's propagation used a module-local variable, the user's
|
|
167
|
+
// \`setGlobalPropagator\` call on their bundled api would never reach
|
|
168
|
+
// this shim's \`inject\` — which is the codepath Next.js actually calls
|
|
169
|
+
// through. Writing/reading via the same global symbol keeps every copy
|
|
170
|
+
// in sync.
|
|
171
|
+
// Fixes e2e/opentelemetry/client-trace-metadata (5 tests).
|
|
172
|
+
const OTEL_GLOBAL_KEY = Symbol.for("opentelemetry.js.api.1");
|
|
173
|
+
function _otelGlobal() {
|
|
174
|
+
let g = globalThis[OTEL_GLOBAL_KEY];
|
|
175
|
+
if (!g) {
|
|
176
|
+
g = { version: "1.9.0" };
|
|
177
|
+
globalThis[OTEL_GLOBAL_KEY] = g;
|
|
178
|
+
}
|
|
179
|
+
return g;
|
|
180
|
+
}
|
|
181
|
+
function _getPropagator() {
|
|
182
|
+
return _otelGlobal().propagation ?? null;
|
|
183
|
+
}
|
|
184
|
+
export const propagation = {
|
|
185
|
+
inject: (ctx, carrier, setter) => {
|
|
186
|
+
const p = _getPropagator();
|
|
187
|
+
if (p && typeof p.inject === "function") {
|
|
188
|
+
try { p.inject(ctx, carrier, setter); } catch {}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
extract: (ctx, carrier, getter) => {
|
|
192
|
+
const p = _getPropagator();
|
|
193
|
+
if (p && typeof p.extract === "function") {
|
|
194
|
+
try { return p.extract(ctx, carrier, getter); } catch {}
|
|
195
|
+
}
|
|
196
|
+
return ctx;
|
|
197
|
+
},
|
|
198
|
+
fields: () => {
|
|
199
|
+
const p = _getPropagator();
|
|
200
|
+
if (p && typeof p.fields === "function") {
|
|
201
|
+
try { return p.fields(); } catch {}
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
},
|
|
205
|
+
setGlobalPropagator: (p) => {
|
|
206
|
+
_otelGlobal().propagation = p;
|
|
207
|
+
return propagation;
|
|
208
|
+
},
|
|
209
|
+
disable: () => { delete _otelGlobal().propagation; },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const diag = {
|
|
213
|
+
setLogger: NOOP, disable: NOOP,
|
|
214
|
+
createComponentLogger: () => diag,
|
|
215
|
+
verbose: NOOP, debug: NOOP, info: NOOP, warn: NOOP, error: NOOP,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export function createContextKey(name) { return Symbol(name); }
|
|
219
|
+
|
|
220
|
+
export const SpanKind = { INTERNAL: 0, SERVER: 1, CLIENT: 2, PRODUCER: 3, CONSUMER: 4 };
|
|
221
|
+
export const SpanStatusCode = { UNSET: 0, OK: 1, ERROR: 2 };
|
|
222
|
+
export const TraceFlags = { NONE: 0, SAMPLED: 1 };
|
|
223
|
+
|
|
224
|
+
export default {
|
|
225
|
+
trace, context, propagation, diag, createContextKey,
|
|
226
|
+
ROOT_CONTEXT, SpanKind, SpanStatusCode, TraceFlags,
|
|
227
|
+
defaultTextMapGetter, defaultTextMapSetter,
|
|
228
|
+
INVALID_SPANID, INVALID_TRACEID, INVALID_SPAN_CONTEXT,
|
|
229
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// sharp shim for Cloudflare Workers.
|
|
2
|
+
//
|
|
3
|
+
// `@vercel/og/index.node.js` tries `(await import("sharp")).default` and,
|
|
4
|
+
// if truthy, uses sharp to rasterize satori's SVG output to PNG. sharp
|
|
5
|
+
// relies on native `.node` bindings (libvips) that workerd cannot load,
|
|
6
|
+
// so the real module half-loads and ends up non-callable — the caller
|
|
7
|
+
// then throws `TypeError: sharp is not a function`.
|
|
8
|
+
//
|
|
9
|
+
// Returning `default: undefined` makes getSharp() return undefined and
|
|
10
|
+
// @vercel/og falls back to its built-in resvg.wasm path, which is already
|
|
11
|
+
// bundled as a CompiledWasm module and works natively on workerd.
|
|
12
|
+
export default undefined;
|