@solcreek/adapter-creek 0.1.1 → 0.1.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.
@@ -1,32 +1,2 @@
1
- /**
2
- * Cache handler for Next.js ISR on Cloudflare Workers.
3
- *
4
- * Implements the cacheHandler interface (get/set/revalidateTag/resetRequestCache).
5
- * Uses in-memory Map for single-instance caching.
6
- *
7
- * TODO: Phase 2 — migrate to Durable Objects for persistent, multi-instance cache
8
- * with tag-based invalidation via DOShardedTagCache.
9
- */
10
- export default class CacheHandler {
11
- constructor(_ctx?: unknown);
12
- get(key: string, _ctx?: {
13
- kind?: string;
14
- }): Promise<{
15
- value: unknown;
16
- lastModified: number;
17
- age: number;
18
- cacheState: "stale";
19
- } | {
20
- value: unknown;
21
- lastModified: number;
22
- age: number;
23
- cacheState: "fresh";
24
- } | null>;
25
- set(key: string, data: unknown | null, ctx?: {
26
- tags?: string[];
27
- revalidate?: number | false;
28
- }): Promise<void>;
29
- revalidateTag(tag: string | string[]): Promise<void>;
30
- resetRequestCache(): void;
31
- }
1
+ export { default } from "@solcreek/adapter-core/cache-handler";
32
2
  //# sourceMappingURL=cache-handler.d.ts.map
@@ -1,100 +1,10 @@
1
- /**
2
- * Cache handler for Next.js ISR on Cloudflare Workers.
3
- *
4
- * Implements the cacheHandler interface (get/set/revalidateTag/resetRequestCache).
5
- * Uses in-memory Map for single-instance caching.
6
- *
7
- * TODO: Phase 2 — migrate to Durable Objects for persistent, multi-instance cache
8
- * with tag-based invalidation via DOShardedTagCache.
9
- */
10
- const cache = new Map();
11
- const tagToKeys = new Map();
12
- // When a tag is invalidated via revalidateTag(), we record the wall-clock
13
- // timestamp here. Subsequent get() calls compare this against the entry's
14
- // lastModified — if the tag was invalidated AFTER the entry was written,
15
- // the entry is treated as stale (cacheState: "stale") rather than missing.
1
+ // Re-export the shared in-memory Next.js ISR cache handler from
2
+ // @solcreek/adapter-core. The implementation now lives there so both
3
+ // adapter-creek and adapter-creekd can share a single tested copy.
16
4
  //
17
- // This implements stale-while-revalidate semantics: Next.js receives the
18
- // old value plus the stale signal and decides to serve it while triggering
19
- // a background re-render. Aligned with opennextjs-cloudflare#1168.
20
- const tagInvalidatedAt = new Map();
21
- function isStaleByTags(entry) {
22
- for (const tag of entry.tags) {
23
- const invalidatedAt = tagInvalidatedAt.get(tag);
24
- if (invalidatedAt !== undefined && invalidatedAt > entry.lastModified) {
25
- return true;
26
- }
27
- }
28
- return false;
29
- }
30
- export default class CacheHandler {
31
- constructor(_ctx) {
32
- // Context includes serverDistDir, dev, etc.
33
- // Not needed for in-memory implementation.
34
- }
35
- async get(key, _ctx) {
36
- const entry = cache.get(key);
37
- if (!entry)
38
- return null;
39
- const age = (Date.now() - entry.lastModified) / 1000;
40
- // Stale if either (a) any of its tags was invalidated since write, or
41
- // (b) time-based revalidate has elapsed.
42
- const staleByTag = isStaleByTags(entry);
43
- const staleByTime = entry.revalidate !== undefined &&
44
- entry.revalidate !== false &&
45
- (entry.revalidate === 0 || age > entry.revalidate);
46
- if (staleByTag || staleByTime) {
47
- return {
48
- value: entry.value,
49
- lastModified: entry.lastModified,
50
- age: Math.floor(age),
51
- cacheState: "stale",
52
- };
53
- }
54
- return {
55
- value: entry.value,
56
- lastModified: entry.lastModified,
57
- age: Math.floor(age),
58
- cacheState: "fresh",
59
- };
60
- }
61
- async set(key, data, ctx) {
62
- if (data === null) {
63
- cache.delete(key);
64
- return;
65
- }
66
- const tags = ctx?.tags ?? [];
67
- const revalidate = typeof ctx?.revalidate === "number" ? ctx.revalidate : undefined;
68
- cache.set(key, {
69
- value: data,
70
- lastModified: Date.now(),
71
- tags,
72
- revalidate,
73
- });
74
- // Index by tags for revalidateTag()
75
- for (const tag of tags) {
76
- let keys = tagToKeys.get(tag);
77
- if (!keys) {
78
- keys = new Set();
79
- tagToKeys.set(tag, keys);
80
- }
81
- keys.add(key);
82
- }
83
- }
84
- async revalidateTag(tag) {
85
- const tags = Array.isArray(tag) ? tag : [tag];
86
- const now = Date.now();
87
- // Mark each tag as invalidated NOW. Existing entries become stale on
88
- // the next get(); fresh writes (lastModified > now) are unaffected.
89
- // We do NOT delete entries — Next.js wants to serve them as stale
90
- // while it re-renders in the background.
91
- for (const t of tags) {
92
- tagInvalidatedAt.set(t, now);
93
- }
94
- }
95
- resetRequestCache() {
96
- // No per-request cache to reset in this implementation.
97
- }
98
- }
99
- ;
5
+ // This file exists for backwards compatibility: users who set
6
+ // `cacheHandler: require.resolve("@solcreek/adapter-creek/cache-handler")`
7
+ // in their next.config continue to work without changes. New users
8
+ // can wire either path; both resolve to the same module at runtime.
9
+ export { default } from "@solcreek/adapter-core/cache-handler";
100
10
  //# sourceMappingURL=cache-handler.js.map
package/dist/index.js CHANGED
@@ -1,221 +1,33 @@
1
1
  import * as path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { existsSync } from "node:fs";
4
+ import { applyBaseModifyConfig } from "@solcreek/adapter-core";
4
5
  import { handleBuild } from "./build.js";
5
- /**
6
- * Detect the monorepo root by walking up looking for workspace markers.
7
- */
8
- function findRepoRoot(startDir) {
9
- let dir = startDir;
10
- while (true) {
11
- if (existsSync(path.join(dir, "pnpm-workspace.yaml")) ||
12
- existsSync(path.join(dir, "turbo.json"))) {
13
- return dir;
14
- }
15
- const pkgPath = path.join(dir, "package.json");
16
- if (existsSync(pkgPath)) {
17
- try {
18
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
- if (pkg.workspaces)
20
- return dir;
21
- }
22
- catch { }
23
- }
24
- const parent = path.dirname(dir);
25
- if (parent === dir)
26
- break;
27
- dir = parent;
28
- }
29
- return startDir;
30
- }
31
- /**
32
- * Detect direct dependencies whose `.js` entry files contain JSX.
33
- *
34
- * Turbopack at the pinned Next.js canary has a regression where it fails
35
- * to parse JSX inside `.js` files shipped by a workspace-linked / third-
36
- * party package (exit message: `Expected ';', got 'ident'`). The
37
- * documented fix is to add the package to `transpilePackages` so Next.js
38
- * runs SWC over it. Vanilla `next build` fails identically on these
39
- * fixtures without our adapter — but user apps don't get to edit their
40
- * own `next.config.js` just because we say so, so we auto-inject.
41
- *
42
- * Scope: only DIRECT deps. Transitive deps are either already transpiled
43
- * by their publisher (the common case) or reachable through the direct
44
- * dep we pick up. Walking all of node_modules would be slow and catch
45
- * unrelated packages.
46
- *
47
- * Detection: we resolve each dep's entry file (`package.json#exports["."]`
48
- * or `main`) and heuristically look for JSX with strong React hints.
49
- * False positives cost a little build time (Next.js transpiles an
50
- * already-ES-code package); false negatives are the status quo. The
51
- * heuristic is conservative — we require BOTH a JSX construct AND a
52
- * React signal ('use client' / react import / createElement) to claim
53
- * a package needs transpile.
54
- */
55
- function detectPackagesNeedingTranspile(projectDir) {
56
- let projectPkg;
57
- try {
58
- projectPkg = JSON.parse(readFileSync(path.join(projectDir, "package.json"), "utf-8"));
59
- }
60
- catch {
61
- return [];
62
- }
63
- const directDeps = new Set();
64
- for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
65
- const map = projectPkg[field];
66
- if (map && typeof map === "object") {
67
- for (const name of Object.keys(map))
68
- directDeps.add(name);
69
- }
70
- }
71
- if (directDeps.size === 0)
72
- return [];
73
- // Don't ever try to transpile Next.js itself or the React runtimes —
74
- // they're pre-bundled and transpilePackages on them would be an
75
- // expensive no-op at best, a breakage at worst.
76
- const SKIP = new Set([
77
- "next", "react", "react-dom", "react-server-dom-webpack",
78
- "react-dom/server", "scheduler", "@next/routing", "@next/swc",
79
- "@solcreek/adapter-creek",
80
- ]);
81
- const needsTranspile = [];
82
- for (const dep of directDeps) {
83
- if (SKIP.has(dep))
84
- continue;
85
- // Ignore subpath-qualified entries that aren't real packages (shouldn't
86
- // show up in dependencies, but be defensive).
87
- if (dep.includes("/") && !dep.startsWith("@"))
88
- continue;
89
- // Locate the package root via direct node_modules path. Can't use
90
- // `require.resolve(dep + '/package.json')` — Node's resolver honors
91
- // the `exports` field and most packages don't expose `./package.json`.
92
- // pnpm's flat node_modules layout hoists a symlink at
93
- // `node_modules/<dep>` for every direct + hoisted dep, so this works
94
- // across npm, yarn, pnpm.
95
- const pkgRoot = path.join(projectDir, "node_modules", dep);
96
- const pkgJsonPath = path.join(pkgRoot, "package.json");
97
- let pkgJson;
98
- try {
99
- pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
100
- }
101
- catch {
102
- continue;
103
- }
104
- // Skip packages that declare themselves ES (module field points at
105
- // .mjs) AND ship no .js siblings — transpile wouldn't kick in for
106
- // them anyway. We still process packages whose `main` is a .js.
107
- const entryCandidates = collectEntryFiles(pkgJson, pkgRoot);
108
- if (entryCandidates.length === 0)
109
- continue;
110
- for (const entry of entryCandidates) {
111
- try {
112
- const content = readFileSync(entry, "utf-8");
113
- if (looksLikeJsxInJs(content, entry)) {
114
- needsTranspile.push(dep);
115
- break; // one hit is enough; move to next package
116
- }
117
- }
118
- catch { }
119
- }
120
- }
121
- return needsTranspile;
122
- }
123
- /**
124
- * Pick the `.js` entry file(s) for a package. Only files that end in `.js`
125
- * (and not `.mjs` / `.cjs`) are eligible — the others are either module-
126
- * specific formats (where Turbopack's JSX-in-JS bug doesn't apply) or
127
- * already indicate transpilation happened upstream.
128
- */
129
- function collectEntryFiles(pkgJson, pkgRoot) {
130
- const candidates = [];
131
- const tryAdd = (rel) => {
132
- if (typeof rel !== "string")
133
- return;
134
- if (!rel.endsWith(".js"))
135
- return;
136
- const abs = path.join(pkgRoot, rel.startsWith("./") ? rel.slice(2) : rel);
137
- try {
138
- if (statSync(abs).isFile())
139
- candidates.push(abs);
140
- }
141
- catch { }
142
- };
143
- tryAdd(pkgJson.main);
144
- // `exports` can be a string, or a nested conditional object. We walk
145
- // the "." entry's import/require/default branches.
146
- const exports_ = pkgJson.exports;
147
- if (typeof exports_ === "string") {
148
- tryAdd(exports_);
149
- }
150
- else if (exports_ && typeof exports_ === "object") {
151
- const rootExport = exports_["."] ?? exports_;
152
- if (typeof rootExport === "string") {
153
- tryAdd(rootExport);
154
- }
155
- else if (rootExport && typeof rootExport === "object") {
156
- for (const cond of ["default", "import", "require", "node", "browser"]) {
157
- tryAdd(rootExport[cond]);
158
- }
159
- }
160
- }
161
- return [...new Set(candidates)];
162
- }
163
- /**
164
- * Heuristic: a `.js` file looks like it contains JSX if it has at least
165
- * one JSX-ish token AND at least one React signal. Both conditions keeps
166
- * false positives low (e.g. plain TS generics `function f<T>()` without
167
- * React imports won't trigger).
168
- */
169
- function looksLikeJsxInJs(content, filePath) {
170
- if (!filePath.endsWith(".js"))
171
- return false;
172
- const head = content.slice(0, 20_000); // cap scan cost
173
- const JSX_HINTS = [
174
- /return\s*\(\s*</, // return (<...
175
- /return\s+<[A-Za-z]/, // return <Tag or <Component
176
- /=>\s*<[A-Za-z]/, // arrow => <...
177
- /\bcreateElement\s*\(/, // raw createElement
178
- ];
179
- const hasJsxHint = JSX_HINTS.some((re) => re.test(head));
180
- if (!hasJsxHint)
181
- return false;
182
- const REACT_HINTS = [
183
- /['"]use client['"]/,
184
- /from\s+['"]react['"]/,
185
- /require\s*\(\s*['"]react['"]\s*\)/,
186
- /\bReact\.createElement\b/,
187
- ];
188
- return REACT_HINTS.some((re) => re.test(head));
189
- }
190
- const cacheHandlerPath = fileURLToPath(new URL("./cache-handler.js", import.meta.url));
6
+ // Dev-fallback path to the cache handler shipped by @solcreek/adapter-core.
7
+ // applyBaseModifyConfig prefers the node_modules-installed copy when one
8
+ // exists (the production path); this resolves the package's own bundled
9
+ // copy as a last resort for the rare case where the adapter is used
10
+ // without `npm install`ing it.
11
+ const coreEntryUrl = new URL("../node_modules/@solcreek/adapter-core/dist/cache-handler.js", import.meta.url);
12
+ const fallbackCacheHandlerPath = existsSync(fileURLToPath(coreEntryUrl))
13
+ ? fileURLToPath(coreEntryUrl)
14
+ : path.join(process.cwd(), "node_modules", "@solcreek", "adapter-core", "dist", "cache-handler.js");
191
15
  const adapter = {
192
16
  name: "adapter-creek",
193
- modifyConfig(config, { phase }) {
194
- if (phase !== "phase-production-build")
195
- return config;
196
- const projectDir = process.cwd();
197
- const repoRoot = findRepoRoot(projectDir);
198
- const isMonorepo = repoRoot !== projectDir;
199
- const installedCacheHandlerPath = path.join(projectDir, "node_modules", "@solcreek", "adapter-creek", "dist", "cache-handler.js");
200
- const resolvedCacheHandlerPath = existsSync(installedCacheHandlerPath)
201
- ? installedCacheHandlerPath
202
- : cacheHandlerPath;
203
- // Auto-add any direct dep that ships JSX in `.js` to transpilePackages.
204
- // Works around a Turbopack regression where JSX inside `.js` files from
205
- // a workspace-linked / third-party package fails to parse with
206
- // `Expected ';', got 'ident'`. The documented upstream fix is exactly
207
- // `transpilePackages: [pkg]`; doing this here means user apps don't
208
- // have to know about it.
209
- const detected = detectPackagesNeedingTranspile(projectDir);
210
- const existing = Array.isArray(config.transpilePackages) ? config.transpilePackages : [];
211
- const transpilePackages = detected.length > 0
212
- ? [...new Set([...existing, ...detected])]
213
- : existing;
214
- if (detected.length > 0) {
215
- console.log(` [Creek Adapter] auto-transpile: ${JSON.stringify(detected)} (JSX in .js entry)`);
216
- }
17
+ modifyConfig(config, ctx) {
18
+ // First apply the shared base — auto-transpile, monorepo tracing
19
+ // root, TS error suppression, cache handler wiring.
20
+ const baseConfig = applyBaseModifyConfig(config, ctx, {
21
+ logLabel: "Creek Adapter",
22
+ cacheHandlerPath: fallbackCacheHandlerPath,
23
+ });
24
+ // Then layer CF-Workers-specific knobs on top. These only apply at
25
+ // production build phase; applyBaseModifyConfig is a passthrough
26
+ // for other phases, so guarding here matches its behaviour.
27
+ if (ctx.phase !== "phase-production-build")
28
+ return baseConfig;
217
29
  return {
218
- ...config,
30
+ ...baseConfig,
219
31
  // Disable memory cache — CF Workers doesn't have persistent fs.
220
32
  // The runtime cache handler is inlined in the worker entry (CreekCacheHandler).
221
33
  cacheMaxMemorySize: 0,
@@ -225,18 +37,9 @@ const adapter = {
225
37
  // → safely under limit. Real PPR fallback shells are typically ≤ a few
226
38
  // KB anyway, so this cap is purely defensive.
227
39
  experimental: {
228
- ...(config.experimental ?? {}),
40
+ ...(baseConfig.experimental ?? {}),
229
41
  maxPostponedStateSize: "20mb",
230
42
  },
231
- // Skip TypeScript type checking during build — CF Workers adapter
232
- // builds run `next build` where TS errors block the build. Type
233
- // checking should happen before deployment, not during bundling.
234
- typescript: { ...config.typescript, ignoreBuildErrors: true },
235
- ...(transpilePackages.length > 0 && { transpilePackages }),
236
- // Monorepo: set tracing root so Next.js traces deps from repo root
237
- ...(isMonorepo && {
238
- outputFileTracingRoot: repoRoot,
239
- }),
240
43
  };
241
44
  },
242
45
  async onBuildComplete(ctx) {
@@ -333,8 +333,17 @@ if (typeof process !== "undefined") {
333
333
  }, { bigint: () => BigInt(Math.floor(performance.now() * 1e6)) });
334
334
  }
335
335
  if (!process.on) process.on = () => process;
336
- if (!process.removeListener) process.removeListener = () => process;
337
336
  if (!process.off) process.off = () => process;
337
+ if (!process.removeListener) process.removeListener = () => process;
338
+ if (!process.removeAllListeners) process.removeAllListeners = () => process;
339
+ if (!process.listeners) process.listeners = () => [];
340
+ if (!process.listenerCount) process.listenerCount = () => 0;
341
+ if (!process.rawListeners) process.rawListeners = () => [];
342
+ if (!process.emit) process.emit = () => false;
343
+ if (!process.prependListener) process.prependListener = () => process;
344
+ if (!process.prependOnceListener) process.prependOnceListener = () => process;
345
+ if (!process.once) process.once = () => process;
346
+ if (!process.addListener) process.addListener = () => process;
338
347
  }
339
348
 
340
349
  import { resolveRoutes, responseToMiddlewareResult } from "@next/routing";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/adapter-creek",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Next.js deployment adapter for Creek (Cloudflare Workers)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@next/routing": "16.2.3",
48
- "@node-rs/xxhash": "^1.7.6",
48
+ "@solcreek/adapter-core": "^0.1.0",
49
49
  "sql.js": "^1.14.1",
50
50
  "wrangler": "^4.82.2"
51
51
  },
@@ -53,6 +53,7 @@
53
53
  "next": ">=16.2.3"
54
54
  },
55
55
  "devDependencies": {
56
+ "@node-rs/xxhash": "^1.7.6",
56
57
  "@types/node": "^22.13.10",
57
58
  "miniflare": "^4.20260410.0",
58
59
  "next": "16.2.3",
@@ -0,0 +1,44 @@
1
+ // Minimal node:net shim for CF Workers.
2
+ // Only Socket is needed — Next.js passes it to the http shim's
3
+ // IncomingMessage as `socket` but never actually connects.
4
+
5
+ import { EventEmitter } from "node:events";
6
+
7
+ export class Socket extends EventEmitter {
8
+ constructor() {
9
+ super();
10
+ this.writable = true;
11
+ this.readable = true;
12
+ this.encrypted = false;
13
+ this.remoteAddress = "127.0.0.1";
14
+ this.remotePort = 0;
15
+ this.localAddress = "127.0.0.1";
16
+ this.localPort = 0;
17
+ }
18
+ address() { return { address: "127.0.0.1", family: "IPv4", port: 443 }; }
19
+ connect() { return this; }
20
+ write() { return true; }
21
+ end() { this.emit("close"); return this; }
22
+ destroy() { this.emit("close"); return this; }
23
+ setTimeout() { return this; }
24
+ setNoDelay() { return this; }
25
+ setKeepAlive() { return this; }
26
+ ref() { return this; }
27
+ unref() { return this; }
28
+ }
29
+
30
+ export class Server extends EventEmitter {
31
+ constructor() { super(); }
32
+ listen() { return this; }
33
+ close() { return this; }
34
+ address() { return null; }
35
+ }
36
+
37
+ export function createServer() { return new Server(); }
38
+ export function createConnection() { return new Socket(); }
39
+ export function connect() { return new Socket(); }
40
+ export function isIP() { return 0; }
41
+ export function isIPv4() { return false; }
42
+ export function isIPv6() { return false; }
43
+
44
+ export default { Socket, Server, createServer, createConnection, connect, isIP, isIPv4, isIPv6 };