@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/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
- format: 'iife',
89
- name: 'ArtifactStudioApp',
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 js = collectEntryJavaScript(output);
96
- if (!js)
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' + js : js;
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: sha256Hex(bundle),
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
- return entryChunks.map((chunk) => chunk.code).join('\n');
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
- }
@@ -0,0 +1,5 @@
1
+ export declare function createScrubbedChildEnv({ homeDir, source, additionalEnv, }: {
2
+ homeDir: string;
3
+ source?: NodeJS.ProcessEnv;
4
+ additionalEnv?: NodeJS.ProcessEnv;
5
+ }): NodeJS.ProcessEnv;
@@ -0,0 +1,83 @@
1
+ import { join } from 'node:path';
2
+ // This prevents ambient credential inheritance and conventional HOME/config
3
+ // discovery. It is not an OS sandbox: the child still has the caller's uid and
4
+ // can read explicitly named filesystem paths and, where the OS permits it,
5
+ // inspect other same-uid processes (for example through Linux /proc).
6
+ const SAFE_CHILD_ENV_NAMES = new Set([
7
+ 'PATH',
8
+ 'PATHEXT',
9
+ 'SYSTEMROOT',
10
+ 'WINDIR',
11
+ 'COMSPEC',
12
+ 'TMPDIR',
13
+ 'TMP',
14
+ 'TEMP',
15
+ 'LANG',
16
+ 'LANGUAGE',
17
+ 'LC_ALL',
18
+ 'TERM',
19
+ 'COLORTERM',
20
+ 'NO_COLOR',
21
+ 'FORCE_COLOR',
22
+ 'CI',
23
+ 'TZ',
24
+ 'SOURCE_DATE_EPOCH',
25
+ 'SSL_CERT_FILE',
26
+ 'SSL_CERT_DIR',
27
+ 'NODE_EXTRA_CA_CERTS',
28
+ ]);
29
+ const PROXY_ENV_NAMES = new Set(['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY']);
30
+ export function createScrubbedChildEnv({ homeDir, source = process.env, additionalEnv = {}, }) {
31
+ const env = {};
32
+ for (const [name, value] of Object.entries(source)) {
33
+ if (value === undefined)
34
+ continue;
35
+ const normalized = name.toUpperCase();
36
+ if (PROXY_ENV_NAMES.has(normalized)) {
37
+ env[name] = value;
38
+ continue;
39
+ }
40
+ if (SAFE_CHILD_ENV_NAMES.has(normalized) || normalized.startsWith('LC_')) {
41
+ env[name] = value;
42
+ }
43
+ }
44
+ Object.assign(env, additionalEnv);
45
+ for (const [name, value] of Object.entries(env)) {
46
+ if (value !== undefined && PROXY_ENV_NAMES.has(name.toUpperCase())) {
47
+ assertProxySettingIsSafe({ name, value });
48
+ }
49
+ }
50
+ return {
51
+ ...env,
52
+ HOME: homeDir,
53
+ USERPROFILE: homeDir,
54
+ XDG_CONFIG_HOME: join(homeDir, '.config'),
55
+ XDG_CACHE_HOME: join(homeDir, '.cache'),
56
+ XDG_DATA_HOME: join(homeDir, '.local', 'share'),
57
+ APPDATA: join(homeDir, 'AppData', 'Roaming'),
58
+ LOCALAPPDATA: join(homeDir, 'AppData', 'Local'),
59
+ USER: 'sequence-build',
60
+ USERNAME: 'sequence-build',
61
+ LOGNAME: 'sequence-build',
62
+ };
63
+ }
64
+ function assertProxySettingIsSafe({ name, value, }) {
65
+ if (name.toUpperCase() === 'NO_PROXY')
66
+ return;
67
+ let proxyUrl;
68
+ try {
69
+ // git/npm accept the legacy credential-free `host:port` form. Prefix a
70
+ // scheme for validation only; preserve the caller's exact value below.
71
+ proxyUrl = new URL(value.includes('://') ? value : `http://${value}`);
72
+ }
73
+ catch {
74
+ throw new Error(`${name} must be a valid proxy endpoint before it can be forwarded to a build worker`);
75
+ }
76
+ if (proxyUrl.username ||
77
+ proxyUrl.password ||
78
+ proxyUrl.search ||
79
+ proxyUrl.hash ||
80
+ (proxyUrl.pathname !== '' && proxyUrl.pathname !== '/')) {
81
+ throw new Error(`${name} contains credential-bearing URL components and cannot be forwarded to source-controlled build tooling`);
82
+ }
83
+ }
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,41 @@
1
+ import { type ArtifactStudioPeerVersions } from './peer-versions.js';
2
+ import { type SourceAuthMode } from './source-resolver.js';
1
3
  export declare function runCli(argv?: string[]): Promise<number>;
2
- export declare function setTokenProvider(provider: (() => Promise<string | null>) | null): void;
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;
26
+ export declare function getConfiguredDefaultEnv(): Promise<string | undefined>;
27
+ /**
28
+ * Optional refreshing token provider, injected by an embedder (e.g. seq-studio)
29
+ * that owns the shared seqapi session and can resolve an access token on
30
+ * demand. Authentication is owned by the embedding `seq-studio` CLI; this
31
+ * package deliberately has no bearer-token argv or environment escape hatch.
32
+ */
33
+ export interface TokenProviderOptions {
34
+ allowInteractiveLogin: boolean;
35
+ }
36
+ export interface ProvidedToken {
37
+ authMode: SourceAuthMode;
38
+ token: string;
39
+ }
40
+ export declare function setTokenProvider(provider: ((options: TokenProviderOptions) => Promise<ProvidedToken | null>) | null): void;
41
+ export {};