@runlot/next 0.1.0-rc.11

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,86 @@
1
+ # @runlot/next
2
+
3
+ Next.js 어댑터. 설계는 `docs/nextjs.md` 에 있고, 이 문서는 표면만 적는다.
4
+
5
+ > **npm 이름은 `runlot/next` 다.** 워크스페이스 안에서만 `@runlot/next` 이고,
6
+ > 랜딩의 코드 예시와 템플릿이 쓰는 지정자는 `runlot/next` 다 (`import { env }
7
+ > from "runlot/next"`). 게시할 때 이름을 바꾸는 자리가 여기 하나뿐이도록,
8
+ > 이 패키지 안에서는 자기 이름을 문자열로 쓰지 않는다 — 예외는 빌드가 만드는
9
+ > `.open-next/runlot-entry.js` 의 `@runlot/next/entry` 한 줄이다.
10
+
11
+ ## 표면
12
+
13
+ ```ts
14
+ import { env, request, waitUntil } from "runlot/next";
15
+
16
+ const user = await env.auth.user(); // 요청은 어댑터가 숨긴다
17
+ const rows = await env.db.exec("select 1");
18
+ const url = await env.storage.presign("covers/a.png");
19
+ const token = env.MY_SECRET; // 나머지는 문자열이다
20
+ ```
21
+
22
+ | 이름 | 무엇 |
23
+ |---|---|
24
+ | `env` | 이 요청의 바인딩. **요청 밖에서 읽으면 던진다** |
25
+ | `env.auth.user(request?)` | 인자가 없으면 이 요청. `signOut` 도 같다 |
26
+ | `request()` | 이 요청의 `Request` |
27
+ | `waitUntil(p)` | 응답 뒤에도 끝까지 도는 일 |
28
+ | `inRequest()` | 지금 요청 안인가. 라이브러리가 갈래를 나눌 때만 |
29
+ | `wrap(worker)` (`runlot/next/entry`) | 빌드가 세우는 진입 wrapper |
30
+ | `buildNext(opts)` (`runlot/next/build`) | `runlot deploy` 가 부른다 |
31
+
32
+ `env` 가 요청 밖에서 던지는 것은 설계다. 모듈 최상단과 빌드 시점 정적 렌더가
33
+ 그 자리이고, 거기서 조용히 빈 값을 주면 **모든 페이지가 로그아웃 상태로**
34
+ 보인다 (`docs/auth.md §4` 의 같은 문장).
35
+
36
+ ## 빌드
37
+
38
+ `runlot deploy` 가 `runlot.json` 의 `"framework": "next"` 를 보고 `buildNext` 를
39
+ 부른다. 그 함수가 하는 일:
40
+
41
+ 1. `next build` — 사용자의 `package.json` scripts 가 아니라 그 프로젝트의 next
42
+ 바이너리를 직접, standalone 으로.
43
+ 2. OpenNext 의 빌드 함수를 **직접** 부른다 (CLI 도 wrangler 도 거치지 않는다).
44
+ 설정은 `.runlot-next/open-next.config.ts` 에 우리가 쓴다 — 프로젝트 루트에
45
+ `open-next.config.ts` 가 있으면 그쪽이 이긴다.
46
+ 3. `.open-next/runlot-entry.js` 를 세운다. `worker.js` 의 기본 export 를 `wrap`
47
+ 으로 감싸고 나머지 export 는 그대로 통과시킨다.
48
+
49
+ 오버라이드 셋 (`docs/nextjs.md §2` 의 표):
50
+
51
+ | 자리 | 우리 값 | 없을 때 |
52
+ |---|---|---|
53
+ | incrementalCache | `env.storage`, 키 `__next/cache/<buildId>/<key>.<type>` | 정적 자산 캐시(읽기 전용) + 한 줄 |
54
+ | tagCache | `env.db`, 표 `runlot_next.tags(tag, path, revalidated_at)` | no-op + 한 줄 |
55
+ | queue | OpenNext 의 `memory-queue` (`WORKER_SELF_REFERENCE` 를 쓴다) | — |
56
+ | enableCacheInterception | `true` | — |
57
+
58
+ ## wrangler 는 의존이 아니다
59
+
60
+ `@opennextjs/cloudflare` 의 `peerDependencies` 에 `wrangler` 가 있지만, 런타임에
61
+ 그것을 import 하는 것은 `dist/cli/commands/*` 뿐이다. 우리가 부르는
62
+ `dist/cli/build/build.js` 는 타입으로만 참조한다. 그래서 이 패키지는 wrangler 를
63
+ `dependencies` 에 두지 않는다 — pnpm 이 peer 를 자동 설치하는 워크스페이스에서는
64
+ 스토어에 내려와 있을 수 있지만, 우리 코드 경로는 그것을 부르지 않는다.
65
+
66
+ ## 저장소는 globalThis 의 심볼이다
67
+
68
+ `Symbol.for("runlot.next.store")`. 모듈 인스턴스가 하나라는 보장이 없어서다:
69
+ OpenNext 의 `worker.js` 는 이미 묶인 번들이라 그 안에 이 패키지의 사본이 있고,
70
+ 그 곁에 세우는 `runlot-entry.js` 를 다시 묶으면 사본이 하나 더 생긴다. 모듈
71
+ 지역 변수에 두면 wrapper 가 넣은 값을 페이지가 못 본다. OpenNext 자신도 같은
72
+ 이유로 `Symbol.for("__cloudflare-context__")` 를 쓴다.
73
+
74
+ 캐시 오버라이드는 그 저장소가 아니라 OpenNext 의 문맥을 읽는다 — 오버라이드는
75
+ 미들웨어 번들에도 들어가고, 그 번들은 `node:crypto` 하나만 external 이라
76
+ `node:async_hooks` 를 끌고 오면 빌드가 깨진다.
77
+
78
+ ## 시험
79
+
80
+ ```
81
+ node build.mjs
82
+ ```
83
+
84
+ `env` 프록시(저장소 밖 오류·인자 없는 `user()`·동시 요청 격리)와 캐시 오버라이드
85
+ 둘을 가짜 바인딩으로 잰다. 실물 `next build` 는 여기서 돌지 않는다 —
86
+ `docs/nextjs.md §6` 의 `TestLiveNext` 자리다.
@@ -0,0 +1,38 @@
1
+ /** 생성한 OpenNext 설정이 사는 곳. `.open-next` 는 빌드가 매번 지우므로 밖에 둔다. */
2
+ export declare const WORK_DIR = ".runlot-next";
3
+ export declare const ENTRY_NAME = "runlot-entry.js";
4
+ export interface BuildNextOptions {
5
+ /** Next 프로젝트 루트 (`runlot.json` 이 있는 곳). */
6
+ dir: string;
7
+ /** `runlot.json` 의 `compatibilityDate`. OpenNext 는 경고 한 줄에만 쓴다. */
8
+ compatibilityDate?: string;
9
+ log?: (line: string) => void;
10
+ }
11
+ export interface BuildNextResult {
12
+ /** 우리 번들러에게 줄 진입점 (절대 경로). */
13
+ worker: string;
14
+ /** 정적 자산 디렉토리 (절대 경로). */
15
+ assets: string;
16
+ /** OpenNext 출력 디렉토리 (절대 경로). 번들러의 `absWorkingDir` 이다. */
17
+ outputDir: string;
18
+ }
19
+ /**
20
+ * 우리가 세우는 진입점.
21
+ *
22
+ * OpenNext 의 `worker.js` 기본 export 를 감싸고 나머지 export(Durable Object
23
+ * 클래스 등)는 그대로 통과시킨다. `export *` 는 default 를 다시 내보내지 않으므로
24
+ * 두 줄이 겹치지 않는다.
25
+ */
26
+ export declare const ENTRY_SOURCE = "// runlot \uC774 \uB9CC\uB4E0 \uD30C\uC77C\uC785\uB2C8\uB2E4. \uACE0\uCCD0\uB3C4 \uB2E4\uC74C \uBC30\uD3EC\uC5D0\uC11C \uB2E4\uC2DC \uC4F0\uC785\uB2C8\uB2E4.\nimport worker from \"./worker.js\";\nimport { wrap } from \"@runlot/next/entry\";\n\nexport * from \"./worker.js\";\nexport default wrap(worker);\n";
27
+ /** 사용자가 자기 것을 두지 않았을 때 우리가 쓰는 OpenNext 설정 (docs/nextjs.md §2 의 표). */
28
+ export declare const OPEN_NEXT_CONFIG_SOURCE = "// runlot \uC774 \uB9CC\uB4E0 \uD30C\uC77C\uC785\uB2C8\uB2E4. \uACE0\uCCD0\uB3C4 \uB2E4\uC74C \uBC30\uD3EC\uC5D0\uC11C \uB2E4\uC2DC \uC4F0\uC785\uB2C8\uB2E4.\n// \uC9C1\uC811 \uB2E4\uB8E8\uB824\uBA74 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 open-next.config.ts \uB97C \uB450\uC138\uC694 \u2014 \uC788\uC73C\uBA74 \uADF8\uCABD\uC744 \uC501\uB2C8\uB2E4.\nimport { defineCloudflareConfig } from \"@opennextjs/cloudflare\";\nimport queue from \"@opennextjs/cloudflare/overrides/queue/memory-queue\";\nimport incrementalCache from \"@runlot/next/opennext/incremental-cache\";\nimport tagCache from \"@runlot/next/opennext/tag-cache\";\n\nexport default defineCloudflareConfig({\n incrementalCache,\n tagCache,\n queue,\n enableCacheInterception: true,\n});\n";
29
+ /** 프로젝트가 자기 OpenNext 설정을 두었으면 그것을, 아니면 우리 것을 만들어 그 경로를 준다. */
30
+ export declare function openNextConfigPath(dir: string, log: (line: string) => void): string;
31
+ /**
32
+ * `next build` → OpenNext → 우리 진입점.
33
+ *
34
+ * `process.cwd()` 를 프로젝트로 옮긴다: OpenNext 의 `normalizeOptions` 가 앱 경로와
35
+ * 출력 경로를 cwd 에서 만든다. 끝나면 되돌린다.
36
+ */
37
+ export declare function buildNext(opts: BuildNextOptions): Promise<BuildNextResult>;
38
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAcA,+DAA+D;AAC/D,eAAO,MAAM,QAAQ,iBAAiB,CAAC;AACvC,eAAO,MAAM,UAAU,oBAAoB,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,uTAMxB,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,6vBAanC,CAAC;AAIF,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAanF;AAUD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2EhF"}
package/dist/build.js ADDED
@@ -0,0 +1,130 @@
1
+ // `@runlot/next/build` — `runlot deploy` 가 Next 프로젝트에서 부르는 한 함수
2
+ // (docs/nextjs.md §2).
3
+ //
4
+ // OpenNext 의 **CLI 가 아니라 빌드 함수를 직접** 부른다. CLI 는 yargs 와
5
+ // wrangler 의 `unstable_readConfig` 를 지고 있고, 그 설정에서 실제로 읽는 값은
6
+ // `compatibility_date` 와 `assets.run_worker_first` 둘뿐이다. 우리는 그 둘을
7
+ // `runlot.json` 에서 준다 — `wrangler.toml` 은 생기지 않는다.
8
+ //
9
+ // 이 파일은 node 에서 돈다(워커가 아니다). 그래서 `node:fs` 를 그냥 쓴다.
10
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
11
+ import { createRequire } from "node:module";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { pathToFileURL } from "node:url";
14
+ /** 생성한 OpenNext 설정이 사는 곳. `.open-next` 는 빌드가 매번 지우므로 밖에 둔다. */
15
+ export const WORK_DIR = ".runlot-next";
16
+ export const ENTRY_NAME = "runlot-entry.js";
17
+ /**
18
+ * 우리가 세우는 진입점.
19
+ *
20
+ * OpenNext 의 `worker.js` 기본 export 를 감싸고 나머지 export(Durable Object
21
+ * 클래스 등)는 그대로 통과시킨다. `export *` 는 default 를 다시 내보내지 않으므로
22
+ * 두 줄이 겹치지 않는다.
23
+ */
24
+ export const ENTRY_SOURCE = `// runlot 이 만든 파일입니다. 고쳐도 다음 배포에서 다시 쓰입니다.
25
+ import worker from "./worker.js";
26
+ import { wrap } from "@runlot/next/entry";
27
+
28
+ export * from "./worker.js";
29
+ export default wrap(worker);
30
+ `;
31
+ /** 사용자가 자기 것을 두지 않았을 때 우리가 쓰는 OpenNext 설정 (docs/nextjs.md §2 의 표). */
32
+ export const OPEN_NEXT_CONFIG_SOURCE = `// runlot 이 만든 파일입니다. 고쳐도 다음 배포에서 다시 쓰입니다.
33
+ // 직접 다루려면 프로젝트 루트에 open-next.config.ts 를 두세요 — 있으면 그쪽을 씁니다.
34
+ import { defineCloudflareConfig } from "@opennextjs/cloudflare";
35
+ import queue from "@opennextjs/cloudflare/overrides/queue/memory-queue";
36
+ import incrementalCache from "@runlot/next/opennext/incremental-cache";
37
+ import tagCache from "@runlot/next/opennext/tag-cache";
38
+
39
+ export default defineCloudflareConfig({
40
+ incrementalCache,
41
+ tagCache,
42
+ queue,
43
+ enableCacheInterception: true,
44
+ });
45
+ `;
46
+ const USER_CONFIG_NAMES = ["open-next.config.ts", "open-next.config.mjs", "open-next.config.js"];
47
+ /** 프로젝트가 자기 OpenNext 설정을 두었으면 그것을, 아니면 우리 것을 만들어 그 경로를 준다. */
48
+ export function openNextConfigPath(dir, log) {
49
+ for (const name of USER_CONFIG_NAMES) {
50
+ const path = join(dir, name);
51
+ if (existsSync(path)) {
52
+ log(`${name} 을 찾았습니다 — 그 설정으로 빌드합니다.`);
53
+ return path;
54
+ }
55
+ }
56
+ const workDir = join(dir, WORK_DIR);
57
+ mkdirSync(workDir, { recursive: true });
58
+ const path = join(workDir, "open-next.config.ts");
59
+ writeFileSync(path, OPEN_NEXT_CONFIG_SOURCE);
60
+ return path;
61
+ }
62
+ async function load(path) {
63
+ return (await import(pathToFileURL(path).href));
64
+ }
65
+ /**
66
+ * `next build` → OpenNext → 우리 진입점.
67
+ *
68
+ * `process.cwd()` 를 프로젝트로 옮긴다: OpenNext 의 `normalizeOptions` 가 앱 경로와
69
+ * 출력 경로를 cwd 에서 만든다. 끝나면 되돌린다.
70
+ */
71
+ export async function buildNext(opts) {
72
+ const dir = resolve(opts.dir);
73
+ const log = opts.log ?? ((line) => console.log(line));
74
+ const here = createRequire(import.meta.url);
75
+ let apiIndex;
76
+ try {
77
+ apiIndex = here.resolve("@opennextjs/cloudflare");
78
+ }
79
+ catch {
80
+ throw new Error("@opennextjs/cloudflare 를 찾지 못했습니다. @runlot/next 를 다시 설치하세요.");
81
+ }
82
+ // 패키지의 exports 는 `dist/api/*` 만 연다. 빌드 함수는 `dist/cli/` 아래라
83
+ // 지정자로는 못 부르고, 해석된 경로에서 옆으로 간다.
84
+ const distDir = resolve(dirname(apiIndex), "..");
85
+ const fromCloudflare = createRequire(apiIndex);
86
+ const awsIndex = fromCloudflare.resolve("@opennextjs/aws/index.js");
87
+ const { build: openNextBuild } = await load(join(distDir, "cli", "build", "build.js"));
88
+ const { ensureCloudflareConfig } = await load(join(distDir, "cli", "build", "utils", "ensure-cf-config.js"));
89
+ const { compileOpenNextConfig } = await load(fromCloudflare.resolve("@opennextjs/aws/build/compileConfig.js"));
90
+ const { normalizeOptions } = await load(fromCloudflare.resolve("@opennextjs/aws/build/helper.js"));
91
+ // Next 자체는 사용자 프로젝트의 것이다. 우리 것을 쓰면 버전이 갈라진다.
92
+ const fromProject = createRequire(join(dir, "package.json"));
93
+ let nextBin;
94
+ try {
95
+ nextBin = fromProject.resolve("next/dist/bin/next");
96
+ }
97
+ catch {
98
+ throw new Error(`${dir} 에서 next 를 찾지 못했습니다. \`npm i next\` 뒤에 다시 배포하세요.`);
99
+ }
100
+ const configPath = openNextConfigPath(dir, log);
101
+ const previousCwd = process.cwd();
102
+ process.chdir(dir);
103
+ try {
104
+ const { config, buildDir } = await compileOpenNextConfig(configPath, { compileEdge: true });
105
+ ensureCloudflareConfig(config);
106
+ // 사용자의 `package.json` scripts 가 아니라 next 바이너리를 직접 부른다.
107
+ // `npm run build`가 배포·린트·타입검사를 겸하는 프로젝트가 흔한데, 배포가
108
+ // 그것들을 다시 도는 것은 우리가 약속한 일이 아니다.
109
+ config.buildCommand = [JSON.stringify(process.execPath), JSON.stringify(nextBin), "build"].join(" ");
110
+ const options = normalizeOptions(config, dirname(awsIndex), buildDir);
111
+ log("Next.js 빌드 중…");
112
+ await openNextBuild(options, config, { sourceDir: dir, skipNextBuild: false, skipWranglerConfigCheck: true, minify: true }, {
113
+ ...(opts.compatibilityDate !== undefined
114
+ ? { compatibility_date: opts.compatibilityDate }
115
+ : {}),
116
+ assets: { run_worker_first: false },
117
+ }, false);
118
+ const outputDir = join(dir, ".open-next");
119
+ const worker = join(outputDir, ENTRY_NAME);
120
+ if (!existsSync(join(outputDir, "worker.js"))) {
121
+ throw new Error(`${outputDir}/worker.js 가 없습니다. OpenNext 빌드가 끝나지 않았습니다.`);
122
+ }
123
+ writeFileSync(worker, ENTRY_SOURCE);
124
+ return { worker, assets: join(outputDir, "assets"), outputDir };
125
+ }
126
+ finally {
127
+ process.chdir(previousCwd);
128
+ }
129
+ }
130
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type RunlotContext, type RunlotEnv } from "./store.js";
2
+ export { als } from "./store.js";
3
+ export type { RunlotBindings, RunlotContext, RunlotEnv, RunlotStore } from "./store.js";
4
+ export interface FetchHandler {
5
+ fetch(request: Request, env: RunlotEnv, ctx: RunlotContext): Response | Promise<Response>;
6
+ }
7
+ /**
8
+ * OpenNext 의 워커를 감싼다. `fetch` 밖의 export(Durable Object 클래스 등)는
9
+ * 이 함수가 아니라 생성된 `runlot-entry.js` 가 그대로 다시 내보낸다 — 감쌀 것이
10
+ * 요청 경로뿐이라서다.
11
+ */
12
+ export declare function wrap(handler: FetchHandler): FetchHandler;
13
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAYA,OAAO,EAAO,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC3F;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAMxD"}
package/dist/entry.js ADDED
@@ -0,0 +1,27 @@
1
+ // 진입 wrapper (docs/nextjs.md §4).
2
+ //
3
+ // 빌드가 `.open-next/runlot-entry.js` 를 내고, 그 파일이 OpenNext 의 `worker.js`
4
+ // 기본 export 를 이것으로 감싼다:
5
+ //
6
+ // import worker from "./worker.js";
7
+ // import { wrap } from "@runlot/next/entry";
8
+ // export default wrap(worker);
9
+ //
10
+ // 하는 일은 하나다 — 요청 하나의 `(request, env, ctx)` 를 저장소에 넣고 그 안에서
11
+ // OpenNext 의 fetch 를 부른다. 그래야 서버 컴포넌트 안의 `env.auth.user()` 가
12
+ // 인자 없이 자기 요청을 찾는다.
13
+ import { als } from "./store.js";
14
+ export { als } from "./store.js";
15
+ /**
16
+ * OpenNext 의 워커를 감싼다. `fetch` 밖의 export(Durable Object 클래스 등)는
17
+ * 이 함수가 아니라 생성된 `runlot-entry.js` 가 그대로 다시 내보낸다 — 감쌀 것이
18
+ * 요청 경로뿐이라서다.
19
+ */
20
+ export function wrap(handler) {
21
+ return {
22
+ fetch(request, env, ctx) {
23
+ return als.run({ request, env, ctx }, () => handler.fetch(request, env, ctx));
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,sEAAsE;AACtE,wBAAwB;AACxB,EAAE;AACF,sCAAsC;AACtC,+CAA+C;AAC/C,iCAAiC;AACjC,EAAE;AACF,2DAA2D;AAC3D,6DAA6D;AAC7D,oBAAoB;AACpB,OAAO,EAAE,GAAG,EAAsC,MAAM,YAAY,CAAC;AAErE,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAOjC;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,OAAqB;IACxC,OAAO;QACL,KAAK,CAAC,OAAgB,EAAE,GAAc,EAAE,GAAkB;YACxD,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAChF,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type RunlotEnv } from "./store.js";
2
+ export { als, OUTSIDE_REQUEST } from "./store.js";
3
+ export type { RunlotAuth, RunlotBindings, RunlotContext, RunlotEnv, RunlotStore, } from "./store.js";
4
+ export type { User, SignOutHeaders } from "@runlot/auth";
5
+ export type { Db, DbSession, Storage, StorageObject } from "@runlot/pg";
6
+ /**
7
+ * 이 요청의 바인딩.
8
+ *
9
+ * 이름은 `env` 하나지만 읽는 시점마다 그 요청의 것이다. 요청 밖에서 읽으면
10
+ * `Error` 를 던진다 (`OUTSIDE_REQUEST`).
11
+ */
12
+ export declare const env: RunlotEnv;
13
+ /** 이 요청의 `Request`. 헤더·URL 을 직접 봐야 할 때. */
14
+ export declare function request(): Request;
15
+ /**
16
+ * 응답을 보낸 뒤에도 끝까지 도는 일. 로그 전송·캐시 채우기가 그 자리다.
17
+ *
18
+ * `ctx.waitUntil` 이 없는 배치(시험용 가짜 문맥 등)에서는 아무것도 하지 않고
19
+ * 프로미스를 그대로 돌려준다 — 떠 있는 거절이 프로세스를 죽이지 않게 삼킨다.
20
+ */
21
+ export declare function waitUntil(promise: Promise<unknown>): void;
22
+ /** 지금 요청 안인가. 라이브러리가 갈래를 나눌 때만 — 앱 코드는 그냥 `env` 를 읽는다. */
23
+ export declare function inRequest(): boolean;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAmBxE;;;;;GAKG;AACH,eAAO,MAAM,GAAG,EAAE,SA8BhB,CAAC;AAEH,2CAA2C;AAC3C,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAOzD;AAED,0DAA0D;AAC1D,wBAAgB,SAAS,IAAI,OAAO,CAEnC"}
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ // `runlot/next` — Next.js 안에서 보는 바인딩 (docs/nextjs.md §4).
2
+ //
3
+ // import { env } from "runlot/next";
4
+ // const user = await env.auth.user(); // 요청은 어댑터가 숨긴다
5
+ // const rows = await env.db.exec("select 1");
6
+ //
7
+ // `env` 는 값이 아니라 Proxy 다. 요청마다 다른 묶음을 모듈 하나의 이름으로
8
+ // 부르는 유일한 길이고, 그래서 **요청 밖에서 읽으면 던진다** — 모듈 최상단과
9
+ // 빌드 시점 정적 렌더가 그 자리다.
10
+ //
11
+ // 패키지 이름: 워크스페이스 안에서는 `@runlot/next`, npm 에서는 `runlot/next`.
12
+ import { als, store } from "./store.js";
13
+ export { als, OUTSIDE_REQUEST } from "./store.js";
14
+ /**
15
+ * `env.auth` 한 겹. `user()`·`signOut()` 에 인자가 없으면 저장소의 요청을 넣고,
16
+ * 나머지(`users`·`sessions`·평평한 별칭)는 그대로 넘긴다.
17
+ */
18
+ function wrapAuth(auth, request) {
19
+ return new Proxy(auth, {
20
+ get(target, prop, receiver) {
21
+ const value = Reflect.get(target, prop, receiver);
22
+ if (typeof value !== "function")
23
+ return value;
24
+ if (prop === "user" || prop === "signOut") {
25
+ return (given) => value.call(target, given ?? request);
26
+ }
27
+ return value.bind(target);
28
+ },
29
+ });
30
+ }
31
+ /**
32
+ * 이 요청의 바인딩.
33
+ *
34
+ * 이름은 `env` 하나지만 읽는 시점마다 그 요청의 것이다. 요청 밖에서 읽으면
35
+ * `Error` 를 던진다 (`OUTSIDE_REQUEST`).
36
+ */
37
+ export const env = new Proxy({}, {
38
+ get(_target, prop) {
39
+ const s = store();
40
+ if (prop === "auth") {
41
+ const auth = s.env["auth"];
42
+ // 바인딩이 없는 프로젝트(`runlot add auth` 전)는 undefined 다. 감싸면
43
+ // "user 가 없다"는 TypeError 가 우리 프록시 안에서 나므로, 그 자리에서
44
+ // 무엇이 빠졌는지 말한다.
45
+ if (auth === undefined || auth === null) {
46
+ throw new Error("env.auth 바인딩이 없습니다. `runlot add auth` 뒤에 다시 배포하세요.");
47
+ }
48
+ return wrapAuth(auth, s.request);
49
+ }
50
+ return Reflect.get(s.env, prop);
51
+ },
52
+ has(_target, prop) {
53
+ return Reflect.has(store().env, prop);
54
+ },
55
+ ownKeys() {
56
+ return Reflect.ownKeys(store().env);
57
+ },
58
+ getOwnPropertyDescriptor(_target, prop) {
59
+ const d = Reflect.getOwnPropertyDescriptor(store().env, prop);
60
+ // 대상은 빈 객체다. `configurable: false` 를 그대로 돌려주면 프록시 불변식
61
+ // 위반으로 `Object.keys(env)` 가 TypeError 를 낸다.
62
+ return d === undefined ? undefined : { ...d, configurable: true };
63
+ },
64
+ set() {
65
+ throw new Error("env 는 읽기 전용입니다. 값을 바꾸려면 시크릿이나 바인딩을 고치세요.");
66
+ },
67
+ });
68
+ /** 이 요청의 `Request`. 헤더·URL 을 직접 봐야 할 때. */
69
+ export function request() {
70
+ return store().request;
71
+ }
72
+ /**
73
+ * 응답을 보낸 뒤에도 끝까지 도는 일. 로그 전송·캐시 채우기가 그 자리다.
74
+ *
75
+ * `ctx.waitUntil` 이 없는 배치(시험용 가짜 문맥 등)에서는 아무것도 하지 않고
76
+ * 프로미스를 그대로 돌려준다 — 떠 있는 거절이 프로세스를 죽이지 않게 삼킨다.
77
+ */
78
+ export function waitUntil(promise) {
79
+ const { ctx } = store();
80
+ if (typeof ctx?.waitUntil === "function") {
81
+ ctx.waitUntil(promise);
82
+ return;
83
+ }
84
+ void promise.catch(() => { });
85
+ }
86
+ /** 지금 요청 안인가. 라이브러리가 갈래를 나눌 때만 — 앱 코드는 그냥 `env` 를 읽는다. */
87
+ export function inRequest() {
88
+ return als.getStore() !== undefined;
89
+ }
90
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** 이 요청의 env. 어느 저장소에도 없으면 빈 묶음 — 부르는 쪽이 바인딩 없음으로 읽는다. */
2
+ export declare function bindings(): Record<string, unknown>;
3
+ /** OpenNext 가 심는 빌드 id. 배포가 바뀌면 캐시 키가 갈라진다. */
4
+ export declare function buildId(): string;
5
+ /** 배포마다 한 번만 찍는 안내. 요청마다 같은 줄을 쌓지 않는다. */
6
+ export declare function sayOnce(key: string, line: string): void;
7
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/opennext/context.ts"],"names":[],"mappings":"AAkBA,0DAA0D;AAC1D,wBAAgB,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAElD;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,IAAI,MAAM,CAKhC;AAID,0CAA0C;AAC1C,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIvD"}
@@ -0,0 +1,30 @@
1
+ // 캐시 오버라이드가 바인딩을 찾는 자리.
2
+ //
3
+ // 우리 저장소(`../store.js`)가 아니라 OpenNext 자신의 요청 문맥
4
+ // (`Symbol.for("__cloudflare-context__")`)을 읽는다. 이유는 둘이다:
5
+ //
6
+ // 1. 오버라이드는 미들웨어 번들에도 들어간다. 그 번들은 esbuild 의 `browser`
7
+ // 플랫폼으로 묶이고 external 은 `node:crypto` 하나라, `node:async_hooks`
8
+ // 를 끌고 오는 우리 저장소를 import 하면 **빌드가 깨진다** (실측 아님 —
9
+ // compileOpenNextConfigEdge 의 external 목록에서 읽었다).
10
+ // 2. 캐시 오버라이드가 도는 자리는 언제나 OpenNext 의 요청 문맥 안이다.
11
+ // 그 문맥에는 우리 바인딩이 그대로 들어 있다 — 같은 `env` 다.
12
+ const CF_CONTEXT = Symbol.for("__cloudflare-context__");
13
+ /** 이 요청의 env. 어느 저장소에도 없으면 빈 묶음 — 부르는 쪽이 바인딩 없음으로 읽는다. */
14
+ export function bindings() {
15
+ return globalThis[CF_CONTEXT]?.env ?? {};
16
+ }
17
+ /** OpenNext 가 심는 빌드 id. 배포가 바뀌면 캐시 키가 갈라진다. */
18
+ export function buildId() {
19
+ const id = globalThis.process?.env?.["OPEN_NEXT_BUILD_ID"];
20
+ return typeof id === "string" && id !== "" ? id : "no-build-id";
21
+ }
22
+ const said = new Set();
23
+ /** 배포마다 한 번만 찍는 안내. 요청마다 같은 줄을 쌓지 않는다. */
24
+ export function sayOnce(key, line) {
25
+ if (said.has(key))
26
+ return;
27
+ said.add(key);
28
+ console.warn(line);
29
+ }
30
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/opennext/context.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,EAAE;AACF,gDAAgD;AAChD,0DAA0D;AAC1D,EAAE;AACF,wDAAwD;AACxD,kEAAkE;AAClE,sDAAsD;AACtD,uDAAuD;AACvD,kDAAkD;AAClD,8CAA8C;AAE9C,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;AAMxD,0DAA0D;AAC1D,MAAM,UAAU,QAAQ;IACtB,OAAQ,UAAqB,CAAC,UAAU,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC;AACvD,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,OAAO;IACrB,MAAM,EAAE,GAAa,UAAyE,CAAC,OAAO,EAAE,GAAG,EAAE,CAC3G,oBAAoB,CACrB,CAAC;IACF,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;AAClE,CAAC;AAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;AAE/B,0CAA0C;AAC1C,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,IAAY;IAC/C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IAC1B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { CacheEntryType, CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
2
+ export declare const NAME = "runlot-storage";
3
+ /** `__next/cache/<buildId>/<key>.<type>`. 앞의 `/` 와 겹친 `/` 는 지운다. */
4
+ export declare function cacheKey(key: string, cacheType?: CacheEntryType, id?: string): string;
5
+ declare class RunlotStorageIncrementalCache implements IncrementalCache {
6
+ name: string;
7
+ get<CacheType extends CacheEntryType = "cache">(key: string, cacheType?: CacheType): Promise<WithLastModified<CacheValue<CacheType>> | null>;
8
+ set<CacheType extends CacheEntryType = "cache">(key: string, value: CacheValue<CacheType>, cacheType?: CacheType): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ }
11
+ declare const _default: RunlotStorageIncrementalCache;
12
+ export default _default;
13
+ //# sourceMappingURL=incremental-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental-cache.d.ts","sourceRoot":"","sources":["../../src/opennext/incremental-cache.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,iCAAiC,CAAC;AAGzC,eAAO,MAAM,IAAI,mBAAmB,CAAC;AAMrC,oEAAoE;AACpE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,GAAE,cAAwB,EAAE,EAAE,SAAY,GAAG,MAAM,CAEjG;AAOD,cAAM,6BAA8B,YAAW,gBAAgB;IAC7D,IAAI,SAAQ;IAEN,GAAG,CAAC,SAAS,SAAS,cAAc,GAAG,OAAO,EAClD,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,SAAS,GACpB,OAAO,CAAC,gBAAgB,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC;IAoBpD,GAAG,CAAC,SAAS,SAAS,cAAc,GAAG,OAAO,EAClD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,EAC5B,SAAS,CAAC,EAAE,SAAS,GACpB,OAAO,CAAC,IAAI,CAAC;IAWV,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKzC;;AAED,wBAAmD"}
@@ -0,0 +1,63 @@
1
+ // ISR 캐시 — `env.storage` 위 (docs/nextjs.md §2 의 표 첫 줄).
2
+ //
3
+ // OpenNext 의 기본값(정적 자산 캐시)은 **읽기 전용**이라 revalidate 가 안 된다:
4
+ // 빌드 때 구운 페이지는 돌려주지만 `revalidatePath` 뒤의 새 페이지를 어디에도
5
+ // 못 쓴다. 쓰기가 되는 자리는 우리에게 오브젝트 스토리지 하나뿐이다.
6
+ //
7
+ // `env.storage` 가 없으면(= `runlot add storage` 전) 정적 자산 캐시로 떨어지고
8
+ // 한 줄 알린다. 던지지 않는 이유: ISR 없이도 페이지는 떠야 한다.
9
+ import staticAssetsCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
10
+ import { bindings, buildId, sayOnce } from "./context.js";
11
+ export const NAME = "runlot-storage";
12
+ const NO_STORAGE = "ISR 캐시를 쓸 곳이 없습니다. `runlot add storage` 뒤에 다시 배포하면 " +
13
+ "revalidate 가 켜집니다. 그때까지는 빌드 때 구운 페이지만 돌려줍니다.";
14
+ /** `__next/cache/<buildId>/<key>.<type>`. 앞의 `/` 와 겹친 `/` 는 지운다. */
15
+ export function cacheKey(key, cacheType = "cache", id = buildId()) {
16
+ return `__next/cache/${id}/${key}.${cacheType}`.replace(/\/+/g, "/").replace(/^\//, "");
17
+ }
18
+ function storageOf() {
19
+ const s = bindings()["storage"];
20
+ return s === undefined || s === null ? undefined : s;
21
+ }
22
+ class RunlotStorageIncrementalCache {
23
+ name = NAME;
24
+ async get(key, cacheType) {
25
+ const storage = storageOf();
26
+ if (!storage) {
27
+ sayOnce("no-storage", NO_STORAGE);
28
+ return staticAssetsCache.get(key, cacheType);
29
+ }
30
+ const object = await storage.get(cacheKey(key, cacheType));
31
+ if (!object || !object.body)
32
+ return null;
33
+ try {
34
+ const value = (await new Response(object.body).json());
35
+ // `lastModified` 는 RFC 1123 이다. 못 읽으면 0 이 아니라 "지금 막 썼다"로
36
+ // 읽어야 한다 — 0 이면 모든 항목이 태그 검사에서 낡은 것이 된다.
37
+ const at = Date.parse(object.lastModified);
38
+ return { value, lastModified: Number.isNaN(at) ? Date.now() : at };
39
+ }
40
+ catch {
41
+ // 반쯤 쓰인 항목. 캐시 미스로 읽고 다음 렌더가 덮어쓴다.
42
+ return null;
43
+ }
44
+ }
45
+ async set(key, value, cacheType) {
46
+ const storage = storageOf();
47
+ if (!storage) {
48
+ sayOnce("no-storage", NO_STORAGE);
49
+ return;
50
+ }
51
+ await storage.put(cacheKey(key, cacheType), JSON.stringify(value), {
52
+ contentType: "application/json",
53
+ });
54
+ }
55
+ async delete(key) {
56
+ const storage = storageOf();
57
+ if (!storage)
58
+ return;
59
+ await storage.delete(cacheKey(key));
60
+ }
61
+ }
62
+ export default new RunlotStorageIncrementalCache();
63
+ //# sourceMappingURL=incremental-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental-cache.js","sourceRoot":"","sources":["../../src/opennext/incremental-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { OriginalTagCache, OriginalTagCacheWriteInput } from "@opennextjs/aws/types/overrides";
2
+ export declare const NAME = "runlot-db";
3
+ export declare const SCHEMA = "runlot_next";
4
+ export declare const TABLE = "runlot_next.tags";
5
+ /** 시험이 격리를 새로 여는 흉내를 낼 때. */
6
+ export declare function resetBootstrap(): void;
7
+ declare class RunlotDbTagCache implements OriginalTagCache {
8
+ #private;
9
+ mode: "original";
10
+ name: string;
11
+ getByTag(tag: string): Promise<string[]>;
12
+ getByPath(path: string): Promise<string[]>;
13
+ /**
14
+ * 이 경로의 태그 중 하나라도 `lastModified` 뒤에 무효화됐으면 `-1`.
15
+ * `-1` 이 어댑터에게 "이 항목은 버려라" 다.
16
+ */
17
+ getLastModified(path: string, lastModified?: number): Promise<number>;
18
+ writeTags(tags: OriginalTagCacheWriteInput[]): Promise<void>;
19
+ }
20
+ declare const _default: RunlotDbTagCache;
21
+ export default _default;
22
+ //# sourceMappingURL=tag-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-cache.d.ts","sourceRoot":"","sources":["../../src/opennext/tag-cache.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAGpG,eAAO,MAAM,IAAI,cAAc,CAAC;AAChC,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,eAAO,MAAM,KAAK,qBAAqB,CAAC;AAqCxC,6BAA6B;AAC7B,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAgBD,cAAM,gBAAiB,YAAW,gBAAgB;;IAChD,IAAI,EAAG,UAAU,CAAU;IAC3B,IAAI,SAAQ;IA4BN,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAOxC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAOhD;;;OAGG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAWrE,SAAS,CAAC,IAAI,EAAE,0BAA0B,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBnE;;AAED,wBAAsC"}
@@ -0,0 +1,123 @@
1
+ import { bindings, buildId, sayOnce } from "./context.js";
2
+ export const NAME = "runlot-db";
3
+ export const SCHEMA = "runlot_next";
4
+ export const TABLE = "runlot_next.tags";
5
+ const NO_DB = "태그 캐시를 쓸 곳이 없습니다. `runlot add pg` 뒤에 다시 배포하면 " +
6
+ "revalidateTag 가 켜집니다. 그때까지는 태그 무효화가 아무 일도 하지 않습니다.";
7
+ const BOOTSTRAP = [
8
+ `create schema if not exists ${SCHEMA}`,
9
+ `create table if not exists ${TABLE} (
10
+ tag text not null,
11
+ path text not null,
12
+ revalidated_at bigint not null default 0,
13
+ primary key (tag, path)
14
+ )`,
15
+ `create index if not exists tags_path_idx on ${TABLE} (path)`,
16
+ ];
17
+ function dbOf() {
18
+ const db = bindings()["db"];
19
+ return db === undefined || db === null ? undefined : db;
20
+ }
21
+ /** 격리 하나에 한 번. 실패하면 다음 호출이 다시 시도한다 — 캐시된 거절을 남기지 않는다. */
22
+ let ready;
23
+ async function ensure(db) {
24
+ ready ??= (async () => {
25
+ // 문장을 하나씩 보낸다. `exec` 는 확장 질의라 세미콜론으로 이은 여러 문장을
26
+ // 한 번에 받지 않는다.
27
+ for (const sql of BOOTSTRAP)
28
+ await db.exec(sql);
29
+ })().catch((err) => {
30
+ ready = undefined;
31
+ throw err;
32
+ });
33
+ return ready;
34
+ }
35
+ /** 시험이 격리를 새로 여는 흉내를 낼 때. */
36
+ export function resetBootstrap() {
37
+ ready = undefined;
38
+ }
39
+ function rowsOf(value) {
40
+ return Array.isArray(value) ? value : [];
41
+ }
42
+ /** bigint 는 드라이버에 따라 문자열로 온다. 숫자로 읽는다. */
43
+ function num(value) {
44
+ if (typeof value === "number")
45
+ return value;
46
+ if (typeof value === "string") {
47
+ const n = Number(value);
48
+ return Number.isFinite(n) ? n : 0;
49
+ }
50
+ return 0;
51
+ }
52
+ class RunlotDbTagCache {
53
+ mode = "original";
54
+ name = NAME;
55
+ /** 배포마다 태그 이름을 갈라 둔다. 옛 배포의 무효화가 새 배포를 흔들지 않는다. */
56
+ #key(tag) {
57
+ return `${buildId()}/${tag}`.replace(/\/+/g, "/");
58
+ }
59
+ #bare(key) {
60
+ const prefix = `${buildId()}/`;
61
+ return key.startsWith(prefix) ? key.slice(prefix.length) : key;
62
+ }
63
+ async #db() {
64
+ const db = dbOf();
65
+ if (!db) {
66
+ sayOnce("no-db", NO_DB);
67
+ return undefined;
68
+ }
69
+ try {
70
+ await ensure(db);
71
+ }
72
+ catch {
73
+ // 권한이 없거나 DB 가 아직 안 붙었다. 태그 없음으로 이어간다.
74
+ sayOnce("no-db-bootstrap", NO_DB);
75
+ return undefined;
76
+ }
77
+ return db;
78
+ }
79
+ async getByTag(tag) {
80
+ const db = await this.#db();
81
+ if (!db)
82
+ return [];
83
+ const rows = rowsOf(await db.exec(`select path from ${TABLE} where tag = $1`, [this.#key(tag)]));
84
+ return rows.map((r) => String(r["path"]));
85
+ }
86
+ async getByPath(path) {
87
+ const db = await this.#db();
88
+ if (!db)
89
+ return [];
90
+ const rows = rowsOf(await db.exec(`select tag from ${TABLE} where path = $1`, [path]));
91
+ return rows.map((r) => this.#bare(String(r["tag"])));
92
+ }
93
+ /**
94
+ * 이 경로의 태그 중 하나라도 `lastModified` 뒤에 무효화됐으면 `-1`.
95
+ * `-1` 이 어댑터에게 "이 항목은 버려라" 다.
96
+ */
97
+ async getLastModified(path, lastModified) {
98
+ const at = lastModified ?? Date.now();
99
+ const db = await this.#db();
100
+ if (!db)
101
+ return at;
102
+ const rows = rowsOf(await db.exec(`select max(revalidated_at) as at from ${TABLE} where path = $1`, [path]));
103
+ const revalidated = rows.length === 0 ? 0 : num(rows[0]?.["at"]);
104
+ return revalidated > at ? -1 : at;
105
+ }
106
+ async writeTags(tags) {
107
+ if (tags.length === 0)
108
+ return;
109
+ const db = await this.#db();
110
+ if (!db)
111
+ return;
112
+ const now = Date.now();
113
+ const params = [];
114
+ const values = tags.map((t, i) => {
115
+ params.push(this.#key(t.tag), t.path, t.revalidatedAt ?? now);
116
+ return `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`;
117
+ });
118
+ await db.exec(`insert into ${TABLE} (tag, path, revalidated_at) values ${values.join(", ")} ` +
119
+ `on conflict (tag, path) do update set revalidated_at = excluded.revalidated_at`, params);
120
+ }
121
+ }
122
+ export default new RunlotDbTagCache();
123
+ //# sourceMappingURL=tag-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-cache.js","sourceRoot":"","sources":["../../src/opennext/tag-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { AuthBinding, SignOutHeaders, User } from "@runlot/auth";
3
+ import type { Db, Storage } from "@runlot/pg";
4
+ /**
5
+ * `env.auth`. `@runlot/auth` 의 `AuthBinding` 과 같되 `user()`·`signOut()` 의 요청
6
+ * 인자가 선택이다 — 없으면 어댑터가 저장소의 요청을 넣는다 (docs/nextjs.md §4).
7
+ */
8
+ export interface RunlotAuth extends Omit<AuthBinding, "user" | "signOut"> {
9
+ user(request?: Request): Promise<User | null>;
10
+ signOut(request?: Request): Promise<SignOutHeaders>;
11
+ }
12
+ /** 워커가 받는 바인딩 묶음. 이름 없는 나머지는 시크릿·환경 변수(문자열)다. */
13
+ export interface RunlotBindings {
14
+ db: Db;
15
+ auth: RunlotAuth;
16
+ storage: Storage;
17
+ }
18
+ export type RunlotEnv = RunlotBindings & Record<string, string>;
19
+ /** workerd 의 `ExecutionContext` 중 우리가 쓰는 것. */
20
+ export interface RunlotContext {
21
+ waitUntil(promise: Promise<unknown>): void;
22
+ passThroughOnException?(): void;
23
+ }
24
+ export interface RunlotStore {
25
+ request: Request;
26
+ env: RunlotEnv;
27
+ ctx: RunlotContext;
28
+ }
29
+ /** 요청 문맥 저장소. 진입 wrapper 가 넣고 `env`·`request()`·`waitUntil()` 이 읽는다. */
30
+ export declare const als: AsyncLocalStorage<RunlotStore>;
31
+ /**
32
+ * 저장소 밖에서 부를 때의 문장.
33
+ *
34
+ * 조용히 `null`·`undefined` 를 주지 않는 이유는 docs/auth.md §4 와 같다 — "안 됨"
35
+ * 이 빈 값이 되면 모든 페이지가 로그아웃 상태로 보이고, 그것을 알아채는 자리는
36
+ * 배포 뒤뿐이다.
37
+ */
38
+ export declare const OUTSIDE_REQUEST: string;
39
+ export declare function store(): RunlotStore;
40
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE9C;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACvE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAC;IACP,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEhE,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C,sBAAsB,CAAC,IAAI,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,SAAS,CAAC;IACf,GAAG,EAAE,aAAa,CAAC;CACpB;AAUD,wEAAwE;AACxE,eAAO,MAAM,GAAG,EAAE,iBAAiB,CAAC,WAAW,CAAuD,CAAC;AAEvG;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAIkC,CAAC;AAE/D,wBAAgB,KAAK,IAAI,WAAW,CAInC"}
package/dist/store.js ADDED
@@ -0,0 +1,36 @@
1
+ // 요청 문맥 저장소 (docs/nextjs.md §4).
2
+ //
3
+ // `runlot/next` 의 `env` 는 워커 진입점이 받은 `(request, env, ctx)` 를 읽는다.
4
+ // Next 의 서버 컴포넌트에는 그 셋을 인자로 내려보낼 자리가 없으므로 —
5
+ // `page.tsx` 는 `props` 만 받는다 — 진입 wrapper 가 AsyncLocalStorage 에 넣고
6
+ // 이 모듈이 그것을 꺼낸다.
7
+ //
8
+ // **저장소는 `globalThis` 의 심볼 하나다.** 모듈 인스턴스가 하나라는 보장이
9
+ // 없어서다: OpenNext 가 낸 `worker.js` 는 이미 묶인 번들이라 그 안에 이 패키지의
10
+ // 사본이 들어 있고, 그 곁에 세우는 `runlot-entry.js` 를 우리가 다시 묶으면 사본이
11
+ // 하나 더 생긴다. 모듈 지역 변수에 저장소를 두면 wrapper 가 넣은 값을 페이지가
12
+ // 못 본다 — 조용히 "요청이 없습니다" 가 된다. OpenNext 자신도 같은 이유로
13
+ // `Symbol.for("__cloudflare-context__")` 를 쓴다.
14
+ import { AsyncLocalStorage } from "node:async_hooks";
15
+ const STORE = Symbol.for("runlot.next.store");
16
+ const g = globalThis;
17
+ /** 요청 문맥 저장소. 진입 wrapper 가 넣고 `env`·`request()`·`waitUntil()` 이 읽는다. */
18
+ export const als = (g[STORE] ??= new AsyncLocalStorage());
19
+ /**
20
+ * 저장소 밖에서 부를 때의 문장.
21
+ *
22
+ * 조용히 `null`·`undefined` 를 주지 않는 이유는 docs/auth.md §4 와 같다 — "안 됨"
23
+ * 이 빈 값이 되면 모든 페이지가 로그아웃 상태로 보이고, 그것을 알아채는 자리는
24
+ * 배포 뒤뿐이다.
25
+ */
26
+ export const OUTSIDE_REQUEST = "runlot/next 의 `env` 는 요청 안에서만 읽을 수 있습니다. " +
27
+ "모듈 최상단이나 빌드 시점 정적 렌더에는 요청이 없습니다 — " +
28
+ "서버 컴포넌트·라우트 핸들러·미들웨어 안으로 옮기세요. " +
29
+ "`next dev` 는 아직 runlot 바인딩을 모릅니다; `runlot deploy` 로 확인합니다.";
30
+ export function store() {
31
+ const s = als.getStore();
32
+ if (!s)
33
+ throw new Error(OUTSIDE_REQUEST);
34
+ return s;
35
+ }
36
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,4CAA4C;AAC5C,mEAAmE;AACnE,iBAAiB;AACjB,EAAE;AACF,oDAAoD;AACpD,0DAA0D;AAC1D,yDAAyD;AACzD,mDAAmD;AACnD,kDAAkD;AAClD,+CAA+C;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAkCrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AAM9C,MAAM,CAAC,GAAG,UAAoB,CAAC;AAE/B,wEAAwE;AACxE,MAAM,CAAC,MAAM,GAAG,GAAmC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,iBAAiB,EAAe,CAAC,CAAC;AAEvG;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,2CAA2C;IAC3C,oCAAoC;IACpC,iCAAiC;IACjC,4DAA4D,CAAC;AAE/D,MAAM,UAAU,KAAK;IACnB,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IACzB,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC;AACX,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@runlot/next",
3
+ "version": "0.1.0-rc.11",
4
+ "description": "Next.js 어댑터 — env 프록시와 OpenNext 빌드 (docs/nextjs.md)",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/runlot/runlot",
11
+ "directory": "web/packages/next"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./entry": {
22
+ "types": "./dist/entry.d.ts",
23
+ "default": "./dist/entry.js"
24
+ },
25
+ "./build": {
26
+ "types": "./dist/build.d.ts",
27
+ "default": "./dist/build.js"
28
+ },
29
+ "./opennext/incremental-cache": {
30
+ "types": "./dist/opennext/incremental-cache.d.ts",
31
+ "default": "./dist/opennext/incremental-cache.js"
32
+ },
33
+ "./opennext/tag-cache": {
34
+ "types": "./dist/opennext/tag-cache.d.ts",
35
+ "default": "./dist/opennext/tag-cache.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "@opennextjs/aws": "4.1.4",
44
+ "@opennextjs/cloudflare": "^1.20.6",
45
+ "@runlot/auth": "0.1.0-rc.11",
46
+ "@runlot/pg": "0.1.0-rc.11"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.2",
50
+ "esbuild": "^0.24.2",
51
+ "typescript": "^5.7.2",
52
+ "@runlot/config": "0.1.0-rc.11"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.build.json",
56
+ "test": "node build.mjs",
57
+ "typecheck": "tsc --noEmit"
58
+ }
59
+ }