@sequenceholdings/artifact-studio 0.2.6 → 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/VERSION-PIN.md +8 -1
- package/dist/api.d.ts +11 -1
- package/dist/api.js +26 -9
- package/dist/build.d.ts +2 -2
- package/dist/build.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +105 -10
- 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 +118 -13
- package/dist/manifest.js +12 -119
- 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 +2 -2
- 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/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 +67 -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/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { checkServerIdentity } from 'node:tls';
|
|
3
|
+
import { Compute } from 'google-auth-library';
|
|
4
|
+
import { Redis } from 'ioredis';
|
|
5
|
+
/** A failed EVAL may have committed. Callers must never automatically replay it. */
|
|
6
|
+
export class ValkeyUnavailableError extends Error {
|
|
7
|
+
outcome;
|
|
8
|
+
code = 'CACHE_UNAVAILABLE';
|
|
9
|
+
constructor(outcome = 'not-dispatched') {
|
|
10
|
+
super('Artifact cache is unavailable.');
|
|
11
|
+
this.outcome = outcome;
|
|
12
|
+
this.name = 'ValkeyUnavailableError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const COMMAND_TIMEOUT_MS = 150;
|
|
16
|
+
const CONNECT_TIMEOUT_MS = 1_500;
|
|
17
|
+
const MAX_PENDING = 128;
|
|
18
|
+
const CREDENTIAL_MAX_AGE_MS = 45 * 60_000;
|
|
19
|
+
const CREDENTIAL_EXPIRY_MARGIN_MS = 60_000;
|
|
20
|
+
export function valkeyTlsOptions(connection) {
|
|
21
|
+
return {
|
|
22
|
+
ca: connection.caCertificates,
|
|
23
|
+
servername: isIP(connection.serverIdentity) ? undefined : connection.serverIdentity,
|
|
24
|
+
rejectUnauthorized: true,
|
|
25
|
+
// A literal assertion preserves the Node TLS option's narrow union type.
|
|
26
|
+
minVersion: 'TLSv1.2',
|
|
27
|
+
checkServerIdentity: (_hostname, certificate) => checkServerIdentity(connection.serverIdentity, certificate),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Bounded, multiplexed TLS transport; credentials come from the attached identity. */
|
|
31
|
+
export class ValkeyScriptExecutor {
|
|
32
|
+
connection;
|
|
33
|
+
auth;
|
|
34
|
+
getAccessToken;
|
|
35
|
+
connections = new Set();
|
|
36
|
+
current;
|
|
37
|
+
connecting;
|
|
38
|
+
active = 0;
|
|
39
|
+
closed = false;
|
|
40
|
+
constructor({ connection, getAccessToken }) {
|
|
41
|
+
if (!connection.host || !connection.serverIdentity
|
|
42
|
+
|| !Number.isInteger(connection.port) || connection.port < 1 || connection.port > 65_535
|
|
43
|
+
|| connection.caCertificates.length === 0 || connection.caCertificates.some((ca) => !ca.trim())) {
|
|
44
|
+
throw new ValkeyUnavailableError();
|
|
45
|
+
}
|
|
46
|
+
this.connection = { ...connection, caCertificates: [...connection.caCertificates] };
|
|
47
|
+
if (getAccessToken) {
|
|
48
|
+
this.getAccessToken = getAccessToken;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Compute uses metadata-server credentials, never developer ADC or a key file.
|
|
52
|
+
this.auth = new Compute({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] });
|
|
53
|
+
const auth = this.auth;
|
|
54
|
+
this.getAccessToken = async () => {
|
|
55
|
+
const access = await auth.getAccessToken();
|
|
56
|
+
if (!access.token)
|
|
57
|
+
throw new ValkeyUnavailableError();
|
|
58
|
+
return access.token;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async eval(input) {
|
|
63
|
+
if (this.closed || this.active >= MAX_PENDING)
|
|
64
|
+
throw new ValkeyUnavailableError();
|
|
65
|
+
this.active++;
|
|
66
|
+
let connection;
|
|
67
|
+
let dispatched = false;
|
|
68
|
+
try {
|
|
69
|
+
connection = await this.acquireConnection();
|
|
70
|
+
if (this.closed || connection.client.status !== 'ready')
|
|
71
|
+
throw new ValkeyUnavailableError();
|
|
72
|
+
connection.pending++;
|
|
73
|
+
dispatched = true;
|
|
74
|
+
return await connection.client.eval(input.script, input.keys.length, ...input.keys, ...input.arguments);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
if (connection)
|
|
78
|
+
this.disconnect(connection);
|
|
79
|
+
// Never chain raw library errors: they can include credentials and endpoints.
|
|
80
|
+
throw new ValkeyUnavailableError(dispatched ? 'unknown' : 'not-dispatched');
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (connection && dispatched) {
|
|
84
|
+
connection.pending--;
|
|
85
|
+
if (connection.retired && connection.pending === 0)
|
|
86
|
+
this.disconnect(connection);
|
|
87
|
+
}
|
|
88
|
+
this.active--;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
this.closed = true;
|
|
93
|
+
for (const connection of this.connections)
|
|
94
|
+
this.disconnect(connection);
|
|
95
|
+
}
|
|
96
|
+
disconnect(connection) {
|
|
97
|
+
connection.retired = true;
|
|
98
|
+
connection.client.disconnect();
|
|
99
|
+
this.connections.delete(connection);
|
|
100
|
+
if (this.current === connection)
|
|
101
|
+
this.current = undefined;
|
|
102
|
+
}
|
|
103
|
+
async acquireConnection() {
|
|
104
|
+
if (this.current?.client.status === 'ready' && Date.now() < this.current.refreshAt)
|
|
105
|
+
return this.current;
|
|
106
|
+
if (this.current) {
|
|
107
|
+
const previous = this.current;
|
|
108
|
+
this.current = undefined;
|
|
109
|
+
previous.retired = true;
|
|
110
|
+
// Token refresh must not interrupt unrelated in-flight cache operations.
|
|
111
|
+
if (previous.pending === 0)
|
|
112
|
+
this.disconnect(previous);
|
|
113
|
+
}
|
|
114
|
+
if (this.connecting)
|
|
115
|
+
return this.connecting;
|
|
116
|
+
const attempt = this.openConnection().then((connection) => {
|
|
117
|
+
this.current = connection;
|
|
118
|
+
return connection;
|
|
119
|
+
});
|
|
120
|
+
this.connecting = attempt;
|
|
121
|
+
try {
|
|
122
|
+
return await attempt;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
if (this.connecting === attempt)
|
|
126
|
+
this.connecting = undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async openConnection() {
|
|
130
|
+
let connection;
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
let timer;
|
|
133
|
+
const attempt = (async () => {
|
|
134
|
+
const token = await this.getAccessToken();
|
|
135
|
+
if (cancelled || this.closed || !token)
|
|
136
|
+
throw new ValkeyUnavailableError();
|
|
137
|
+
const refreshAt = Math.min(Date.now() + CREDENTIAL_MAX_AGE_MS, (this.auth?.credentials.expiry_date ?? Infinity) - CREDENTIAL_EXPIRY_MARGIN_MS);
|
|
138
|
+
if (refreshAt <= Date.now())
|
|
139
|
+
throw new ValkeyUnavailableError();
|
|
140
|
+
connection = {
|
|
141
|
+
client: this.createClient({ token }), refreshAt, pending: 0, retired: false,
|
|
142
|
+
};
|
|
143
|
+
this.connections.add(connection);
|
|
144
|
+
await connection.client.connect();
|
|
145
|
+
if (cancelled || this.closed)
|
|
146
|
+
throw new ValkeyUnavailableError();
|
|
147
|
+
return connection;
|
|
148
|
+
})();
|
|
149
|
+
try {
|
|
150
|
+
return await Promise.race([
|
|
151
|
+
attempt,
|
|
152
|
+
new Promise((_, reject) => {
|
|
153
|
+
timer = setTimeout(() => reject(new ValkeyUnavailableError()), CONNECT_TIMEOUT_MS);
|
|
154
|
+
}),
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
cancelled = true;
|
|
159
|
+
if (connection)
|
|
160
|
+
this.disconnect(connection);
|
|
161
|
+
throw new ValkeyUnavailableError();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
createClient({ token }) {
|
|
168
|
+
const client = new Redis({
|
|
169
|
+
host: this.connection.host,
|
|
170
|
+
port: this.connection.port,
|
|
171
|
+
username: 'default',
|
|
172
|
+
password: token,
|
|
173
|
+
tls: valkeyTlsOptions(this.connection),
|
|
174
|
+
lazyConnect: true,
|
|
175
|
+
connectTimeout: CONNECT_TIMEOUT_MS,
|
|
176
|
+
commandTimeout: COMMAND_TIMEOUT_MS,
|
|
177
|
+
enableOfflineQueue: false,
|
|
178
|
+
enableReadyCheck: false,
|
|
179
|
+
retryStrategy: () => null,
|
|
180
|
+
maxRetriesPerRequest: 0,
|
|
181
|
+
autoResendUnfulfilledCommands: false,
|
|
182
|
+
autoResubscribe: false,
|
|
183
|
+
});
|
|
184
|
+
client.on('error', () => {
|
|
185
|
+
// Sanitized at the operation boundary; never log provider errors here.
|
|
186
|
+
});
|
|
187
|
+
return client;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const devLeaseSchema: z.ZodObject<{
|
|
3
|
+
session: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
status: z.ZodString;
|
|
6
|
+
previewUrl: z.ZodString;
|
|
7
|
+
publicOrigin: z.ZodString;
|
|
8
|
+
expiresAt: z.ZodISODateTime;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
tunnel: z.ZodObject<{
|
|
11
|
+
url: z.ZodString;
|
|
12
|
+
token: z.ZodString;
|
|
13
|
+
epoch: z.ZodNumber;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
runtime: z.ZodObject<{
|
|
16
|
+
environment: z.ZodString;
|
|
17
|
+
atlasUrl: z.ZodString;
|
|
18
|
+
sessionToken: z.ZodString;
|
|
19
|
+
publicKeys: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
20
|
+
bindings: z.ZodOptional<z.ZodUnknown>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type NextDevLease = z.infer<typeof devLeaseSchema>;
|
|
24
|
+
export declare function isTransientNextControlFailure(error: unknown): boolean;
|
|
25
|
+
export declare function requestNextDevLease({ baseUrl, token, projectId, sessionId, body, environment, signal }: {
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
token: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
body: unknown;
|
|
31
|
+
environment: string;
|
|
32
|
+
/** Aborted when artifact dev shuts down, so a stalled request cannot delay revocation and cleanup. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}): Promise<NextDevLease>;
|
|
35
|
+
export declare function writeNextRuntimeFile({ path, projectId, lease }: {
|
|
36
|
+
path: string;
|
|
37
|
+
projectId: string;
|
|
38
|
+
lease: NextDevLease;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
export declare function renewNextDevLease({ baseUrl, token, projectId, lease, status, signal }: {
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
token: string;
|
|
43
|
+
projectId: string;
|
|
44
|
+
lease: NextDevLease;
|
|
45
|
+
status: string;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { chmod, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ArtifactApiError, postJson } from './api.js';
|
|
4
|
+
import { validateNextPublicOrigin } from './next-project.js';
|
|
5
|
+
const devLeaseSchema = z.object({
|
|
6
|
+
session: z.object({
|
|
7
|
+
id: z.string().min(1), status: z.string(), previewUrl: z.string().url(),
|
|
8
|
+
publicOrigin: z.string().url(), expiresAt: z.iso.datetime(),
|
|
9
|
+
}),
|
|
10
|
+
tunnel: z.object({ url: z.string().url(), token: z.string().min(1), epoch: z.number().int().nonnegative() }),
|
|
11
|
+
runtime: z.object({
|
|
12
|
+
environment: z.string().min(1), atlasUrl: z.string().url(), sessionToken: z.string().min(16),
|
|
13
|
+
publicKeys: z.record(z.string(), z.string()), bindings: z.unknown().optional(),
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
export function isTransientNextControlFailure(error) {
|
|
17
|
+
if (error instanceof ArtifactApiError)
|
|
18
|
+
return error.status === 429 || error.status >= 500;
|
|
19
|
+
return error instanceof TypeError && /fetch failed|network|connection/i.test(error.message);
|
|
20
|
+
}
|
|
21
|
+
export async function requestNextDevLease({ baseUrl, token, projectId, sessionId, body, environment, signal }) {
|
|
22
|
+
const root = `/api/artifact-studio/projects/${encodeURIComponent(projectId)}/next/dev-sessions`;
|
|
23
|
+
const path = sessionId ? `${root}/${encodeURIComponent(sessionId)}/lease` : root;
|
|
24
|
+
// Session creation/epoch rotation cannot replay after an ambiguous response.
|
|
25
|
+
const lease = devLeaseSchema.parse(await postJson({ baseUrl, token, path, body, retry503: false, signal }));
|
|
26
|
+
validateNextPublicOrigin({ value: lease.session.publicOrigin, environment });
|
|
27
|
+
if (sessionId && lease.session.id !== sessionId)
|
|
28
|
+
throw new Error('Atlas returned credentials for a different development session.');
|
|
29
|
+
// CLI aliases (including "local") need not equal the deployment's APP_ENV.
|
|
30
|
+
// Bind to the authenticated Atlas origin; the SDK verifies the server's
|
|
31
|
+
// environment identifier against the signed request context.
|
|
32
|
+
if (new URL(lease.runtime.atlasUrl).origin !== new URL(baseUrl).origin)
|
|
33
|
+
throw new Error('Atlas returned a runtime credential for a different environment.');
|
|
34
|
+
if (new URL(lease.session.previewUrl).origin !== new URL(baseUrl).origin)
|
|
35
|
+
throw new Error('Atlas returned a preview link outside the selected environment.');
|
|
36
|
+
return lease;
|
|
37
|
+
}
|
|
38
|
+
export async function writeNextRuntimeFile({ path, projectId, lease }) {
|
|
39
|
+
const temporary = `${path}.pending`;
|
|
40
|
+
await writeFile(temporary, JSON.stringify({
|
|
41
|
+
atlasUrl: new URL(lease.runtime.atlasUrl).origin,
|
|
42
|
+
projectId,
|
|
43
|
+
environment: lease.runtime.environment,
|
|
44
|
+
sessionToken: lease.runtime.sessionToken,
|
|
45
|
+
publicKeys: lease.runtime.publicKeys,
|
|
46
|
+
devSessionId: lease.session.id,
|
|
47
|
+
}), { mode: 0o600 });
|
|
48
|
+
await chmod(temporary, 0o600);
|
|
49
|
+
await rename(temporary, path);
|
|
50
|
+
}
|
|
51
|
+
export async function renewNextDevLease({ baseUrl, token, projectId, lease, status, signal }) {
|
|
52
|
+
const response = z.object({ session: z.object({ id: z.string(), expiresAt: z.iso.datetime() }), epoch: z.number().int() }).parse(await postJson({
|
|
53
|
+
baseUrl, token,
|
|
54
|
+
path: `/api/artifact-studio/projects/${encodeURIComponent(projectId)}/next/dev-sessions/${encodeURIComponent(lease.session.id)}/lease`,
|
|
55
|
+
body: { epoch: lease.tunnel.epoch, status },
|
|
56
|
+
retry503: false,
|
|
57
|
+
signal,
|
|
58
|
+
}));
|
|
59
|
+
if (response.session.id !== lease.session.id || response.epoch !== lease.tunnel.epoch) {
|
|
60
|
+
throw new Error('The development session was replaced. Start artifact dev again.');
|
|
61
|
+
}
|
|
62
|
+
lease.session.expiresAt = response.session.expiresAt;
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function selectNextDevPort({ requested }: {
|
|
2
|
+
requested?: string;
|
|
3
|
+
}): Promise<number>;
|
|
4
|
+
export interface NextDevProcess {
|
|
5
|
+
ready: Promise<void>;
|
|
6
|
+
closed: Promise<void>;
|
|
7
|
+
stop(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function openNextPreview(url: string): void;
|
|
10
|
+
export declare function startNextDevProcess({ appRoot, nextBin, port, env, signal, onStatus }: {
|
|
11
|
+
appRoot: string;
|
|
12
|
+
nextBin: string;
|
|
13
|
+
port: number;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
onStatus: (status: 'starting' | 'compiling' | 'ready' | 'failed') => void;
|
|
17
|
+
}): NextDevProcess;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
export async function selectNextDevPort({ requested }) {
|
|
5
|
+
const port = requested === undefined ? 0 : Number(requested);
|
|
6
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535 || (requested !== undefined && port < 1024)) {
|
|
7
|
+
throw new Error('--port must be an integer between 1024 and 65535.');
|
|
8
|
+
}
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const server = createServer();
|
|
11
|
+
server.once('error', reject);
|
|
12
|
+
server.listen(port, '127.0.0.1', () => {
|
|
13
|
+
const address = server.address();
|
|
14
|
+
server.close((error) => {
|
|
15
|
+
if (error)
|
|
16
|
+
reject(error);
|
|
17
|
+
else if (address && typeof address !== 'string')
|
|
18
|
+
resolve(address.port);
|
|
19
|
+
else
|
|
20
|
+
reject(new Error('Unable to reserve a loopback port for Next.js.'));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function openNextPreview(url) {
|
|
26
|
+
if (!process.stdout.isTTY)
|
|
27
|
+
return;
|
|
28
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer.exe' : 'xdg-open';
|
|
29
|
+
const browser = spawn(command, [url], { stdio: 'ignore', detached: true });
|
|
30
|
+
browser.once('error', () => console.warn('[seq-studio] Open the printed Atlas preview link in your browser.'));
|
|
31
|
+
browser.unref();
|
|
32
|
+
}
|
|
33
|
+
function stopChild(child, signal) {
|
|
34
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
if (process.platform === 'win32')
|
|
38
|
+
child.kill(signal);
|
|
39
|
+
else
|
|
40
|
+
process.kill(-child.pid, signal);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH'))
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function startNextDevProcess({ appRoot, nextBin, port, env, signal, onStatus }) {
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
const child = spawn(process.execPath, [nextBin, 'dev', '--webpack', '--hostname', '127.0.0.1', '--port', String(port)], {
|
|
50
|
+
cwd: appRoot, env, stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32',
|
|
51
|
+
});
|
|
52
|
+
let isReady = false;
|
|
53
|
+
let stopping = false;
|
|
54
|
+
let resolveReady = () => { };
|
|
55
|
+
let rejectReady = () => { };
|
|
56
|
+
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
57
|
+
// Startup can fail while the caller is waiting for another connection.
|
|
58
|
+
void ready.catch(() => { });
|
|
59
|
+
const startupTimeout = setTimeout(() => {
|
|
60
|
+
rejectReady(new Error('Next.js did not become ready within 90 seconds. Inspect the local compiler output.'));
|
|
61
|
+
stopChild(child, 'SIGTERM');
|
|
62
|
+
}, 90_000);
|
|
63
|
+
const observe = (chunk) => {
|
|
64
|
+
const message = chunk.toString('utf8').replace(/\u001b\[[0-9;]*m/g, '');
|
|
65
|
+
if (/Ready in|✓ Ready/.test(message)) {
|
|
66
|
+
isReady = true;
|
|
67
|
+
clearTimeout(startupTimeout);
|
|
68
|
+
resolveReady();
|
|
69
|
+
onStatus('ready');
|
|
70
|
+
}
|
|
71
|
+
else if (/Compiling/.test(message))
|
|
72
|
+
onStatus('compiling');
|
|
73
|
+
else if (/⨯|Module not found|Failed to compile/.test(message))
|
|
74
|
+
onStatus('failed');
|
|
75
|
+
else if (/Compiled|GET |POST /.test(message) && isReady)
|
|
76
|
+
onStatus('ready');
|
|
77
|
+
};
|
|
78
|
+
child.stdout?.on('data', observe);
|
|
79
|
+
child.stderr?.on('data', observe);
|
|
80
|
+
child.stdout?.pipe(process.stdout, { end: false });
|
|
81
|
+
child.stderr?.pipe(process.stderr, { end: false });
|
|
82
|
+
onStatus('starting');
|
|
83
|
+
const closed = new Promise((resolve, reject) => {
|
|
84
|
+
child.once('error', (error) => { rejectReady(error); reject(error); });
|
|
85
|
+
child.once('close', (code, exitSignal) => {
|
|
86
|
+
clearTimeout(startupTimeout);
|
|
87
|
+
signal.removeEventListener('abort', onAbort);
|
|
88
|
+
if (!isReady)
|
|
89
|
+
rejectReady(new Error('Next.js exited before becoming ready.'));
|
|
90
|
+
if (stopping || signal.aborted) {
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
onStatus('failed');
|
|
95
|
+
reject(new Error(`Next.js exited (${exitSignal ?? code ?? 'unknown'}). Fix the reported error and restart artifact dev.`));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
void closed.catch(() => { });
|
|
99
|
+
async function stop() {
|
|
100
|
+
stopping = true;
|
|
101
|
+
stopChild(child, 'SIGTERM');
|
|
102
|
+
const exited = await Promise.race([closed.then(() => true, () => true), delay(3000).then(() => false)]);
|
|
103
|
+
if (!exited)
|
|
104
|
+
stopChild(child, 'SIGKILL');
|
|
105
|
+
await closed.catch(() => { });
|
|
106
|
+
}
|
|
107
|
+
const onAbort = () => { void stop(); };
|
|
108
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
return { ready, closed, stop };
|
|
110
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ArtifactStudioNextManifest } from './manifest.js';
|
|
2
|
+
export declare function runNextDevelopment({ root, manifest, projectId, baseUrl, environment, previewKey, port: requestedPort, links, getToken }: {
|
|
3
|
+
root: string;
|
|
4
|
+
manifest: ArtifactStudioNextManifest;
|
|
5
|
+
projectId: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
environment: string;
|
|
8
|
+
previewKey: string;
|
|
9
|
+
port?: string;
|
|
10
|
+
links: string[];
|
|
11
|
+
getToken: () => Promise<string>;
|
|
12
|
+
}): Promise<number>;
|