@pikacss/integration 0.0.47 → 0.0.48

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,85 @@ 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
+ /** Event hooks for notifying plugins when generated outputs need refreshing. `styleUpdated` fires on CSS changes; `tsCodegenUpdated` fires on TypeScript declaration changes. */
67
174
  hooks: {
68
175
  styleUpdated: ReturnType<typeof createEventHook<void>>;
69
176
  tsCodegenUpdated: ReturnType<typeof createEventHook<void>>;
70
177
  };
178
+ /** The initialized PikaCSS engine instance. Throws if accessed before `setup()` completes. */
71
179
  engine: Engine;
180
+ /** Glob patterns for the bundler's transform pipeline, derived from the scan config with codegen files excluded. */
72
181
  transformFilter: {
73
182
  include: string[];
74
183
  exclude: string[];
75
184
  };
185
+ /** 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
186
  transform: (code: string, id: string) => Promise<{
77
187
  code: string;
78
188
  map: SourceMap;
79
189
  } | Nullish>;
190
+ /** Generates the full CSS output string, including layer declarations, preflights, and all atomic styles collected from transforms. */
80
191
  getCssCodegenContent: () => Promise<string | Nullish>;
192
+ /** Generates the full TypeScript declaration content for `pika.gen.ts`, or `null` if TypeScript codegen is disabled. */
81
193
  getTsCodegenContent: () => Promise<string | Nullish>;
194
+ /** Generates and writes the CSS codegen file to disk at `cssCodegenFilepath`. */
82
195
  writeCssCodegenFile: () => Promise<void>;
196
+ /** Generates and writes the TypeScript codegen file to disk at `tsCodegenFilepath`. No-op if TypeScript codegen is disabled. */
83
197
  writeTsCodegenFile: () => Promise<void>;
198
+ /** Scans all matching source files, collects usages via transform, then writes the CSS codegen file. Used for full rebuilds. */
84
199
  fullyCssCodegen: () => Promise<void>;
200
+ /** The pending setup promise while initialization is in progress, or `null` when idle. Transform calls await this before proceeding. */
85
201
  setupPromise: Promise<void> | null;
202
+ /** 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
203
  setup: () => Promise<void>;
87
204
  }
88
205
  //#endregion
89
206
  //#region src/ctx.d.ts
207
+ /**
208
+ * Creates an `IntegrationContext` that wires together config loading, engine initialization, source file transformation, and codegen output.
209
+ *
210
+ * @param options - The integration configuration including paths, function name, scan globs, and codegen settings.
211
+ * @returns A fully constructed `IntegrationContext`. Call `setup()` on the returned context before using transforms.
212
+ *
213
+ * @remarks
214
+ * The context uses reactive signals internally so that computed paths (CSS and TS codegen
215
+ * file paths) automatically update when `cwd` changes. The `setup()` method must be called
216
+ * before any transform or codegen operations - transform calls automatically await the
217
+ * pending setup promise.
218
+ */
90
219
  declare function createCtx(options: IntegrationContextOptions): IntegrationContext;
91
220
  //#endregion
92
221
  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,7 +270,7 @@ 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
  };
@@ -63,7 +279,7 @@ function generateAutocomplete(ctx) {
63
279
  return [
64
280
  "export type Autocomplete = DefineAutocomplete<{",
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)}`,
@@ -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,6 +370,19 @@ 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 = [
@@ -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
  }
@@ -295,161 +526,14 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
295
526
  };
296
527
  }
297
528
  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
- }
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
+ const functionCalls = findFunctionCalls(code, fnUtils);
453
537
  if (functionCalls.length === 0) return;
454
538
  log.debug(`Found ${functionCalls.length} style function calls in ${id}`);
455
539
  const usageList = [];
@@ -479,7 +563,7 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
479
563
  map: transformed.generateMap({ hires: true })
480
564
  };
481
565
  } catch (error) {
482
- log.error(`Failed to transform code (${join(cwd(), id)}): ${error.message}`, error);
566
+ log.error(`Failed to transform code (${isAbsolute(id) ? id : join(cwd(), id)}): ${error.message}`, error);
483
567
  return;
484
568
  }
485
569
  }
@@ -488,13 +572,25 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
488
572
  include: scan.include,
489
573
  exclude: [
490
574
  ...scan.exclude,
491
- cssCodegenFilepath(),
492
- ...tsCodegenFilepath() ? [tsCodegenFilepath()] : []
575
+ relative(cwd(), cssCodegenFilepath()),
576
+ ...tsCodegenFilepath() ? [relative(cwd(), tsCodegenFilepath())] : []
493
577
  ]
494
578
  },
495
579
  transform
496
580
  };
497
581
  }
582
+ /**
583
+ * Creates an `IntegrationContext` that wires together config loading, engine initialization, source file transformation, and codegen output.
584
+ *
585
+ * @param options - The integration configuration including paths, function name, scan globs, and codegen settings.
586
+ * @returns A fully constructed `IntegrationContext`. Call `setup()` on the returned context before using transforms.
587
+ *
588
+ * @remarks
589
+ * The context uses reactive signals internally so that computed paths (CSS and TS codegen
590
+ * file paths) automatically update when `cwd` changes. The `setup()` method must be called
591
+ * before any transform or codegen operations - transform calls automatically await the
592
+ * pending setup promise.
593
+ */
498
594
  function createCtx(options) {
499
595
  const { cwd, cssCodegenFilepath, tsCodegenFilepath } = usePaths(options);
500
596
  const { resolvedConfig, resolvedConfigPath, resolvedConfigContent, loadConfig } = useConfig({
@@ -597,12 +693,16 @@ function createCtx(options) {
597
693
  fullyCssCodegen: async () => {
598
694
  await ctx.setupPromise;
599
695
  log.debug("Starting full CSS code generation scan");
600
- const stream = globbyStream(options.scan.include, { ignore: options.scan.exclude });
601
- let fileCount = 0;
602
696
  const _cwd = cwd();
697
+ const stream = globbyStream(options.scan.include, {
698
+ cwd: _cwd,
699
+ ignore: options.scan.exclude
700
+ });
701
+ let fileCount = 0;
603
702
  for await (const entry of stream) {
604
- const code = await readFile(join(_cwd, entry), "utf-8");
605
- await ctx.transform(code, entry);
703
+ const filePath = join(_cwd, entry);
704
+ const code = await readFile(filePath, "utf-8");
705
+ await ctx.transform(code, filePath);
606
706
  fileCount++;
607
707
  }
608
708
  log.debug(`Scanned ${fileCount} files for style collection`);
@@ -645,6 +745,5 @@ function createCtx(options) {
645
745
  }
646
746
  return ctx;
647
747
  }
648
-
649
748
  //#endregion
650
- export { createCtx };
749
+ 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.48",
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.48"
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
  }