@omnimod/plugin-utils 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 salnika
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @omnimod/plugin-utils
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40omnimod%2Fplugin-utils?label=npm)](https://www.npmjs.com/package/@omnimod/plugin-utils)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/Salnika/omnimod/ci.yml?branch=master&label=ci)](https://github.com/Salnika/omnimod/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/github/license/Salnika/omnimod)](../../LICENSE)
6
+
7
+ Helpers for authoring omnimod plugins.
8
+
9
+ This package contains shared AST types, CSS-in-JS parsing helpers,
10
+ vanilla-extract serialization helpers, and import editing utilities used by the
11
+ official plugins.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @omnimod/plugin-utils
17
+ ```
18
+
19
+ ## API
20
+
21
+ ```ts
22
+ import { ImportManager, cast, parseScss, serializeVe } from "@omnimod/plugin-utils";
23
+ ```
24
+
25
+ Main exports:
26
+
27
+ - `cast`: typed AST node casting helper.
28
+ - `parseScss`, `cssToVeStyle`, `globalRules`, `keyframesToVe`: CSS and SCSS helpers.
29
+ - `ImportManager`: utility for editing import declarations.
30
+ - `serializeVe` and `ve*` helpers: build and print vanilla-extract object values.
@@ -0,0 +1,316 @@
1
+ import { Node } from "@omnimod/core";
2
+ import { Root } from "postcss";
3
+
4
+ //#region src/ast.d.ts
5
+ /** Reinterpret a generic `Node` as one of the narrowed views below. */
6
+ declare function cast<T extends Node>(node: Node): T;
7
+ interface Identifier extends Node {
8
+ name: string;
9
+ }
10
+ interface JSXIdentifier extends Node {
11
+ name: string;
12
+ }
13
+ interface Literal extends Node {
14
+ value: string | number | boolean | null;
15
+ raw?: string;
16
+ }
17
+ /** A `Literal` already known to carry a string value. */
18
+ interface StringLiteral extends Node {
19
+ value: string;
20
+ }
21
+ interface ThisExpression extends Node {
22
+ type: "ThisExpression";
23
+ }
24
+ interface MemberExpression extends Node {
25
+ object: Node;
26
+ property: Node;
27
+ computed: boolean;
28
+ }
29
+ interface CallExpression extends Node {
30
+ callee: Node;
31
+ arguments: Node[];
32
+ }
33
+ interface NewExpression extends Node {
34
+ callee: Node;
35
+ arguments: Node[];
36
+ }
37
+ interface ConditionalExpression extends Node {
38
+ test: Node;
39
+ consequent: Node;
40
+ alternate: Node;
41
+ }
42
+ interface BinaryExpression extends Node {
43
+ operator: string;
44
+ left: Node;
45
+ right: Node;
46
+ }
47
+ interface LogicalExpression extends Node {
48
+ operator: string;
49
+ left: Node;
50
+ right: Node;
51
+ }
52
+ interface UnaryExpression extends Node {
53
+ operator: string;
54
+ argument: Node;
55
+ }
56
+ interface AssignmentExpression extends Node {
57
+ operator: string;
58
+ left: Node;
59
+ right: Node;
60
+ }
61
+ interface ArrowFunctionExpression extends Node {
62
+ params: Node[];
63
+ body: Node;
64
+ expression: boolean;
65
+ async?: boolean;
66
+ }
67
+ interface FunctionExpression extends Node {
68
+ params: Node[];
69
+ body: Node;
70
+ async?: boolean;
71
+ }
72
+ interface FunctionDeclaration extends Node {
73
+ id: Node | null;
74
+ params: Node[];
75
+ body: Node;
76
+ }
77
+ interface ObjectExpression extends Node {
78
+ properties: Node[];
79
+ }
80
+ interface ObjectPattern extends Node {
81
+ properties: Node[];
82
+ }
83
+ interface ArrayExpression extends Node {
84
+ elements: (Node | null)[];
85
+ }
86
+ interface Property extends Node {
87
+ key: Node;
88
+ value: Node;
89
+ computed: boolean;
90
+ shorthand: boolean;
91
+ kind: string;
92
+ }
93
+ interface AssignmentPattern extends Node {
94
+ left: Node;
95
+ right: Node;
96
+ }
97
+ interface BlockStatement extends Node {
98
+ body: Node[];
99
+ }
100
+ interface ReturnStatement extends Node {
101
+ argument: Node | null;
102
+ }
103
+ interface ExpressionStatement extends Node {
104
+ expression: Node;
105
+ }
106
+ interface SwitchStatement extends Node {
107
+ discriminant: Node;
108
+ cases: Node[];
109
+ }
110
+ interface VariableDeclarator extends Node {
111
+ id: Node;
112
+ init: Node | null;
113
+ }
114
+ interface VariableDeclaration extends Node {
115
+ declarations: VariableDeclarator[];
116
+ kind: string;
117
+ }
118
+ interface ImportSpecifier extends Node {
119
+ imported: Node;
120
+ local: Identifier;
121
+ /** "type" for `import type { X }` / `import { type X }`, else "value". */
122
+ importKind?: string;
123
+ }
124
+ interface ImportDefaultSpecifier extends Node {
125
+ local: Identifier;
126
+ }
127
+ interface ImportNamespaceSpecifier extends Node {
128
+ local: Identifier;
129
+ }
130
+ interface ImportDeclaration extends Node {
131
+ source: StringLiteral;
132
+ specifiers: Node[];
133
+ }
134
+ interface ExportNamedDeclaration extends Node {
135
+ declaration: Node | null;
136
+ }
137
+ interface ExportDefaultDeclaration extends Node {
138
+ declaration: Node;
139
+ }
140
+ interface ClassBody extends Node {
141
+ body: Node[];
142
+ }
143
+ interface MethodDefinition extends Node {
144
+ key: Node;
145
+ value: FunctionExpression;
146
+ kind: string;
147
+ static: boolean;
148
+ computed: boolean;
149
+ }
150
+ interface PropertyDefinition extends Node {
151
+ key: Node;
152
+ value: Node | null;
153
+ static: boolean;
154
+ computed: boolean;
155
+ }
156
+ interface ClassDeclaration extends Node {
157
+ id: Identifier | null;
158
+ superClass: Node | null;
159
+ body: ClassBody;
160
+ }
161
+ interface ClassExpression extends Node {
162
+ id: Identifier | null;
163
+ superClass: Node | null;
164
+ body: ClassBody;
165
+ }
166
+ interface TemplateElementValue {
167
+ raw: string;
168
+ cooked: string | null;
169
+ }
170
+ interface TemplateElement extends Node {
171
+ value: TemplateElementValue;
172
+ }
173
+ interface TemplateLiteral extends Node {
174
+ quasis: TemplateElement[];
175
+ expressions: Node[];
176
+ }
177
+ interface TaggedTemplateExpression extends Node {
178
+ tag: Node;
179
+ quasi: TemplateLiteral;
180
+ }
181
+ interface JSXExpressionContainer extends Node {
182
+ expression: Node;
183
+ }
184
+ interface JSXAttribute extends Node {
185
+ name: Node;
186
+ value: Node | null;
187
+ }
188
+ interface JSXSpreadAttribute extends Node {
189
+ argument: Node;
190
+ }
191
+ interface JSXOpeningElement extends Node {
192
+ name: Node;
193
+ attributes: Node[];
194
+ selfClosing: boolean;
195
+ }
196
+ interface JSXClosingElement extends Node {
197
+ name: Node;
198
+ }
199
+ interface JSXElement extends Node {
200
+ openingElement: JSXOpeningElement;
201
+ }
202
+ //#endregion
203
+ //#region src/casing.d.ts
204
+ /**
205
+ * Convert a CSS property name to the camelCase key vanilla-extract expects.
206
+ *
207
+ * - `background-color` → `backgroundColor`
208
+ * - `-webkit-transition` → `WebkitTransition` (vendor prefixes → PascalCase)
209
+ * - `-ms-flex` → `msFlex` (the `-ms-` prefix is lowercased, per csstype/React)
210
+ * - `--my-var` → `--my-var` (custom properties are kept verbatim)
211
+ */
212
+ declare function cssPropToCamel(prop: string): string;
213
+ //#endregion
214
+ //#region src/ve.d.ts
215
+ /**
216
+ * A small value model for the object literals vanilla-extract consumes
217
+ * (`style({...})`, recipe variants, theme contracts). Kept as data so callers
218
+ * can build/merge objects programmatically, then serialize to source once.
219
+ *
220
+ * The `raw` kind escapes serialization: it emits unquoted JS (e.g. a `vars.x.y`
221
+ * token reference or a `createVar()` handle).
222
+ */
223
+ type VeValue = {
224
+ readonly kind: "string";
225
+ readonly value: string;
226
+ } | {
227
+ readonly kind: "number";
228
+ readonly value: number;
229
+ } | {
230
+ readonly kind: "boolean";
231
+ readonly value: boolean;
232
+ } | {
233
+ readonly kind: "raw";
234
+ readonly code: string;
235
+ } | {
236
+ readonly kind: "object";
237
+ readonly entries: readonly VeEntry[];
238
+ } | {
239
+ readonly kind: "array";
240
+ readonly items: readonly VeValue[];
241
+ };
242
+ interface VeEntry {
243
+ readonly key: string;
244
+ readonly value: VeValue;
245
+ }
246
+ declare const veString: (value: string) => VeValue;
247
+ declare const veNumber: (value: number) => VeValue;
248
+ declare const veBoolean: (value: boolean) => VeValue;
249
+ declare const veRaw: (code: string) => VeValue;
250
+ declare const veObject: (entries: VeEntry[]) => VeValue;
251
+ declare const veArray: (items: VeValue[]) => VeValue;
252
+ /** Serialize a VeValue to formatted TypeScript source (2-space indentation). */
253
+ declare function serializeVe(value: VeValue, indent?: number): string;
254
+ /** True when the value is an object with no entries. */
255
+ declare function isEmptyObject(value: VeValue): boolean;
256
+ //#endregion
257
+ //#region src/css.d.ts
258
+ /** The token inserted where interpolation `index` sat in a styled template. */
259
+ declare function placeholderToken(index: number): string;
260
+ /**
261
+ * Reconstruct a parseable CSS string from a styled template's static chunks,
262
+ * inserting an identifier-like placeholder token between each pair (one per
263
+ * interpolation). `quasis` has length N+1 for N interpolations.
264
+ */
265
+ declare function buildPlaceholderCss(quasis: string[]): string;
266
+ declare function parseScss(css: string): Root;
267
+ /**
268
+ * Resolve an interpolation placeholder to a vanilla-extract value. `context`
269
+ * tells the resolver where the placeholder appeared. Returning `null` means the
270
+ * interpolation could not be converted (the caller records a warning).
271
+ */
272
+ type PlaceholderResolver = (token: string, context: "value" | "property" | "selector") => VeValue | null;
273
+ /** A selector that must become `globalStyle(...)`; `selector` keeps `&` as the component placeholder. */
274
+ interface DescendantRule {
275
+ selector: string;
276
+ style: VeValue;
277
+ }
278
+ interface VeStyleResult {
279
+ /** The object for `style({...})` (declarations, pseudos via `selectors`, `@media`). */
280
+ base: VeValue;
281
+ /** Selectors that can't live inside `style()` and need `globalStyle()`. */
282
+ descendants: DescendantRule[];
283
+ /** Human-readable notes about anything that couldn't be fully converted. */
284
+ warnings: string[];
285
+ }
286
+ /** Convert a parsed styled-components CSS block into a vanilla-extract style model. */
287
+ declare function cssToVeStyle(root: Root, resolve?: PlaceholderResolver): VeStyleResult;
288
+ /**
289
+ * Convert a `keyframes` CSS block into the object vanilla-extract's `keyframes()`
290
+ * expects, keyed by step (`from`, `to`, `0%`, `50%`, …).
291
+ */
292
+ declare function keyframesToVe(root: Root): VeValue;
293
+ /**
294
+ * Convert a `createGlobalStyle`/global CSS block into a flat list of
295
+ * `globalStyle(selector, style)` rules (one per top-level selector).
296
+ */
297
+ declare function globalRules(root: Root): DescendantRule[];
298
+ //#endregion
299
+ //#region src/imports.d.ts
300
+ /**
301
+ * Collects import specifiers needed by generated code and renders them as
302
+ * `import` statements. Deduplicates and sorts for deterministic output.
303
+ */
304
+ declare class ImportManager {
305
+ private readonly named;
306
+ private readonly defaults;
307
+ /** Require `import { <imported> as <local> } from "<module>"`. */
308
+ addNamed(module: string, imported: string, local?: string): void;
309
+ /** Require `import <local> from "<module>"`. */
310
+ addDefault(module: string, local: string): void;
311
+ isEmpty(): boolean;
312
+ /** Render one import statement per module, sorted by module specifier. */
313
+ render(): string;
314
+ }
315
+ //#endregion
316
+ export { type ArrayExpression, type ArrowFunctionExpression, type AssignmentExpression, type AssignmentPattern, type BinaryExpression, type BlockStatement, type CallExpression, type ClassBody, type ClassDeclaration, type ClassExpression, type ConditionalExpression, type DescendantRule, type ExportDefaultDeclaration, type ExportNamedDeclaration, type ExpressionStatement, type FunctionDeclaration, type FunctionExpression, type Identifier, type ImportDeclaration, type ImportDefaultSpecifier, ImportManager, type ImportNamespaceSpecifier, type ImportSpecifier, type JSXAttribute, type JSXClosingElement, type JSXElement, type JSXExpressionContainer, type JSXIdentifier, type JSXOpeningElement, type JSXSpreadAttribute, type Literal, type LogicalExpression, type MemberExpression, type MethodDefinition, type NewExpression, type ObjectExpression, type ObjectPattern, type PlaceholderResolver, type Property, type PropertyDefinition, type ReturnStatement, type StringLiteral, type SwitchStatement, type TaggedTemplateExpression, type TemplateElement, type TemplateElementValue, type TemplateLiteral, type ThisExpression, type UnaryExpression, type VariableDeclaration, type VariableDeclarator, type VeEntry, type VeStyleResult, type VeValue, buildPlaceholderCss, cast, cssPropToCamel, cssToVeStyle, globalRules, isEmptyObject, keyframesToVe, parseScss, placeholderToken, serializeVe, veArray, veBoolean, veNumber, veObject, veRaw, veString };
package/dist/index.mjs ADDED
@@ -0,0 +1,326 @@
1
+ import scss from "postcss-scss";
2
+ //#region src/ast.ts
3
+ /** Reinterpret a generic `Node` as one of the narrowed views below. */
4
+ function cast(node) {
5
+ return node;
6
+ }
7
+ //#endregion
8
+ //#region src/casing.ts
9
+ /**
10
+ * Convert a CSS property name to the camelCase key vanilla-extract expects.
11
+ *
12
+ * - `background-color` → `backgroundColor`
13
+ * - `-webkit-transition` → `WebkitTransition` (vendor prefixes → PascalCase)
14
+ * - `-ms-flex` → `msFlex` (the `-ms-` prefix is lowercased, per csstype/React)
15
+ * - `--my-var` → `--my-var` (custom properties are kept verbatim)
16
+ */
17
+ function cssPropToCamel(prop) {
18
+ const trimmed = prop.trim();
19
+ if (trimmed.startsWith("--")) return trimmed;
20
+ return trimmed.replace(/^-ms-/, "ms-").replace(/-([a-z])/g, (_match, char) => char.toUpperCase());
21
+ }
22
+ //#endregion
23
+ //#region src/ve.ts
24
+ const veString = (value) => ({
25
+ kind: "string",
26
+ value
27
+ });
28
+ const veNumber = (value) => ({
29
+ kind: "number",
30
+ value
31
+ });
32
+ const veBoolean = (value) => ({
33
+ kind: "boolean",
34
+ value
35
+ });
36
+ const veRaw = (code) => ({
37
+ kind: "raw",
38
+ code
39
+ });
40
+ const veObject = (entries) => ({
41
+ kind: "object",
42
+ entries
43
+ });
44
+ const veArray = (items) => ({
45
+ kind: "array",
46
+ items
47
+ });
48
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
49
+ function serializeKey(key) {
50
+ return IDENTIFIER.test(key) ? key : JSON.stringify(key);
51
+ }
52
+ /** Serialize a VeValue to formatted TypeScript source (2-space indentation). */
53
+ function serializeVe(value, indent = 0) {
54
+ const pad = " ".repeat(indent);
55
+ const padInner = " ".repeat(indent + 1);
56
+ switch (value.kind) {
57
+ case "string": return JSON.stringify(value.value);
58
+ case "number": return String(value.value);
59
+ case "boolean": return String(value.value);
60
+ case "raw": return value.code;
61
+ case "array":
62
+ if (value.items.length === 0) return "[]";
63
+ return `[${value.items.map((item) => serializeVe(item, indent)).join(", ")}]`;
64
+ case "object":
65
+ if (value.entries.length === 0) return "{}";
66
+ return `{\n${value.entries.map((entry) => `${padInner}${serializeKey(entry.key)}: ${serializeVe(entry.value, indent + 1)},`).join("\n")}\n${pad}}`;
67
+ }
68
+ }
69
+ /** True when the value is an object with no entries. */
70
+ function isEmptyObject(value) {
71
+ return value.kind === "object" && value.entries.length === 0;
72
+ }
73
+ //#endregion
74
+ //#region src/css.ts
75
+ const PLACEHOLDER_PREFIX = "__OMNIMOD_EXPR_";
76
+ const SINGLE_PLACEHOLDER = /^__OMNIMOD_EXPR_(\d+)__$/;
77
+ const PLACEHOLDER_GLOBAL = /__OMNIMOD_EXPR_\d+__/g;
78
+ const NUMERIC = /^-?\d+(?:\.\d+)?$/;
79
+ const DESCENDANT_COMBINATORS = [
80
+ " ",
81
+ " ",
82
+ "\n",
83
+ ">",
84
+ "+",
85
+ "~"
86
+ ];
87
+ /** The token inserted where interpolation `index` sat in a styled template. */
88
+ function placeholderToken(index) {
89
+ return `${PLACEHOLDER_PREFIX}${index}__`;
90
+ }
91
+ /**
92
+ * Reconstruct a parseable CSS string from a styled template's static chunks,
93
+ * inserting an identifier-like placeholder token between each pair (one per
94
+ * interpolation). `quasis` has length N+1 for N interpolations.
95
+ */
96
+ function buildPlaceholderCss(quasis) {
97
+ let css = "";
98
+ for (let i = 0; i < quasis.length; i++) {
99
+ css += quasis[i];
100
+ if (i < quasis.length - 1) css += placeholderToken(i);
101
+ }
102
+ return css;
103
+ }
104
+ function parseScss(css) {
105
+ return scss.parse(css);
106
+ }
107
+ /** Convert a parsed styled-components CSS block into a vanilla-extract style model. */
108
+ function cssToVeStyle(root, resolve) {
109
+ const descendants = [];
110
+ const warnings = [];
111
+ return {
112
+ base: convertContainer(root, resolve, descendants, warnings),
113
+ descendants,
114
+ warnings
115
+ };
116
+ }
117
+ /**
118
+ * Convert a `keyframes` CSS block into the object vanilla-extract's `keyframes()`
119
+ * expects, keyed by step (`from`, `to`, `0%`, `50%`, …).
120
+ */
121
+ function keyframesToVe(root) {
122
+ const steps = [];
123
+ const scratchDescendants = [];
124
+ const scratchWarnings = [];
125
+ root.each((node) => {
126
+ if (node.type !== "rule") return;
127
+ const inner = convertContainer(node, void 0, scratchDescendants, scratchWarnings);
128
+ for (const rawStep of splitTopLevel(node.selector, [","])) {
129
+ const step = normalizeSelector(rawStep);
130
+ if (step.length > 0) steps.push({
131
+ key: step,
132
+ value: inner
133
+ });
134
+ }
135
+ });
136
+ return veObject(steps);
137
+ }
138
+ /**
139
+ * Convert a `createGlobalStyle`/global CSS block into a flat list of
140
+ * `globalStyle(selector, style)` rules (one per top-level selector).
141
+ */
142
+ function globalRules(root) {
143
+ const rules = [];
144
+ const scratchDescendants = [];
145
+ const scratchWarnings = [];
146
+ root.each((node) => {
147
+ if (node.type !== "rule") return;
148
+ const inner = convertContainer(node, void 0, scratchDescendants, scratchWarnings);
149
+ for (const rawSelector of splitTopLevel(node.selector, [","])) {
150
+ const selector = normalizeSelector(rawSelector);
151
+ if (selector.length > 0) rules.push({
152
+ selector,
153
+ style: inner
154
+ });
155
+ }
156
+ });
157
+ return rules;
158
+ }
159
+ function convertContainer(container, resolve, descendants, warnings) {
160
+ const entries = [];
161
+ const selectorEntries = [];
162
+ const mediaEntries = [];
163
+ container.each((node) => {
164
+ if (node.type === "decl") entries.push(declToEntry(node, resolve, warnings));
165
+ else if (node.type === "rule") classifyRule(node, resolve, descendants, warnings, selectorEntries);
166
+ else if (node.type === "atrule") handleAtRule(node, resolve, descendants, warnings, mediaEntries);
167
+ });
168
+ if (selectorEntries.length > 0) entries.push({
169
+ key: "selectors",
170
+ value: veObject(selectorEntries)
171
+ });
172
+ if (mediaEntries.length > 0) entries.push({
173
+ key: "@media",
174
+ value: veObject(mediaEntries)
175
+ });
176
+ return veObject(entries);
177
+ }
178
+ function handleAtRule(atrule, resolve, descendants, warnings, mediaEntries) {
179
+ if (atrule.name === "media") {
180
+ const inner = convertContainer(atrule, resolve, descendants, warnings);
181
+ mediaEntries.push({
182
+ key: atrule.params,
183
+ value: inner
184
+ });
185
+ } else warnings.push(`Unsupported at-rule @${atrule.name} was left unconverted`);
186
+ }
187
+ function classifyRule(rule, resolve, descendants, warnings, selectorEntries) {
188
+ const inner = convertContainer(rule, resolve, descendants, warnings);
189
+ for (const rawSelector of splitTopLevel(rule.selector, [","])) {
190
+ const selector = normalizeSelector(rawSelector);
191
+ if (selector.length === 0) continue;
192
+ if (lastCompound(selector).includes("&")) selectorEntries.push({
193
+ key: selector,
194
+ value: inner
195
+ });
196
+ else {
197
+ const withAmp = selector.includes("&") ? selector : `& ${selector}`;
198
+ descendants.push({
199
+ selector: withAmp,
200
+ style: inner
201
+ });
202
+ }
203
+ }
204
+ }
205
+ function declToEntry(decl, resolve, warnings) {
206
+ return {
207
+ key: decl.prop.includes(PLACEHOLDER_PREFIX) ? resolvePropName(decl.prop, resolve, warnings) : cssPropToCamel(decl.prop),
208
+ value: valueToVe(decl.important ? `${decl.value} !important` : decl.value, resolve, warnings)
209
+ };
210
+ }
211
+ function resolvePropName(prop, resolve, warnings) {
212
+ if (SINGLE_PLACEHOLDER.exec(prop.trim()) && resolve) {
213
+ const resolved = resolve(prop.trim(), "property");
214
+ if (resolved && resolved.kind === "string") return resolved.value;
215
+ }
216
+ warnings.push(`Could not statically convert dynamic property name: ${prop}`);
217
+ return cssPropToCamel(prop);
218
+ }
219
+ function valueToVe(raw, resolve, warnings) {
220
+ const value = raw.trim();
221
+ if (!value.includes(PLACEHOLDER_PREFIX)) return NUMERIC.test(value) ? veNumber(Number(value)) : veString(value);
222
+ if (resolve) if (SINGLE_PLACEHOLDER.test(value)) {
223
+ const resolved = resolve(value, "value");
224
+ if (resolved) return resolved;
225
+ } else {
226
+ const template = mixedValueToTemplate(value, resolve);
227
+ if (template !== null) return veRaw(template);
228
+ }
229
+ warnings.push(`Could not statically convert dynamic value: ${value}`);
230
+ return veString(value);
231
+ }
232
+ /** Build a JS template-literal source for a value mixing static text and interpolations. */
233
+ function mixedValueToTemplate(value, resolve) {
234
+ const parts = [];
235
+ let lastIndex = 0;
236
+ for (const match of value.matchAll(PLACEHOLDER_GLOBAL)) {
237
+ parts.push(escapeTemplate(value.slice(lastIndex, match.index)));
238
+ const resolved = resolve(match[0], "value");
239
+ if (!resolved) return null;
240
+ parts.push(embedInTemplate(resolved));
241
+ lastIndex = match.index + match[0].length;
242
+ }
243
+ parts.push(escapeTemplate(value.slice(lastIndex)));
244
+ return `\`${parts.join("")}\``;
245
+ }
246
+ function embedInTemplate(value) {
247
+ switch (value.kind) {
248
+ case "raw": return `\${${value.code}}`;
249
+ case "string": return escapeTemplate(value.value);
250
+ case "number":
251
+ case "boolean": return String(value.value);
252
+ default: return "";
253
+ }
254
+ }
255
+ function escapeTemplate(text) {
256
+ return text.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
257
+ }
258
+ function normalizeSelector(selector) {
259
+ return selector.replace(/\s+/g, " ").trim();
260
+ }
261
+ /** The subject of a selector = its last compound (after descendant combinators). */
262
+ function lastCompound(selector) {
263
+ const parts = splitTopLevel(selector, DESCENDANT_COMBINATORS).map((part) => part.trim()).filter((part) => part.length > 0);
264
+ return parts.length > 0 ? parts[parts.length - 1] : selector;
265
+ }
266
+ /** Split on any of `seps` at bracket/paren depth 0. */
267
+ function splitTopLevel(input, seps) {
268
+ const parts = [];
269
+ let depth = 0;
270
+ let current = "";
271
+ for (const char of input) {
272
+ if (char === "(" || char === "[") depth++;
273
+ else if (char === ")" || char === "]") depth = Math.max(0, depth - 1);
274
+ if (depth === 0 && seps.includes(char)) {
275
+ parts.push(current);
276
+ current = "";
277
+ } else current += char;
278
+ }
279
+ parts.push(current);
280
+ return parts;
281
+ }
282
+ //#endregion
283
+ //#region src/imports.ts
284
+ /**
285
+ * Collects import specifiers needed by generated code and renders them as
286
+ * `import` statements. Deduplicates and sorts for deterministic output.
287
+ */
288
+ var ImportManager = class {
289
+ named = /* @__PURE__ */ new Map();
290
+ defaults = /* @__PURE__ */ new Map();
291
+ /** Require `import { <imported> as <local> } from "<module>"`. */
292
+ addNamed(module, imported, local = imported) {
293
+ let entry = this.named.get(module);
294
+ if (!entry) {
295
+ entry = /* @__PURE__ */ new Map();
296
+ this.named.set(module, entry);
297
+ }
298
+ entry.set(local, imported);
299
+ }
300
+ /** Require `import <local> from "<module>"`. */
301
+ addDefault(module, local) {
302
+ this.defaults.set(module, local);
303
+ }
304
+ isEmpty() {
305
+ return this.named.size === 0 && this.defaults.size === 0;
306
+ }
307
+ /** Render one import statement per module, sorted by module specifier. */
308
+ render() {
309
+ const modules = [.../* @__PURE__ */ new Set([...this.named.keys(), ...this.defaults.keys()])].sort();
310
+ const lines = [];
311
+ for (const module of modules) {
312
+ const clauses = [];
313
+ const defaultLocal = this.defaults.get(module);
314
+ if (defaultLocal) clauses.push(defaultLocal);
315
+ const named = this.named.get(module);
316
+ if (named && named.size > 0) {
317
+ const specifiers = [...named.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([local, imported]) => local === imported ? imported : `${imported} as ${local}`);
318
+ clauses.push(`{ ${specifiers.join(", ")} }`);
319
+ }
320
+ lines.push(`import ${clauses.join(", ")} from ${JSON.stringify(module)};`);
321
+ }
322
+ return lines.join("\n");
323
+ }
324
+ };
325
+ //#endregion
326
+ export { ImportManager, buildPlaceholderCss, cast, cssPropToCamel, cssToVeStyle, globalRules, isEmptyObject, keyframesToVe, parseScss, placeholderToken, serializeVe, veArray, veBoolean, veNumber, veObject, veRaw, veString };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@omnimod/plugin-utils",
3
+ "version": "0.1.0",
4
+ "description": "Helpers for authoring omnimod plugins: AST matchers, CSS-in-JS parsing, import management.",
5
+ "keywords": [
6
+ "codemod",
7
+ "css-in-js",
8
+ "omnimod-plugin",
9
+ "postcss",
10
+ "vanilla-extract"
11
+ ],
12
+ "homepage": "https://salnika.github.io/omnimod/",
13
+ "bugs": "https://github.com/salnika/omnimod/issues",
14
+ "license": "MIT",
15
+ "author": "salnika",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/salnika/omnimod.git",
19
+ "directory": "packages/plugin-utils"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./dist/index.mjs",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "postcss": "^8.5.16",
34
+ "postcss-scss": "^4.0.9",
35
+ "@omnimod/core": "0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.6.2",
39
+ "@typescript/native-preview": "7.0.0-dev.20260509.2",
40
+ "typescript": "^6.0.3",
41
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
42
+ "vite-plus": "0.2.2"
43
+ },
44
+ "scripts": {
45
+ "build": "vp pack",
46
+ "dev": "vp pack --watch",
47
+ "test": "vp test",
48
+ "check": "vp check"
49
+ }
50
+ }