@sequenceholdings/artifact-studio 0.2.2 → 0.2.3

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 CHANGED
@@ -43,12 +43,15 @@ source (and older rows with null peer fields are rebuilt once to populate them).
43
43
  `seq-studio artifact plan` reports the same decision, so its `action:` line
44
44
  never claims `no-op` for a deploy that would actually rebuild.
45
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.
46
+ ## CSS entry points (sandbox previews)
47
+
48
+ `tokens.css` is the canonical stylesheet and includes semantic roles plus the
49
+ Tailwind utility bridge. The legacy `tokens.v2.css` and `theme.v2.css` paths are
50
+ temporary forwarding aliases; the compiler normalizes their source imports to one
51
+ canonical layer and reports a migration warning. An explicit
52
+ `seq-studio artifact dev --link <atlas-ui-package-dir>` preview additionally
53
+ reports retired numbered black/white opacity utilities and uncertain dynamic
54
+ class construction; ordinary released-CLI builds stay compatible until global
55
+ cutover. The runtime forwards brand variables independently of `data-tokens`.
56
+ Import `tokens.css` for standalone/`vite dev` compatibility. **Do not** import
57
+ `tokens.v2.tenants.css`.
@@ -1,8 +1,8 @@
1
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.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main` with
3
+ * a resolved commit and a clean artifact source directory — whether the source
4
+ * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
5
+ * Preview deploys may use feature branches, dirty trees, or other refs.
6
6
  *
7
7
  * This checks reported provenance (branch name + explicit clean flag + commit).
8
8
  * It does **not** independently verify that `gitCommit` equals the remote
@@ -1,8 +1,8 @@
1
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.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main` with
3
+ * a resolved commit and a clean artifact source directory — whether the source
4
+ * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
5
+ * Preview deploys may use feature branches, dirty trees, or other refs.
6
6
  *
7
7
  * This checks reported provenance (branch name + explicit clean flag + commit).
8
8
  * It does **not** independently verify that `gitCommit` equals the remote
@@ -41,9 +41,12 @@ export function isActiveDeployDefaultBranch(branch) {
41
41
  * Preview callers must not invoke this.
42
42
  */
43
43
  export function assertActiveDeployProvenance(provenance) {
44
- // Require an explicit clean recording — omitting gitDirty must not pass.
44
+ // Require an explicit clean source-directory recording — omitting gitDirty
45
+ // must not pass. The local source resolver scopes this value to the
46
+ // artifact directory, so unrelated changes elsewhere in a containing
47
+ // checkout do not block an official deploy.
45
48
  if (provenance.gitDirty !== false) {
46
- throw new ActiveDeployPolicyError('Active (official) artifact deploys require a clean git tree ' +
49
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require a clean artifact source tree ' +
47
50
  '(`gitDirty: false`). Commit or stash your changes, or use ' +
48
51
  '`seq-studio artifact dev` / a preview deploy for dirty feature-branch work.');
49
52
  }
@@ -1,15 +1,36 @@
1
1
  import type { Plugin } from 'vite';
2
+ export interface AtlasUiResolvedTokenSelection {
3
+ specifier: '@sequenceholdings/atlas-ui/tokens.css';
4
+ injectedBy: string | null;
5
+ }
2
6
  /**
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
+ * Every supported source shape becomes one canonical import before Tailwind
8
+ * expands it. The aliases remain package-compatible, but are no longer allowed
9
+ * to create duplicate token layers in an Artifact Studio bundle (DES-437).
7
10
  */
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, }: {
11
+ export declare function normalizeAtlasUiTokenImports(code: string): {
12
+ code: string;
13
+ deprecatedImports: string[];
14
+ };
15
+ /**
16
+ * Evidence mirrors the CSS transform: aliases are normalized away, so the only
17
+ * selected sheet is the canonical token entry point.
18
+ */
19
+ export declare function collectAtlasUiResolvedTokenSelections({ cssModules, }: {
20
+ cssModules: readonly {
21
+ id: string;
22
+ code: string;
23
+ }[];
24
+ atlasUiDir: string;
25
+ atlasUiDist: string;
26
+ formRendererDist: string | null;
27
+ }): AtlasUiResolvedTokenSelection[];
28
+ export declare function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }: {
12
29
  atlasUiDir: string;
13
30
  atlasUiDist: string;
14
31
  formRendererDist: string | null;
32
+ onTransform?: (cssModule: {
33
+ id: string;
34
+ code: string;
35
+ }) => void;
15
36
  }): Plugin;
@@ -17,36 +17,67 @@ function resolveTwAnimateCssPath() {
17
17
  }
18
18
  return resolved;
19
19
  }
20
+ const CANONICAL_TOKEN_IMPORT = '@import "@sequenceholdings/atlas-ui/tokens.css";';
21
+ const ATLAS_UI_TOKEN_IMPORT = /@import\s+(?:url\(\s*)?(?:["'][^"']*?@sequenceholdings\/atlas-ui\/|[^"'()\s;]*@sequenceholdings\/atlas-ui\/)(tokens\.css|tokens\.v2\.css|theme\.v2\.css)(?:["'])?\s*\)?[^;\n]*;/gi;
22
+ function atlasUiTokenImportSheets(code) {
23
+ return [...code.matchAll(ATLAS_UI_TOKEN_IMPORT)]
24
+ .map((match) => match[1]?.toLowerCase())
25
+ .filter((sheet) => sheet !== undefined);
26
+ }
20
27
  /**
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.
28
+ * Every supported source shape becomes one canonical import before Tailwind
29
+ * expands it. The aliases remain package-compatible, but are no longer allowed
30
+ * to create duplicate token layers in an Artifact Studio bundle (DES-437).
25
31
  */
26
- export function cssAlreadyImportsSheet(code, sheetFileName) {
27
- return code.includes(sheetFileName);
32
+ export function normalizeAtlasUiTokenImports(code) {
33
+ const deprecatedImports = new Set();
34
+ const withoutTokenImports = code.replace(ATLAS_UI_TOKEN_IMPORT, (importLine, sheet) => {
35
+ const normalizedSheet = String(sheet).toLowerCase();
36
+ if (normalizedSheet !== 'tokens.css')
37
+ deprecatedImports.add(normalizedSheet);
38
+ return '';
39
+ });
40
+ return {
41
+ code: insertAfterImports(withoutTokenImports, [CANONICAL_TOKEN_IMPORT]),
42
+ deprecatedImports: [...deprecatedImports],
43
+ };
28
44
  }
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";');
45
+ /**
46
+ * Evidence mirrors the CSS transform: aliases are normalized away, so the only
47
+ * selected sheet is the canonical token entry point.
48
+ */
49
+ export function collectAtlasUiResolvedTokenSelections({ cssModules, }) {
50
+ for (const { code } of cssModules) {
51
+ if (!code.includes('@import "tailwindcss"'))
52
+ continue;
53
+ return [{
54
+ specifier: '@sequenceholdings/atlas-ui/tokens.css',
55
+ injectedBy: atlasUiTokenImportSheets(code).includes('tokens.css') ? null : 'atlas-ui-css-resolver',
56
+ }];
37
57
  }
38
- if (!cssAlreadyImportsSheet(code, 'theme.v2.css')) {
39
- imports.push('@import "@sequenceholdings/atlas-ui/theme.v2.css";');
58
+ return [];
59
+ }
60
+ /** Put Tailwind source directives after the import block but before style rules. */
61
+ function insertAfterImports(code, lines) {
62
+ const importPattern = /@import\s+[^;]+;/gi;
63
+ let lastImport = null;
64
+ for (let match = importPattern.exec(code); match; match = importPattern.exec(code)) {
65
+ lastImport = match;
40
66
  }
41
- return imports;
67
+ const block = `\n${lines.join('\n')}`;
68
+ if (lastImport?.index === undefined)
69
+ return `${lines.join('\n')}\n${code}`;
70
+ const end = lastImport.index + lastImport[0].length;
71
+ return `${code.slice(0, end)}${block}${code.slice(end)}`;
42
72
  }
43
- export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRendererDist, }) {
73
+ export function atlasUiCssResolverPlugin({ atlasUiDir: _atlasUiDir, atlasUiDist, formRendererDist, onTransform, }) {
44
74
  return {
45
75
  name: 'atlas-ui-css-resolver',
46
76
  enforce: 'pre',
47
77
  transform(code, id) {
48
78
  if (!id.endsWith('.css'))
49
79
  return undefined;
80
+ onTransform?.({ id, code });
50
81
  let result = code;
51
82
  if (result.includes('@import "tailwindcss"')) {
52
83
  result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
@@ -57,8 +88,9 @@ export function atlasUiCssResolverPlugin({ atlasUiDir, atlasUiDist, formRenderer
57
88
  if (formRendererDist) {
58
89
  sources.push(`@source ${JSON.stringify(formRendererDist)};`);
59
90
  }
60
- const v2Imports = planAtlasUiV2CssImports(result, atlasUiDir);
61
- result = `${sources.join('\n')}\n${v2Imports.join('\n')}${v2Imports.length > 0 ? '\n' : ''}${result}`;
91
+ const tokenImports = normalizeAtlasUiTokenImports(result);
92
+ result = tokenImports.code;
93
+ result = insertAfterImports(result, sources);
62
94
  }
63
95
  if (result.includes('@import "tw-animate-css"')) {
64
96
  result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
@@ -0,0 +1,50 @@
1
+ import type { ArtifactStudioPeerVersions } from './peer-versions.js';
2
+ import type { AtlasUiResolvedTokenSelection } from './atlas-ui-css-plugin.js';
3
+ export declare const ARTIFACT_BUILD_PROVENANCE_VERSION = 1;
4
+ export interface ArtifactSourceRevision {
5
+ commit: string | null;
6
+ branch: string | null;
7
+ dirty: boolean | null;
8
+ }
9
+ export interface ArtifactTokenResolution {
10
+ specifier: string;
11
+ resolvedPath: string;
12
+ contentHash: string;
13
+ cssLayers: string[];
14
+ injectedBy: string | null;
15
+ }
16
+ /** Observed local build evidence; values are not independently verified by Atlas. */
17
+ export interface ObservedBuildEvidence {
18
+ schemaVersion: typeof ARTIFACT_BUILD_PROVENANCE_VERSION;
19
+ channel: 'active' | 'preview';
20
+ toolchain: {
21
+ cliVersion: string;
22
+ selection: 'published' | 'linked';
23
+ };
24
+ atlasUi: {
25
+ version: string;
26
+ contentHash: string;
27
+ };
28
+ tokenResolution: {
29
+ modelVersion: 1;
30
+ entries: ArtifactTokenResolution[];
31
+ };
32
+ sourceRevision: ArtifactSourceRevision;
33
+ }
34
+ /**
35
+ * Evidence from the exact Atlas UI alias targets selected by this build. The
36
+ * format deliberately records paths and resolved layers, rather than naming a
37
+ * migration generation; future canonical token entry points need no schema
38
+ * change.
39
+ */
40
+ export declare function createObservedBuildEvidence({ atlasUiDir, atlasUiDist, atlasUiTokens, channel, linked, peerVersions, sourceRevision, tokenSelections }: {
41
+ atlasUiDir: string;
42
+ atlasUiDist: string;
43
+ /** Exact tokens.css alias target; releases and linked candidates use different layouts. */
44
+ atlasUiTokens: string;
45
+ channel: ObservedBuildEvidence['channel'];
46
+ linked: boolean;
47
+ peerVersions: ArtifactStudioPeerVersions;
48
+ sourceRevision: ArtifactSourceRevision;
49
+ tokenSelections: readonly AtlasUiResolvedTokenSelection[];
50
+ }): ObservedBuildEvidence;
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { relative } from 'node:path';
3
+ import { sha256Hex } from './hash.js';
4
+ export const ARTIFACT_BUILD_PROVENANCE_VERSION = 1;
5
+ function cssLayers(content) {
6
+ return [...content.matchAll(/@layer\s+([\w-]+)/g)].map((match) => match[1]).filter((x) => x !== undefined);
7
+ }
8
+ function contentIdentity(entries) {
9
+ return sha256Hex(entries.slice().sort((a, b) => a.path.localeCompare(b.path)).map((entry) => `${entry.path}\0${entry.contentHash}`).join('\0'));
10
+ }
11
+ /**
12
+ * Evidence from the exact Atlas UI alias targets selected by this build. The
13
+ * format deliberately records paths and resolved layers, rather than naming a
14
+ * migration generation; future canonical token entry points need no schema
15
+ * change.
16
+ */
17
+ export function createObservedBuildEvidence({ atlasUiDir, atlasUiDist, atlasUiTokens, channel, linked, peerVersions, sourceRevision, tokenSelections }) {
18
+ const knownTargets = new Map([
19
+ ['@sequenceholdings/atlas-ui/tokens.css', { filePath: atlasUiTokens }],
20
+ ['@sequenceholdings/atlas-ui/tokens.v2.css', { filePath: `${atlasUiDir}/tokens.v2.css` }],
21
+ ['@sequenceholdings/atlas-ui/theme.v2.css', { filePath: `${atlasUiDir}/theme.v2.css` }],
22
+ ]);
23
+ const entries = tokenSelections.map((target) => {
24
+ const resolved = knownTargets.get(target.specifier);
25
+ if (!resolved)
26
+ throw new Error(`Unknown Atlas UI token target: ${target.specifier}`);
27
+ const filePath = resolved.filePath;
28
+ const content = readFileSync(filePath, 'utf8');
29
+ return { specifier: target.specifier, resolvedPath: relative(atlasUiDir, filePath), contentHash: sha256Hex(content), cssLayers: cssLayers(content), injectedBy: target.injectedBy };
30
+ });
31
+ const identityEntries = [
32
+ ...entries.map((entry) => ({ path: entry.resolvedPath, contentHash: entry.contentHash })),
33
+ { path: 'dist/index.mjs', contentHash: sha256Hex(readFileSync(`${atlasUiDist}/index.mjs`)) },
34
+ { path: 'dist/charts.mjs', contentHash: sha256Hex(readFileSync(`${atlasUiDist}/charts.mjs`)) },
35
+ ];
36
+ return {
37
+ schemaVersion: ARTIFACT_BUILD_PROVENANCE_VERSION,
38
+ channel,
39
+ toolchain: { cliVersion: peerVersions.cliVersion, selection: linked ? 'linked' : 'published' },
40
+ atlasUi: { version: peerVersions.atlasUiVersion, contentHash: contentIdentity(identityEntries) },
41
+ tokenResolution: { modelVersion: 1, entries },
42
+ sourceRevision,
43
+ };
44
+ }
@@ -6,6 +6,8 @@ export interface IsolatedBuildOptions {
6
6
  signal?: AbortSignal;
7
7
  /** Parent environment override for deterministic security tests. */
8
8
  sourceEnv?: NodeJS.ProcessEnv;
9
+ /** Explicitly report canonical-token compatibility for an Atlas UI --link preview. */
10
+ candidateTokenCompatibility?: boolean;
9
11
  }
10
12
  /**
11
13
  * Run a single artifact build in a short-lived child process and return its
@@ -14,4 +16,4 @@ export interface IsolatedBuildOptions {
14
16
  * long-running watch loops; one-shot commands can call
15
17
  * `buildArtifactStudioProject` directly.
16
18
  */
17
- export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal, sourceEnv }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
19
+ export declare function buildArtifactStudioProjectIsolated(dir: string, { workerPath, signal, sourceEnv, candidateTokenCompatibility }?: IsolatedBuildOptions): Promise<ArtifactStudioBuildResult>;
@@ -17,7 +17,7 @@ const DEFAULT_WORKER_PATH = existsSync(BUILT_WORKER_PATH)
17
17
  * long-running watch loops; one-shot commands can call
18
18
  * `buildArtifactStudioProject` directly.
19
19
  */
20
- export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal, sourceEnv } = {}) {
20
+ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEFAULT_WORKER_PATH, signal, sourceEnv, candidateTokenCompatibility = false } = {}) {
21
21
  const scratch = await mkdtemp(join(tmpdir(), 'artifact-build-'));
22
22
  const outFile = join(scratch, 'result.json');
23
23
  const homeDir = join(scratch, 'home');
@@ -29,6 +29,7 @@ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEF
29
29
  outFile,
30
30
  homeDir,
31
31
  signal,
32
+ candidateTokenCompatibility,
32
33
  ...(sourceEnv ? { sourceEnv } : {}),
33
34
  });
34
35
  return JSON.parse(await readFile(outFile, 'utf8'));
@@ -37,7 +38,7 @@ export async function buildArtifactStudioProjectIsolated(dir, { workerPath = DEF
37
38
  await rm(scratch, { recursive: true, force: true });
38
39
  }
39
40
  }
40
- function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, }) {
41
+ function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, candidateTokenCompatibility, }) {
41
42
  return new Promise((resolve, reject) => {
42
43
  if (signal?.aborted) {
43
44
  reject(new Error('build aborted'));
@@ -46,6 +47,8 @@ function runWorker({ workerPath, dir, outFile, homeDir, signal, sourceEnv, }) {
46
47
  const workerArgs = workerPath.endsWith('.ts')
47
48
  ? ['--import', 'tsx', workerPath, dir, outFile]
48
49
  : [workerPath, dir, outFile];
50
+ if (candidateTokenCompatibility)
51
+ workerArgs.push('--candidate-token-compatibility');
49
52
  const child = spawn(process.execPath, workerArgs, {
50
53
  stdio: ['ignore', 'inherit', 'pipe'],
51
54
  env: createScrubbedChildEnv({
@@ -13,13 +13,15 @@
13
13
  import { writeFile } from 'node:fs/promises';
14
14
  import { buildArtifactStudioProject } from './build.js';
15
15
  async function main() {
16
- const [dir, outFile] = process.argv.slice(2);
16
+ const [dir, outFile, candidateFlag] = process.argv.slice(2);
17
17
  if (!dir || !outFile) {
18
18
  process.stderr.write('usage: build-worker <projectDir> <outFile>\n');
19
19
  process.exitCode = 2;
20
20
  return;
21
21
  }
22
- const result = await buildArtifactStudioProject(dir);
22
+ const result = await buildArtifactStudioProject(dir, {
23
+ candidateTokenCompatibility: candidateFlag === '--candidate-token-compatibility',
24
+ });
23
25
  await writeFile(outFile, JSON.stringify(result), 'utf8');
24
26
  }
25
27
  main().catch((error) => {
package/dist/build.d.ts CHANGED
@@ -1,20 +1,14 @@
1
+ import { type ObservedBuildEvidence } from './build-provenance.js';
1
2
  import { type ArtifactStudioSourceFile } from './project.js';
2
3
  import type { ArtifactStudioManifest } from './manifest.js';
3
4
  import { type ArtifactStudioPeerVersions } from './peer-versions.js';
4
5
  /**
5
- * Import aliases for the Vite build. Artifacts are built against their own
6
- * source tree, which usually has no installed node_modules, so every bare
7
- * import the scaffold relies on is aliased to this package's pinned copy.
8
- * This also pins a single instance of each library — one React, one
9
- * react-query QueryClient context — so a duplicate copy can't silently break
10
- * hooks or the provider/consumer link. Exported so it can be regression-tested
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`.
6
+ * Released Atlas UI 2.0.1 exports its canonical sheet from dist, while the
7
+ * candidate layout puts the generated entry at package root so it can be read
8
+ * before a workspace build. Accept both: ordinary CLI builds retain their
9
+ * released pin, and explicit linked previews prefer the synced root entry.
16
10
  */
17
- /** Platform + common artifact runtime peers — always aliased over node_modules. */
11
+ export declare function resolveAtlasUiTokensPath(packageDir: string): string;
18
12
  export declare const PEER_RESOLVE_ALIAS: Record<string, string>;
19
13
  export interface ArtifactStudioRuntimeChunk {
20
14
  /** Vite/Rollup output path, always under `chunks/`. */
@@ -39,8 +33,16 @@ export interface ArtifactStudioBuildResult {
39
33
  bundleHash: string;
40
34
  /** CLI + force-aliased atlas-ui versions that built this bundle (DES-254). */
41
35
  peerVersions: ArtifactStudioPeerVersions;
36
+ /** Exact CLI-resolved UI/token evidence; deployment adds source revision + channel. */
37
+ observedBuildEvidence: Omit<ObservedBuildEvidence, 'channel' | 'sourceRevision'>;
38
+ /** Source diagnostics that do not prevent a build. */
39
+ diagnostics?: string[];
42
40
  }
43
- export declare function buildArtifactStudioProject(rootDir: string): Promise<ArtifactStudioBuildResult>;
41
+ export declare function buildArtifactStudioProject(rootDir: string, { candidateTokenCompatibility, atlasUiPackageDir, }?: {
42
+ candidateTokenCompatibility?: boolean;
43
+ /** Explicit linked Atlas UI package, used by candidate toolchain hosts. */
44
+ atlasUiPackageDir?: string;
45
+ }): Promise<ArtifactStudioBuildResult>;
44
46
  export declare function writeBuildArtifact(result: ArtifactStudioBuildResult, outPath: string): Promise<void>;
45
47
  export declare function hashBundleAndChunks({ bundle, chunks, }: {
46
48
  bundle: string;
package/dist/build.js CHANGED
@@ -1,14 +1,18 @@
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, relative, resolve } from 'node:path';
4
5
  import { build as viteBuild } from 'vite';
5
- import { atlasUiCssResolverPlugin } from './atlas-ui-css-plugin.js';
6
+ import { atlasUiCssResolverPlugin, collectAtlasUiResolvedTokenSelections } from './atlas-ui-css-plugin.js';
7
+ import { createObservedBuildEvidence } from './build-provenance.js';
6
8
  import { sha256Hex } from './hash.js';
7
9
  import { isArtifactIgnoredPath } from './artifact-ignore.js';
8
10
  import { readArtifactStudioSource } from './project.js';
9
11
  import { normalizeArtifactPath } from './paths.js';
10
12
  import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
11
13
  import { ARTIFACT_RUNTIME_CHUNK_DIR, assertSafeRuntimeChunkFileName } from './runtime-chunks.js';
14
+ import { rejectOutsideSourcePlugin } from './source-boundary.js';
15
+ import { collectCandidateTokenCompatibilityDiagnostics, collectDeprecatedTokenImportDiagnostics, } from './token-contract.js';
12
16
  /** Keep in sync with `atlas/src/server/services/artifact-studio/limits.ts`. */
13
17
  const MAX_DEPLOY_SOURCEMAP_BYTES = 10 * 1024 * 1024;
14
18
  const cliRequire = createRequire(import.meta.url);
@@ -25,6 +29,17 @@ const reactRouterDomDir = dirname(cliRequire.resolve('react-router-dom/package.j
25
29
  const sonnerDir = dirname(cliRequire.resolve('sonner'));
26
30
  const atlasUiDir = dirname(cliRequire.resolve('@sequenceholdings/atlas-ui/package.json'));
27
31
  const atlasUiDist = resolve(atlasUiDir, 'dist');
32
+ /**
33
+ * Released Atlas UI 2.0.1 exports its canonical sheet from dist, while the
34
+ * candidate layout puts the generated entry at package root so it can be read
35
+ * before a workspace build. Accept both: ordinary CLI builds retain their
36
+ * released pin, and explicit linked previews prefer the synced root entry.
37
+ */
38
+ export function resolveAtlasUiTokensPath(packageDir) {
39
+ const rootEntry = resolve(packageDir, 'tokens.css');
40
+ return existsSync(rootEntry) ? rootEntry : resolve(packageDir, 'dist', 'tokens.css');
41
+ }
42
+ const atlasUiTokens = resolveAtlasUiTokensPath(atlasUiDir);
28
43
  // The shared Lattice form renderer is aliased the same way as atlas-ui (it is
29
44
  // an atlas-ui-peer library, prebuilt to dist). Resolved via ./package.json —
30
45
  // the package's exports map exposes it precisely so this resolve (and
@@ -45,53 +60,74 @@ const formRendererDist = resolve(formRendererDir, 'dist');
45
60
  * See `VERSION-PIN.md`.
46
61
  */
47
62
  /** Platform + common artifact runtime peers — always aliased over node_modules. */
48
- export const PEER_RESOLVE_ALIAS = {
49
- 'react/jsx-runtime': resolve(reactDir, 'jsx-runtime.js'),
50
- 'react/jsx-dev-runtime': resolve(reactDir, 'jsx-dev-runtime.js'),
51
- react: reactDir,
52
- 'react-dom/client': resolve(reactDomDir, 'client'),
53
- 'react-dom': reactDomDir,
54
- '@tanstack/react-query': reactQueryDir,
55
- 'react-router-dom': reactRouterDomDir,
56
- sonner: sonnerDir,
57
- // v2 token entry points live at the PACKAGE ROOT, not in dist (the generator
58
- // writes them there and `files` publishes them from there). Without these
59
- // keys the imports fall through to the bare `atlas-ui` alias below — which
60
- // prefix-matches and rewrites them to `<dist>/index.mjs/tokens.v2.css`, a
61
- // path that doesn't exist, so the artifact build fails on a CSS import.
62
- '@sequenceholdings/atlas-ui/tokens.v2.css': resolve(atlasUiDir, 'tokens.v2.css'),
63
- '@sequenceholdings/atlas-ui/theme.v2.css': resolve(atlasUiDir, 'theme.v2.css'),
64
- '@sequenceholdings/atlas-ui/tokens.css': resolve(atlasUiDist, 'tokens.css'),
65
- '@sequenceholdings/atlas-ui/charts': resolve(atlasUiDist, 'charts.mjs'),
66
- '@sequenceholdings/atlas-ui': resolve(atlasUiDist, 'index.mjs'),
67
- // Alias keys prefix-match, so subpath entries MUST precede their bare key
68
- // (same ordering as tokens.css/charts above the bare atlas-ui key).
69
- '@sequenceholdings/lattice-form-renderer/golden': resolve(formRendererDist, 'golden.mjs'),
70
- '@sequenceholdings/lattice-form-renderer': resolve(formRendererDist, 'index.mjs'),
71
- };
63
+ function createPeerResolveAlias({ atlasUiPackageDir = atlasUiDir } = {}) {
64
+ const selectedAtlasUiDist = resolve(atlasUiPackageDir, 'dist');
65
+ const selectedAtlasUiTokens = resolveAtlasUiTokensPath(atlasUiPackageDir);
66
+ return {
67
+ 'react/jsx-runtime': resolve(reactDir, 'jsx-runtime.js'),
68
+ 'react/jsx-dev-runtime': resolve(reactDir, 'jsx-dev-runtime.js'),
69
+ react: reactDir,
70
+ 'react-dom/client': resolve(reactDomDir, 'client'),
71
+ 'react-dom': reactDomDir,
72
+ '@tanstack/react-query': reactQueryDir,
73
+ 'react-router-dom': reactRouterDomDir,
74
+ sonner: sonnerDir,
75
+ // Candidate Atlas UI exposes canonical tokens at the package root; released
76
+ // 2.0.1 keeps them in dist. Preserve explicit alias resolution so legacy
77
+ // source reaches the normalizer instead of falling through to the bare alias.
78
+ '@sequenceholdings/atlas-ui/tokens.v2.css': resolve(atlasUiPackageDir, 'tokens.v2.css'),
79
+ '@sequenceholdings/atlas-ui/theme.v2.css': resolve(atlasUiPackageDir, 'theme.v2.css'),
80
+ '@sequenceholdings/atlas-ui/tokens.css': selectedAtlasUiTokens,
81
+ '@sequenceholdings/atlas-ui/charts': resolve(selectedAtlasUiDist, 'charts.mjs'),
82
+ '@sequenceholdings/atlas-ui': resolve(selectedAtlasUiDist, 'index.mjs'),
83
+ // Alias keys prefix-match, so subpath entries MUST precede their bare key
84
+ // (same ordering as tokens.css/charts above the bare atlas-ui key).
85
+ '@sequenceholdings/lattice-form-renderer/golden': resolve(formRendererDist, 'golden.mjs'),
86
+ '@sequenceholdings/lattice-form-renderer': resolve(formRendererDist, 'index.mjs'),
87
+ };
88
+ }
89
+ export const PEER_RESOLVE_ALIAS = createPeerResolveAlias();
72
90
  function isAssetOutput(value) {
73
91
  return value.type === 'asset';
74
92
  }
75
- export async function buildArtifactStudioProject(rootDir) {
93
+ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompatibility = false, atlasUiPackageDir, } = {}) {
94
+ const peerResolveAlias = atlasUiPackageDir
95
+ ? createPeerResolveAlias({ atlasUiPackageDir })
96
+ : PEER_RESOLVE_ALIAS;
97
+ const selectedAtlasUiDir = atlasUiPackageDir ?? atlasUiDir;
98
+ const selectedAtlasUiDist = resolve(atlasUiPackageDir ?? atlasUiDir, 'dist');
99
+ const selectedAtlasUiTokens = resolveAtlasUiTokensPath(selectedAtlasUiDir);
76
100
  const source = await readArtifactStudioSource(rootDir);
101
+ const diagnostics = [
102
+ ...collectDeprecatedTokenImportDiagnostics({ files: source.files }),
103
+ ...(candidateTokenCompatibility
104
+ ? collectCandidateTokenCompatibilityDiagnostics({ files: source.files })
105
+ : []),
106
+ ];
77
107
  const root = source.root;
108
+ const transformedCssModules = new Map();
78
109
  const result = await viteBuild({
79
110
  root,
80
111
  configFile: false,
81
112
  logLevel: 'silent',
82
113
  plugins: [
114
+ rejectOutsideSourcePlugin({
115
+ root,
116
+ trustedExternalRoots: Object.values(peerResolveAlias).map((target) => dirname(target)),
117
+ }),
83
118
  rejectIgnoredSourcePlugin(root, source.ignorePatterns),
84
119
  artifactSdkVirtualModule(),
85
120
  atlasUiCssResolverPlugin({
86
- atlasUiDir,
87
- atlasUiDist,
121
+ atlasUiDir: selectedAtlasUiDir,
122
+ atlasUiDist: selectedAtlasUiDist,
88
123
  formRendererDist,
124
+ onTransform: ({ id, code }) => transformedCssModules.set(id, code),
89
125
  }),
90
126
  nextStubPlugin(),
91
127
  (await import('@tailwindcss/vite')).default(),
92
128
  ],
93
129
  resolve: {
94
- alias: PEER_RESOLVE_ALIAS,
130
+ alias: peerResolveAlias,
95
131
  },
96
132
  build: {
97
133
  write: false,
@@ -119,6 +155,22 @@ export async function buildArtifactStudioProject(rootDir) {
119
155
  const bundle = css ? injectCssRuntime(css) + '\n' + entry.code : entry.code;
120
156
  const chunks = collectRuntimeChunks(output);
121
157
  const sourceMap = fitSourceMapForDeploy(collectSourceMap(output));
158
+ const peerVersions = resolveArtifactStudioPeerVersions();
159
+ const observedBuildEvidence = createObservedBuildEvidence({
160
+ atlasUiDir: selectedAtlasUiDir,
161
+ atlasUiDist: selectedAtlasUiDist,
162
+ atlasUiTokens: selectedAtlasUiTokens,
163
+ channel: 'active',
164
+ linked: atlasUiPackageDir !== undefined,
165
+ peerVersions,
166
+ sourceRevision: { commit: null, branch: null, dirty: null },
167
+ tokenSelections: collectAtlasUiResolvedTokenSelections({
168
+ cssModules: [...transformedCssModules].map(([id, code]) => ({ id, code })),
169
+ atlasUiDir: selectedAtlasUiDir,
170
+ atlasUiDist: selectedAtlasUiDist,
171
+ formRendererDist,
172
+ }),
173
+ });
122
174
  return {
123
175
  manifest: source.manifest,
124
176
  files: source.files,
@@ -128,7 +180,14 @@ export async function buildArtifactStudioProject(rootDir) {
128
180
  bundleFormat: 'esm',
129
181
  sourceMap,
130
182
  bundleHash: hashBundleAndChunks({ bundle, chunks }),
131
- peerVersions: resolveArtifactStudioPeerVersions(),
183
+ peerVersions,
184
+ diagnostics,
185
+ observedBuildEvidence: {
186
+ schemaVersion: observedBuildEvidence.schemaVersion,
187
+ toolchain: observedBuildEvidence.toolchain,
188
+ atlasUi: observedBuildEvidence.atlasUi,
189
+ tokenResolution: observedBuildEvidence.tokenResolution,
190
+ },
132
191
  };
133
192
  }
134
193
  /**