@plumeria/codemod 18.2.19 → 18.2.21

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/README.md CHANGED
@@ -134,6 +134,7 @@ export function Card({ width }: { width: number }) {
134
134
  /* Card.tsx — after */
135
135
  - import * as css from '@plumeria/core';
136
136
  + import styles from './Card.module.css';
137
+ + export { styles };
137
138
 
138
139
  - export const styles = css.create({
139
140
  - base: { fontSize: 12 },
@@ -143,14 +144,13 @@ export function Card({ width }: { width: number }) {
143
144
  - },
144
145
  - size: (width: number) => ({ width }),
145
146
  - });
146
- + export { styles };
147
147
 
148
148
  export function Card({ width }: { width: number }) {
149
149
  - return <div classStyle={[styles.base, styles.card, styles.size(width)]} />;
150
150
  + return (
151
151
  + <div
152
- + className={`${styles.base} ${styles.card} ${styles.size}`}
153
- + style={{ '--styles-size-width': width }}
152
+ + className={[styles.base, styles.card, styles.size].join(' ')}
153
+ + style={{ ['--styles-size-width' as string]: width }}
154
154
  + />
155
155
  + );
156
156
  }
@@ -162,15 +162,54 @@ equivalent, so each parameter becomes a CSS custom property named
162
162
  values move to the inline `style` prop. If a `style` prop already exists, the
163
163
  custom properties are merged into it.
164
164
 
165
- An array on the styling prop is joined into a template literal. When the array
166
- contains a conditional expression, it uses `.filter(Boolean).join(' ')` instead,
167
- so the falsy branch drops out at runtime:
165
+ An array with no condition in it collapses into one class that `composes` its
166
+ members, so nothing is duplicated:
167
+
168
+ ```diff
169
+ - <div classStyle={[styles.base, styles.card]} />
170
+ + <div className={styles.baseCard} />
171
+ ```
172
+
173
+ Otherwise the array is joined as it stands. `filter` is added only when a
174
+ condition can make a member falsy, so that branch drops out at runtime:
168
175
 
169
176
  ```diff
177
+ - <div classStyle={[styles.base, styles.card, styles.size(width)]} />
178
+ + <div className={[styles.base, styles.card, styles.size].join(' ')} style={{ … }} />
179
+
170
180
  - <div classStyle={[styles.base, active && styles.active]} />
171
181
  + <div className={[styles.base, active && styles.active].filter(Boolean).join(' ')} />
172
182
  ```
173
183
 
184
+ `classStyle={[b, a]}` gives `a` the last word, while a stylesheet gives it to
185
+ whichever rule is written later. The export reproduces the array order by
186
+ emitting the classes in an order that satisfies every call site at once. Where
187
+ two call sites compose the same pair in opposite orders, the one that cannot be
188
+ satisfied carries an extra class holding only the disputed declarations, applied
189
+ under the same condition as the style that has to win:
190
+
191
+ ```diff
192
+ - <div classStyle={[styles.raised, flat && styles.surface]} />
193
+ + <div className={[styles.raised, flat && styles.surface, flat && styles.surfaceOverRaised].filter(Boolean).join(' ')} />
194
+ ```
195
+
196
+ Order is only forced where the two classes actually disagree, measured per
197
+ at-rule and per selector. Anything left unsettled is reported as
198
+ `composition-order`.
199
+
200
+ A variant picked with a bracket is resolved to the class its key names, whether
201
+ the constant is declared in the same file or imported, and a constant left
202
+ naming nothing is removed with it:
203
+
204
+ ```diff
205
+ - const size = 'small';
206
+ - <div classStyle={styles[size]} />
207
+ + <div className={styles.small} />
208
+ ```
209
+
210
+ Only a `const` holding a string literal is followed. A key computed at runtime is
211
+ reported as `dynamic-style-access` and its definitions stay in Plumeria.
212
+
174
213
  ### Global styles
175
214
 
176
215
  `css.createTheme`, `css.keyframes`, and `css.viewTransition` produce rules that
@@ -195,12 +234,25 @@ src/Card.tsx
195
234
  ```
196
235
 
197
236
  Constructs that cannot be resolved statically — dynamic values, computed keys,
198
- runtime function calls, object spreads, and multiple `css.create` calls in one
199
- file — are reported and left in place. A target `*.module.css` that already
200
- exists is reported as `target-exists` and will not be overwritten.
237
+ runtime function calls, and object spreads — are reported and left in place,
238
+ together with every file whose definitions they still need. A target
239
+ `*.module.css` that already exists is reported as `target-exists` and will not be
240
+ overwritten.
241
+
242
+ Several `css.create` calls in one file are not reported: they are written to the
243
+ same stylesheet, and a key an earlier call claimed is renamed.
201
244
 
202
245
  The exit code is 1 while anything remains, so the command composes with a script.
203
246
 
247
+ The migration is idempotent — the first run converts, every run after it is a
248
+ fixed point:
249
+
250
+ ```
251
+ f(f(x)) = f(x)
252
+ ```
253
+
254
+ Clear a report and run again, and only the newly resolvable files move.
255
+
204
256
  Start with `-d, --dry-run` to preview the export without writing files:
205
257
 
206
258
  ```sh
package/dist/cli.js CHANGED
@@ -331,7 +331,7 @@ async function migrateFromCssModules(options) {
331
331
  console.log(line);
332
332
  return 1;
333
333
  }
334
- function releaseConfig(modules, themes, animations, statics) {
334
+ function releaseConfig(modules, themes, animations, statics, values, complete, constants) {
335
335
  return [
336
336
  { ignores: IGNORES },
337
337
  {
@@ -347,7 +347,15 @@ function releaseConfig(modules, themes, animations, statics) {
347
347
  rules: {
348
348
  'codemod/release-styles': [
349
349
  'error',
350
- { modules, themes, animations, statics },
350
+ {
351
+ modules,
352
+ themes,
353
+ animations,
354
+ statics,
355
+ values,
356
+ complete,
357
+ constants,
358
+ },
351
359
  ],
352
360
  },
353
361
  },
@@ -365,6 +373,8 @@ async function migrateFromPlumeria(options) {
365
373
  return 0;
366
374
  }
367
375
  for (const sheet of planned.stylesheets) {
376
+ if (sheet.reports.length > 0)
377
+ continue;
368
378
  console.log(` ${relative(sheet.source)} -> ${relative(sheet.target)}`);
369
379
  }
370
380
  if (planned.global) {
@@ -372,7 +382,7 @@ async function migrateFromPlumeria(options) {
372
382
  }
373
383
  const config = {
374
384
  overrideConfigFile: true,
375
- overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics),
385
+ overrideConfig: releaseConfig(planned.modules, planned.themes, planned.animations, planned.statics, planned.values, planned.complete, planned.constants),
376
386
  errorOnUnmatchedPattern: false,
377
387
  };
378
388
  const preview = await new eslint_1.ESLint(config).lintFiles(targets);
package/dist/migrate.js CHANGED
@@ -83,10 +83,12 @@ function plan(targets) {
83
83
  const converted = (0, css_modules_1.convertStylesheet)(fs.readFileSync(source, 'utf-8'));
84
84
  const target = (0, exports.targetPath)(source);
85
85
  stylesheets.push({ source, target, reports: converted.reports });
86
- modules[path.basename(source)] = {
86
+ modules[source] = {
87
87
  source: `./${path.basename(target, '.ts')}`,
88
+ target,
88
89
  names: converted.names,
89
90
  composes: converted.composes,
91
+ functions: converted.functions,
90
92
  };
91
93
  }
92
94
  return { stylesheets, modules };
package/dist/release.d.ts CHANGED
@@ -12,6 +12,9 @@ export interface ReleasePlan {
12
12
  themes: Record<string, string[]>;
13
13
  animations: Record<string, string[]>;
14
14
  statics: Record<string, string[]>;
15
+ values: Record<string, Record<string, unknown>>;
16
+ complete: boolean;
17
+ constants: Record<string, Record<string, string>>;
15
18
  global?: {
16
19
  target: string;
17
20
  css: string;
package/dist/release.js CHANGED
@@ -42,6 +42,8 @@ const path = __importStar(require("node:path"));
42
42
  const parser_1 = require("@typescript-eslint/parser");
43
43
  const resolve_1 = require("./resolve");
44
44
  const from_plumeria_1 = require("./transforms/from-plumeria");
45
+ const GLOBAL_OPEN = '/* Generated from css.createTheme, css.keyframes, and css.viewTransition by @plumeria/codemod */';
46
+ const GLOBAL_CLOSE = '/* End of @plumeria/codemod generated styles */';
45
47
  const EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
46
48
  const IGNORED = new Set([
47
49
  'node_modules',
@@ -75,6 +77,44 @@ const findSources = (targets) => {
75
77
  }
76
78
  return found.sort();
77
79
  };
80
+ const STYLE_PROP = 'classStyle';
81
+ const memberOf = (node) => {
82
+ if (node?.type !== 'MemberExpression' || node.object.type !== 'Identifier')
83
+ return undefined;
84
+ const key = node.computed
85
+ ? node.property.type === 'Literal'
86
+ ? String(node.property.value)
87
+ : undefined
88
+ : node.property.name;
89
+ return key === undefined ? undefined : { object: node.object.name, key };
90
+ };
91
+ const slotOf = (node) => {
92
+ const member = memberOf(node);
93
+ if (member)
94
+ return [member];
95
+ if (node?.type === 'LogicalExpression')
96
+ return slotOf(node.right);
97
+ if (node?.type === 'ConditionalExpression')
98
+ return [...slotOf(node.consequent), ...slotOf(node.alternate)];
99
+ return [];
100
+ };
101
+ const walk = (node, visit) => {
102
+ if (!node || typeof node !== 'object')
103
+ return;
104
+ if (Array.isArray(node)) {
105
+ for (const child of node)
106
+ walk(child, visit);
107
+ return;
108
+ }
109
+ if (typeof node.type !== 'string')
110
+ return;
111
+ visit(node);
112
+ for (const key of Object.keys(node)) {
113
+ if (key === 'parent' || key === 'loc' || key === 'range')
114
+ continue;
115
+ walk(node[key], visit);
116
+ }
117
+ };
78
118
  const releasedPath = (source) => source.replace(/(?:\.styles)?\.[^.]+$/, '.module.css');
79
119
  exports.releasedPath = releasedPath;
80
120
  const globalPath = (targets) => {
@@ -95,12 +135,34 @@ function planRelease(targets) {
95
135
  const themes = {};
96
136
  const animations = {};
97
137
  const statics = {};
98
- const globalRules = [];
99
138
  const sources = findSources(targets);
100
139
  const sourceText = new Map(sources.map((source) => [source, fs.readFileSync(source, 'utf8')]));
101
140
  const themeValues = new Map();
102
141
  const animationValues = new Map();
103
142
  const staticValues = new Map();
143
+ const reportsBySource = new Map();
144
+ const globalBySource = new Map();
145
+ const themeKeys = new Map();
146
+ const animationKeys = new Map();
147
+ const staticKeys = new Map();
148
+ const dependencies = new Map();
149
+ const note = (source, reports) => {
150
+ if (reports.length === 0)
151
+ return;
152
+ reportsBySource.set(source, [
153
+ ...(reportsBySource.get(source) ?? []),
154
+ ...reports,
155
+ ]);
156
+ };
157
+ const addGlobal = (source, css) => {
158
+ const trimmed = css.trim();
159
+ if (trimmed.length === 0)
160
+ return;
161
+ globalBySource.set(source, [
162
+ ...(globalBySource.get(source) ?? []),
163
+ trimmed,
164
+ ]);
165
+ };
104
166
  for (const source of sources) {
105
167
  try {
106
168
  const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source));
@@ -116,8 +178,6 @@ function planRelease(targets) {
116
178
  if (Object.keys(extracted.bindings).length === 0)
117
179
  continue;
118
180
  themeValues.set(source, extracted.bindings);
119
- themes[source] = Object.keys(extracted.bindings);
120
- globalRules.push(extracted.globalCss.trim());
121
181
  }
122
182
  catch {
123
183
  }
@@ -152,11 +212,14 @@ function planRelease(targets) {
152
212
  for (const specifier of node.specifiers) {
153
213
  if (specifier.type === 'ImportSpecifier') {
154
214
  const imported = specifier.imported.name ?? specifier.imported.value;
155
- if (available[imported])
215
+ if (available[imported]) {
156
216
  result[specifier.local.name] = available[imported];
217
+ dependencies.set(source, (dependencies.get(source) ?? new Set()).add(target));
218
+ }
157
219
  }
158
220
  else if (specifier.type === 'ImportNamespaceSpecifier') {
159
221
  result[specifier.local.name] = available;
222
+ dependencies.set(source, (dependencies.get(source) ?? new Set()).add(target));
160
223
  }
161
224
  }
162
225
  }
@@ -178,13 +241,26 @@ function planRelease(targets) {
178
241
  }
179
242
  return merged;
180
243
  };
244
+ for (const source of sources) {
245
+ try {
246
+ const extracted = (0, from_plumeria_1.extractPlumeriaThemes)(sourceText.get(source));
247
+ note(source, extracted.reports);
248
+ if (Object.keys(extracted.bindings).length === 0)
249
+ continue;
250
+ themeKeys.set(source, Object.keys(extracted.bindings));
251
+ addGlobal(source, extracted.globalCss);
252
+ }
253
+ catch {
254
+ }
255
+ }
181
256
  for (const source of sources) {
182
257
  try {
183
258
  const extracted = (0, from_plumeria_1.extractPlumeriaStatics)(sourceText.get(source), importedPlumeriaValues(source));
259
+ note(source, extracted.reports);
184
260
  if (Object.keys(extracted.bindings).length === 0)
185
261
  continue;
186
262
  staticValues.set(source, extracted.bindings);
187
- statics[source] = Object.keys(extracted.bindings);
263
+ staticKeys.set(source, Object.keys(extracted.bindings));
188
264
  }
189
265
  catch {
190
266
  }
@@ -195,59 +271,301 @@ function planRelease(targets) {
195
271
  ...importedPlumeriaValues(source),
196
272
  ...(themeValues.get(source) ?? {}),
197
273
  });
274
+ note(source, extracted.reports);
198
275
  if (Object.keys(extracted.bindings).length === 0)
199
276
  continue;
200
277
  animationValues.set(source, extracted.bindings);
201
- animations[source] = [
278
+ animationKeys.set(source, [
202
279
  ...extracted.keyframes,
203
280
  ...extracted.viewTransitions,
204
- ];
205
- globalRules.push(extracted.globalCss.trim());
281
+ ]);
282
+ addGlobal(source, extracted.globalCss);
283
+ }
284
+ catch {
285
+ }
286
+ }
287
+ const createBindings = new Map();
288
+ for (const source of sources) {
289
+ try {
290
+ const ast = (0, parser_1.parse)(sourceText.get(source), {
291
+ sourceType: 'module',
292
+ ecmaFeatures: { jsx: true },
293
+ });
294
+ const declared = new Set();
295
+ for (const raw of ast.body) {
296
+ const statement = raw.type === 'ExportNamedDeclaration' ? raw.declaration : raw;
297
+ if (statement?.type !== 'VariableDeclaration')
298
+ continue;
299
+ for (const declaration of statement.declarations) {
300
+ if (declaration.id.type === 'Identifier' &&
301
+ declaration.init?.type === 'CallExpression' &&
302
+ declaration.init.callee.type === 'MemberExpression' &&
303
+ declaration.init.callee.property?.name === 'create')
304
+ declared.add(declaration.id.name);
305
+ }
306
+ }
307
+ if (declared.size > 0)
308
+ createBindings.set(source, declared);
309
+ }
310
+ catch {
311
+ }
312
+ }
313
+ const stringConstants = new Map();
314
+ const exportedConstants = new Map();
315
+ for (const source of sources) {
316
+ let ast;
317
+ try {
318
+ ast = (0, parser_1.parse)(sourceText.get(source), {
319
+ sourceType: 'module',
320
+ ecmaFeatures: { jsx: true },
321
+ });
322
+ }
323
+ catch {
324
+ continue;
325
+ }
326
+ const held = new Map();
327
+ const exported = new Map();
328
+ const duplicated = new Set();
329
+ const read = (statement, isExport) => {
330
+ if (statement?.type !== 'VariableDeclaration' ||
331
+ statement.kind !== 'const')
332
+ return;
333
+ for (const declaration of statement.declarations) {
334
+ if (declaration.id.type !== 'Identifier' ||
335
+ declaration.init?.type !== 'Literal' ||
336
+ typeof declaration.init.value !== 'string')
337
+ continue;
338
+ if (held.has(declaration.id.name))
339
+ duplicated.add(declaration.id.name);
340
+ held.set(declaration.id.name, declaration.init.value);
341
+ if (isExport)
342
+ exported.set(declaration.id.name, declaration.init.value);
343
+ }
344
+ };
345
+ walk(ast.body, (node) => {
346
+ if (node.type === 'ExportNamedDeclaration')
347
+ read(node.declaration, true);
348
+ else
349
+ read(node, false);
350
+ });
351
+ for (const name of duplicated) {
352
+ held.delete(name);
353
+ exported.delete(name);
354
+ }
355
+ if (held.size > 0)
356
+ stringConstants.set(source, held);
357
+ if (exported.size > 0)
358
+ exportedConstants.set(source, exported);
359
+ }
360
+ const resolvedConstants = new Map();
361
+ const compositions = new Map();
362
+ for (const source of sources) {
363
+ let ast;
364
+ try {
365
+ ast = (0, parser_1.parse)(sourceText.get(source), {
366
+ sourceType: 'module',
367
+ ecmaFeatures: { jsx: true },
368
+ loc: true,
369
+ range: true,
370
+ });
206
371
  }
207
372
  catch {
373
+ continue;
374
+ }
375
+ const constants = new Map(stringConstants.get(source) ?? []);
376
+ for (const node of ast.body) {
377
+ if (node.type !== 'ImportDeclaration')
378
+ continue;
379
+ const target = (0, resolve_1.resolveSourcePath)(String(node.source.value), source);
380
+ const available = target ? exportedConstants.get(target) : undefined;
381
+ if (!available)
382
+ continue;
383
+ for (const specifier of node.specifiers) {
384
+ if (specifier.type !== 'ImportSpecifier')
385
+ continue;
386
+ const imported = specifier.imported.name ?? specifier.imported.value;
387
+ const value = available.get(imported);
388
+ if (value !== undefined)
389
+ constants.set(specifier.local.name, value);
390
+ }
391
+ }
392
+ if (constants.size > 0)
393
+ resolvedConstants.set(source, constants);
394
+ const owners = new Map();
395
+ for (const binding of createBindings.get(source) ?? [])
396
+ owners.set(binding, { source, binding });
397
+ for (const node of ast.body) {
398
+ if (node.type !== 'ImportDeclaration')
399
+ continue;
400
+ const target = (0, resolve_1.resolveSourcePath)(String(node.source.value), source);
401
+ const declared = target ? createBindings.get(target) : undefined;
402
+ if (!target || !declared)
403
+ continue;
404
+ for (const specifier of node.specifiers) {
405
+ if (specifier.type !== 'ImportSpecifier')
406
+ continue;
407
+ const imported = specifier.imported.name ?? specifier.imported.value;
408
+ if (declared.has(imported))
409
+ owners.set(specifier.local.name, {
410
+ source: target,
411
+ binding: imported,
412
+ });
413
+ }
208
414
  }
415
+ walk(ast.body, (node) => {
416
+ if (node.type !== 'MemberExpression' ||
417
+ !node.computed ||
418
+ node.property.type === 'Literal' ||
419
+ node.object.type !== 'Identifier')
420
+ return;
421
+ const owner = owners.get(node.object.name);
422
+ if (!owner)
423
+ return;
424
+ if (node.property.type === 'Identifier' &&
425
+ constants.has(node.property.name))
426
+ return;
427
+ note(owner.source, [
428
+ {
429
+ line: node.loc.start.line,
430
+ column: node.loc.start.column + 1,
431
+ kind: 'dynamic-style-access',
432
+ hint: `\`${node.object.name}\` is read with a computed key, which has no class name until it runs.`,
433
+ },
434
+ ]);
435
+ });
436
+ walk(ast.body, (node) => {
437
+ if (node.type !== 'JSXAttribute' ||
438
+ node.name?.name !== STYLE_PROP ||
439
+ node.value?.type !== 'JSXExpressionContainer')
440
+ return;
441
+ const expression = node.value.expression;
442
+ if (expression?.type !== 'ArrayExpression' ||
443
+ expression.elements.length < 2)
444
+ return;
445
+ const slots = expression.elements.map(slotOf);
446
+ if (slots.some((slot) => slot.length === 0))
447
+ return;
448
+ const owners_ = slots.flat().map((part) => owners.get(part.object));
449
+ if (owners_.some((candidate) => !candidate))
450
+ return;
451
+ const owner = owners_[0].source;
452
+ if (owners_.some((candidate) => candidate.source !== owner))
453
+ return;
454
+ const named = slots.map((slot) => slot.map((part) => ({
455
+ binding: owners.get(part.object).binding,
456
+ key: part.key,
457
+ })));
458
+ const collapsible = expression.elements.every((element) => memberOf(element)) &&
459
+ named.every((slot) => slot.length === 1);
460
+ compositions.set(owner, [
461
+ ...(compositions.get(owner) ?? []),
462
+ {
463
+ parts: collapsible ? named.map((slot) => slot[0]) : [],
464
+ slots: named,
465
+ file: source,
466
+ line: node.loc.start.line,
467
+ column: node.loc.start.column + 1,
468
+ },
469
+ ]);
470
+ });
209
471
  }
472
+ const converted = new Map();
210
473
  for (const source of sources) {
211
- let converted;
474
+ let module;
212
475
  try {
213
- converted = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source));
476
+ module = (0, from_plumeria_1.convertPlumeriaModule)(sourceText.get(source), importedPlumeriaValues(source), compositions.get(source) ?? []);
214
477
  }
215
478
  catch {
216
479
  continue;
217
480
  }
218
- if (!converted)
481
+ if (!module)
219
482
  continue;
220
483
  const target = (0, exports.releasedPath)(source);
221
- const specifier = `./${path.basename(target)}`;
222
- const reports = [...converted.reports];
484
+ converted.set(source, module);
485
+ note(source, module.reports);
223
486
  if (fs.existsSync(target)) {
224
- reports.push({
225
- line: 0,
226
- column: 0,
227
- kind: 'target-exists',
228
- hint: `${path.basename(target)} already exists and was not overwritten.`,
487
+ note(source, [
488
+ {
489
+ line: 0,
490
+ column: 0,
491
+ kind: 'target-exists',
492
+ hint: `${path.basename(target)} already exists and was not overwritten.`,
493
+ },
494
+ ]);
495
+ }
496
+ }
497
+ const blocked = new Set(reportsBySource.keys());
498
+ for (let changed = true; changed;) {
499
+ changed = false;
500
+ for (const source of blocked) {
501
+ for (const dependency of dependencies.get(source) ?? []) {
502
+ if (blocked.has(dependency))
503
+ continue;
504
+ blocked.add(dependency);
505
+ note(dependency, [
506
+ {
507
+ line: 0,
508
+ column: 0,
509
+ kind: 'blocked-dependency',
510
+ hint: `${path.relative(path.dirname(dependency), source)} still needs these definitions.`,
511
+ },
512
+ ]);
513
+ changed = true;
514
+ }
515
+ }
516
+ }
517
+ const globalRules = [];
518
+ const values = {};
519
+ for (const source of sources) {
520
+ const reports = reportsBySource.get(source) ?? [];
521
+ const module = converted.get(source);
522
+ if (module) {
523
+ stylesheets.push({
524
+ source,
525
+ target: (0, exports.releasedPath)(source),
526
+ css: module.css,
527
+ reports,
229
528
  });
230
529
  }
231
- stylesheets.push({
232
- source,
233
- target,
234
- css: converted.css,
235
- reports,
236
- });
237
- if (reports.length === 0) {
238
- modules[source] = {
239
- source: specifier,
240
- target,
241
- binding: converted.binding,
242
- ...(converted.definitionOnly ? { definitionOnly: true } : {}),
243
- ...(Object.keys(converted.aliases).length > 0
244
- ? { aliases: converted.aliases }
245
- : {}),
246
- ...(Object.keys(converted.functions).length > 0
247
- ? { functions: converted.functions }
248
- : {}),
249
- };
530
+ else if (reports.length > 0) {
531
+ stylesheets.push({ source, target: '', css: '', reports });
250
532
  }
533
+ if (blocked.has(source))
534
+ continue;
535
+ const resolved = {
536
+ ...(staticValues.get(source) ?? {}),
537
+ ...(themeValues.get(source) ?? {}),
538
+ ...(animationValues.get(source) ?? {}),
539
+ };
540
+ if (Object.keys(resolved).length > 0)
541
+ values[source] = resolved;
542
+ globalRules.push(...(globalBySource.get(source) ?? []));
543
+ if (themeKeys.has(source))
544
+ themes[source] = themeKeys.get(source);
545
+ if (animationKeys.has(source))
546
+ animations[source] = animationKeys.get(source);
547
+ if (staticKeys.has(source))
548
+ statics[source] = staticKeys.get(source);
549
+ if (!module)
550
+ continue;
551
+ modules[source] = {
552
+ source: `./${path.basename((0, exports.releasedPath)(source))}`,
553
+ target: (0, exports.releasedPath)(source),
554
+ binding: module.binding,
555
+ ...(module.definitionOnly ? { definitionOnly: true } : {}),
556
+ ...(Object.keys(module.aliases).length > 0
557
+ ? { aliases: module.aliases }
558
+ : {}),
559
+ ...(Object.keys(module.functions).length > 0
560
+ ? { functions: module.functions }
561
+ : {}),
562
+ ...(Object.keys(module.merges).length > 0
563
+ ? { merges: module.merges }
564
+ : {}),
565
+ ...(Object.keys(module.overrides).length > 0
566
+ ? { overrides: module.overrides }
567
+ : {}),
568
+ };
251
569
  }
252
570
  return {
253
571
  stylesheets,
@@ -255,10 +573,16 @@ function planRelease(targets) {
255
573
  themes,
256
574
  animations,
257
575
  statics,
576
+ values,
577
+ complete: stylesheets.every((sheet) => sheet.reports.length === 0),
578
+ constants: Object.fromEntries([...resolvedConstants].map(([source, held]) => [
579
+ source,
580
+ Object.fromEntries(held),
581
+ ])),
258
582
  global: globalRules.length > 0
259
583
  ? {
260
584
  target: globalPath(targets),
261
- css: `/* Generated from css.createTheme, css.keyframes, and css.viewTransition by @plumeria/codemod */\n${globalRules.join('\n\n')}\n`,
585
+ css: `${GLOBAL_OPEN}\n${globalRules.join('\n\n')}\n${GLOBAL_CLOSE}\n`,
262
586
  }
263
587
  : undefined,
264
588
  };
@@ -274,6 +598,13 @@ function writeRelease(plan) {
274
598
  const previous = fs.existsSync(plan.global.target)
275
599
  ? fs.readFileSync(plan.global.target, 'utf8')
276
600
  : '';
601
+ const opening = previous.indexOf(GLOBAL_OPEN);
602
+ const closing = previous.lastIndexOf(GLOBAL_CLOSE);
603
+ if (opening !== -1 && closing > opening) {
604
+ const kept = `${previous.slice(0, opening)}${previous.slice(closing + GLOBAL_CLOSE.length)}`;
605
+ fs.writeFileSync(plan.global.target, `${kept.replace(/\n+$/, '\n')}${plan.global.css}`, 'utf8');
606
+ return;
607
+ }
277
608
  const separator = previous.length > 0 && !previous.endsWith('\n') ? '\n\n' : '';
278
609
  fs.writeFileSync(plan.global.target, `${previous}${separator}${plan.global.css}`, 'utf8');
279
610
  }