@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.
Files changed (49) hide show
  1. package/dist/api.js +66 -9
  2. package/dist/auth.js +1 -1
  3. package/dist/build-subprocess.d.ts +15 -0
  4. package/dist/build-subprocess.js +65 -0
  5. package/dist/build-worker.d.ts +1 -0
  6. package/dist/build-worker.js +28 -0
  7. package/dist/build.d.ts +11 -0
  8. package/dist/build.js +85 -14
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.js +493 -121
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.js +4 -1
  13. package/dist/dev-link.d.ts +60 -0
  14. package/dist/dev-link.js +155 -0
  15. package/dist/dev-lock.d.ts +5 -0
  16. package/dist/dev-lock.js +83 -0
  17. package/dist/git-clone.d.ts +41 -0
  18. package/dist/git-clone.js +178 -0
  19. package/dist/git-service-client.d.ts +32 -0
  20. package/dist/git-service-client.js +122 -0
  21. package/dist/hash.d.ts +12 -0
  22. package/dist/hash.js +13 -1
  23. package/dist/lockfile-origin.d.ts +6 -0
  24. package/dist/lockfile-origin.js +41 -0
  25. package/dist/manifest-custom-roles.d.ts +63 -0
  26. package/dist/manifest-custom-roles.js +194 -0
  27. package/dist/manifest.d.ts +31 -0
  28. package/dist/manifest.js +77 -0
  29. package/dist/prepare-build.d.ts +21 -0
  30. package/dist/prepare-build.js +94 -0
  31. package/dist/project.js +2 -1
  32. package/dist/sandbox-lint.d.ts +21 -0
  33. package/dist/sandbox-lint.js +95 -0
  34. package/dist/sanitize-remote-tree.d.ts +19 -0
  35. package/dist/sanitize-remote-tree.js +52 -0
  36. package/dist/sdk.d.ts +49 -0
  37. package/dist/source-resolver.d.ts +68 -0
  38. package/dist/source-resolver.js +202 -0
  39. package/dist/templates/react-vite/CLAUDE.md +18 -7
  40. package/dist/templates/react-vite/package.json +8 -7
  41. package/dist/trusted-install.d.ts +13 -0
  42. package/dist/trusted-install.js +186 -0
  43. package/dist/watch-loop.d.ts +26 -0
  44. package/dist/watch-loop.js +31 -0
  45. package/package.json +29 -9
  46. package/templates/react-vite/CLAUDE.md +18 -7
  47. package/templates/react-vite/package.json +8 -7
  48. package/dist/bin.d.ts +0 -2
  49. 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(`[artifact-studio] backend warming up (503); retrying in ${retryAfter}s (attempt ${attempt}/${MAX_503_RETRIES})`);
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
- async function responseError(response) {
47
- const body = await response.json().catch(() => ({ detail: `HTTP ${response.status}` }));
48
- return body.detail ?? body.error ?? `HTTP ${response.status}`;
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 artifact-studio login again.');
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 = resolve(cliDir, '..', 'node_modules', 'tw-animate-css', 'dist', 'tw-animate.css');
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: collectSourceMap(output),
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
- result = `@source ${JSON.stringify(atlasUiDist)};\n${result}`;
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
@@ -1 +1,2 @@
1
1
  export declare function runCli(argv?: string[]): Promise<number>;
2
+ export declare function setTokenProvider(provider: (() => Promise<string | null>) | null): void;