@sequenceholdings/artifact-studio 0.1.15 → 0.2.1
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 +54 -0
- package/dist/active-deploy-policy.d.ts +36 -0
- package/dist/active-deploy-policy.js +66 -0
- package/dist/api.js +36 -0
- package/dist/artifact-ignore.d.ts +16 -0
- package/dist/artifact-ignore.js +38 -0
- package/dist/atlas-ui-css-plugin.d.ts +15 -0
- package/dist/atlas-ui-css-plugin.js +69 -0
- package/dist/build.d.ts +25 -0
- package/dist/build.js +97 -50
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +85 -24
- package/dist/peer-versions.d.ts +19 -0
- package/dist/peer-versions.js +44 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +22 -6
- package/dist/runtime-chunks.d.ts +4 -0
- package/dist/runtime-chunks.js +15 -0
- package/dist/sdk.d.ts +17 -0
- package/dist/source-resolver.js +26 -2
- package/dist/templates/react-vite/AGENTS.md +90 -0
- package/dist/templates/react-vite/CLAUDE.md +4 -63
- package/dist/templates/react-vite/src/main.tsx +7 -1
- package/dist/templates/react-vite/src/styles.css +7 -4
- package/package.json +9 -4
- package/templates/react-vite/AGENTS.md +90 -0
- package/templates/react-vite/CLAUDE.md +4 -63
- package/templates/react-vite/src/main.tsx +7 -1
- package/templates/react-vite/src/styles.css +7 -4
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
|
@@ -107,6 +107,14 @@ async function responseError(method, path, response) {
|
|
|
107
107
|
serverMessage = body.detail;
|
|
108
108
|
else if (typeof body.error === 'string' && body.error)
|
|
109
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;
|
|
110
118
|
}
|
|
111
119
|
catch {
|
|
112
120
|
// Non-JSON body (HTML error page, plain text) — fall back to raw text.
|
|
@@ -114,3 +122,31 @@ async function responseError(method, path, response) {
|
|
|
114
122
|
const summary = serverMessage ?? (text.trim() ? text.trim().slice(0, 300) : '(empty response body)');
|
|
115
123
|
return `${method} ${path} failed (HTTP ${response.status}): ${summary}`;
|
|
116
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
|
+
}
|
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;
|
package/dist/build.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
2
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
6
4
|
import { build as viteBuild } from 'vite';
|
|
5
|
+
import { atlasUiCssResolverPlugin } from './atlas-ui-css-plugin.js';
|
|
7
6
|
import { sha256Hex } from './hash.js';
|
|
7
|
+
import { isArtifactIgnoredPath } from './artifact-ignore.js';
|
|
8
8
|
import { readArtifactStudioSource } from './project.js';
|
|
9
|
+
import { normalizeArtifactPath } from './paths.js';
|
|
10
|
+
import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
|
|
11
|
+
import { ARTIFACT_RUNTIME_CHUNK_DIR, assertSafeRuntimeChunkFileName } from './runtime-chunks.js';
|
|
9
12
|
/** Keep in sync with `atlas/src/server/services/artifact-studio/limits.ts`. */
|
|
10
13
|
const MAX_DEPLOY_SOURCEMAP_BYTES = 10 * 1024 * 1024;
|
|
11
14
|
const cliRequire = createRequire(import.meta.url);
|
|
12
|
-
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
const reactDir = dirname(cliRequire.resolve('react/package.json'));
|
|
14
16
|
const reactDomDir = dirname(cliRequire.resolve('react-dom/package.json'));
|
|
15
17
|
// Artifacts (and the `react-vite` scaffold) import `@tanstack/react-query`
|
|
@@ -29,8 +31,6 @@ const atlasUiDist = resolve(atlasUiDir, 'dist');
|
|
|
29
31
|
// `seq-studio dev --link`) work.
|
|
30
32
|
const formRendererDir = dirname(cliRequire.resolve('@sequenceholdings/lattice-form-renderer/package.json'));
|
|
31
33
|
const formRendererDist = resolve(formRendererDir, 'dist');
|
|
32
|
-
const tailwindCssPath = resolve(dirname(cliRequire.resolve('tailwindcss/package.json')), 'index.css');
|
|
33
|
-
const twAnimateCssPath = resolveTwAnimateCssPath();
|
|
34
34
|
/**
|
|
35
35
|
* Import aliases for the Vite build. Artifacts are built against their own
|
|
36
36
|
* source tree, which usually has no installed node_modules, so every bare
|
|
@@ -39,6 +39,10 @@ const twAnimateCssPath = resolveTwAnimateCssPath();
|
|
|
39
39
|
* react-query QueryClient context — so a duplicate copy can't silently break
|
|
40
40
|
* hooks or the provider/consumer link. Exported so it can be regression-tested
|
|
41
41
|
* (a missing/unresolvable peer must fail loudly).
|
|
42
|
+
*
|
|
43
|
+
* **CLI atlas-ui stamp (DES-254):** the atlas-ui a bundle gets is whatever
|
|
44
|
+
* *this* CLI package resolves — not a semver the artifact declares.
|
|
45
|
+
* See `VERSION-PIN.md`.
|
|
42
46
|
*/
|
|
43
47
|
/** Platform + common artifact runtime peers — always aliased over node_modules. */
|
|
44
48
|
export const PEER_RESOLVE_ALIAS = {
|
|
@@ -50,6 +54,13 @@ export const PEER_RESOLVE_ALIAS = {
|
|
|
50
54
|
'@tanstack/react-query': reactQueryDir,
|
|
51
55
|
'react-router-dom': reactRouterDomDir,
|
|
52
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'),
|
|
53
64
|
'@sequenceholdings/atlas-ui/tokens.css': resolve(atlasUiDist, 'tokens.css'),
|
|
54
65
|
'@sequenceholdings/atlas-ui/charts': resolve(atlasUiDist, 'charts.mjs'),
|
|
55
66
|
'@sequenceholdings/atlas-ui': resolve(atlasUiDist, 'index.mjs'),
|
|
@@ -69,8 +80,13 @@ export async function buildArtifactStudioProject(rootDir) {
|
|
|
69
80
|
configFile: false,
|
|
70
81
|
logLevel: 'silent',
|
|
71
82
|
plugins: [
|
|
83
|
+
rejectIgnoredSourcePlugin(root, source.ignorePatterns),
|
|
72
84
|
artifactSdkVirtualModule(),
|
|
73
|
-
atlasUiCssResolverPlugin(
|
|
85
|
+
atlasUiCssResolverPlugin({
|
|
86
|
+
atlasUiDir,
|
|
87
|
+
atlasUiDist,
|
|
88
|
+
formRendererDist,
|
|
89
|
+
}),
|
|
74
90
|
nextStubPlugin(),
|
|
75
91
|
(await import('@tailwindcss/vite')).default(),
|
|
76
92
|
],
|
|
@@ -82,29 +98,77 @@ export async function buildArtifactStudioProject(rootDir) {
|
|
|
82
98
|
sourcemap: true,
|
|
83
99
|
cssCodeSplit: false,
|
|
84
100
|
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
|
101
|
+
modulePreload: false,
|
|
85
102
|
rollupOptions: {
|
|
86
103
|
input: resolve(root, 'index.html'),
|
|
87
104
|
output: {
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
// ESM (not IIFE) so dynamic import() can emit async chunks (REP-87).
|
|
106
|
+
format: 'es',
|
|
107
|
+
entryFileNames: 'bundle.js',
|
|
108
|
+
chunkFileNames: `${ARTIFACT_RUNTIME_CHUNK_DIR}/[name]-[hash].js`,
|
|
109
|
+
assetFileNames: 'assets/[name]-[hash][extname]',
|
|
90
110
|
},
|
|
91
111
|
},
|
|
92
112
|
},
|
|
93
113
|
});
|
|
94
114
|
const output = Array.isArray(result) ? result.flatMap((r) => r.output) : result.output;
|
|
95
|
-
const
|
|
96
|
-
if (!
|
|
115
|
+
const entry = collectEntryJavaScript(output);
|
|
116
|
+
if (!entry)
|
|
97
117
|
throw new Error('Build produced no JavaScript output');
|
|
98
118
|
const css = collectCss(output);
|
|
99
|
-
const bundle = css ? injectCssRuntime(css) + '\n' +
|
|
119
|
+
const bundle = css ? injectCssRuntime(css) + '\n' + entry.code : entry.code;
|
|
120
|
+
const chunks = collectRuntimeChunks(output);
|
|
100
121
|
const sourceMap = fitSourceMapForDeploy(collectSourceMap(output));
|
|
101
122
|
return {
|
|
102
123
|
manifest: source.manifest,
|
|
103
124
|
files: source.files,
|
|
104
125
|
sourceHash: source.sourceHash,
|
|
105
126
|
bundle,
|
|
127
|
+
chunks,
|
|
128
|
+
bundleFormat: 'esm',
|
|
106
129
|
sourceMap,
|
|
107
|
-
bundleHash:
|
|
130
|
+
bundleHash: hashBundleAndChunks({ bundle, chunks }),
|
|
131
|
+
peerVersions: resolveArtifactStudioPeerVersions(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* `.artifactignore` drops paths from sourceHash / deploy payloads, but Vite
|
|
136
|
+
* still sees the full tree. Reject any resolved import under an ignored prefix
|
|
137
|
+
* so `deploy --skip-unchanged` cannot silently keep a stale bundle when an
|
|
138
|
+
* ignored-but-imported file changes.
|
|
139
|
+
*/
|
|
140
|
+
function rejectIgnoredSourcePlugin(root, patterns) {
|
|
141
|
+
if (patterns.length === 0) {
|
|
142
|
+
return { name: 'reject-artifact-ignored-noop' };
|
|
143
|
+
}
|
|
144
|
+
const rootResolved = resolve(root);
|
|
145
|
+
return {
|
|
146
|
+
name: 'reject-artifact-ignored',
|
|
147
|
+
enforce: 'pre',
|
|
148
|
+
async resolveId(source, importer, options) {
|
|
149
|
+
const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
|
|
150
|
+
if (!resolved || resolved.external)
|
|
151
|
+
return resolved;
|
|
152
|
+
// Virtual modules (`\0…`) and query suffixes (`?worker`) are not source paths.
|
|
153
|
+
const filePath = resolved.id.split('\0').pop().split('?')[0];
|
|
154
|
+
if (!filePath.startsWith(rootResolved))
|
|
155
|
+
return resolved;
|
|
156
|
+
let rel;
|
|
157
|
+
try {
|
|
158
|
+
rel = normalizeArtifactPath(relative(rootResolved, filePath));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
163
|
+
if (!isArtifactIgnoredPath(rel, patterns))
|
|
164
|
+
return resolved;
|
|
165
|
+
const from = importer
|
|
166
|
+
? ` (imported from ${relative(rootResolved, importer.split('?')[0])})`
|
|
167
|
+
: '';
|
|
168
|
+
throw new Error(`Cannot import "${rel}"${from}: path is listed in .artifactignore. ` +
|
|
169
|
+
'Ignored trees are excluded from sourceHash / deploy --skip-unchanged, ' +
|
|
170
|
+
'so build dependencies must not live under them.');
|
|
171
|
+
},
|
|
108
172
|
};
|
|
109
173
|
}
|
|
110
174
|
function fitSourceMapForDeploy(sourceMap) {
|
|
@@ -164,25 +228,30 @@ function nextStubPlugin() {
|
|
|
164
228
|
},
|
|
165
229
|
};
|
|
166
230
|
}
|
|
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
|
-
}
|
|
181
231
|
function collectEntryJavaScript(output) {
|
|
182
232
|
const entryChunks = output.filter((chunk) => chunk.type === 'chunk' && chunk.isEntry);
|
|
183
233
|
if (entryChunks.length === 0)
|
|
184
234
|
return null;
|
|
185
|
-
|
|
235
|
+
// HTML multi-page inputs can emit multiple entries; Artifact Studio has one.
|
|
236
|
+
const entry = entryChunks[0];
|
|
237
|
+
return { code: entry.code, fileName: entry.fileName };
|
|
238
|
+
}
|
|
239
|
+
function collectRuntimeChunks(output) {
|
|
240
|
+
return output
|
|
241
|
+
.filter((chunk) => chunk.type === 'chunk' && !chunk.isEntry)
|
|
242
|
+
.map((chunk) => ({
|
|
243
|
+
fileName: assertSafeRuntimeChunkFileName(chunk.fileName),
|
|
244
|
+
code: chunk.code,
|
|
245
|
+
}))
|
|
246
|
+
.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
247
|
+
}
|
|
248
|
+
export function hashBundleAndChunks({ bundle, chunks, }) {
|
|
249
|
+
const h = sha256Hex([
|
|
250
|
+
'artifact-studio-bundle-v2',
|
|
251
|
+
bundle,
|
|
252
|
+
...chunks.flatMap((chunk) => [chunk.fileName, chunk.code]),
|
|
253
|
+
].join('\0'));
|
|
254
|
+
return h;
|
|
186
255
|
}
|
|
187
256
|
function collectCss(output) {
|
|
188
257
|
const cssAssets = output.filter((chunk) => isAssetOutput(chunk) && chunk.fileName.endsWith('.css'));
|
|
@@ -203,25 +272,3 @@ function collectSourceMap(output) {
|
|
|
203
272
|
.map((chunk) => ({ fileName: chunk.fileName, map: chunk.map }));
|
|
204
273
|
return maps.length > 0 ? JSON.stringify(maps) : null;
|
|
205
274
|
}
|
|
206
|
-
function atlasUiCssResolverPlugin() {
|
|
207
|
-
return {
|
|
208
|
-
name: 'atlas-ui-css-resolver',
|
|
209
|
-
enforce: 'pre',
|
|
210
|
-
transform(code, id) {
|
|
211
|
-
if (!id.endsWith('.css'))
|
|
212
|
-
return undefined;
|
|
213
|
-
let result = code;
|
|
214
|
-
if (result.includes('@import "tailwindcss"')) {
|
|
215
|
-
result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
|
|
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}`;
|
|
220
|
-
}
|
|
221
|
-
if (result.includes('@import "tw-animate-css"')) {
|
|
222
|
-
result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
|
|
223
|
-
}
|
|
224
|
-
return result !== code ? result : undefined;
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
+
import { type ArtifactStudioPeerVersions } from './peer-versions.js';
|
|
1
2
|
import { type SourceAuthMode } from './source-resolver.js';
|
|
2
3
|
export declare function runCli(argv?: string[]): Promise<number>;
|
|
4
|
+
interface RemoteProject {
|
|
5
|
+
id: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
visibility?: string;
|
|
9
|
+
activeDeployment?: {
|
|
10
|
+
version: string;
|
|
11
|
+
sourceHash: string;
|
|
12
|
+
cliVersion?: string | null;
|
|
13
|
+
atlasUiVersion?: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether `--skip-unchanged` may no-op: same source *and* same CLI/atlas-ui
|
|
18
|
+
* pin. Missing peer fields on older rows force a rebuild so the pin becomes
|
|
19
|
+
* observable (DES-254).
|
|
20
|
+
*/
|
|
21
|
+
export declare function activeDeploymentMatchesSkipPin({ active, sourceHash, peerVersions, }: {
|
|
22
|
+
active: RemoteProject['activeDeployment'];
|
|
23
|
+
sourceHash: string;
|
|
24
|
+
peerVersions: ArtifactStudioPeerVersions;
|
|
25
|
+
}): boolean;
|
|
3
26
|
export declare function getConfiguredDefaultEnv(): Promise<string | undefined>;
|
|
4
27
|
/**
|
|
5
28
|
* Optional refreshing token provider, injected by an embedder (e.g. seq-studio)
|
|
@@ -15,3 +38,4 @@ export interface ProvidedToken {
|
|
|
15
38
|
token: string;
|
|
16
39
|
}
|
|
17
40
|
export declare function setTokenProvider(provider: ((options: TokenProviderOptions) => Promise<ProvidedToken | null>) | null): void;
|
|
41
|
+
export {};
|