@astryxdesign/cli 0.6.3-canary.3ee796c → 0.6.3-canary.45a9bf5

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.
@@ -12,9 +12,10 @@
12
12
  * - A .d.ts (plus an optional .variants.d.ts for custom prop values)
13
13
  *
14
14
  * It performs the writes and returns a `theme.build` receipt — its `warnings`
15
- * carry override problems and any fonts the theme names but does not load
16
- * (font-warning.mjs) — or `null` when the theme produced no CSS (nothing to
17
- * build). Errors throw AstryxError (with
15
+ * carry override problems, every declaration core's generator dropped because
16
+ * its value could not stay one CSS declaration, and any fonts the theme names
17
+ * but does not load (font-warning.mjs) — or `null` when the theme produced no
18
+ * CSS (nothing to build). Errors throw AstryxError (with
18
19
  * a stable code). Human progress is emitted through the shared `logger`
19
20
  * (silent by default), so the CLI keeps its exact output while a programmatic
20
21
  * caller stays quiet.
@@ -2303,7 +2304,15 @@ async function themeBuildInternal(
2303
2304
  const scopeSelector = themeScopeStart(themeDef.name);
2304
2305
  const scopeTo = THEME_SCOPE_TO;
2305
2306
 
2306
- const {component, prose} = _generateThemeRulesSplit(resolvedTheme);
2307
+ // Older cores ignore this optional collector. Current core writes the same
2308
+ // warning text here that runtime callers receive on the console.
2309
+ /** @type {string[]} */
2310
+ const droppedDeclarations = [];
2311
+
2312
+ const {component, prose} = _generateThemeRulesSplit(
2313
+ resolvedTheme,
2314
+ droppedDeclarations,
2315
+ );
2307
2316
  const cssParts = [];
2308
2317
  // Prose element defaults always ship — the `<Theme>` runtime
2309
2318
  // (generateThemeCSS) always emits them, so the build must too, or the
@@ -2322,7 +2331,7 @@ async function themeBuildInternal(
2322
2331
  let adaptationCss;
2323
2332
  try {
2324
2333
  adaptationCss = _generateAdaptationCSS
2325
- ? _generateAdaptationCSS(resolvedTheme)
2334
+ ? _generateAdaptationCSS(resolvedTheme, droppedDeclarations)
2326
2335
  : {component: '', prose: ''};
2327
2336
  } catch (error) {
2328
2337
  const message =
@@ -2368,11 +2377,16 @@ async function themeBuildInternal(
2368
2377
  // adaptations on the same resolved leaf.
2369
2378
  let onMediaCss = '';
2370
2379
  if (_generateOnMediaCSS) {
2371
- onMediaCss = _generateOnMediaCSS(resolvedTheme);
2380
+ onMediaCss = _generateOnMediaCSS(resolvedTheme, droppedDeclarations);
2372
2381
  if (onMediaCss) {
2373
2382
  cssParts.push(`@layer astryx-theme {\n${onMediaCss}\n}`);
2374
2383
  }
2375
2384
  }
2385
+ for (const message of droppedDeclarations) {
2386
+ const w = `Declaration ${message}. The generated CSS omits it; fix the value in the theme source.`;
2387
+ warningMessages.push(w);
2388
+ logger.warn(` ⚠ ${w}`);
2389
+ }
2376
2390
  if (cssParts.length === 0) {
2377
2391
  logger.log('No overrides found — nothing to build.');
2378
2392
  return null;
@@ -206,6 +206,133 @@ describe('themeBuild() — receipt', () => {
206
206
  });
207
207
  });
208
208
 
209
+ describe('themeBuild() — dropped declarations reach the receipt', () => {
210
+ // Core's generator refuses a declaration whose value would end it early
211
+ // (an unquoted `;` or brace, an unclosed string/comment/url). The runtime
212
+ // says so on the console; a build has a receipt, so every drop must land in
213
+ // `warnings` — a programmatic caller otherwise sees `warnings: []` while the
214
+ // CSS silently omits a value the generated JS still carries.
215
+ it('reports every dropped declaration, from every generator path, in warnings', async () => {
216
+ const themeFile = path.join(tmpDir, 'dropped.mjs');
217
+ fs.writeFileSync(
218
+ themeFile,
219
+ `export default {
220
+ name: 'dropped',
221
+ tokens: {
222
+ '--color-bg': '#0a0a0a } body { background: url(https://example.com/token) ',
223
+ },
224
+ components: {
225
+ button: {
226
+ 'variant:secondary': {
227
+ backgroundColor: 'red; background-image: url(https://example.com/component)',
228
+ ':hover': {color: '"https://example.com/pseudo'},
229
+ },
230
+ },
231
+ },
232
+ onDark: {
233
+ tokens: {'--color-bg': 'url(https://example.com/dark'},
234
+ },
235
+ adaptations: {
236
+ rules: [
237
+ {
238
+ when: {pointer: 'coarse'},
239
+ value: {tokens: {'--color-bg': 'red /* https://example.com/adaptation'}},
240
+ },
241
+ ],
242
+ },
243
+ };\n`,
244
+ );
245
+
246
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
247
+ let result;
248
+ try {
249
+ result = await themeBuild('dropped.mjs', {}, {cwd: tmpDir});
250
+ // Drops are collected, not printed: the programmatic API stays silent.
251
+ expect(warnSpy).not.toHaveBeenCalled();
252
+ } finally {
253
+ warnSpy.mockRestore();
254
+ }
255
+
256
+ const css = fs.readFileSync(path.join(tmpDir, 'dropped.css'), 'utf8');
257
+ expect(css).not.toContain('example.com');
258
+
259
+ const warnings = result?.data.warnings ?? [];
260
+ expect(warnings).toHaveLength(5);
261
+ expect(warnings).toEqual(
262
+ expect.arrayContaining([
263
+ expect.stringMatching(
264
+ /^Declaration dropped "--color-bg" in tokens: an unquoted "}" would close the rule/,
265
+ ),
266
+ expect.stringMatching(
267
+ /^Declaration dropped "background-color" in components\.button\["variant:secondary"\]: an unquoted ";" would end the declaration/,
268
+ ),
269
+ expect.stringMatching(
270
+ /^Declaration dropped "color" in components\.button\["variant:secondary"\]\[":hover"\]: an unclosed " string/,
271
+ ),
272
+ expect.stringMatching(
273
+ /^Declaration dropped "--color-bg" in onDark\.tokens: an unclosed url\(/,
274
+ ),
275
+ expect.stringMatching(
276
+ /^Declaration dropped "--color-bg" in adaptations\[0\]\.tokens: an unclosed \/\* comment/,
277
+ ),
278
+ ]),
279
+ );
280
+ for (const w of warnings) {
281
+ expect(w).toContain('The generated CSS omits it');
282
+ }
283
+ });
284
+
285
+ it('generates CSS for legacy null tokens without losing neighboring declarations', async () => {
286
+ fs.writeFileSync(
287
+ path.join(tmpDir, 'legacy.mjs'),
288
+ `export default {
289
+ name: 'legacy',
290
+ tokens: {'--color-background-body': null, '--spacing-4': 12},
291
+ onDark: {tokens: {'--color-background-body': null}},
292
+ components: {button: {base: {borderRadius: '4px'}}},
293
+ };\n`,
294
+ );
295
+
296
+ const result = await themeBuild('legacy.mjs', {}, {cwd: tmpDir});
297
+ const css = fs.readFileSync(path.join(tmpDir, 'legacy.css'), 'utf8');
298
+ expect(css).toContain('--color-background-body: null;');
299
+ expect(css).toContain('--spacing-4: 12;');
300
+ expect(css).toContain('border-radius: 4px;');
301
+ expect(result?.data.warnings).toEqual([]);
302
+ });
303
+
304
+ it('keeps valid CSS that carries semicolons, and reports nothing', async () => {
305
+ const themeFile = path.join(tmpDir, 'kept.mjs');
306
+ fs.writeFileSync(
307
+ themeFile,
308
+ `export default {
309
+ name: 'kept',
310
+ tokens: {'--font-family-body': 'Gill\\\\ Sans, "Segoe;UI", serif /* ; */'},
311
+ components: {
312
+ button: {
313
+ 'variant:secondary': {
314
+ backgroundImage: 'URL(data:image/svg+xml;base64,PHN2Zz4=)',
315
+ WebkitLineClamp: '2',
316
+ },
317
+ },
318
+ },
319
+ };\n`,
320
+ );
321
+
322
+ const result = await themeBuild('kept.mjs', {}, {cwd: tmpDir});
323
+ const css = fs.readFileSync(path.join(tmpDir, 'kept.css'), 'utf8');
324
+
325
+ expect(css).toContain(
326
+ '--font-family-body: Gill\\ Sans, "Segoe;UI", serif /* ; */;',
327
+ );
328
+ expect(css).toContain(
329
+ 'background-image: URL(data:image/svg+xml;base64,PHN2Zz4=);',
330
+ );
331
+ expect(css).toContain('-webkit-line-clamp: 2;');
332
+ expect(result?.data.warnings).toEqual([]);
333
+ });
334
+ });
335
+
209
336
  describe('themeBuild() — nothing to build', () => {
210
337
  it('returns null and writes nothing when the generator yields no CSS', async () => {
211
338
  const themeFile = path.join(tmpDir, 'empty.mjs');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.6.3-canary.3ee796c",
3
+ "version": "0.6.3-canary.45a9bf5",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -100,10 +100,10 @@
100
100
  "zod": "^4.4.3"
101
101
  },
102
102
  "peerDependencies": {
103
- "@astryxdesign/charts": "0.6.3-canary.3ee796c",
104
- "@astryxdesign/core": "0.6.3-canary.3ee796c",
105
- "@astryxdesign/lab": "0.6.3-canary.3ee796c",
106
- "@astryxdesign/theme-neutral": "0.6.3-canary.3ee796c",
103
+ "@astryxdesign/charts": "0.6.3-canary.45a9bf5",
104
+ "@astryxdesign/core": "0.6.3-canary.45a9bf5",
105
+ "@astryxdesign/lab": "0.6.3-canary.45a9bf5",
106
+ "@astryxdesign/theme-neutral": "0.6.3-canary.45a9bf5",
107
107
  "gpt-tokenizer": "^3.4.0"
108
108
  },
109
109
  "peerDependenciesMeta": {
@@ -121,10 +121,10 @@
121
121
  }
122
122
  },
123
123
  "devDependencies": {
124
- "@astryxdesign/charts": "0.6.3-canary.3ee796c",
125
- "@astryxdesign/core": "0.6.3-canary.3ee796c",
126
- "@astryxdesign/lab": "0.6.3-canary.3ee796c",
127
- "@astryxdesign/theme-neutral": "0.6.3-canary.3ee796c",
124
+ "@astryxdesign/charts": "0.6.3-canary.45a9bf5",
125
+ "@astryxdesign/core": "0.6.3-canary.45a9bf5",
126
+ "@astryxdesign/lab": "0.6.3-canary.45a9bf5",
127
+ "@astryxdesign/theme-neutral": "0.6.3-canary.45a9bf5",
128
128
  "@heroicons/react": "^2.2.0",
129
129
  "@stylexjs/stylex": "^0.19.0",
130
130
  "@types/babel__core": "^7.20.5",