@pikacss/integration 0.0.47 → 0.0.49

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 ADDED
@@ -0,0 +1,27 @@
1
+ # @pikacss/integration
2
+
3
+ Integration layer between the PikaCSS core engine and bundler plugins. Handles config loading, file scanning, source transformation, and code generation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @pikacss/integration
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createCtx } from '@pikacss/integration'
15
+
16
+ const ctx = createCtx({
17
+ cwd: process.cwd(),
18
+ })
19
+ ```
20
+
21
+ ## Documentation
22
+
23
+ See the [full documentation](https://pikacss.com/guide/integrations/).
24
+
25
+ ## License
26
+
27
+ MIT
package/dist/index.d.mts CHANGED
@@ -3,41 +3,127 @@ import { SourceMap } from "magic-string";
3
3
  export * from "@pikacss/core";
4
4
 
5
5
  //#region src/eventHook.d.ts
6
+ /**
7
+ * Callback function invoked when an event is triggered on an `EventHook`.
8
+ * @internal
9
+ *
10
+ * @typeParam EventPayload - The type of data passed to the listener when the event fires.
11
+ */
6
12
  type EventHookListener<EventPayload> = (payload: EventPayload) => void;
13
+ /**
14
+ * Lightweight publish-subscribe event hook for internal reactive state notifications.
15
+ * @internal
16
+ *
17
+ * @typeParam EventPayload - The type of data passed to listeners when the event fires.
18
+ *
19
+ * @remarks
20
+ * Used by the integration context to notify build-tool plugins when styles
21
+ * or TypeScript codegen content have changed and need to be regenerated.
22
+ */
7
23
  interface EventHook<EventPayload> {
24
+ /** The set of currently registered listener callbacks. */
8
25
  listeners: Set<EventHookListener<EventPayload>>;
26
+ /** Invokes all registered listeners synchronously with the given payload. No-op when the listener set is empty. */
9
27
  trigger: (payload: EventPayload) => void;
28
+ /** Registers a listener and returns an unsubscribe function that removes it. */
10
29
  on: (listener: EventHookListener<EventPayload>) => () => void;
30
+ /** Removes a previously registered listener. No-op if the listener is not registered. */
11
31
  off: (listener: EventHookListener<EventPayload>) => void;
12
32
  }
33
+ /**
34
+ * Creates a new event hook instance for publish-subscribe event dispatching.
35
+ * @internal
36
+ *
37
+ * @typeParam EventPayload - The type of data passed to listeners when the event fires.
38
+ * @returns A new `EventHook` with an empty listener set.
39
+ *
40
+ * @remarks
41
+ * The returned hook can register listeners via `on`, remove them via `off`,
42
+ * and broadcast payloads to all listeners via `trigger`. Calling `trigger`
43
+ * with no registered listeners is a no-op.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const hook = createEventHook<string>()
48
+ * const off = hook.on((msg) => console.log(msg))
49
+ * hook.trigger('hello') // logs "hello"
50
+ * off() // unsubscribes
51
+ * ```
52
+ */
13
53
  declare function createEventHook<EventPayload>(): EventHook<EventPayload>;
14
54
  //#endregion
15
55
  //#region src/types.d.ts
56
+ /**
57
+ * Records a single `pika()` call result, pairing the resolved atomic style IDs with the original call arguments.
58
+ *
59
+ * @remarks
60
+ * Each source file may produce multiple `UsageRecord` entries — one per `pika()` call site.
61
+ * These records drive both CSS output (via `atomicStyleIds`) and IDE preview overloads (via `params`).
62
+ */
16
63
  interface UsageRecord {
64
+ /** The list of atomic CSS class names generated by the engine for this call. */
17
65
  atomicStyleIds: string[];
66
+ /** The original arguments passed to `engine.use()`, preserved for TypeScript codegen overload generation. */
18
67
  params: Parameters<Engine['use']>;
19
68
  }
69
+ /**
70
+ * Classifier and regex utilities for recognizing all `pika()` function call variants in source code.
71
+ *
72
+ * @remarks
73
+ * The function name is configurable via `IntegrationContextOptions.fnName`, so all
74
+ * variants (`.str`, `.arr`, preview `p` suffix, bracket notation) are derived
75
+ * dynamically from that base name.
76
+ */
20
77
  interface FnUtils {
78
+ /** Returns `true` if the given function name is a normal call (output format determined by `transformedFormat`). */
21
79
  isNormal: (fnName: string) => boolean;
80
+ /** Returns `true` if the given function name forces string output (e.g., `pika.str`). */
22
81
  isForceString: (fnName: string) => boolean;
82
+ /** Returns `true` if the given function name forces array output (e.g., `pika.arr`). */
23
83
  isForceArray: (fnName: string) => boolean;
84
+ /** Returns `true` if the given function name is a preview variant (e.g., `pikap`, `pikap.str`). */
24
85
  isPreview: (fnName: string) => boolean;
86
+ /** A compiled global regex that matches all recognized function call variants, including bracket-notation accessors. */
25
87
  RE: RegExp;
26
88
  }
89
+ /**
90
+ * Configuration options for creating an integration context.
91
+ *
92
+ * @remarks
93
+ * These options are set by bundler plugin adapters (Vite, webpack, Nuxt) and are
94
+ * not typically configured by end users directly.
95
+ */
27
96
  interface IntegrationContextOptions {
97
+ /** The working directory used to resolve relative paths for config files, codegen outputs, and source scanning. */
28
98
  cwd: string;
99
+ /** The npm package name of the integration consumer (e.g., `'@pikacss/unplugin'`), embedded in generated file headers and import paths. */
29
100
  currentPackageName: string;
101
+ /** Glob patterns controlling which source files are scanned for `pika()` calls. `include` specifies files to process; `exclude` specifies files to skip. */
30
102
  scan: {
31
103
  include: string[];
32
104
  exclude: string[];
33
105
  };
106
+ /** The engine configuration object, a path to a config file, or `null`/`undefined` to trigger auto-discovery of `pika.config.*` files. */
34
107
  configOrPath: EngineConfig | string | Nullish;
108
+ /** The base function name to recognize in source code (e.g., `'pika'`). All variants (`.str`, `.arr`, preview) are derived from this name. */
35
109
  fnName: string;
110
+ /** Controls the default output format of normal `pika()` calls: `'string'` produces a space-joined class string, `'array'` produces a string array. */
36
111
  transformedFormat: 'string' | 'array';
112
+ /** Path to the generated TypeScript declaration file (`pika.gen.ts`), or `false` to disable TypeScript codegen entirely. */
37
113
  tsCodegen: false | string;
114
+ /** Path to the generated CSS output file (e.g., `'pika.gen.css'`). */
38
115
  cssCodegen: string;
116
+ /** When `true`, automatically scaffolds a default `pika.config.js` file if no config file is found. */
39
117
  autoCreateConfig: boolean;
40
118
  }
119
+ /**
120
+ * Discriminated union representing the outcome of loading an engine configuration file.
121
+ *
122
+ * @remarks
123
+ * Three shapes are possible: an inline config (no file), a successfully loaded file-based
124
+ * config, or a failed/missing load (all fields `null`). The `file` and `content` fields are
125
+ * populated only when the config was loaded from disk, enabling hot-reload detection.
126
+ */
41
127
  type LoadedConfigResult = {
42
128
  config: EngineConfig;
43
129
  file: null;
@@ -51,42 +137,87 @@ type LoadedConfigResult = {
51
137
  file: string;
52
138
  content: string;
53
139
  };
140
+ /**
141
+ * The main build-tool integration context that bridges the PikaCSS engine with bundler plugins.
142
+ *
143
+ * @remarks
144
+ * Created via `createCtx()`. The context manages the full build lifecycle: config loading,
145
+ * engine initialization, source file transformation, usage tracking, and output file generation.
146
+ * All transform and codegen calls automatically await `setup()` completion before proceeding.
147
+ */
54
148
  interface IntegrationContext {
149
+ /** The current working directory. Can be updated at runtime (e.g., when the project root changes). */
55
150
  cwd: string;
151
+ /** The npm package name of the integration consumer, used in generated file headers and module declarations. */
56
152
  currentPackageName: string;
153
+ /** The base function name recognized in source transforms (e.g., `'pika'`). */
57
154
  fnName: string;
155
+ /** The default output format for normal `pika()` calls: `'string'` or `'array'`. */
58
156
  transformedFormat: 'string' | 'array';
157
+ /** Absolute path to the generated CSS output file, computed from `cwd` and the configured relative path. */
59
158
  cssCodegenFilepath: string;
159
+ /** Absolute path to the generated TypeScript declaration file, or `null` if TypeScript codegen is disabled. */
60
160
  tsCodegenFilepath: string | Nullish;
161
+ /** Whether the `vue` package is installed in the project, used to include Vue-specific type declarations in codegen. */
61
162
  hasVue: boolean;
163
+ /** The loaded engine configuration object, or `null` if loading failed or no config was found. */
62
164
  resolvedConfig: EngineConfig | Nullish;
165
+ /** Absolute path to the resolved config file on disk, or `null` for inline configs or when no config was loaded. */
63
166
  resolvedConfigPath: string | Nullish;
167
+ /** Raw string content of the config file, or `null` for inline configs or when no config was loaded. */
64
168
  resolvedConfigContent: string | Nullish;
169
+ /** Loads (or reloads) the engine configuration from disk or inline source, updating `resolvedConfig`, `resolvedConfigPath`, and `resolvedConfigContent`. */
65
170
  loadConfig: () => Promise<LoadedConfigResult>;
171
+ /** Map from source file ID to the list of `UsageRecord` entries extracted during transforms. Keyed by the file path relative to `cwd`. */
66
172
  usages: Map<string, UsageRecord[]>;
173
+ /** Map from source file ID to preview-only `UsageRecord` entries (from `pikap()` calls). Only these drive TypeScript preview overload generation. */
174
+ previewUsages: Map<string, UsageRecord[]>;
175
+ /** Event hooks for notifying plugins when generated outputs need refreshing. `styleUpdated` fires on CSS changes; `tsCodegenUpdated` fires on TypeScript declaration changes. */
67
176
  hooks: {
68
177
  styleUpdated: ReturnType<typeof createEventHook<void>>;
69
178
  tsCodegenUpdated: ReturnType<typeof createEventHook<void>>;
70
179
  };
180
+ /** The initialized PikaCSS engine instance. Throws if accessed before `setup()` completes. */
71
181
  engine: Engine;
182
+ /** Glob patterns for the bundler's transform pipeline, derived from the scan config with codegen files excluded. */
72
183
  transformFilter: {
73
184
  include: string[];
74
185
  exclude: string[];
75
186
  };
187
+ /** Processes a source file by extracting `pika()` calls, resolving them through the engine, and replacing them with computed output. Returns the transformed code and source map, or `null` if no calls were found. */
76
188
  transform: (code: string, id: string) => Promise<{
77
189
  code: string;
78
190
  map: SourceMap;
79
191
  } | Nullish>;
192
+ /** Generates the full CSS output string, including layer declarations, preflights, and all atomic styles collected from transforms. */
80
193
  getCssCodegenContent: () => Promise<string | Nullish>;
194
+ /** Generates the full TypeScript declaration content for `pika.gen.ts`, or `null` if TypeScript codegen is disabled. */
81
195
  getTsCodegenContent: () => Promise<string | Nullish>;
196
+ /** Generates and writes the CSS codegen file to disk at `cssCodegenFilepath`. */
82
197
  writeCssCodegenFile: () => Promise<void>;
198
+ /** Generates and writes the TypeScript codegen file to disk at `tsCodegenFilepath`. No-op if TypeScript codegen is disabled. */
83
199
  writeTsCodegenFile: () => Promise<void>;
200
+ /** Scans all matching source files, collects usages via transform, then writes the CSS codegen file. Used for full rebuilds. */
84
201
  fullyCssCodegen: () => Promise<void>;
202
+ /** The pending setup promise while initialization is in progress, or `null` when idle. Transform calls await this before proceeding. */
85
203
  setupPromise: Promise<void> | null;
204
+ /** Initializes (or reinitializes) the context by clearing state, loading config, creating the engine, and wiring up dev hooks. Returns a promise that resolves when setup is complete. */
86
205
  setup: () => Promise<void>;
87
206
  }
88
207
  //#endregion
89
208
  //#region src/ctx.d.ts
209
+ /**
210
+ * Creates an `IntegrationContext` that wires together config loading, engine initialization, source file transformation, and codegen output.
211
+ *
212
+ * @param options - The integration configuration including paths, function name, scan globs, and codegen settings.
213
+ * @returns A fully constructed `IntegrationContext`. Call `setup()` on the returned context before using transforms.
214
+ *
215
+ * @remarks
216
+ * The context uses reactive signals internally so that computed paths (CSS and TS codegen
217
+ * file paths) automatically update when `cwd` changes. The `setup()` method must be called
218
+ * before any transform or codegen operations - transform calls automatically await the
219
+ * pending setup promise.
220
+ */
90
221
  declare function createCtx(options: IntegrationContextOptions): IntegrationContext;
91
222
  //#endregion
92
223
  export { FnUtils, IntegrationContext, IntegrationContextOptions, LoadedConfigResult, UsageRecord, createCtx };
package/dist/index.mjs CHANGED
@@ -7,10 +7,226 @@ import { klona } from "klona";
7
7
  import { isPackageExists } from "local-pkg";
8
8
  import MagicString from "magic-string";
9
9
  import { dirname, isAbsolute, join, relative, resolve } from "pathe";
10
-
11
- export * from "@pikacss/core"
12
-
10
+ export * from "@pikacss/core";
11
+ //#region src/ctx.transform-utils.ts
12
+ const ESCAPE_REPLACE_RE = /[.*+?^${}()|[\]\\/]/g;
13
+ /**
14
+ * Builds classifier functions and a compiled regex for all `pika()` function call variants derived from the given base name.
15
+ * @internal
16
+ *
17
+ * @param fnName - The base function name (e.g., `'pika'`). All variants (`.str`, `.arr`, `p` suffix, bracket notation) are derived from this.
18
+ * @returns An `FnUtils` object with classifier methods and a global regex for matching all call variants.
19
+ *
20
+ * @remarks
21
+ * The generated regex handles bracket-notation property access (e.g., `pika['str']`)
22
+ * in addition to dot notation, and includes word-boundary anchors to avoid false
23
+ * matches within longer identifiers.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const fnUtils = createFnUtils('pika')
28
+ * fnUtils.isNormal('pika') // true
29
+ * fnUtils.isForceString('pika.str') // true
30
+ * fnUtils.RE.test('pika(') // true
31
+ * ```
32
+ */
33
+ function createFnUtils(fnName) {
34
+ const available = {
35
+ normal: new Set([fnName]),
36
+ forceString: new Set([
37
+ `${fnName}.str`,
38
+ `${fnName}['str']`,
39
+ `${fnName}["str"]`,
40
+ `${fnName}[\`str\`]`
41
+ ]),
42
+ forceArray: new Set([
43
+ `${fnName}.arr`,
44
+ `${fnName}['arr']`,
45
+ `${fnName}["arr"]`,
46
+ `${fnName}[\`arr\`]`
47
+ ]),
48
+ normalPreview: new Set([`${fnName}p`]),
49
+ forceStringPreview: new Set([
50
+ `${fnName}p.str`,
51
+ `${fnName}p['str']`,
52
+ `${fnName}p["str"]`,
53
+ `${fnName}p[\`str\`]`
54
+ ]),
55
+ forceArrayPreview: new Set([
56
+ `${fnName}p.arr`,
57
+ `${fnName}p['arr']`,
58
+ `${fnName}p["arr"]`,
59
+ `${fnName}p[\`arr\`]`
60
+ ])
61
+ };
62
+ return {
63
+ isNormal: (name) => available.normal.has(name) || available.normalPreview.has(name),
64
+ isForceString: (name) => available.forceString.has(name) || available.forceStringPreview.has(name),
65
+ isForceArray: (name) => available.forceArray.has(name) || available.forceArrayPreview.has(name),
66
+ isPreview: (name) => available.normalPreview.has(name) || available.forceStringPreview.has(name) || available.forceArrayPreview.has(name),
67
+ RE: new RegExp(`\\b(${Object.values(available).flatMap((set) => Array.from(set, (name) => `(${name.replace(ESCAPE_REPLACE_RE, "\\$&")})`)).join("|")})\\(`, "g")
68
+ };
69
+ }
70
+ /**
71
+ * Finds the index of the closing `}` that terminates a template literal `${...}` expression.
72
+ * @internal
73
+ *
74
+ * @param code - The full source code string to search within.
75
+ * @param start - The index of the opening `{` of the template expression.
76
+ * @returns The index of the matching closing `}`, or `-1` if the expression is malformed or unterminated.
77
+ *
78
+ * @remarks
79
+ * Tracks nested braces, string literals (single, double, and backtick), escape sequences,
80
+ * line comments, and block comments. Recursively handles nested template expressions
81
+ * within backtick strings.
82
+ */
83
+ function findTemplateExpressionEnd(code, start) {
84
+ let end = start;
85
+ let depth = 1;
86
+ let inString = false;
87
+ let isEscaped = false;
88
+ while (depth > 0 && end < code.length - 1) {
89
+ end++;
90
+ const char = code[end];
91
+ if (isEscaped) {
92
+ isEscaped = false;
93
+ continue;
94
+ }
95
+ if (char === "\\") {
96
+ isEscaped = true;
97
+ continue;
98
+ }
99
+ if (inString !== false) {
100
+ if (char === inString) inString = false;
101
+ else if (inString === "`" && char === "$" && code[end + 1] === "{") {
102
+ if ((end = findTemplateExpressionEnd(code, end + 1)) < 0) return -1;
103
+ }
104
+ continue;
105
+ }
106
+ if (char === "{") depth++;
107
+ else if (char === "}") depth--;
108
+ else if (char === "'" || char === "\"" || char === "`") inString = char;
109
+ else if (char === "/" && code[end + 1] === "/") {
110
+ const lineEnd = code.indexOf("\n", end);
111
+ if (lineEnd === -1) return -1;
112
+ end = lineEnd;
113
+ } else if (char === "/" && code[end + 1] === "*") {
114
+ if ((end = code.indexOf("*/", end + 2) + 1) === 0) return -1;
115
+ }
116
+ }
117
+ return depth === 0 ? end : -1;
118
+ }
119
+ /**
120
+ * Scans source code and returns all `pika()` function call matches found by the provided regex.
121
+ * @internal
122
+ *
123
+ * @param code - The full source code string to scan for function calls.
124
+ * @param fnUtils - An object providing the `RE` regex used to locate function call start positions.
125
+ * @returns An array of `FunctionCallMatch` objects, one per matched call. Malformed calls (unbalanced parentheses) are logged and skipped.
126
+ *
127
+ * @remarks
128
+ * Correctly handles nested parentheses, string literals, template expressions,
129
+ * line comments, and block comments within function arguments. Each match
130
+ * includes the full call snippet for later evaluation.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const fnUtils = createFnUtils('pika')
135
+ * const matches = findFunctionCalls(`pika('bg:red', 'c:white')`, fnUtils)
136
+ * // [{ fnName: 'pika', start: 0, end: 25, snippet: "pika('bg:red', 'c:white')" }]
137
+ * ```
138
+ */
139
+ function findFunctionCalls(code, fnUtils) {
140
+ const RE = fnUtils.RE;
141
+ const result = [];
142
+ let matched = RE.exec(code);
143
+ while (matched != null) {
144
+ const fnName = matched[1];
145
+ const start = matched.index;
146
+ let end = start + fnName.length;
147
+ let depth = 1;
148
+ let inString = false;
149
+ let isEscaped = false;
150
+ while (depth > 0 && end < code.length) {
151
+ end++;
152
+ const char = code[end];
153
+ if (isEscaped) {
154
+ isEscaped = false;
155
+ continue;
156
+ }
157
+ if (char === "\\") {
158
+ isEscaped = true;
159
+ continue;
160
+ }
161
+ if (inString !== false) {
162
+ if (char === inString) inString = false;
163
+ else if (inString === "`" && char === "$" && code[end + 1] === "{") {
164
+ const templateExpressionEnd = findTemplateExpressionEnd(code, end + 1);
165
+ if (templateExpressionEnd === -1) {
166
+ log.warn(`Malformed template literal expression in function call at position ${start}`);
167
+ break;
168
+ }
169
+ end = templateExpressionEnd;
170
+ }
171
+ continue;
172
+ }
173
+ if (char === "(") depth++;
174
+ else if (char === ")") depth--;
175
+ else if (char === "'" || char === "\"" || char === "`") inString = char;
176
+ else if (char === "/" && code[end + 1] === "/") {
177
+ const lineEnd = code.indexOf("\n", end);
178
+ if (lineEnd === -1) {
179
+ log.warn(`Unclosed function call at position ${start}`);
180
+ break;
181
+ }
182
+ end = lineEnd;
183
+ } else if (char === "/" && code[end + 1] === "*") {
184
+ const commentEnd = code.indexOf("*/", end + 2);
185
+ if (commentEnd === -1) {
186
+ log.warn(`Unclosed comment in function call at position ${start}`);
187
+ break;
188
+ }
189
+ end = commentEnd + 1;
190
+ }
191
+ }
192
+ if (depth !== 0) {
193
+ log.warn(`Malformed function call at position ${start}, skipping`);
194
+ matched = RE.exec(code);
195
+ continue;
196
+ }
197
+ const snippet = code.slice(start, end + 1);
198
+ result.push({
199
+ fnName,
200
+ start,
201
+ end,
202
+ snippet
203
+ });
204
+ matched = RE.exec(code);
205
+ }
206
+ return result;
207
+ }
208
+ //#endregion
13
209
  //#region src/eventHook.ts
210
+ /**
211
+ * Creates a new event hook instance for publish-subscribe event dispatching.
212
+ * @internal
213
+ *
214
+ * @typeParam EventPayload - The type of data passed to listeners when the event fires.
215
+ * @returns A new `EventHook` with an empty listener set.
216
+ *
217
+ * @remarks
218
+ * The returned hook can register listeners via `on`, remove them via `off`,
219
+ * and broadcast payloads to all listeners via `trigger`. Calling `trigger`
220
+ * with no registered listeners is a no-op.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * const hook = createEventHook<string>()
225
+ * const off = hook.on((msg) => console.log(msg))
226
+ * hook.trigger('hello') // logs "hello"
227
+ * off() // unsubscribes
228
+ * ```
229
+ */
14
230
  function createEventHook() {
15
231
  const listeners = /* @__PURE__ */ new Set();
16
232
  function trigger(payload) {
@@ -32,9 +248,9 @@ function createEventHook() {
32
248
  off
33
249
  };
34
250
  }
35
-
36
251
  //#endregion
37
252
  //#region src/tsCodegen.ts
253
+ const RE_LEADING_INDENT = /^(\s*)/;
38
254
  function formatUnionType(parts) {
39
255
  return parts.length > 0 ? parts.join(" | ") : "never";
40
256
  }
@@ -42,7 +258,7 @@ function formatUnionStringType(list) {
42
258
  return formatUnionType(list.map((i) => JSON.stringify(i)));
43
259
  }
44
260
  function formatAutocompleteUnion(literals, patterns) {
45
- return formatUnionType([...Array.from(literals, (value) => JSON.stringify(value)), ...patterns == null ? [] : [...patterns]]);
261
+ return formatUnionType([...Array.from(literals, (value) => JSON.stringify(value)), ...patterns]);
46
262
  }
47
263
  function formatAutocompleteValueMap(keys, entries, patternEntries, formatValue) {
48
264
  const mergedKeys = new Set(keys);
@@ -54,20 +270,20 @@ function generateAutocomplete(ctx) {
54
270
  const autocomplete = ctx.engine.config.autocomplete;
55
271
  const patterns = autocomplete.patterns ?? {
56
272
  selectors: /* @__PURE__ */ new Set(),
57
- styleItemStrings: /* @__PURE__ */ new Set(),
273
+ shortcuts: /* @__PURE__ */ new Set(),
58
274
  properties: /* @__PURE__ */ new Map(),
59
275
  cssProperties: /* @__PURE__ */ new Map()
60
276
  };
61
277
  const { layers } = ctx.engine.config;
62
278
  const layerNames = sortLayerNames(layers);
63
279
  return [
64
- "export type Autocomplete = DefineAutocomplete<{",
280
+ "export type Autocomplete = {",
65
281
  ` Selector: ${formatAutocompleteUnion(autocomplete.selectors, patterns.selectors)}`,
66
- ` StyleItemString: ${formatAutocompleteUnion(autocomplete.styleItemStrings, patterns.styleItemStrings)}`,
282
+ ` Shortcut: ${formatAutocompleteUnion(autocomplete.shortcuts, patterns.shortcuts)}`,
67
283
  ` PropertyValue: ${formatAutocompleteValueMap(autocomplete.extraProperties, autocomplete.properties, patterns.properties, (values, patterns) => formatUnionType([...values, ...patterns]))}`,
68
284
  ` CSSPropertyValue: ${formatAutocompleteValueMap(autocomplete.extraCssProperties, autocomplete.cssProperties, patterns.cssProperties, (values, patterns) => formatAutocompleteUnion(values, patterns))}`,
69
285
  ` Layer: ${formatUnionStringType(layerNames)}`,
70
- "}>",
286
+ "}",
71
287
  ""
72
288
  ];
73
289
  }
@@ -120,8 +336,8 @@ async function generateOverloadContent(ctx) {
120
336
  log.debug("Generating TypeScript overload content");
121
337
  const paramsLines = [];
122
338
  const fnsLines = [];
123
- const usages = [...ctx.usages.values()].flat();
124
- log.debug(`Processing ${usages.length} style usages for overload generation`);
339
+ const usages = [...ctx.previewUsages.values()].flat();
340
+ log.debug(`Processing ${usages.length} preview usages for overload generation`);
125
341
  for (let i = 0; i < usages.length; i++) {
126
342
  const usage = usages[i];
127
343
  try {
@@ -133,7 +349,7 @@ async function generateOverloadContent(ctx) {
133
349
  ...(await ctx.engine.renderAtomicStyles(true, {
134
350
  atomicStyleIds: usage.atomicStyleIds,
135
351
  isPreview: true
136
- })).trim().split("\n").map((line) => ` * ‎${line.replace(/^(\s*)/, "$1‎")}`),
352
+ })).trim().split("\n").map((line) => ` * ‎${line.replace(RE_LEADING_INDENT, "$1‎")}`),
137
353
  " * ```",
138
354
  " */",
139
355
  ` fn(...params: [${usage.params.map((_, index) => `p${index}: P${i}_${index}`).join(", ")}]): ReturnType<StyleFn>`
@@ -154,11 +370,24 @@ async function generateOverloadContent(ctx) {
154
370
  ...paramsLines
155
371
  ];
156
372
  }
373
+ /**
374
+ * Generates the full content of the `pika.gen.ts` TypeScript declaration file from the current engine and usage state.
375
+ * @internal
376
+ *
377
+ * @param ctx - The integration context providing engine config, usage records, and codegen settings.
378
+ * @returns The complete TypeScript source string for the generated declaration file.
379
+ *
380
+ * @remarks
381
+ * The output includes module augmentation for `PikaAugment`, autocomplete type literals
382
+ * derived from selectors/shortcuts/properties, style function type overloads (respecting
383
+ * `transformedFormat`), global declarations, optional Vue component property declarations,
384
+ * and per-usage preview overloads with inline CSS previews.
385
+ */
157
386
  async function generateTsCodegenContent(ctx) {
158
387
  log.debug("Generating TypeScript code generation content");
159
388
  const lines = [
160
389
  `// Auto-generated by ${ctx.currentPackageName}`,
161
- `import type { CSSProperty, CSSSelector, DefineAutocomplete, Properties, StyleDefinition, StyleItem } from \'${ctx.currentPackageName}\'`,
390
+ `import type { CSSProperty, CSSSelector, Properties, StyleDefinition, StyleItem } from \'${ctx.currentPackageName}\'`,
162
391
  "",
163
392
  `declare module \'${ctx.currentPackageName}\' {`,
164
393
  " interface PikaAugment {",
@@ -180,9 +409,9 @@ async function generateTsCodegenContent(ctx) {
180
409
  log.debug("TypeScript code generation content completed");
181
410
  return lines.join("\n");
182
411
  }
183
-
184
412
  //#endregion
185
413
  //#region src/ctx.ts
414
+ const RE_VALID_CONFIG_EXT = /\.(?:js|cjs|mjs|ts|cts|mts)$/;
186
415
  function createConfigScaffoldContent({ currentPackageName, resolvedConfigPath, tsCodegenFilepath }) {
187
416
  const relativeTsCodegenFilepath = tsCodegenFilepath == null ? null : `./${relative(dirname(resolvedConfigPath), tsCodegenFilepath)}`;
188
417
  return [
@@ -222,7 +451,6 @@ function usePaths({ cwd: _cwd, cssCodegen, tsCodegen }) {
222
451
  };
223
452
  }
224
453
  function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfig, configOrPath, scan }) {
225
- const RE_VALID_CONFIG_EXT = /\.(?:js|cjs|mjs|ts|cts|mts)$/;
226
454
  const specificConfigPath = computed(() => {
227
455
  if (typeof configOrPath === "string" && RE_VALID_CONFIG_EXT.test(configOrPath)) return isAbsolute(configOrPath) ? configOrPath : join(cwd(), configOrPath);
228
456
  return null;
@@ -231,7 +459,10 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
231
459
  const _cwd = cwd();
232
460
  const _specificConfigPath = specificConfigPath();
233
461
  if (_specificConfigPath != null && statSync(_specificConfigPath, { throwIfNoEntry: false })?.isFile()) return _specificConfigPath;
234
- const stream = globbyStream("**/{pika,pikacss}.config.{js,cjs,mjs,ts,cts,mts}", { ignore: scan.exclude });
462
+ const stream = globbyStream("**/{pika,pikacss}.config.{js,cjs,mjs,ts,cts,mts}", {
463
+ cwd: _cwd,
464
+ ignore: scan.exclude
465
+ });
235
466
  for await (const entry of stream) return join(_cwd, entry);
236
467
  return null;
237
468
  }
@@ -294,165 +525,20 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
294
525
  loadConfig
295
526
  };
296
527
  }
297
- function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName, usages, engine, transformedFormat, triggerStyleUpdated, triggerTsCodegenUpdated }) {
298
- const ESCAPE_REPLACE_RE = /[.*+?^${}()|[\]\\/]/g;
299
- function createFnUtils(fnName) {
300
- const available = {
301
- normal: new Set([fnName]),
302
- forceString: new Set([
303
- `${fnName}.str`,
304
- `${fnName}['str']`,
305
- `${fnName}["str"]`,
306
- `${fnName}[\`str\`]`
307
- ]),
308
- forceArray: new Set([
309
- `${fnName}.arr`,
310
- `${fnName}['arr']`,
311
- `${fnName}["arr"]`,
312
- `${fnName}[\`arr\`]`
313
- ]),
314
- normalPreview: new Set([`${fnName}p`]),
315
- forceStringPreview: new Set([
316
- `${fnName}p.str`,
317
- `${fnName}p['str']`,
318
- `${fnName}p["str"]`,
319
- `${fnName}p[\`str\`]`
320
- ]),
321
- forceArrayPreview: new Set([
322
- `${fnName}p.arr`,
323
- `${fnName}p['arr']`,
324
- `${fnName}p["arr"]`,
325
- `${fnName}p[\`arr\`]`
326
- ])
327
- };
328
- return {
329
- isNormal: (fnName) => available.normal.has(fnName) || available.normalPreview.has(fnName),
330
- isForceString: (fnName) => available.forceString.has(fnName) || available.forceStringPreview.has(fnName),
331
- isForceArray: (fnName) => available.forceArray.has(fnName) || available.forceArrayPreview.has(fnName),
332
- isPreview: (fnName) => available.normalPreview.has(fnName) || available.forceStringPreview.has(fnName) || available.forceArrayPreview.has(fnName),
333
- RE: new RegExp(`\\b(${Object.values(available).flatMap((s) => [...s].map((f) => `(${f.replace(ESCAPE_REPLACE_RE, "\\$&")})`)).join("|")})\\(`, "g")
334
- };
335
- }
528
+ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName, usages, previewUsages, engine, transformedFormat, triggerStyleUpdated, triggerTsCodegenUpdated }) {
336
529
  const fnUtils = createFnUtils(fnName);
337
- function findTemplateExpressionEnd(code, start) {
338
- let end = start;
339
- let depth = 1;
340
- let inString = false;
341
- let isEscaped = false;
342
- while (depth > 0 && end < code.length - 1) {
343
- end++;
344
- const char = code[end];
345
- if (isEscaped) {
346
- isEscaped = false;
347
- continue;
348
- }
349
- if (char === "\\") {
350
- isEscaped = true;
351
- continue;
352
- }
353
- if (inString !== false) {
354
- if (char === inString) inString = false;
355
- else if (inString === "`" && char === "$" && code[end + 1] === "{") {
356
- const nestedExpressionEnd = findTemplateExpressionEnd(code, end + 1);
357
- if (nestedExpressionEnd === -1) return -1;
358
- end = nestedExpressionEnd;
359
- }
360
- continue;
361
- }
362
- if (char === "{") depth++;
363
- else if (char === "}") depth--;
364
- else if (char === "'" || char === "\"" || char === "`") inString = char;
365
- else if (char === "/" && code[end + 1] === "/") {
366
- const lineEnd = code.indexOf("\n", end);
367
- if (lineEnd === -1) return -1;
368
- end = lineEnd;
369
- } else if (char === "/" && code[end + 1] === "*") {
370
- const commentEnd = code.indexOf("*/", end + 2);
371
- if (commentEnd === -1) return -1;
372
- end = commentEnd + 1;
373
- }
374
- }
375
- return depth === 0 ? end : -1;
376
- }
377
- function findFunctionCalls(code) {
378
- const RE = fnUtils.RE;
379
- const result = [];
380
- let matched = RE.exec(code);
381
- while (matched != null) {
382
- const fnName = matched[1];
383
- const start = matched.index;
384
- let end = start + fnName.length;
385
- let depth = 1;
386
- let inString = false;
387
- let isEscaped = false;
388
- while (depth > 0 && end < code.length) {
389
- end++;
390
- const char = code[end];
391
- if (isEscaped) {
392
- isEscaped = false;
393
- continue;
394
- }
395
- if (char === "\\") {
396
- isEscaped = true;
397
- continue;
398
- }
399
- if (inString !== false) {
400
- if (char === inString) inString = false;
401
- else if (inString === "`" && char === "$" && code[end + 1] === "{") {
402
- const templateExpressionEnd = findTemplateExpressionEnd(code, end + 1);
403
- if (templateExpressionEnd === -1) {
404
- log.warn(`Malformed template literal expression in function call at position ${start}`);
405
- break;
406
- }
407
- end = templateExpressionEnd;
408
- }
409
- continue;
410
- }
411
- if (char === "(") depth++;
412
- else if (char === ")") depth--;
413
- else if (char === "'" || char === "\"" || char === "`") inString = char;
414
- else if (char === "/" && code[end + 1] === "/") {
415
- const lineEnd = code.indexOf("\n", end);
416
- if (lineEnd === -1) {
417
- log.warn(`Unclosed function call at position ${start}`);
418
- break;
419
- }
420
- end = lineEnd;
421
- } else if (char === "/" && code[end + 1] === "*") {
422
- const commentEnd = code.indexOf("*/", end + 2);
423
- if (commentEnd === -1) {
424
- log.warn(`Unclosed comment in function call at position ${start}`);
425
- break;
426
- }
427
- end = commentEnd + 1;
428
- }
429
- }
430
- if (depth !== 0) {
431
- log.warn(`Malformed function call at position ${start}, skipping`);
432
- matched = RE.exec(code);
433
- continue;
434
- }
435
- const snippet = code.slice(start, end + 1);
436
- result.push({
437
- fnName,
438
- start,
439
- end,
440
- snippet
441
- });
442
- matched = RE.exec(code);
443
- }
444
- return result;
445
- }
446
530
  async function transform(code, id) {
447
531
  const _engine = engine();
448
532
  if (_engine == null) return null;
449
533
  try {
450
534
  log.debug(`Transforming file: ${id}`);
451
535
  usages.delete(id);
452
- const functionCalls = findFunctionCalls(code);
536
+ previewUsages.delete(id);
537
+ const functionCalls = findFunctionCalls(code, fnUtils);
453
538
  if (functionCalls.length === 0) return;
454
539
  log.debug(`Found ${functionCalls.length} style function calls in ${id}`);
455
540
  const usageList = [];
541
+ const previewUsageList = [];
456
542
  const transformed = new MagicString(code);
457
543
  for (const fnCall of functionCalls) {
458
544
  const argsStr = `[${fnCall.snippet.slice(fnCall.fnName.length + 1, -1)}]`;
@@ -463,6 +549,7 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
463
549
  params: args
464
550
  };
465
551
  usageList.push(usage);
552
+ if (fnUtils.isPreview(fnCall.fnName)) previewUsageList.push(usage);
466
553
  let transformedContent;
467
554
  if (fnUtils.isNormal(fnCall.fnName)) transformedContent = transformedFormat === "array" ? `[${names.map((n) => `'${n}'`).join(", ")}]` : `'${names.join(" ")}'`;
468
555
  else if (fnUtils.isForceString(fnCall.fnName)) transformedContent = `'${names.join(" ")}'`;
@@ -471,6 +558,7 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
471
558
  transformed.update(fnCall.start, fnCall.end + 1, transformedContent);
472
559
  }
473
560
  usages.set(id, usageList);
561
+ if (previewUsageList.length > 0) previewUsages.set(id, previewUsageList);
474
562
  triggerStyleUpdated();
475
563
  triggerTsCodegenUpdated();
476
564
  log.debug(`Transformed ${usageList.length} style usages in ${id}`);
@@ -479,7 +567,7 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
479
567
  map: transformed.generateMap({ hires: true })
480
568
  };
481
569
  } catch (error) {
482
- log.error(`Failed to transform code (${join(cwd(), id)}): ${error.message}`, error);
570
+ log.error(`Failed to transform code (${isAbsolute(id) ? id : join(cwd(), id)}): ${error.message}`, error);
483
571
  return;
484
572
  }
485
573
  }
@@ -488,13 +576,25 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
488
576
  include: scan.include,
489
577
  exclude: [
490
578
  ...scan.exclude,
491
- cssCodegenFilepath(),
492
- ...tsCodegenFilepath() ? [tsCodegenFilepath()] : []
579
+ relative(cwd(), cssCodegenFilepath()),
580
+ ...tsCodegenFilepath() ? [relative(cwd(), tsCodegenFilepath())] : []
493
581
  ]
494
582
  },
495
583
  transform
496
584
  };
497
585
  }
586
+ /**
587
+ * Creates an `IntegrationContext` that wires together config loading, engine initialization, source file transformation, and codegen output.
588
+ *
589
+ * @param options - The integration configuration including paths, function name, scan globs, and codegen settings.
590
+ * @returns A fully constructed `IntegrationContext`. Call `setup()` on the returned context before using transforms.
591
+ *
592
+ * @remarks
593
+ * The context uses reactive signals internally so that computed paths (CSS and TS codegen
594
+ * file paths) automatically update when `cwd` changes. The `setup()` method must be called
595
+ * before any transform or codegen operations - transform calls automatically await the
596
+ * pending setup promise.
597
+ */
498
598
  function createCtx(options) {
499
599
  const { cwd, cssCodegenFilepath, tsCodegenFilepath } = usePaths(options);
500
600
  const { resolvedConfig, resolvedConfigPath, resolvedConfigContent, loadConfig } = useConfig({
@@ -503,6 +603,7 @@ function createCtx(options) {
503
603
  tsCodegenFilepath
504
604
  });
505
605
  const usages = /* @__PURE__ */ new Map();
606
+ const previewUsages = /* @__PURE__ */ new Map();
506
607
  const engine = signal(null);
507
608
  const hooks = {
508
609
  styleUpdated: createEventHook(),
@@ -514,6 +615,7 @@ function createCtx(options) {
514
615
  cssCodegenFilepath,
515
616
  tsCodegenFilepath,
516
617
  usages,
618
+ previewUsages,
517
619
  engine,
518
620
  triggerStyleUpdated: () => hooks.styleUpdated.trigger(),
519
621
  triggerTsCodegenUpdated: () => hooks.tsCodegenUpdated.trigger()
@@ -548,6 +650,7 @@ function createCtx(options) {
548
650
  },
549
651
  loadConfig,
550
652
  usages,
653
+ previewUsages,
551
654
  hooks,
552
655
  get engine() {
553
656
  const _engine = engine();
@@ -597,12 +700,16 @@ function createCtx(options) {
597
700
  fullyCssCodegen: async () => {
598
701
  await ctx.setupPromise;
599
702
  log.debug("Starting full CSS code generation scan");
600
- const stream = globbyStream(options.scan.include, { ignore: options.scan.exclude });
601
- let fileCount = 0;
602
703
  const _cwd = cwd();
704
+ const stream = globbyStream(options.scan.include, {
705
+ cwd: _cwd,
706
+ ignore: options.scan.exclude
707
+ });
708
+ let fileCount = 0;
603
709
  for await (const entry of stream) {
604
- const code = await readFile(join(_cwd, entry), "utf-8");
605
- await ctx.transform(code, entry);
710
+ const filePath = join(_cwd, entry);
711
+ const code = await readFile(filePath, "utf-8");
712
+ await ctx.transform(code, filePath);
606
713
  fileCount++;
607
714
  }
608
715
  log.debug(`Scanned ${fileCount} files for style collection`);
@@ -621,6 +728,7 @@ function createCtx(options) {
621
728
  async function setup() {
622
729
  log.debug("Setting up integration context");
623
730
  usages.clear();
731
+ previewUsages.clear();
624
732
  hooks.styleUpdated.listeners.clear();
625
733
  hooks.tsCodegenUpdated.listeners.clear();
626
734
  engine(null);
@@ -645,6 +753,5 @@ function createCtx(options) {
645
753
  }
646
754
  return ctx;
647
755
  }
648
-
649
756
  //#endregion
650
- export { createCtx };
757
+ export { createCtx };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@pikacss/integration",
3
3
  "type": "module",
4
- "version": "0.0.47",
4
+ "version": "0.0.49",
5
5
  "author": "DevilTea <ch19980814@gmail.com>",
6
6
  "license": "MIT",
7
+ "homepage": "https://pikacss.com",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/pikacss/pikacss.git",
10
+ "url": "git+https://github.com/pikacss/pikacss.git",
10
11
  "directory": "packages/integration"
11
12
  },
12
13
  "bugs": {
@@ -18,6 +19,7 @@
18
19
  "css-in-js",
19
20
  "atomic-css-in-js-engine"
20
21
  ],
22
+ "sideEffects": false,
21
23
  "exports": {
22
24
  ".": {
23
25
  "import": {
@@ -34,28 +36,27 @@
34
36
  "files": [
35
37
  "dist"
36
38
  ],
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
37
42
  "dependencies": {
38
43
  "alien-signals": "^3.1.2",
39
- "globby": "^16.1.1",
44
+ "globby": "^16.2.0",
40
45
  "jiti": "^2.6.1",
41
46
  "klona": "^2.0.6",
42
47
  "local-pkg": "^1.1.2",
43
48
  "magic-string": "^0.30.21",
44
- "micromatch": "^4.0.8",
45
49
  "pathe": "^2.0.3",
46
50
  "perfect-debounce": "^2.1.0",
47
- "@pikacss/core": "0.0.47"
48
- },
49
- "devDependencies": {
50
- "@types/micromatch": "^4.0.10"
51
+ "@pikacss/core": "0.0.49"
51
52
  },
52
53
  "scripts": {
53
- "build": "tsdown && pnpm exec publint",
54
+ "build": "tsdown",
54
55
  "build:watch": "tsdown --watch",
55
56
  "typecheck": "pnpm typecheck:package && pnpm typecheck:test",
56
57
  "typecheck:package": "tsc --project ./tsconfig.package.json --noEmit",
57
58
  "typecheck:test": "tsc --project ./tsconfig.tests.json --noEmit",
58
- "test": "vitest run",
59
- "test:watch": "vitest"
59
+ "test": "vitest run --config ./vitest.config.ts",
60
+ "test:watch": "vitest --config ./vitest.config.ts"
60
61
  }
61
62
  }