@sequenceholdings/artifact-studio 0.2.5 → 0.3.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/README.md +35 -0
- package/VERSION-PIN.md +14 -3
- package/dist/api.d.ts +11 -1
- package/dist/api.js +26 -9
- package/dist/atlas-ui-css-plugin.d.ts +11 -7
- package/dist/atlas-ui-css-plugin.js +38 -10
- package/dist/build.d.ts +2 -2
- package/dist/build.js +14 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +108 -37
- package/dist/dev-link.d.ts +1 -1
- package/dist/dev-link.js +2 -1
- package/dist/gateway/admission.d.ts +55 -0
- package/dist/gateway/admission.js +132 -0
- package/dist/gateway/control.d.ts +91 -0
- package/dist/gateway/control.js +113 -0
- package/dist/gateway/hosted-proxy.d.ts +36 -0
- package/dist/gateway/hosted-proxy.js +347 -0
- package/dist/gateway/release-admission.d.ts +23 -0
- package/dist/gateway/release-admission.js +259 -0
- package/dist/gateway/server.d.ts +29 -0
- package/dist/gateway/server.js +298 -0
- package/dist/gateway/session.d.ts +71 -0
- package/dist/gateway/session.js +143 -0
- package/dist/gateway/usage.d.ts +37 -0
- package/dist/gateway/usage.js +46 -0
- package/dist/gateway-main.d.ts +1 -0
- package/dist/gateway-main.js +84 -0
- package/dist/git-clone.d.ts +23 -1
- package/dist/git-clone.js +32 -15
- package/dist/manifest-next.d.ts +217 -0
- package/dist/manifest-next.js +120 -0
- package/dist/manifest-static.d.ts +76 -0
- package/dist/manifest-static.js +124 -0
- package/dist/manifest.d.ts +124 -19
- package/dist/manifest.js +12 -118
- package/dist/next/bridge.d.ts +47 -0
- package/dist/next/bridge.js +229 -0
- package/dist/next/browser-logs.d.ts +7 -0
- package/dist/next/browser-logs.js +51 -0
- package/dist/next/browser.d.ts +6 -0
- package/dist/next/browser.js +57 -0
- package/dist/next/protocol.d.ts +73 -0
- package/dist/next/protocol.js +56 -0
- package/dist/next/release.d.ts +15 -0
- package/dist/next/release.js +46 -0
- package/dist/next/requests.d.ts +15 -0
- package/dist/next/requests.js +59 -0
- package/dist/next/routes.d.ts +4 -0
- package/dist/next/routes.js +21 -0
- package/dist/next/streams.d.ts +16 -0
- package/dist/next/streams.js +132 -0
- package/dist/next/theme.d.ts +10 -0
- package/dist/next/theme.js +45 -0
- package/dist/next/transport.d.ts +15 -0
- package/dist/next/transport.js +13 -0
- package/dist/next-build.d.ts +29 -0
- package/dist/next-build.js +129 -0
- package/dist/next-cache/adapter-runtime.d.ts +51 -0
- package/dist/next-cache/adapter-runtime.js +73 -0
- package/dist/next-cache/component-handler.d.ts +3 -0
- package/dist/next-cache/component-handler.js +10 -0
- package/dist/next-cache/components.d.ts +20 -0
- package/dist/next-cache/components.js +113 -0
- package/dist/next-cache/config.d.ts +9 -0
- package/dist/next-cache/config.js +78 -0
- package/dist/next-cache/incremental-handler.d.ts +18 -0
- package/dist/next-cache/incremental-handler.js +17 -0
- package/dist/next-cache/incremental.d.ts +27 -0
- package/dist/next-cache/incremental.js +99 -0
- package/dist/next-cache/index.d.ts +18 -0
- package/dist/next-cache/index.js +16 -0
- package/dist/next-cache/memory-store.d.ts +27 -0
- package/dist/next-cache/memory-store.js +118 -0
- package/dist/next-cache/redis-scripts.d.ts +3 -0
- package/dist/next-cache/redis-scripts.js +85 -0
- package/dist/next-cache/redis-store.d.ts +18 -0
- package/dist/next-cache/redis-store.js +56 -0
- package/dist/next-cache/runtime.d.ts +29 -0
- package/dist/next-cache/runtime.js +63 -0
- package/dist/next-cache/serialization.d.ts +39 -0
- package/dist/next-cache/serialization.js +333 -0
- package/dist/next-cache/store.d.ts +54 -0
- package/dist/next-cache/store.js +45 -0
- package/dist/next-cache/valkey-transport.d.ts +52 -0
- package/dist/next-cache/valkey-transport.js +189 -0
- package/dist/next-dev-control.d.ts +48 -0
- package/dist/next-dev-control.js +63 -0
- package/dist/next-dev-process.d.ts +17 -0
- package/dist/next-dev-process.js +110 -0
- package/dist/next-dev.d.ts +12 -0
- package/dist/next-dev.js +267 -0
- package/dist/next-image-config.d.ts +9 -0
- package/dist/next-image-config.js +101 -0
- package/dist/next-project.d.ts +22 -0
- package/dist/next-project.js +64 -0
- package/dist/next-public-fonts-local.d.ts +7 -0
- package/dist/next-public-fonts-local.js +64 -0
- package/dist/next-public-fonts.d.ts +13 -0
- package/dist/next-public-fonts.js +49 -0
- package/dist/next-source-hash.d.ts +14 -0
- package/dist/next-source-hash.js +18 -0
- package/dist/next-source.d.ts +35 -0
- package/dist/next-source.js +155 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +5 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +9 -1
- package/dist/runtime-contracts.d.ts +229 -0
- package/dist/runtime-contracts.js +224 -0
- package/dist/sdk.d.ts +420 -1
- package/dist/sdk.js +35 -0
- package/dist/server/broker-contract.d.ts +68 -0
- package/dist/server/broker-contract.js +33 -0
- package/dist/server/client.d.ts +67 -0
- package/dist/server/client.js +159 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.js +24 -0
- package/dist/server/release-forms.d.ts +13 -0
- package/dist/server/release-forms.js +124 -0
- package/dist/server/request-context.d.ts +74 -0
- package/dist/server/request-context.js +103 -0
- package/dist/server/runtime-config.d.ts +53 -0
- package/dist/server/runtime-config.js +64 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +4 -0
- package/dist/templates/nextjs/.artifactignore +14 -0
- package/dist/templates/nextjs/README.md +151 -0
- package/dist/templates/nextjs/app/about/page.tsx +26 -0
- package/dist/templates/nextjs/app/actions.ts +18 -0
- package/dist/templates/nextjs/app/api/health/route.ts +6 -0
- package/dist/templates/nextjs/app/components/counter.tsx +19 -0
- package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/dist/templates/nextjs/app/globals.css +4 -0
- package/dist/templates/nextjs/app/layout.tsx +27 -0
- package/dist/templates/nextjs/app/page.tsx +51 -0
- package/dist/templates/nextjs/app/providers.tsx +107 -0
- package/dist/templates/nextjs/artifact.bundle.yml +55 -0
- package/dist/templates/nextjs/next.config.mjs +27 -0
- package/dist/templates/nextjs/package.json +32 -0
- package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/dist/templates/nextjs/postcss.config.mjs +5 -0
- package/dist/templates/nextjs/tsconfig.json +20 -0
- package/dist/templates/react-vite/artifact.bundle.yml +0 -1
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/broker.d.ts +75 -0
- package/dist/tunnel/broker.js +451 -0
- package/dist/tunnel/client.d.ts +22 -0
- package/dist/tunnel/client.js +177 -0
- package/dist/tunnel/index.d.ts +6 -0
- package/dist/tunnel/index.js +4 -0
- package/dist/tunnel/io.d.ts +33 -0
- package/dist/tunnel/io.js +108 -0
- package/dist/tunnel/peer.d.ts +25 -0
- package/dist/tunnel/peer.js +147 -0
- package/dist/tunnel/protocol.d.ts +69 -0
- package/dist/tunnel/protocol.js +196 -0
- package/dist/tunnel/redis-ownership.d.ts +38 -0
- package/dist/tunnel/redis-ownership.js +100 -0
- package/dist/tunnel/redis-socket.d.ts +13 -0
- package/dist/tunnel/redis-socket.js +48 -0
- package/dist/tunnel/redis-types.d.ts +70 -0
- package/dist/tunnel/redis-types.js +9 -0
- package/dist/tunnel/redis.d.ts +53 -0
- package/dist/tunnel/redis.js +391 -0
- package/dist/tunnel/transport.d.ts +17 -0
- package/dist/tunnel/transport.js +1 -0
- package/package.json +68 -4
- package/templates/nextjs/.artifactignore +14 -0
- package/templates/nextjs/README.md +151 -0
- package/templates/nextjs/app/about/page.tsx +26 -0
- package/templates/nextjs/app/actions.ts +18 -0
- package/templates/nextjs/app/api/health/route.ts +6 -0
- package/templates/nextjs/app/components/counter.tsx +19 -0
- package/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/templates/nextjs/app/globals.css +4 -0
- package/templates/nextjs/app/layout.tsx +27 -0
- package/templates/nextjs/app/page.tsx +51 -0
- package/templates/nextjs/app/providers.tsx +107 -0
- package/templates/nextjs/artifact.bundle.yml +55 -0
- package/templates/nextjs/next.config.mjs +27 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/templates/nextjs/postcss.config.mjs +5 -0
- package/templates/nextjs/tsconfig.json +20 -0
- package/templates/react-vite/artifact.bundle.yml +0 -1
- package/templates/react-vite/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function createNextArtifactRequests({ send, timeoutMs }) {
|
|
2
|
+
const pending = new Map();
|
|
3
|
+
// A React effect remount may reuse the session nonce. Old responses must not
|
|
4
|
+
// settle requests created by the new bridge instance.
|
|
5
|
+
const instanceId = globalThis.crypto.randomUUID();
|
|
6
|
+
let sequence = 0;
|
|
7
|
+
function cancel({ id, error }) {
|
|
8
|
+
const request = pending.get(id);
|
|
9
|
+
if (!request)
|
|
10
|
+
return;
|
|
11
|
+
pending.delete(id);
|
|
12
|
+
request.cleanup();
|
|
13
|
+
request.reject(error);
|
|
14
|
+
try {
|
|
15
|
+
send({ type: 'artifact-studio-bridge-cancel', id });
|
|
16
|
+
}
|
|
17
|
+
catch { /* A lost host cannot accept cancellation. */ }
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
request({ method, args = [], signal }) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
return Promise.reject(new DOMException('Artifact request aborted', 'AbortError'));
|
|
23
|
+
if (pending.size >= 100)
|
|
24
|
+
return Promise.reject(new Error('Too many pending Artifact requests'));
|
|
25
|
+
if (!method || method.length > 128 || args.length > 20)
|
|
26
|
+
return Promise.reject(new Error('Invalid Artifact bridge request'));
|
|
27
|
+
const id = `artifact-next-${instanceId}-${++sequence}`;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const onAbort = () => cancel({ id, error: new DOMException('Artifact request aborted', 'AbortError') });
|
|
30
|
+
const timer = setTimeout(() => cancel({ id, error: new Error('Artifact bridge request timed out') }), timeoutMs);
|
|
31
|
+
pending.set(id, { resolve, reject, cleanup() { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); } });
|
|
32
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
33
|
+
try {
|
|
34
|
+
send({ type: 'artifact-studio-bridge-request', id, method, args });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
cancel({ id, error: error instanceof Error ? error : new Error('Artifact request could not be sent') });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
receive({ payload }) {
|
|
42
|
+
if (payload.type !== 'artifact-studio-bridge-response' || typeof payload.id !== 'string')
|
|
43
|
+
return;
|
|
44
|
+
const request = pending.get(payload.id);
|
|
45
|
+
if (!request)
|
|
46
|
+
return;
|
|
47
|
+
pending.delete(payload.id);
|
|
48
|
+
request.cleanup();
|
|
49
|
+
if (typeof payload.error === 'string')
|
|
50
|
+
request.reject(new Error(payload.error));
|
|
51
|
+
else
|
|
52
|
+
request.resolve(payload.result);
|
|
53
|
+
},
|
|
54
|
+
dispose() {
|
|
55
|
+
for (const id of pending.keys())
|
|
56
|
+
cancel({ id, error: new Error('Artifact bridge disposed') });
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ARTIFACT_RELEASE_FORM_FIELD } from './release.js';
|
|
2
|
+
/** Remove transport metadata without reserializing application query values. */
|
|
3
|
+
export function stripArtifactReleaseQuery({ path }) {
|
|
4
|
+
const hashAt = path.indexOf('#');
|
|
5
|
+
const beforeHash = hashAt < 0 ? path : path.slice(0, hashAt);
|
|
6
|
+
const queryAt = beforeHash.indexOf('?');
|
|
7
|
+
if (queryAt < 0)
|
|
8
|
+
return path;
|
|
9
|
+
const query = beforeHash.slice(queryAt + 1);
|
|
10
|
+
const parts = query.split('&');
|
|
11
|
+
const kept = parts.filter((part) => {
|
|
12
|
+
const key = part.split('=', 1)[0] ?? '';
|
|
13
|
+
// URLSearchParams decodes only the key here. Non-reserved parameters retain
|
|
14
|
+
// their byte-for-byte spelling, duplicate order and original escaping.
|
|
15
|
+
return !new URLSearchParams(`${key}=`).has(ARTIFACT_RELEASE_FORM_FIELD);
|
|
16
|
+
});
|
|
17
|
+
if (kept.length === parts.length)
|
|
18
|
+
return path;
|
|
19
|
+
const remaining = kept.join('&');
|
|
20
|
+
return `${beforeHash.slice(0, queryAt)}${remaining ? `?${remaining}` : ''}${hashAt < 0 ? '' : path.slice(hashAt)}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SequenceApiFetchOptions } from '../sdk.js';
|
|
2
|
+
export interface NextArtifactStreamRequest {
|
|
3
|
+
path: string;
|
|
4
|
+
options?: SequenceApiFetchOptions;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/** One pull grants one <=64KiB chunk; unread responses cannot fill a message queue. */
|
|
8
|
+
export declare function createNextArtifactStreams({ send }: {
|
|
9
|
+
send(payload: Record<string, unknown>): void;
|
|
10
|
+
}): {
|
|
11
|
+
open: ({ path, options, signal }: NextArtifactStreamRequest) => Promise<Response>;
|
|
12
|
+
receive({ payload }: {
|
|
13
|
+
payload: Record<string, unknown>;
|
|
14
|
+
}): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isNextArtifactRelativePath } from './protocol.js';
|
|
2
|
+
/** One pull grants one <=64KiB chunk; unread responses cannot fill a message queue. */
|
|
3
|
+
export function createNextArtifactStreams({ send }) {
|
|
4
|
+
const active = new Map();
|
|
5
|
+
const instanceId = globalThis.crypto.randomUUID();
|
|
6
|
+
let sequence = 0;
|
|
7
|
+
function open({ path, options, signal }) {
|
|
8
|
+
if (!isNextArtifactRelativePath({ path }) || !path.startsWith('/api/'))
|
|
9
|
+
return Promise.reject(new Error('Artifact stream requires an /api/ path'));
|
|
10
|
+
if (signal?.aborted)
|
|
11
|
+
return Promise.reject(new DOMException('Artifact stream aborted', 'AbortError'));
|
|
12
|
+
if (active.size >= 32)
|
|
13
|
+
return Promise.reject(new Error('Too many Artifact streams'));
|
|
14
|
+
const id = `artifact-next-stream-${instanceId}-${++sequence}`;
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let controller;
|
|
17
|
+
let ready = false;
|
|
18
|
+
let credit = false;
|
|
19
|
+
let totalBytes = 0;
|
|
20
|
+
let resolvePull;
|
|
21
|
+
const timeout = setTimeout(() => fail(new Error('Artifact stream timed out')), 120_000);
|
|
22
|
+
const onAbort = () => fail(new DOMException('Artifact stream aborted', 'AbortError'));
|
|
23
|
+
function cleanup({ cancel }) {
|
|
24
|
+
active.delete(id);
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
signal?.removeEventListener('abort', onAbort);
|
|
27
|
+
resolvePull?.();
|
|
28
|
+
if (cancel) {
|
|
29
|
+
try {
|
|
30
|
+
send({ type: 'artifact-studio-bridge-cancel', id });
|
|
31
|
+
}
|
|
32
|
+
catch { /* The parent has already disconnected. */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fail(error) {
|
|
36
|
+
if (!active.has(id))
|
|
37
|
+
return;
|
|
38
|
+
cleanup({ cancel: true });
|
|
39
|
+
controller?.error(error);
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
const stream = new ReadableStream({
|
|
43
|
+
start(value) { controller = value; },
|
|
44
|
+
pull() {
|
|
45
|
+
if (!active.has(id) || credit)
|
|
46
|
+
return;
|
|
47
|
+
credit = true;
|
|
48
|
+
return new Promise((done) => {
|
|
49
|
+
resolvePull = done;
|
|
50
|
+
try {
|
|
51
|
+
send({ type: 'artifact-studio-stream-pull', id });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
fail(new Error('Artifact stream disconnected'));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
cancel() { cleanup({ cancel: true }); },
|
|
59
|
+
}, { highWaterMark: 0 });
|
|
60
|
+
function receiveReady(payload) {
|
|
61
|
+
if (ready || typeof payload.status !== 'number' || !Number.isInteger(payload.status) || payload.status < 200 || payload.status > 599) {
|
|
62
|
+
fail(new Error('Invalid Artifact stream response'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
ready = true;
|
|
66
|
+
const headers = new Headers();
|
|
67
|
+
const rawHeaders = payload.headers;
|
|
68
|
+
if (typeof rawHeaders === 'object' && rawHeaders !== null && 'content-type' in rawHeaders && typeof rawHeaders['content-type'] === 'string') {
|
|
69
|
+
try {
|
|
70
|
+
headers.set('content-type', rawHeaders['content-type']);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
fail(new Error('Invalid Artifact stream headers'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const nullBody = [204, 205, 304].includes(payload.status);
|
|
78
|
+
resolve(new Response(nullBody ? null : stream, { status: payload.status, headers }));
|
|
79
|
+
if (nullBody) {
|
|
80
|
+
controller?.close();
|
|
81
|
+
cleanup({ cancel: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function receiveChunk(payload) {
|
|
85
|
+
const chunk = payload.chunk instanceof ArrayBuffer ? new Uint8Array(payload.chunk) : payload.chunk instanceof Uint8Array ? payload.chunk : null;
|
|
86
|
+
if (!ready || !credit || !chunk || chunk.byteLength > 65_536 || totalBytes + chunk.byteLength > 64 * 1024 * 1024) {
|
|
87
|
+
fail(new Error('Invalid or oversized Artifact stream chunk'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
credit = false;
|
|
91
|
+
totalBytes += chunk.byteLength;
|
|
92
|
+
controller?.enqueue(chunk);
|
|
93
|
+
resolvePull?.();
|
|
94
|
+
resolvePull = undefined;
|
|
95
|
+
}
|
|
96
|
+
function receive(payload) {
|
|
97
|
+
if (payload.event === 'error') {
|
|
98
|
+
fail(new Error(typeof payload.error === 'string' ? payload.error : 'Artifact stream failed'));
|
|
99
|
+
}
|
|
100
|
+
else if (payload.event === 'ready')
|
|
101
|
+
receiveReady(payload);
|
|
102
|
+
else if (payload.event === 'chunk')
|
|
103
|
+
receiveChunk(payload);
|
|
104
|
+
else if (payload.event === 'done') {
|
|
105
|
+
if (!ready) {
|
|
106
|
+
fail(new Error('Artifact stream ended before response'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
cleanup({ cancel: false });
|
|
110
|
+
controller?.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
active.set(id, { receive, fail });
|
|
114
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
115
|
+
try {
|
|
116
|
+
send({ type: 'artifact-studio-stream-open', id, path, options: options ?? {}, flowControl: 'pull' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
fail(new Error('Artifact stream could not be opened'));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
open,
|
|
125
|
+
receive({ payload }) {
|
|
126
|
+
if (payload.type === 'artifact-studio-stream' && typeof payload.id === 'string')
|
|
127
|
+
active.get(payload.id)?.receive(payload);
|
|
128
|
+
},
|
|
129
|
+
dispose() { for (const stream of active.values())
|
|
130
|
+
stream.fail(new Error('Artifact bridge disposed')); },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NextArtifactTheme } from './protocol.js';
|
|
2
|
+
/** Apply a host snapshot without requiring React or reading the DOM during SSR. */
|
|
3
|
+
export declare function createNextArtifactThemeApplier({ root }: {
|
|
4
|
+
root: HTMLElement;
|
|
5
|
+
}): {
|
|
6
|
+
apply({ theme }: {
|
|
7
|
+
theme: NextArtifactTheme;
|
|
8
|
+
}): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Apply a host snapshot without requiring React or reading the DOM during SSR. */
|
|
2
|
+
export function createNextArtifactThemeApplier({ root }) {
|
|
3
|
+
const originalClass = root.classList.contains('dark');
|
|
4
|
+
const originalTheme = root.getAttribute('data-theme');
|
|
5
|
+
const originalTokens = root.getAttribute('data-tokens');
|
|
6
|
+
const originalProperties = new Map();
|
|
7
|
+
let applied = new Set();
|
|
8
|
+
function attribute({ name, value }) {
|
|
9
|
+
if (value === null)
|
|
10
|
+
root.removeAttribute(name);
|
|
11
|
+
else
|
|
12
|
+
root.setAttribute(name, value);
|
|
13
|
+
}
|
|
14
|
+
function restoreProperty({ name }) {
|
|
15
|
+
const original = originalProperties.get(name);
|
|
16
|
+
if (original?.value)
|
|
17
|
+
root.style.setProperty(name, original.value, original.priority);
|
|
18
|
+
else
|
|
19
|
+
root.style.removeProperty(name);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
apply({ theme }) {
|
|
23
|
+
root.classList.toggle('dark', theme.dark);
|
|
24
|
+
attribute({ name: 'data-theme', value: theme.theme ?? originalTheme });
|
|
25
|
+
attribute({ name: 'data-tokens', value: theme.tokens ?? originalTokens });
|
|
26
|
+
for (const name of applied)
|
|
27
|
+
if (!(name in theme.vars))
|
|
28
|
+
restoreProperty({ name });
|
|
29
|
+
for (const [name, value] of Object.entries(theme.vars)) {
|
|
30
|
+
if (!originalProperties.has(name))
|
|
31
|
+
originalProperties.set(name, { value: root.style.getPropertyValue(name), priority: root.style.getPropertyPriority(name) });
|
|
32
|
+
root.style.setProperty(name, value);
|
|
33
|
+
}
|
|
34
|
+
applied = new Set(Object.keys(theme.vars));
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
root.classList.toggle('dark', originalClass);
|
|
38
|
+
attribute({ name: 'data-theme', value: originalTheme });
|
|
39
|
+
attribute({ name: 'data-tokens', value: originalTokens });
|
|
40
|
+
for (const name of applied)
|
|
41
|
+
restoreProperty({ name });
|
|
42
|
+
applied.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NextArtifactMessageEvent {
|
|
2
|
+
origin: string;
|
|
3
|
+
source: unknown;
|
|
4
|
+
data: unknown;
|
|
5
|
+
}
|
|
6
|
+
/** Transport injection keeps SSR import/construction free from browser globals. */
|
|
7
|
+
export interface NextArtifactMessageTransport {
|
|
8
|
+
readonly parent: object;
|
|
9
|
+
listen(listener: (event: NextArtifactMessageEvent) => void): () => void;
|
|
10
|
+
send(input: {
|
|
11
|
+
message: unknown;
|
|
12
|
+
targetOrigin: string;
|
|
13
|
+
}): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createNextArtifactWindowTransport(): NextArtifactMessageTransport;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createNextArtifactWindowTransport() {
|
|
2
|
+
if (typeof window === 'undefined' || window.parent === window)
|
|
3
|
+
throw new Error('Connect the Artifact bridge inside its Atlas iframe');
|
|
4
|
+
const target = window.parent;
|
|
5
|
+
return {
|
|
6
|
+
parent: target,
|
|
7
|
+
listen(listener) {
|
|
8
|
+
window.addEventListener('message', listener);
|
|
9
|
+
return () => window.removeEventListener('message', listener);
|
|
10
|
+
},
|
|
11
|
+
send({ message, targetOrigin }) { target.postMessage(message, targetOrigin); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ArtifactStudioNextManifest } from './manifest.js';
|
|
2
|
+
import type { SourceProvenance, SourceSpec } from './source-resolver.js';
|
|
3
|
+
export declare function planNextProject({ root, manifest, environment }: {
|
|
4
|
+
root: string;
|
|
5
|
+
manifest: ArtifactStudioNextManifest;
|
|
6
|
+
environment: string;
|
|
7
|
+
}): Promise<number>;
|
|
8
|
+
export declare function nextSourceRepository({ spec, root }: {
|
|
9
|
+
spec: SourceSpec;
|
|
10
|
+
root: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function resolveNextRepositoryId({ spec, repository, baseUrl, token }: {
|
|
13
|
+
spec: SourceSpec;
|
|
14
|
+
repository: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
token: string;
|
|
17
|
+
}): Promise<string>;
|
|
18
|
+
export declare function submitNextBuild({ root, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }: {
|
|
19
|
+
root: string;
|
|
20
|
+
manifest: ArtifactStudioNextManifest;
|
|
21
|
+
projectId: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
getToken: () => Promise<string>;
|
|
24
|
+
provenance: SourceProvenance;
|
|
25
|
+
repository: string;
|
|
26
|
+
repositoryId?: string;
|
|
27
|
+
channel: 'active' | 'preview';
|
|
28
|
+
out?: string;
|
|
29
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getJson, postJson } from './api.js';
|
|
8
|
+
import { resolveRepo } from './git-service-client.js';
|
|
9
|
+
import { readArtifactStudioSource } from './project.js';
|
|
10
|
+
import { validateNextProject } from './next-project.js';
|
|
11
|
+
const operationSchema = z.object({
|
|
12
|
+
operation: z.object({
|
|
13
|
+
id: z.string().min(1), status: z.string().min(1), kind: z.string().optional(),
|
|
14
|
+
error: z.union([z.string(), z.object({ message: z.string() })]).nullable().optional(),
|
|
15
|
+
}),
|
|
16
|
+
deployment: z.object({ id: z.string(), version: z.string() }).nullable().optional(),
|
|
17
|
+
});
|
|
18
|
+
export async function planNextProject({ root, manifest, environment }) {
|
|
19
|
+
await validateNextProject({ root, manifest, installed: false });
|
|
20
|
+
const source = await readArtifactStudioSource(root);
|
|
21
|
+
console.log(`[seq-studio] plan ${manifest.artifact.project_id} -> ${environment}`);
|
|
22
|
+
console.log(` runtime: Next.js ${manifest.runtime.next}, React ${manifest.runtime.react}, Node ${manifest.runtime.node}`);
|
|
23
|
+
console.log(` source: ${source.sourceHash} (${source.files.length} files)`);
|
|
24
|
+
console.log(' action: submit immutable source to Atlas for an isolated build and governed deployment');
|
|
25
|
+
console.log(' local validation only: Atlas validates environment bindings, permissions and policy before building');
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
export function nextSourceRepository({ spec, root }) {
|
|
29
|
+
if (spec.kind === 'git-service')
|
|
30
|
+
return `${spec.namespace}/${spec.name}`;
|
|
31
|
+
if (spec.kind === 'git-url')
|
|
32
|
+
return spec.url;
|
|
33
|
+
let origin;
|
|
34
|
+
try {
|
|
35
|
+
origin = execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error('Next.js builds require a source repository. Create/clone a repository with seq-studio repos and commit the application before building.');
|
|
39
|
+
}
|
|
40
|
+
if (/^[\w.-]+@[\w.-]+:[\w./-]+$/.test(origin))
|
|
41
|
+
return origin;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(origin);
|
|
44
|
+
if (url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash)
|
|
45
|
+
return origin;
|
|
46
|
+
}
|
|
47
|
+
catch { /* Refuse opaque/local origins; do not include potential credentials in diagnostics. */ }
|
|
48
|
+
throw new Error('The Git origin must be a credential-free HTTPS or SSH repository URL. Store Git credentials in your credential manager.');
|
|
49
|
+
}
|
|
50
|
+
export async function resolveNextRepositoryId({ spec, repository, baseUrl, token }) {
|
|
51
|
+
if (spec.kind === 'git-service') {
|
|
52
|
+
return (await resolveRepo({ baseUrl, token, namespace: spec.namespace, name: spec.name })).id;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(repository);
|
|
56
|
+
const match = /^\/api\/git-service\/repos\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\/git\/?$/i.exec(url.pathname);
|
|
57
|
+
if (url.origin === new URL(baseUrl).origin && match)
|
|
58
|
+
return match[1];
|
|
59
|
+
}
|
|
60
|
+
catch { /* Opaque labels/SSH origins are valid preview provenance but cannot attest a release. */ }
|
|
61
|
+
throw new Error('Official Next.js releases require a Git Service repository in this Atlas environment. Push the reviewed revision, then run artifact deploy --repo <namespace>/<name> --ref main --env <env>.');
|
|
62
|
+
}
|
|
63
|
+
/** A verified repository build names a commit; anything else uploads the committed source snapshot. */
|
|
64
|
+
function nextBuildRequestBody({ manifest, files, commit, provenance, repository, repositoryId, channel }) {
|
|
65
|
+
if (repositoryId)
|
|
66
|
+
return { repositoryId, commit, idempotencyKey: randomUUID(), channel };
|
|
67
|
+
return {
|
|
68
|
+
manifest,
|
|
69
|
+
files: files.map(({ path, content, encoding }) => ({ path, content, encoding: encoding ?? 'utf8' })),
|
|
70
|
+
git: { commit, ...(provenance.gitBranch ? { branch: provenance.gitBranch } : {}), dirty: provenance.gitDirty !== false, repository },
|
|
71
|
+
idempotencyKey: randomUUID(), channel,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function submitNextBuild({ root, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }) {
|
|
75
|
+
if (!provenance.gitCommit || !/^[a-f0-9]{40}$/i.test(provenance.gitCommit)) {
|
|
76
|
+
throw new Error('Commit the application source before requesting a hosted build. Use artifact dev for uncommitted local development.');
|
|
77
|
+
}
|
|
78
|
+
await validateNextProject({ root, manifest, installed: false });
|
|
79
|
+
const source = await readArtifactStudioSource(root);
|
|
80
|
+
if (channel === 'active' && !repositoryId)
|
|
81
|
+
throw new Error('An official release requires a verified Git Service repository.');
|
|
82
|
+
const path = `/api/artifact-studio/projects/${encodeURIComponent(projectId)}/next`;
|
|
83
|
+
const body = nextBuildRequestBody({ manifest, files: source.files, commit: provenance.gitCommit, provenance, repository, repositoryId, channel });
|
|
84
|
+
const response = operationSchema.parse(await postJson({ baseUrl, token: await getToken(), path: `${path}/builds${repositoryId ? '/from-repository' : ''}`, body }));
|
|
85
|
+
console.log(`[seq-studio] Atlas build ${response.operation.id} (${channel}) from ${provenance.gitCommit.slice(0, 12)}`);
|
|
86
|
+
console.log(`${baseUrl}/artifact-studio/projects/${projectId}`);
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const onSignal = () => controller.abort();
|
|
89
|
+
process.once('SIGINT', onSignal);
|
|
90
|
+
process.once('SIGTERM', onSignal);
|
|
91
|
+
let current = response;
|
|
92
|
+
let previousStatus = '';
|
|
93
|
+
try {
|
|
94
|
+
while (!controller.signal.aborted) {
|
|
95
|
+
if (current.operation.status !== previousStatus) {
|
|
96
|
+
previousStatus = current.operation.status;
|
|
97
|
+
console.log(`[seq-studio] ${previousStatus}`);
|
|
98
|
+
}
|
|
99
|
+
if (current.operation.status === 'failed' || current.operation.status === 'cancelled') {
|
|
100
|
+
const error = current.operation.error;
|
|
101
|
+
throw new Error(typeof error === 'string' ? error : error?.message ?? `Atlas build ${current.operation.status}. Inspect its build logs in Artifact Studio.`);
|
|
102
|
+
}
|
|
103
|
+
if (current.operation.status === 'succeeded') {
|
|
104
|
+
const completed = { ...current, deployment: current.deployment ?? response.deployment };
|
|
105
|
+
if (out) {
|
|
106
|
+
await mkdir(dirname(out), { recursive: true });
|
|
107
|
+
await writeFile(out, JSON.stringify(completed, null, 2) + '\n');
|
|
108
|
+
}
|
|
109
|
+
console.log(channel === 'active' ? '[seq-studio] application deployed by Atlas.' : '[seq-studio] hosted preview ready in Artifact Studio.');
|
|
110
|
+
if (completed.deployment)
|
|
111
|
+
console.log(`${baseUrl}/artifact-studio/projects/${projectId}/deployments/${completed.deployment.id}`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await delay(1500, undefined, { signal: controller.signal });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
current = operationSchema.parse(await getJson({ baseUrl, token: await getToken(), path: `${path}/operations/${encodeURIComponent(response.operation.id)}` }));
|
|
121
|
+
}
|
|
122
|
+
console.log('[seq-studio] Stopped waiting. The Atlas build continues; inspect or cancel it in Artifact Studio.');
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
process.removeListener('SIGINT', onSignal);
|
|
127
|
+
process.removeListener('SIGTERM', onSignal);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type CacheLookup, type NextCacheNamespace, type NextCacheStore } from './store.js';
|
|
2
|
+
export interface NextCacheOptions {
|
|
3
|
+
store: NextCacheStore;
|
|
4
|
+
namespace: NextCacheNamespace;
|
|
5
|
+
maxEntryBytes?: number;
|
|
6
|
+
maxEntryAgeMs?: number;
|
|
7
|
+
streamTimeoutMs?: number;
|
|
8
|
+
/** Framework timestamps use performance.timeOrigin + performance.now(). */
|
|
9
|
+
now?: () => number;
|
|
10
|
+
onCacheError?: (event: {
|
|
11
|
+
operation: 'read' | 'write' | 'invalidate';
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ObservedLookup extends CacheLookup {
|
|
15
|
+
observedAt: number;
|
|
16
|
+
}
|
|
17
|
+
/** What a later set() needs from a lookup. Retained tickets never keep the entry payload. */
|
|
18
|
+
export type ObservedWriteTicket = Pick<ObservedLookup, 'ticket' | 'observedAt'>;
|
|
19
|
+
/**
|
|
20
|
+
* The ticket a later set() should use after this lookup. The earliest ticket is kept while the store
|
|
21
|
+
* would still accept it, since its observedAt is the fence against renders that began before it. Once
|
|
22
|
+
* the store has moved past it (an invalidation or another commit changed incarnation, epoch or version,
|
|
23
|
+
* or it outlived MAX_CACHE_WRITE_AGE_MS) a regeneration using it would be rejected, so this lookup's
|
|
24
|
+
* ticket replaces it; its later observedAt then fences out renders that began before the change.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nextWriteTicket({ retained, lookup, now }: {
|
|
27
|
+
retained: ObservedWriteTicket | undefined;
|
|
28
|
+
lookup: ObservedLookup;
|
|
29
|
+
now: number;
|
|
30
|
+
}): ObservedWriteTicket;
|
|
31
|
+
export declare class CacheAdapterRuntime {
|
|
32
|
+
readonly store: NextCacheStore;
|
|
33
|
+
readonly namespace: NextCacheNamespace;
|
|
34
|
+
readonly maxEntryBytes: number;
|
|
35
|
+
readonly maxEntryAgeMs: number;
|
|
36
|
+
readonly streamTimeoutMs: number;
|
|
37
|
+
readonly now: () => number;
|
|
38
|
+
private readonly onCacheError?;
|
|
39
|
+
constructor(options: NextCacheOptions);
|
|
40
|
+
report(operation: 'read' | 'write' | 'invalidate'): void;
|
|
41
|
+
read({ key, tags }: {
|
|
42
|
+
key: string;
|
|
43
|
+
tags: string[];
|
|
44
|
+
}): Promise<ObservedLookup | undefined>;
|
|
45
|
+
invalidate({ tags, durations }: {
|
|
46
|
+
tags: string[];
|
|
47
|
+
durations?: {
|
|
48
|
+
expire?: number;
|
|
49
|
+
};
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { cacheNamespacePrefix, MAX_CACHE_ENTRY_AGE_MS, MAX_CACHE_ENTRY_BYTES, MAX_CACHE_WRITE_AGE_MS, } from './store.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ticket a later set() should use after this lookup. The earliest ticket is kept while the store
|
|
4
|
+
* would still accept it, since its observedAt is the fence against renders that began before it. Once
|
|
5
|
+
* the store has moved past it (an invalidation or another commit changed incarnation, epoch or version,
|
|
6
|
+
* or it outlived MAX_CACHE_WRITE_AGE_MS) a regeneration using it would be rejected, so this lookup's
|
|
7
|
+
* ticket replaces it; its later observedAt then fences out renders that began before the change.
|
|
8
|
+
*/
|
|
9
|
+
export function nextWriteTicket({ retained, lookup, now }) {
|
|
10
|
+
const current = { ticket: lookup.ticket, observedAt: lookup.observedAt };
|
|
11
|
+
if (!retained)
|
|
12
|
+
return current;
|
|
13
|
+
const unchanged = retained.ticket.incarnation === lookup.ticket.incarnation
|
|
14
|
+
&& retained.ticket.epoch === lookup.ticket.epoch && retained.ticket.version === lookup.ticket.version;
|
|
15
|
+
return unchanged && now - retained.ticket.startedAt <= MAX_CACHE_WRITE_AGE_MS ? retained : current;
|
|
16
|
+
}
|
|
17
|
+
export class CacheAdapterRuntime {
|
|
18
|
+
store;
|
|
19
|
+
namespace;
|
|
20
|
+
maxEntryBytes;
|
|
21
|
+
maxEntryAgeMs;
|
|
22
|
+
streamTimeoutMs;
|
|
23
|
+
now;
|
|
24
|
+
onCacheError;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
cacheNamespacePrefix(options.namespace);
|
|
27
|
+
this.store = options.store;
|
|
28
|
+
this.namespace = { ...options.namespace };
|
|
29
|
+
this.maxEntryBytes = options.maxEntryBytes ?? MAX_CACHE_ENTRY_BYTES;
|
|
30
|
+
this.maxEntryAgeMs = options.maxEntryAgeMs ?? MAX_CACHE_ENTRY_AGE_MS;
|
|
31
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 10_000;
|
|
32
|
+
if (!Number.isSafeInteger(this.maxEntryBytes) || this.maxEntryBytes < 1 || this.maxEntryBytes > MAX_CACHE_ENTRY_BYTES
|
|
33
|
+
|| !Number.isSafeInteger(this.maxEntryAgeMs) || this.maxEntryAgeMs < 1 || this.maxEntryAgeMs > MAX_CACHE_ENTRY_AGE_MS
|
|
34
|
+
|| !Number.isSafeInteger(this.streamTimeoutMs) || this.streamTimeoutMs < 1 || this.streamTimeoutMs > 60_000) {
|
|
35
|
+
throw new Error('Invalid Next cache adapter limits');
|
|
36
|
+
}
|
|
37
|
+
this.now = options.now ?? (() => performance.timeOrigin + performance.now());
|
|
38
|
+
this.onCacheError = options.onCacheError;
|
|
39
|
+
}
|
|
40
|
+
report(operation) {
|
|
41
|
+
// Observability must not throw or expose payloads, tags, keys or credentials.
|
|
42
|
+
try {
|
|
43
|
+
this.onCacheError?.({ operation });
|
|
44
|
+
}
|
|
45
|
+
catch { /* Ignore telemetry failure. */ }
|
|
46
|
+
}
|
|
47
|
+
async read({ key, tags }) {
|
|
48
|
+
const observedAt = this.now();
|
|
49
|
+
try {
|
|
50
|
+
return { ...await this.store.read({ namespace: this.namespace, key, tags }), observedAt };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.report('read');
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async invalidate({ tags, durations }) {
|
|
58
|
+
try {
|
|
59
|
+
// A duration without expire means stale indefinitely. Entries are bounded by
|
|
60
|
+
// maxEntryAgeMs, so this upper bound preserves that behavior without infinity.
|
|
61
|
+
const expireAfterMs = durations ? Math.min(this.maxEntryAgeMs, (durations.expire ?? this.maxEntryAgeMs / 1000) * 1000) : 0;
|
|
62
|
+
if (durations?.expire !== undefined && (!Number.isFinite(durations.expire) || durations.expire < 0)) {
|
|
63
|
+
throw new Error('Invalid cache invalidation duration');
|
|
64
|
+
}
|
|
65
|
+
await this.store.invalidate({ namespace: this.namespace, tags, expireAfterMs: Math.ceil(expireAfterMs) });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
this.report('invalidate');
|
|
69
|
+
// A mutation must never be reported successful when invalidation is uncertain.
|
|
70
|
+
throw new Error('Next cache invalidation did not complete');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getArtifactNextRuntimeCache } from './runtime.js';
|
|
2
|
+
const handler = {
|
|
3
|
+
get: (key, tags) => getArtifactNextRuntimeCache().cacheComponents.get(key, tags),
|
|
4
|
+
set: (key, pending) => getArtifactNextRuntimeCache().cacheComponents.set(key, pending),
|
|
5
|
+
refreshTags: () => getArtifactNextRuntimeCache().cacheComponents.refreshTags(),
|
|
6
|
+
getExpiration: (tags) => getArtifactNextRuntimeCache().cacheComponents.getExpiration(tags),
|
|
7
|
+
updateTags: (tags, durations) => getArtifactNextRuntimeCache().cacheComponents.updateTags(tags, durations),
|
|
8
|
+
};
|
|
9
|
+
// Next's configuration loader requires a default export from handler modules.
|
|
10
|
+
export { handler as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type NextCacheOptions } from './adapter-runtime.js';
|
|
2
|
+
/** Next.js 16.3 Cache Components contract; no unstable Next internal imports. */
|
|
3
|
+
export interface NextComponentCacheEntry {
|
|
4
|
+
value: ReadableStream<Uint8Array>;
|
|
5
|
+
tags: string[];
|
|
6
|
+
stale: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
expire: number;
|
|
9
|
+
revalidate: number;
|
|
10
|
+
}
|
|
11
|
+
export interface NextComponentCacheHandler {
|
|
12
|
+
get(key: string, softTags: string[]): Promise<NextComponentCacheEntry | undefined>;
|
|
13
|
+
set(key: string, pendingEntry: Promise<NextComponentCacheEntry>): Promise<void>;
|
|
14
|
+
refreshTags(): Promise<void>;
|
|
15
|
+
getExpiration(tags: string[]): Promise<number>;
|
|
16
|
+
updateTags(tags: string[], durations?: {
|
|
17
|
+
expire?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createComponentCacheHandler(options: NextCacheOptions): NextComponentCacheHandler;
|