@sequenceholdings/artifact-studio 0.1.5 → 0.1.9
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/dist/api.js +66 -9
- package/dist/auth.js +1 -1
- package/dist/build-subprocess.d.ts +15 -0
- package/dist/build-subprocess.js +65 -0
- package/dist/build-worker.d.ts +1 -0
- package/dist/build-worker.js +28 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +85 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +493 -121
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -1
- package/dist/dev-link.d.ts +60 -0
- package/dist/dev-link.js +155 -0
- package/dist/dev-lock.d.ts +5 -0
- package/dist/dev-lock.js +83 -0
- package/dist/git-clone.d.ts +41 -0
- package/dist/git-clone.js +178 -0
- package/dist/git-service-client.d.ts +32 -0
- package/dist/git-service-client.js +122 -0
- package/dist/hash.d.ts +12 -0
- package/dist/hash.js +13 -1
- package/dist/lockfile-origin.d.ts +6 -0
- package/dist/lockfile-origin.js +41 -0
- package/dist/manifest-custom-roles.d.ts +63 -0
- package/dist/manifest-custom-roles.js +194 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +77 -0
- package/dist/prepare-build.d.ts +21 -0
- package/dist/prepare-build.js +94 -0
- package/dist/project.js +2 -1
- package/dist/sandbox-lint.d.ts +21 -0
- package/dist/sandbox-lint.js +95 -0
- package/dist/sanitize-remote-tree.d.ts +19 -0
- package/dist/sanitize-remote-tree.js +52 -0
- package/dist/sdk.d.ts +49 -0
- package/dist/source-resolver.d.ts +68 -0
- package/dist/source-resolver.js +202 -0
- package/dist/templates/react-vite/CLAUDE.md +18 -7
- package/dist/templates/react-vite/package.json +8 -7
- package/dist/trusted-install.d.ts +13 -0
- package/dist/trusted-install.js +186 -0
- package/dist/watch-loop.d.ts +26 -0
- package/dist/watch-loop.js +31 -0
- package/package.json +29 -9
- package/templates/react-vite/CLAUDE.md +18 -7
- package/templates/react-vite/package.json +8 -7
- package/dist/bin.d.ts +0 -2
- package/dist/bin.js +0 -8
package/dist/api.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
const MAX_503_RETRIES = 5;
|
|
2
2
|
const DEFAULT_RETRY_AFTER_SECONDS = 2;
|
|
3
|
+
/**
|
|
4
|
+
* Extra request headers, supplied by an embedder (e.g. `seq-studio artifact`)
|
|
5
|
+
* via the `ARTIFACT_STUDIO_EXTRA_HEADERS` env var (a JSON object). This is the
|
|
6
|
+
* minimal env-var-driven hook that lets the delegate thread the Cloudflare WAF
|
|
7
|
+
* bypass header (`x-preview-access: <PREVIEW_ACCESS_HEADER>`) when targeting a
|
|
8
|
+
* per-PR preview env. Built-in caller-supplied headers (Authorization,
|
|
9
|
+
* Content-Type) always win over anything here. Malformed JSON is ignored — a
|
|
10
|
+
* bad bypass header should never crash a deploy before the request is even made.
|
|
11
|
+
*/
|
|
12
|
+
function extraHeaders() {
|
|
13
|
+
const raw = process.env.ARTIFACT_STUDIO_EXTRA_HEADERS?.trim();
|
|
14
|
+
if (!raw)
|
|
15
|
+
return {};
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
19
|
+
return {};
|
|
20
|
+
const headers = {};
|
|
21
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
22
|
+
const lower = key.toLowerCase();
|
|
23
|
+
// Prevent case-variant duplicates from being merged by Headers/fetch
|
|
24
|
+
// (e.g. `authorization` + `Authorization` => "a, b"). The client always
|
|
25
|
+
// sets these explicitly below, so ignore any env-supplied variants.
|
|
26
|
+
if (lower === 'authorization' || lower === 'content-type')
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof value === 'string')
|
|
29
|
+
headers[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return headers;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
console.warn('[seq-studio] ignoring malformed ARTIFACT_STUDIO_EXTRA_HEADERS (expected a JSON object)');
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
3
38
|
async function fetchWith503Retry(input, init) {
|
|
4
39
|
let attempt = 0;
|
|
5
40
|
while (true) {
|
|
@@ -8,42 +43,64 @@ async function fetchWith503Retry(input, init) {
|
|
|
8
43
|
return response;
|
|
9
44
|
const retryAfter = Number(response.headers.get('Retry-After')) || DEFAULT_RETRY_AFTER_SECONDS;
|
|
10
45
|
attempt += 1;
|
|
11
|
-
console.warn(`[
|
|
46
|
+
console.warn(`[seq-studio] backend warming up (503); retrying in ${retryAfter}s (attempt ${attempt}/${MAX_503_RETRIES})`);
|
|
12
47
|
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
13
48
|
}
|
|
14
49
|
}
|
|
15
50
|
export async function getJson({ baseUrl, token, path }) {
|
|
16
51
|
const response = await fetchWith503Retry(`${baseUrl}${path}`, {
|
|
17
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
52
|
+
headers: { ...extraHeaders(), Authorization: `Bearer ${token}` },
|
|
18
53
|
});
|
|
19
54
|
if (!response.ok)
|
|
20
|
-
throw new Error(await responseError(response));
|
|
55
|
+
throw new Error(await responseError('GET', path, response));
|
|
21
56
|
return response.json();
|
|
22
57
|
}
|
|
23
58
|
export async function getJsonOr404({ baseUrl, token, path }) {
|
|
24
59
|
const response = await fetchWith503Retry(`${baseUrl}${path}`, {
|
|
25
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
60
|
+
headers: { ...extraHeaders(), Authorization: `Bearer ${token}` },
|
|
26
61
|
});
|
|
27
62
|
if (response.status === 404)
|
|
28
63
|
return null;
|
|
29
64
|
if (!response.ok)
|
|
30
|
-
throw new Error(await responseError(response));
|
|
65
|
+
throw new Error(await responseError('GET', path, response));
|
|
31
66
|
return response.json();
|
|
32
67
|
}
|
|
33
68
|
export async function postJson({ baseUrl, token, path, body, }) {
|
|
34
69
|
const response = await fetchWith503Retry(`${baseUrl}${path}`, {
|
|
35
70
|
method: 'POST',
|
|
36
71
|
headers: {
|
|
72
|
+
...extraHeaders(),
|
|
37
73
|
Authorization: `Bearer ${token}`,
|
|
38
74
|
'Content-Type': 'application/json',
|
|
39
75
|
},
|
|
40
76
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
41
77
|
});
|
|
42
78
|
if (!response.ok)
|
|
43
|
-
throw new Error(await responseError(response));
|
|
79
|
+
throw new Error(await responseError('POST', path, response));
|
|
44
80
|
return response.json();
|
|
45
81
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Build an actionable error message from a failed response: which request
|
|
84
|
+
* failed, with what status, and what the server said. A deploy runs several
|
|
85
|
+
* requests (project lookup, relink by slug, deployment upload), so a bare
|
|
86
|
+
* body like "Internal server error" with no request context is undebuggable.
|
|
87
|
+
* Prefers structured `{detail}`/`{error}` bodies; falls back to the raw body
|
|
88
|
+
* text (truncated — error pages can be huge) so an unexpected shape is never
|
|
89
|
+
* swallowed.
|
|
90
|
+
*/
|
|
91
|
+
async function responseError(method, path, response) {
|
|
92
|
+
const text = await response.text().catch(() => '');
|
|
93
|
+
let serverMessage = null;
|
|
94
|
+
try {
|
|
95
|
+
const body = JSON.parse(text);
|
|
96
|
+
if (typeof body.detail === 'string' && body.detail)
|
|
97
|
+
serverMessage = body.detail;
|
|
98
|
+
else if (typeof body.error === 'string' && body.error)
|
|
99
|
+
serverMessage = body.error;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Non-JSON body (HTML error page, plain text) — fall back to raw text.
|
|
103
|
+
}
|
|
104
|
+
const summary = serverMessage ?? (text.trim() ? text.trim().slice(0, 300) : '(empty response body)');
|
|
105
|
+
return `${method} ${path} failed (HTTP ${response.status}): ${summary}`;
|
|
49
106
|
}
|
package/dist/auth.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function getAccessToken() {
|
|
|
87
87
|
}),
|
|
88
88
|
});
|
|
89
89
|
if (!response.ok) {
|
|
90
|
-
throw new Error('Stored refresh token is no longer valid. Run
|
|
90
|
+
throw new Error('Stored refresh token is no longer valid. Run seqapi login again.');
|
|
91
91
|
}
|
|
92
92
|
const refreshed = await response.json();
|
|
93
93
|
await writeTokenConfig({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ArtifactStudioBuildResult } from './build.js';
|
|
2
|
+
export interface IsolatedBuildOptions {
|
|
3
|
+
/** Override the worker entry script. Primarily for tests. */
|
|
4
|
+
workerPath?: string;
|
|
5
|
+
/** Abort to kill the in-flight build child (e.g. on watcher shutdown). */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run a single artifact build in a short-lived child process and return its
|
|
10
|
+
* result. The child exits as soon as the build finishes, so the OS reclaims
|
|
11
|
+
* the memory that repeated in-process Vite builds otherwise leak. Use this in
|
|
12
|
+
* long-running watch loops; one-shot commands can call
|
|
13
|
+
* `buildArtifactStudioProject` directly.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const DEFAULT_WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'build-worker.js');
|
|
7
|
+
/**
|
|
8
|
+
* Run a single artifact build in a short-lived child process and return its
|
|
9
|
+
* result. The child exits as soon as the build finishes, so the OS reclaims
|
|
10
|
+
* the memory that repeated in-process Vite builds otherwise leak. Use this in
|
|
11
|
+
* long-running watch loops; one-shot commands can call
|
|
12
|
+
* `buildArtifactStudioProject` directly.
|
|
13
|
+
*/
|
|
14
|
+
export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal } = {}) {
|
|
15
|
+
const scratch = await mkdtemp(join(tmpdir(), 'artifact-build-'));
|
|
16
|
+
const outFile = join(scratch, 'result.json');
|
|
17
|
+
try {
|
|
18
|
+
await runWorker({ workerPath, dir, outFile, signal });
|
|
19
|
+
return JSON.parse(await readFile(outFile, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await rm(scratch, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function runWorker({ workerPath, dir, outFile, signal, }) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
reject(new Error('build aborted'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const child = spawn(process.execPath, [workerPath, dir, outFile], {
|
|
32
|
+
stdio: ['ignore', 'inherit', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
child.kill('SIGTERM');
|
|
36
|
+
};
|
|
37
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
38
|
+
let stderr = '';
|
|
39
|
+
child.stderr.on('data', (chunk) => {
|
|
40
|
+
stderr += chunk.toString();
|
|
41
|
+
});
|
|
42
|
+
child.on('error', (error) => {
|
|
43
|
+
signal?.removeEventListener('abort', onAbort);
|
|
44
|
+
reject(error);
|
|
45
|
+
});
|
|
46
|
+
// Use 'close', not 'exit': 'exit' can fire before the stderr pipe has
|
|
47
|
+
// flushed all its 'data' events, which truncates (or drops) a multi-line
|
|
48
|
+
// build-error stack. 'close' is guaranteed to fire only after the stdio
|
|
49
|
+
// streams have fully drained, so `stderr` is complete here.
|
|
50
|
+
child.on('close', (code) => {
|
|
51
|
+
signal?.removeEventListener('abort', onAbort);
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
reject(new Error('build aborted'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (code === 0) {
|
|
57
|
+
if (stderr.trim())
|
|
58
|
+
process.stderr.write(stderr);
|
|
59
|
+
resolve();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
reject(new Error(stderr.trim() || `build worker exited with code ${code ?? 'null'}`));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Child-process entry for a single isolated artifact build.
|
|
2
|
+
//
|
|
3
|
+
// Repeatedly invoking Vite/rolldown's `build()` in one long-lived process
|
|
4
|
+
// leaks retained native + module-graph state that GC cannot reclaim, so a
|
|
5
|
+
// watch loop that builds in-process eventually OOMs. Running each build in a
|
|
6
|
+
// short-lived child that exits afterward hands the leak to the OS: every
|
|
7
|
+
// cycle starts from a clean process and memory stays flat.
|
|
8
|
+
//
|
|
9
|
+
// Usage (invoked by build-subprocess.ts, not by humans):
|
|
10
|
+
// node build-worker.js <projectDir> <outFile>
|
|
11
|
+
// The build result is written as JSON to <outFile>; build errors go to stderr
|
|
12
|
+
// with a non-zero exit code.
|
|
13
|
+
import { writeFile } from 'node:fs/promises';
|
|
14
|
+
import { buildArtifactStudioProject } from './build.js';
|
|
15
|
+
async function main() {
|
|
16
|
+
const [dir, outFile] = process.argv.slice(2);
|
|
17
|
+
if (!dir || !outFile) {
|
|
18
|
+
process.stderr.write('usage: build-worker <projectDir> <outFile>\n');
|
|
19
|
+
process.exitCode = 2;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const result = await buildArtifactStudioProject(dir);
|
|
23
|
+
await writeFile(outFile, JSON.stringify(result), 'utf8');
|
|
24
|
+
}
|
|
25
|
+
main().catch((error) => {
|
|
26
|
+
process.stderr.write((error instanceof Error ? (error.stack ?? error.message) : String(error)) + '\n');
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|
package/dist/build.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type ArtifactStudioSourceFile } from './project.js';
|
|
2
2
|
import type { ArtifactStudioManifest } from './manifest.js';
|
|
3
|
+
/**
|
|
4
|
+
* Import aliases for the Vite build. Artifacts are built against their own
|
|
5
|
+
* source tree, which usually has no installed node_modules, so every bare
|
|
6
|
+
* import the scaffold relies on is aliased to this package's pinned copy.
|
|
7
|
+
* This also pins a single instance of each library — one React, one
|
|
8
|
+
* react-query QueryClient context — so a duplicate copy can't silently break
|
|
9
|
+
* hooks or the provider/consumer link. Exported so it can be regression-tested
|
|
10
|
+
* (a missing/unresolvable peer must fail loudly).
|
|
11
|
+
*/
|
|
12
|
+
/** Platform + common artifact runtime peers — always aliased over node_modules. */
|
|
13
|
+
export declare const PEER_RESOLVE_ALIAS: Record<string, string>;
|
|
3
14
|
export interface ArtifactStudioBuildResult {
|
|
4
15
|
manifest: ArtifactStudioManifest;
|
|
5
16
|
files: ArtifactStudioSourceFile[];
|
package/dist/build.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
@@ -5,14 +6,58 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
6
|
import { build as viteBuild } from 'vite';
|
|
6
7
|
import { sha256Hex } from './hash.js';
|
|
7
8
|
import { readArtifactStudioSource } from './project.js';
|
|
9
|
+
/** Keep in sync with `atlas/src/server/services/artifact-studio/limits.ts`. */
|
|
10
|
+
const MAX_DEPLOY_SOURCEMAP_BYTES = 10 * 1024 * 1024;
|
|
8
11
|
const cliRequire = createRequire(import.meta.url);
|
|
9
12
|
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
10
13
|
const reactDir = dirname(cliRequire.resolve('react/package.json'));
|
|
11
14
|
const reactDomDir = dirname(cliRequire.resolve('react-dom/package.json'));
|
|
15
|
+
// Artifacts (and the `react-vite` scaffold) import `@tanstack/react-query`
|
|
16
|
+
// directly — they own their QueryClient. The build runs against the artifact's
|
|
17
|
+
// source tree, which usually has no installed node_modules, so alias react-query
|
|
18
|
+
// to this package's pinned copy (same pattern as react/react-dom/atlas-ui). This
|
|
19
|
+
// also guarantees a single react-query instance — one QueryClient context — so a
|
|
20
|
+
// duplicate copy can't silently break the provider/consumer link.
|
|
21
|
+
const reactQueryDir = dirname(cliRequire.resolve('@tanstack/react-query/package.json'));
|
|
22
|
+
const reactRouterDomDir = dirname(cliRequire.resolve('react-router-dom/package.json'));
|
|
23
|
+
const sonnerDir = dirname(cliRequire.resolve('sonner'));
|
|
12
24
|
const atlasUiDir = dirname(cliRequire.resolve('@sequenceholdings/atlas-ui/package.json'));
|
|
13
25
|
const atlasUiDist = resolve(atlasUiDir, 'dist');
|
|
26
|
+
// The shared Lattice form renderer is aliased the same way as atlas-ui (it is
|
|
27
|
+
// an atlas-ui-peer library, prebuilt to dist). Resolved via ./package.json —
|
|
28
|
+
// the package's exports map exposes it precisely so this resolve (and
|
|
29
|
+
// `seq-studio dev --link`) work.
|
|
30
|
+
const formRendererDir = dirname(cliRequire.resolve('@sequenceholdings/lattice-form-renderer/package.json'));
|
|
31
|
+
const formRendererDist = resolve(formRendererDir, 'dist');
|
|
14
32
|
const tailwindCssPath = resolve(dirname(cliRequire.resolve('tailwindcss/package.json')), 'index.css');
|
|
15
|
-
const twAnimateCssPath =
|
|
33
|
+
const twAnimateCssPath = resolveTwAnimateCssPath();
|
|
34
|
+
/**
|
|
35
|
+
* Import aliases for the Vite build. Artifacts are built against their own
|
|
36
|
+
* source tree, which usually has no installed node_modules, so every bare
|
|
37
|
+
* import the scaffold relies on is aliased to this package's pinned copy.
|
|
38
|
+
* This also pins a single instance of each library — one React, one
|
|
39
|
+
* react-query QueryClient context — so a duplicate copy can't silently break
|
|
40
|
+
* hooks or the provider/consumer link. Exported so it can be regression-tested
|
|
41
|
+
* (a missing/unresolvable peer must fail loudly).
|
|
42
|
+
*/
|
|
43
|
+
/** Platform + common artifact runtime peers — always aliased over node_modules. */
|
|
44
|
+
export const PEER_RESOLVE_ALIAS = {
|
|
45
|
+
'react/jsx-runtime': resolve(reactDir, 'jsx-runtime.js'),
|
|
46
|
+
'react/jsx-dev-runtime': resolve(reactDir, 'jsx-dev-runtime.js'),
|
|
47
|
+
react: reactDir,
|
|
48
|
+
'react-dom/client': resolve(reactDomDir, 'client'),
|
|
49
|
+
'react-dom': reactDomDir,
|
|
50
|
+
'@tanstack/react-query': reactQueryDir,
|
|
51
|
+
'react-router-dom': reactRouterDomDir,
|
|
52
|
+
sonner: sonnerDir,
|
|
53
|
+
'@sequenceholdings/atlas-ui/tokens.css': resolve(atlasUiDist, 'tokens.css'),
|
|
54
|
+
'@sequenceholdings/atlas-ui/charts': resolve(atlasUiDist, 'charts.mjs'),
|
|
55
|
+
'@sequenceholdings/atlas-ui': resolve(atlasUiDist, 'index.mjs'),
|
|
56
|
+
// Alias keys prefix-match, so subpath entries MUST precede their bare key
|
|
57
|
+
// (same ordering as tokens.css/charts above the bare atlas-ui key).
|
|
58
|
+
'@sequenceholdings/lattice-form-renderer/golden': resolve(formRendererDist, 'golden.mjs'),
|
|
59
|
+
'@sequenceholdings/lattice-form-renderer': resolve(formRendererDist, 'index.mjs'),
|
|
60
|
+
};
|
|
16
61
|
function isAssetOutput(value) {
|
|
17
62
|
return value.type === 'asset';
|
|
18
63
|
}
|
|
@@ -30,16 +75,7 @@ export async function buildArtifactStudioProject(rootDir) {
|
|
|
30
75
|
(await import('@tailwindcss/vite')).default(),
|
|
31
76
|
],
|
|
32
77
|
resolve: {
|
|
33
|
-
alias:
|
|
34
|
-
'react/jsx-runtime': resolve(reactDir, 'jsx-runtime.js'),
|
|
35
|
-
'react/jsx-dev-runtime': resolve(reactDir, 'jsx-dev-runtime.js'),
|
|
36
|
-
react: reactDir,
|
|
37
|
-
'react-dom/client': resolve(reactDomDir, 'client'),
|
|
38
|
-
'react-dom': reactDomDir,
|
|
39
|
-
'@sequenceholdings/atlas-ui/tokens.css': resolve(atlasUiDist, 'tokens.css'),
|
|
40
|
-
'@sequenceholdings/atlas-ui/charts': resolve(atlasUiDist, 'charts.mjs'),
|
|
41
|
-
'@sequenceholdings/atlas-ui': resolve(atlasUiDist, 'index.mjs'),
|
|
42
|
-
},
|
|
78
|
+
alias: PEER_RESOLVE_ALIAS,
|
|
43
79
|
},
|
|
44
80
|
build: {
|
|
45
81
|
write: false,
|
|
@@ -50,7 +86,6 @@ export async function buildArtifactStudioProject(rootDir) {
|
|
|
50
86
|
input: resolve(root, 'index.html'),
|
|
51
87
|
output: {
|
|
52
88
|
format: 'iife',
|
|
53
|
-
inlineDynamicImports: true,
|
|
54
89
|
name: 'ArtifactStudioApp',
|
|
55
90
|
},
|
|
56
91
|
},
|
|
@@ -62,15 +97,26 @@ export async function buildArtifactStudioProject(rootDir) {
|
|
|
62
97
|
throw new Error('Build produced no JavaScript output');
|
|
63
98
|
const css = collectCss(output);
|
|
64
99
|
const bundle = css ? injectCssRuntime(css) + '\n' + js : js;
|
|
100
|
+
const sourceMap = fitSourceMapForDeploy(collectSourceMap(output));
|
|
65
101
|
return {
|
|
66
102
|
manifest: source.manifest,
|
|
67
103
|
files: source.files,
|
|
68
104
|
sourceHash: source.sourceHash,
|
|
69
105
|
bundle,
|
|
70
|
-
sourceMap
|
|
106
|
+
sourceMap,
|
|
71
107
|
bundleHash: sha256Hex(bundle),
|
|
72
108
|
};
|
|
73
109
|
}
|
|
110
|
+
function fitSourceMapForDeploy(sourceMap) {
|
|
111
|
+
if (!sourceMap)
|
|
112
|
+
return null;
|
|
113
|
+
const bytes = Buffer.byteLength(sourceMap, 'utf8');
|
|
114
|
+
if (bytes <= MAX_DEPLOY_SOURCEMAP_BYTES)
|
|
115
|
+
return sourceMap;
|
|
116
|
+
const mib = (bytes / (1024 * 1024)).toFixed(1);
|
|
117
|
+
console.warn(`[artifact-studio] omitting source map (${mib} MiB exceeds ${MAX_DEPLOY_SOURCEMAP_BYTES / (1024 * 1024)} MiB deploy limit)`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
74
120
|
export async function writeBuildArtifact(result, outPath) {
|
|
75
121
|
const target = resolve(outPath);
|
|
76
122
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -81,6 +127,14 @@ function artifactSdkVirtualModule() {
|
|
|
81
127
|
const resolved = '\0sequence-artifact-sdk';
|
|
82
128
|
return {
|
|
83
129
|
name: 'sequence-artifact-sdk',
|
|
130
|
+
// `enforce: 'pre'` so this virtual module wins over an installed copy in the
|
|
131
|
+
// source tree's node_modules. The deployed bundle always runs against the
|
|
132
|
+
// host-injected `window.seq`, so a tree that happens to be `pnpm install`ed
|
|
133
|
+
// must produce the same bundle as a bare tree (e.g. one materialized from a
|
|
134
|
+
// git repo) — otherwise the same source yields different bytes depending on
|
|
135
|
+
// whether node_modules exists. The package's sole runtime export is `seq`
|
|
136
|
+
// (everything else is type-only), so this stub is a complete substitute.
|
|
137
|
+
enforce: 'pre',
|
|
84
138
|
resolveId(id) {
|
|
85
139
|
return virtualIds.has(id) ? resolved : undefined;
|
|
86
140
|
},
|
|
@@ -110,6 +164,20 @@ function nextStubPlugin() {
|
|
|
110
164
|
},
|
|
111
165
|
};
|
|
112
166
|
}
|
|
167
|
+
function resolveTwAnimateCssPath() {
|
|
168
|
+
const candidates = [
|
|
169
|
+
// Workspace layout: shared/services/artifact-studio/node_modules/...
|
|
170
|
+
resolve(cliDir, '..', 'node_modules', 'tw-animate-css', 'dist', 'tw-animate.css'),
|
|
171
|
+
// pnpm file-dependency layout: .../node_modules/@sequenceholdings/artifact-studio/dist
|
|
172
|
+
// with dependencies hoisted beside the scope directory.
|
|
173
|
+
resolve(cliDir, '..', '..', '..', 'tw-animate-css', 'dist', 'tw-animate.css'),
|
|
174
|
+
];
|
|
175
|
+
const resolved = candidates.find((candidate) => existsSync(candidate));
|
|
176
|
+
if (!resolved) {
|
|
177
|
+
throw new Error(`Unable to resolve tw-animate-css dist file. Tried: ${candidates.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
return resolved;
|
|
180
|
+
}
|
|
113
181
|
function collectEntryJavaScript(output) {
|
|
114
182
|
const entryChunks = output.filter((chunk) => chunk.type === 'chunk' && chunk.isEntry);
|
|
115
183
|
if (entryChunks.length === 0)
|
|
@@ -145,7 +213,10 @@ function atlasUiCssResolverPlugin() {
|
|
|
145
213
|
let result = code;
|
|
146
214
|
if (result.includes('@import "tailwindcss"')) {
|
|
147
215
|
result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
|
|
148
|
-
|
|
216
|
+
// Prebuilt dists whose class names Tailwind must scan: atlas-ui plus
|
|
217
|
+
// the form renderer (field-scope.tsx emits Tailwind utility classes
|
|
218
|
+
// that only appear in the renderer's dist, not the artifact source).
|
|
219
|
+
result = `@source ${JSON.stringify(atlasUiDist)};\n@source ${JSON.stringify(formRendererDist)};\n${result}`;
|
|
149
220
|
}
|
|
150
221
|
if (result.includes('@import "tw-animate-css"')) {
|
|
151
222
|
result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
|
package/dist/cli.d.ts
CHANGED