@sequenceholdings/artifact-studio 0.1.12 → 0.2.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.
Files changed (43) hide show
  1. package/VERSION-PIN.md +54 -0
  2. package/dist/active-deploy-policy.d.ts +36 -0
  3. package/dist/active-deploy-policy.js +66 -0
  4. package/dist/api.js +59 -13
  5. package/dist/artifact-ignore.d.ts +16 -0
  6. package/dist/artifact-ignore.js +38 -0
  7. package/dist/atlas-ui-css-plugin.d.ts +15 -0
  8. package/dist/atlas-ui-css-plugin.js +69 -0
  9. package/dist/build-subprocess.d.ts +3 -1
  10. package/dist/build-subprocess.js +28 -6
  11. package/dist/build.d.ts +25 -0
  12. package/dist/build.js +97 -50
  13. package/dist/child-environment.d.ts +5 -0
  14. package/dist/child-environment.js +83 -0
  15. package/dist/cli.d.ts +40 -1
  16. package/dist/cli.js +173 -87
  17. package/dist/config.d.ts +0 -9
  18. package/dist/config.js +0 -18
  19. package/dist/deployment-validation.d.ts +9 -0
  20. package/dist/deployment-validation.js +70 -0
  21. package/dist/git-clone.js +16 -10
  22. package/dist/peer-versions.d.ts +19 -0
  23. package/dist/peer-versions.js +44 -0
  24. package/dist/prepare-build.d.ts +2 -0
  25. package/dist/prepare-build.js +27 -7
  26. package/dist/project.d.ts +3 -0
  27. package/dist/project.js +22 -6
  28. package/dist/runtime-chunks.d.ts +4 -0
  29. package/dist/runtime-chunks.js +15 -0
  30. package/dist/sdk.d.ts +10 -0
  31. package/dist/source-resolver.d.ts +8 -5
  32. package/dist/source-resolver.js +81 -32
  33. package/dist/templates/react-vite/AGENTS.md +90 -0
  34. package/dist/templates/react-vite/CLAUDE.md +4 -63
  35. package/dist/templates/react-vite/src/main.tsx +7 -1
  36. package/dist/templates/react-vite/src/styles.css +11 -0
  37. package/package.json +18 -5
  38. package/templates/react-vite/AGENTS.md +90 -0
  39. package/templates/react-vite/CLAUDE.md +4 -63
  40. package/templates/react-vite/src/main.tsx +7 -1
  41. package/templates/react-vite/src/styles.css +11 -0
  42. package/dist/auth.d.ts +0 -2
  43. package/dist/auth.js +0 -129
package/VERSION-PIN.md ADDED
@@ -0,0 +1,54 @@
1
+ # CLI atlas-ui stamp (DES-254)
2
+
3
+ A (re)built Artifact Studio app gets whatever `@sequenceholdings/atlas-ui` the
4
+ **building CLI** carries — not a semver the artifact's own `package.json`
5
+ declares.
6
+
7
+ ## What this means
8
+
9
+ `shared/services/artifact-studio/src/build.ts` force-aliases platform peers via
10
+ `PEER_RESOLVE_ALIAS`, including:
11
+
12
+ - `@sequenceholdings/atlas-ui` (+ tokens / charts subpaths)
13
+ - `@sequenceholdings/lattice-form-renderer`
14
+ - `react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`
15
+
16
+ So the deploy/build path always bundles the CLI package's copy. Listing
17
+ `@sequenceholdings/atlas-ui` in an artifact's `devDependencies` is for editor /
18
+ type-check only; it does not select the bundled version.
19
+
20
+ ## Why
21
+
22
+ - One React / atlas-ui instance per bundle (no context-breaking duplicates).
23
+ - No consumer version matrix — artifacts move when rebuilt with a newer CLI
24
+ (or a monorepo checkout whose workspace atlas-ui changed).
25
+ - Matches the product default: artifacts render as platform UI.
26
+
27
+ ## Observability
28
+
29
+ Each deployment records:
30
+
31
+ | Field | Meaning |
32
+ | --- | --- |
33
+ | `cliVersion` | `@sequenceholdings/artifact-studio` version that built the bundle |
34
+ | `atlasUiVersion` | Resolved `@sequenceholdings/atlas-ui` version from that CLI's dependency tree |
35
+
36
+ These feed blast-radius / "who is on which stamp?" questions (DES-259). They are
37
+ `package.json` versions, not content hashes — a `--link`'d local rebuild can
38
+ change code without bumping the version string.
39
+
40
+ `seq-studio artifact deploy --skip-unchanged` compares both `sourceHash` and
41
+ these peer versions before no-op'ing, so a newer CLI still rebuilds identical
42
+ source (and older rows with null peer fields are rebuilt once to populate them).
43
+ `seq-studio artifact plan` reports the same decision, so its `action:` line
44
+ never claims `no-op` for a deploy that would actually rebuild.
45
+
46
+ ## V2 CSS sheets (sandbox previews)
47
+
48
+ `seq-studio artifact build` idempotently injects `tokens.v2.css` + `theme.v2.css`
49
+ when the artifact CSS entry lacks them (iframes do not inherit Atlas CSSOM; the
50
+ host only stamps `data-tokens` / brand vars). Keep listing `tokens.css` for the
51
+ ungated standalone/`vite dev` fallback. **Do not** import `tokens.v2.tenants.css`.
52
+
53
+ **DES-227 cutover:** remove the build inject; make `tokens.css` the v2 default;
54
+ optionally sweep hand-written `@import …/tokens.v2.css` / `theme.v2.css` lines.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Official (active-channel) Artifact Studio deploys must come from a **clean
3
+ * `main` checkout with a resolved commit** — whether the source is a platform
4
+ * git-service repo (`--repo`) or a monorepo / local checkout. Preview deploys
5
+ * may use feature branches, dirty trees, or other refs.
6
+ *
7
+ * This checks reported provenance (branch name + explicit clean flag + commit).
8
+ * It does **not** independently verify that `gitCommit` equals the remote
9
+ * `origin/main` tip — callers still need to merge and pull/materialize `main`
10
+ * before deploying.
11
+ *
12
+ * Canonical module for the CLI (`@sequenceholdings/artifact-studio`). Atlas
13
+ * keeps a behavior-identical copy at
14
+ * `atlas/src/server/services/artifact-studio/active-deploy-policy.ts` so the
15
+ * API can enforce the same rule without depending on this package's heavy
16
+ * build graph — keep the two in sync.
17
+ */
18
+ export interface ActiveDeployProvenance {
19
+ gitCommit?: string | null;
20
+ gitBranch?: string | null;
21
+ gitDirty?: boolean | null;
22
+ }
23
+ /** Canonical default branch name for official artifact deploys. */
24
+ export declare const ACTIVE_DEPLOY_DEFAULT_BRANCH = "main";
25
+ export declare class ActiveDeployPolicyError extends Error {
26
+ readonly code: "ACTIVE_DEPLOY_POLICY";
27
+ constructor(message: string);
28
+ }
29
+ /** Strip `refs/heads/` so `refs/heads/main` and `main` compare equal. */
30
+ export declare function normalizeGitBranchName(branch: string | null | undefined): string | null;
31
+ export declare function isActiveDeployDefaultBranch(branch: string | null | undefined): boolean;
32
+ /**
33
+ * Throw if provenance is not eligible for an official (active) deploy.
34
+ * Preview callers must not invoke this.
35
+ */
36
+ export declare function assertActiveDeployProvenance(provenance: ActiveDeployProvenance): void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Official (active-channel) Artifact Studio deploys must come from a **clean
3
+ * `main` checkout with a resolved commit** — whether the source is a platform
4
+ * git-service repo (`--repo`) or a monorepo / local checkout. Preview deploys
5
+ * may use feature branches, dirty trees, or other refs.
6
+ *
7
+ * This checks reported provenance (branch name + explicit clean flag + commit).
8
+ * It does **not** independently verify that `gitCommit` equals the remote
9
+ * `origin/main` tip — callers still need to merge and pull/materialize `main`
10
+ * before deploying.
11
+ *
12
+ * Canonical module for the CLI (`@sequenceholdings/artifact-studio`). Atlas
13
+ * keeps a behavior-identical copy at
14
+ * `atlas/src/server/services/artifact-studio/active-deploy-policy.ts` so the
15
+ * API can enforce the same rule without depending on this package's heavy
16
+ * build graph — keep the two in sync.
17
+ */
18
+ /** Canonical default branch name for official artifact deploys. */
19
+ export const ACTIVE_DEPLOY_DEFAULT_BRANCH = 'main';
20
+ export class ActiveDeployPolicyError extends Error {
21
+ code = 'ACTIVE_DEPLOY_POLICY';
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'ActiveDeployPolicyError';
25
+ }
26
+ }
27
+ /** Strip `refs/heads/` so `refs/heads/main` and `main` compare equal. */
28
+ export function normalizeGitBranchName(branch) {
29
+ if (branch == null)
30
+ return null;
31
+ const trimmed = branch.trim();
32
+ if (!trimmed)
33
+ return null;
34
+ return trimmed.replace(/^refs\/heads\//, '');
35
+ }
36
+ export function isActiveDeployDefaultBranch(branch) {
37
+ return normalizeGitBranchName(branch) === ACTIVE_DEPLOY_DEFAULT_BRANCH;
38
+ }
39
+ /**
40
+ * Throw if provenance is not eligible for an official (active) deploy.
41
+ * Preview callers must not invoke this.
42
+ */
43
+ export function assertActiveDeployProvenance(provenance) {
44
+ // Require an explicit clean recording — omitting gitDirty must not pass.
45
+ if (provenance.gitDirty !== false) {
46
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require a clean git tree ' +
47
+ '(`gitDirty: false`). Commit or stash your changes, or use ' +
48
+ '`seq-studio artifact dev` / a preview deploy for dirty feature-branch work.');
49
+ }
50
+ const branch = normalizeGitBranchName(provenance.gitBranch);
51
+ if (!branch) {
52
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require git provenance on `main`. ' +
53
+ 'Check out `main` (monorepo or local clone) or deploy with ' +
54
+ '`seq-studio artifact deploy --repo <ns>/<slug>` (defaults to the repo default branch). ' +
55
+ 'Feature branches and detached HEAD (unless at the tip of `main`) are preview-only.');
56
+ }
57
+ if (branch !== ACTIVE_DEPLOY_DEFAULT_BRANCH) {
58
+ throw new ActiveDeployPolicyError(`Active (official) artifact deploys must use \`${ACTIVE_DEPLOY_DEFAULT_BRANCH}\` ` +
59
+ `(got branch/ref \`${branch}\`). Merge via a Pull, then redeploy from \`main\`; ` +
60
+ 'use a preview channel for feature-branch or dirty local work.');
61
+ }
62
+ if (!provenance.gitCommit?.trim()) {
63
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require a resolved git commit on `main`. ' +
64
+ 'Deploy from a git checkout or `--repo` so provenance is recorded.');
65
+ }
66
+ }
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { authenticatedRequestUrl } from './deployment-validation.js';
1
2
  const MAX_503_RETRIES = 5;
2
3
  const DEFAULT_RETRY_AFTER_SECONDS = 2;
3
4
  /**
@@ -47,18 +48,26 @@ async function fetchWith503Retry(input, init) {
47
48
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
48
49
  }
49
50
  }
50
- export async function getJson({ baseUrl, token, path }) {
51
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
52
- headers: { ...extraHeaders(), Authorization: `Bearer ${token}` },
51
+ async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
52
+ const url = authenticatedRequestUrl({ baseUrl, path });
53
+ return fetchWith503Retry(url, {
54
+ ...init,
55
+ redirect: 'manual',
56
+ headers: {
57
+ ...extraHeaders(),
58
+ ...init.headers,
59
+ Authorization: `Bearer ${token}`,
60
+ },
53
61
  });
62
+ }
63
+ export async function getJson({ baseUrl, token, path }) {
64
+ const response = await authenticatedFetch({ baseUrl, token, path });
54
65
  if (!response.ok)
55
66
  throw new Error(await responseError('GET', path, response));
56
67
  return response.json();
57
68
  }
58
69
  export async function getJsonOr404({ baseUrl, token, path }) {
59
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
60
- headers: { ...extraHeaders(), Authorization: `Bearer ${token}` },
61
- });
70
+ const response = await authenticatedFetch({ baseUrl, token, path });
62
71
  if (response.status === 404)
63
72
  return null;
64
73
  if (!response.ok)
@@ -66,14 +75,15 @@ export async function getJsonOr404({ baseUrl, token, path }) {
66
75
  return response.json();
67
76
  }
68
77
  export async function postJson({ baseUrl, token, path, body, }) {
69
- const response = await fetchWith503Retry(`${baseUrl}${path}`, {
70
- method: 'POST',
71
- headers: {
72
- ...extraHeaders(),
73
- Authorization: `Bearer ${token}`,
74
- 'Content-Type': 'application/json',
78
+ const response = await authenticatedFetch({
79
+ baseUrl,
80
+ token,
81
+ path,
82
+ init: {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: body === undefined ? undefined : JSON.stringify(body),
75
86
  },
76
- body: body === undefined ? undefined : JSON.stringify(body),
77
87
  });
78
88
  if (!response.ok)
79
89
  throw new Error(await responseError('POST', path, response));
@@ -97,6 +107,14 @@ async function responseError(method, path, response) {
97
107
  serverMessage = body.detail;
98
108
  else if (typeof body.error === 'string' && body.error)
99
109
  serverMessage = body.error;
110
+ // Zod / registry payloads often put the actionable bits next to a generic
111
+ // detail ("Validation error"). Append a compact summary so deploys aren't
112
+ // undebuggable when the CLI only prints `detail`.
113
+ const extras = summarizeStructuredIssues(body.errors ?? body.issues);
114
+ if (serverMessage && extras)
115
+ serverMessage = `${serverMessage}: ${extras}`;
116
+ else if (!serverMessage && extras)
117
+ serverMessage = extras;
100
118
  }
101
119
  catch {
102
120
  // Non-JSON body (HTML error page, plain text) — fall back to raw text.
@@ -104,3 +122,31 @@ async function responseError(method, path, response) {
104
122
  const summary = serverMessage ?? (text.trim() ? text.trim().slice(0, 300) : '(empty response body)');
105
123
  return `${method} ${path} failed (HTTP ${response.status}): ${summary}`;
106
124
  }
125
+ function summarizeStructuredIssues(value) {
126
+ if (!Array.isArray(value) || value.length === 0)
127
+ return null;
128
+ const parts = [];
129
+ for (const issue of value.slice(0, 5)) {
130
+ // RegistryValidationError ships `issues` as string[]; Zod uses objects.
131
+ if (typeof issue === 'string' && issue.trim()) {
132
+ parts.push(issue.trim());
133
+ continue;
134
+ }
135
+ if (!issue || typeof issue !== 'object')
136
+ continue;
137
+ const row = issue;
138
+ const path = Array.isArray(row.path) ? row.path.map(String).join('.') : '';
139
+ const message = typeof row.message === 'string' ? row.message : null;
140
+ if (message) {
141
+ parts.push(path ? `${path}: ${message}` : message);
142
+ continue;
143
+ }
144
+ if (row.code === 'too_big' && typeof row.maximum === 'number' && path) {
145
+ parts.push(`${path}: at most ${row.maximum}`);
146
+ }
147
+ }
148
+ if (parts.length === 0)
149
+ return null;
150
+ const more = value.length > parts.length ? ` (+${value.length - parts.length} more)` : '';
151
+ return parts.join('; ') + more;
152
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `.artifactignore` — committed trees that must not ship in a deploy payload.
3
+ *
4
+ * Sidecar packs (BI catalogs, local agent workspaces, etc.) can live next to
5
+ * artifact source in git without counting against the API's 500-file / 10MB
6
+ * source caps. Patterns are path prefixes relative to the artifact root
7
+ * (gitignore-lite: comments, blanks, trailing slashes; no globs).
8
+ *
9
+ * Ignored paths are also rejected as Vite build dependencies — they are
10
+ * excluded from sourceHash, so an import under an ignored prefix would let
11
+ * `deploy --skip-unchanged` keep a stale bundle.
12
+ */
13
+ export declare const ARTIFACT_IGNORE_FILE = ".artifactignore";
14
+ export declare function parseArtifactIgnore(contents: string): string[];
15
+ /** True when `relPath` is exactly a pattern or under a patterned directory. */
16
+ export declare function isArtifactIgnoredPath(relPath: string, patterns: readonly string[]): boolean;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `.artifactignore` — committed trees that must not ship in a deploy payload.
3
+ *
4
+ * Sidecar packs (BI catalogs, local agent workspaces, etc.) can live next to
5
+ * artifact source in git without counting against the API's 500-file / 10MB
6
+ * source caps. Patterns are path prefixes relative to the artifact root
7
+ * (gitignore-lite: comments, blanks, trailing slashes; no globs).
8
+ *
9
+ * Ignored paths are also rejected as Vite build dependencies — they are
10
+ * excluded from sourceHash, so an import under an ignored prefix would let
11
+ * `deploy --skip-unchanged` keep a stale bundle.
12
+ */
13
+ export const ARTIFACT_IGNORE_FILE = '.artifactignore';
14
+ export function parseArtifactIgnore(contents) {
15
+ const patterns = [];
16
+ for (const rawLine of contents.split(/\r?\n/)) {
17
+ const line = rawLine.trim();
18
+ if (!line || line.startsWith('#'))
19
+ continue;
20
+ const normalized = line.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
21
+ if (normalized)
22
+ patterns.push(normalized);
23
+ }
24
+ return patterns;
25
+ }
26
+ /** True when `relPath` is exactly a pattern or under a patterned directory. */
27
+ export function isArtifactIgnoredPath(relPath, patterns) {
28
+ if (patterns.length === 0)
29
+ return false;
30
+ const normalized = relPath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
31
+ if (!normalized)
32
+ return false;
33
+ for (const pattern of patterns) {
34
+ if (normalized === pattern || normalized.startsWith(`${pattern}/`))
35
+ return true;
36
+ }
37
+ return false;
38
+ }
@@ -0,0 +1,15 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Idempotent v2 token sheets for Artifact Studio builds. Sandbox iframes do not
4
+ * inherit Atlas CSSOM; the host stamps `data-tokens` (floors to v2) but roles
5
+ * only resolve if the bundle includes these sheets. Skip when the entry already
6
+ * imports them. Remove this inject at DES-227 cutover when `tokens.css` *is* v2.
7
+ */
8
+ export declare function cssAlreadyImportsSheet(code: string, sheetFileName: string): boolean;
9
+ /** Absolute or package `@import` lines to prepend when the CSS entry lacks v2 sheets. */
10
+ export declare function planAtlasUiV2CssImports(code: string, _atlasUiDir: string): string[];
11
+ export declare function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, }: {
12
+ atlasUiDir: string;
13
+ atlasUiDist: string;
14
+ formRendererDist: string | null;
15
+ }): Plugin;
@@ -0,0 +1,69 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const cliRequire = createRequire(import.meta.url);
6
+ const cliDir = dirname(fileURLToPath(import.meta.url));
7
+ const tailwindCssPath = resolve(dirname(cliRequire.resolve('tailwindcss/package.json')), 'index.css');
8
+ const twAnimateCssPath = resolveTwAnimateCssPath();
9
+ function resolveTwAnimateCssPath() {
10
+ const candidates = [
11
+ resolve(cliDir, '..', 'node_modules', 'tw-animate-css', 'dist', 'tw-animate.css'),
12
+ resolve(cliDir, '..', '..', '..', 'tw-animate-css', 'dist', 'tw-animate.css'),
13
+ ];
14
+ const resolved = candidates.find((candidate) => existsSync(candidate));
15
+ if (!resolved) {
16
+ throw new Error(`Unable to resolve tw-animate-css dist file. Tried: ${candidates.join(', ')}`);
17
+ }
18
+ return resolved;
19
+ }
20
+ /**
21
+ * Idempotent v2 token sheets for Artifact Studio builds. Sandbox iframes do not
22
+ * inherit Atlas CSSOM; the host stamps `data-tokens` (floors to v2) but roles
23
+ * only resolve if the bundle includes these sheets. Skip when the entry already
24
+ * imports them. Remove this inject at DES-227 cutover when `tokens.css` *is* v2.
25
+ */
26
+ export function cssAlreadyImportsSheet(code, sheetFileName) {
27
+ return code.includes(sheetFileName);
28
+ }
29
+ /** Absolute or package `@import` lines to prepend when the CSS entry lacks v2 sheets. */
30
+ export function planAtlasUiV2CssImports(code, _atlasUiDir) {
31
+ const imports = [];
32
+ // Package subpaths — resolved via Vite alias (PEER_RESOLVE_ALIAS). Prefer
33
+ // package form over absolute paths so Tailwind/Vite follow the same path as
34
+ // hand-written author imports.
35
+ if (!cssAlreadyImportsSheet(code, 'tokens.v2.css')) {
36
+ imports.push('@import "@sequenceholdings/atlas-ui/tokens.v2.css";');
37
+ }
38
+ if (!cssAlreadyImportsSheet(code, 'theme.v2.css')) {
39
+ imports.push('@import "@sequenceholdings/atlas-ui/theme.v2.css";');
40
+ }
41
+ return imports;
42
+ }
43
+ export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, }) {
44
+ return {
45
+ name: 'atlas-ui-css-resolver',
46
+ enforce: 'pre',
47
+ transform(code, id) {
48
+ if (!id.endsWith('.css'))
49
+ return undefined;
50
+ let result = code;
51
+ if (result.includes('@import "tailwindcss"')) {
52
+ result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
53
+ // Prebuilt dists whose class names Tailwind must scan: CLI-pinned
54
+ // atlas-ui plus the form renderer (field-scope.tsx emits Tailwind
55
+ // utility classes that only appear in the renderer's dist).
56
+ const sources = [`@source ${JSON.stringify(atlasUiDist)};`];
57
+ if (formRendererDist) {
58
+ sources.push(`@source ${JSON.stringify(formRendererDist)};`);
59
+ }
60
+ const v2Imports = planAtlasUiV2CssImports(result, atlasUiDir);
61
+ result = `${sources.join('\n')}\n${v2Imports.join('\n')}${v2Imports.length > 0 ? '\n' : ''}${result}`;
62
+ }
63
+ if (result.includes('@import "tw-animate-css"')) {
64
+ result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
65
+ }
66
+ return result !== code ? result : undefined;
67
+ },
68
+ };
69
+ }
@@ -4,6 +4,8 @@ export interface IsolatedBuildOptions {
4
4
  workerPath?: string;
5
5
  /** Abort to kill the in-flight build child (e.g. on watcher shutdown). */
6
6
  signal?: AbortSignal;
7
+ /** Parent environment override for deterministic security tests. */
8
+ sourceEnv?: NodeJS.ProcessEnv;
7
9
  }
8
10
  /**
9
11
  * Run a single artifact build in a short-lived child process and return its
@@ -12,4 +14,4 @@ export interface IsolatedBuildOptions {
12
14
  * long-running watch loops; one-shot commands can call
13
15
  * `buildArtifactStudioProject` directly.
14
16
  */
15
- export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
17
+ export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal, sourceEnv }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
@@ -1,9 +1,15 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
- const DEFAULT_WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'build-worker.js');
7
+ import { createScrubbedChildEnv } from './child-environment.js';
8
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
9
+ const BUILT_WORKER_PATH = join(MODULE_DIR, 'build-worker.js');
10
+ const DEFAULT_WORKER_PATH = existsSync(BUILT_WORKER_PATH)
11
+ ? BUILT_WORKER_PATH
12
+ : join(MODULE_DIR, 'build-worker.ts');
7
13
  /**
8
14
  * Run a single artifact build in a short-lived child process and return its
9
15
  * result. The child exits as soon as the build finishes, so the OS reclaims
@@ -11,25 +17,41 @@ const DEFAULT_WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'build
11
17
  * long-running watch loops; one-shot commands can call
12
18
  * `buildArtifactStudioProject` directly.
13
19
  */
14
- export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal } = {}) {
20
+ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal, sourceEnv } = {}) {
15
21
  const scratch = await mkdtemp(join(tmpdir(), 'artifact-build-'));
16
22
  const outFile = join(scratch, 'result.json');
23
+ const homeDir = join(scratch, 'home');
17
24
  try {
18
- await runWorker({ workerPath, dir, outFile, signal });
25
+ await mkdir(homeDir, { recursive: true });
26
+ await runWorker({
27
+ workerPath,
28
+ dir,
29
+ outFile,
30
+ homeDir,
31
+ signal,
32
+ ...(sourceEnv ? { sourceEnv } : {}),
33
+ });
19
34
  return JSON.parse(await readFile(outFile, 'utf8'));
20
35
  }
21
36
  finally {
22
37
  await rm(scratch, { recursive: true, force: true });
23
38
  }
24
39
  }
25
- function runWorker({ workerPath, dir, outFile, signal, }) {
40
+ function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, }) {
26
41
  return new Promise((resolve, reject) => {
27
42
  if (signal?.aborted) {
28
43
  reject(new Error('build aborted'));
29
44
  return;
30
45
  }
31
- const child = spawn(process.execPath, [workerPath, dir, outFile], {
46
+ const workerArgs = workerPath.endsWith('.ts')
47
+ ? ['--import', 'tsx', workerPath, dir, outFile]
48
+ : [workerPath, dir, outFile];
49
+ const child = spawn(process.execPath, workerArgs, {
32
50
  stdio: ['ignore', 'inherit', 'pipe'],
51
+ env: createScrubbedChildEnv({
52
+ homeDir,
53
+ ...(sourceEnv ? { source: sourceEnv } : {}),
54
+ }),
33
55
  });
34
56
  const onAbort = () => {
35
57
  child.kill('SIGTERM');
package/dist/build.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ArtifactStudioSourceFile } from './project.js';
2
2
  import type { ArtifactStudioManifest } from './manifest.js';
3
+ import { type ArtifactStudioPeerVersions } from './peer-versions.js';
3
4
  /**
4
5
  * Import aliases for the Vite build. Artifacts are built against their own
5
6
  * source tree, which usually has no installed node_modules, so every bare
@@ -8,16 +9,40 @@ import type { ArtifactStudioManifest } from './manifest.js';
8
9
  * react-query QueryClient context — so a duplicate copy can't silently break
9
10
  * hooks or the provider/consumer link. Exported so it can be regression-tested
10
11
  * (a missing/unresolvable peer must fail loudly).
12
+ *
13
+ * **CLI atlas-ui stamp (DES-254):** the atlas-ui a bundle gets is whatever
14
+ * *this* CLI package resolves — not a semver the artifact declares.
15
+ * See `VERSION-PIN.md`.
11
16
  */
12
17
  /** Platform + common artifact runtime peers — always aliased over node_modules. */
13
18
  export declare const PEER_RESOLVE_ALIAS: Record<string, string>;
19
+ export interface ArtifactStudioRuntimeChunk {
20
+ /** Vite/Rollup output path, always under `chunks/`. */
21
+ fileName: string;
22
+ code: string;
23
+ }
24
+ export type ArtifactStudioBundleFormat = 'esm';
14
25
  export interface ArtifactStudioBuildResult {
15
26
  manifest: ArtifactStudioManifest;
16
27
  files: ArtifactStudioSourceFile[];
28
+ /** Entry module (ESM). Lazy routes land in `chunks`. */
17
29
  bundle: string;
30
+ /**
31
+ * Async/shared sub-chunks emitted by Vite when the graph uses dynamic
32
+ * `import()`. Empty when the entry graph has no split points.
33
+ */
34
+ chunks: ArtifactStudioRuntimeChunk[];
35
+ /** Deploy/serve format. IIFE is gone — it cannot code-split (REP-87). */
36
+ bundleFormat: ArtifactStudioBundleFormat;
18
37
  sourceMap: string | null;
19
38
  sourceHash: string;
20
39
  bundleHash: string;
40
+ /** CLI + force-aliased atlas-ui versions that built this bundle (DES-254). */
41
+ peerVersions: ArtifactStudioPeerVersions;
21
42
  }
22
43
  export declare function buildArtifactStudioProject(rootDir: string): Promise<ArtifactStudioBuildResult>;
23
44
  export declare function writeBuildArtifact(result: ArtifactStudioBuildResult, outPath: string): Promise<void>;
45
+ export declare function hashBundleAndChunks({ bundle, chunks, }: {
46
+ bundle: string;
47
+ chunks: readonly ArtifactStudioRuntimeChunk[];
48
+ }): string;