@pikacss/integration 0.0.46 → 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,86 +3,219 @@ import { SourceMap } from "magic-string";
3
3
  export * from "@pikacss/core";
4
4
 
5
5
  //#region src/eventHook.d.ts
6
- type EventHookListener<EventPayload> = (payload: EventPayload) => void | Promise<void>;
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
+ */
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
+ */
127
+ type LoadedConfigResult = {
128
+ config: EngineConfig;
129
+ file: null;
130
+ content: null;
131
+ } | {
132
+ config: null;
133
+ file: null;
134
+ content: null;
135
+ } | {
136
+ config: EngineConfig;
137
+ file: string;
138
+ content: string;
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
+ */
41
148
  interface IntegrationContext {
149
+ /** The current working directory. Can be updated at runtime (e.g., when the project root changes). */
42
150
  cwd: string;
151
+ /** The npm package name of the integration consumer, used in generated file headers and module declarations. */
43
152
  currentPackageName: string;
153
+ /** The base function name recognized in source transforms (e.g., `'pika'`). */
44
154
  fnName: string;
155
+ /** The default output format for normal `pika()` calls: `'string'` or `'array'`. */
45
156
  transformedFormat: 'string' | 'array';
157
+ /** Absolute path to the generated CSS output file, computed from `cwd` and the configured relative path. */
46
158
  cssCodegenFilepath: string;
159
+ /** Absolute path to the generated TypeScript declaration file, or `null` if TypeScript codegen is disabled. */
47
160
  tsCodegenFilepath: string | Nullish;
161
+ /** Whether the `vue` package is installed in the project, used to include Vue-specific type declarations in codegen. */
48
162
  hasVue: boolean;
163
+ /** The loaded engine configuration object, or `null` if loading failed or no config was found. */
49
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. */
50
166
  resolvedConfigPath: string | Nullish;
167
+ /** Raw string content of the config file, or `null` for inline configs or when no config was loaded. */
51
168
  resolvedConfigContent: string | Nullish;
52
- loadConfig: () => Promise<{
53
- config: EngineConfig;
54
- file: null;
55
- } | {
56
- config: null;
57
- file: null;
58
- } | {
59
- config: EngineConfig;
60
- file: string;
61
- }>;
169
+ /** Loads (or reloads) the engine configuration from disk or inline source, updating `resolvedConfig`, `resolvedConfigPath`, and `resolvedConfigContent`. */
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`. */
62
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. */
63
174
  hooks: {
64
175
  styleUpdated: ReturnType<typeof createEventHook<void>>;
65
176
  tsCodegenUpdated: ReturnType<typeof createEventHook<void>>;
66
177
  };
178
+ /** The initialized PikaCSS engine instance. Throws if accessed before `setup()` completes. */
67
179
  engine: Engine;
180
+ /** Glob patterns for the bundler's transform pipeline, derived from the scan config with codegen files excluded. */
68
181
  transformFilter: {
69
182
  include: string[];
70
183
  exclude: string[];
71
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. */
72
186
  transform: (code: string, id: string) => Promise<{
73
187
  code: string;
74
188
  map: SourceMap;
75
189
  } | Nullish>;
190
+ /** Generates the full CSS output string, including layer declarations, preflights, and all atomic styles collected from transforms. */
76
191
  getCssCodegenContent: () => Promise<string | Nullish>;
192
+ /** Generates the full TypeScript declaration content for `pika.gen.ts`, or `null` if TypeScript codegen is disabled. */
77
193
  getTsCodegenContent: () => Promise<string | Nullish>;
194
+ /** Generates and writes the CSS codegen file to disk at `cssCodegenFilepath`. */
78
195
  writeCssCodegenFile: () => Promise<void>;
196
+ /** Generates and writes the TypeScript codegen file to disk at `tsCodegenFilepath`. No-op if TypeScript codegen is disabled. */
79
197
  writeTsCodegenFile: () => Promise<void>;
198
+ /** Scans all matching source files, collects usages via transform, then writes the CSS codegen file. Used for full rebuilds. */
80
199
  fullyCssCodegen: () => Promise<void>;
200
+ /** The pending setup promise while initialization is in progress, or `null` when idle. Transform calls await this before proceeding. */
81
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. */
82
203
  setup: () => Promise<void>;
83
204
  }
84
205
  //#endregion
85
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
+ */
86
219
  declare function createCtx(options: IntegrationContextOptions): IntegrationContext;
87
220
  //#endregion
88
- export { FnUtils, IntegrationContext, IntegrationContextOptions, UsageRecord, createCtx };
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,24 +248,40 @@ 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*)/;
254
+ function formatUnionType(parts) {
255
+ return parts.length > 0 ? parts.join(" | ") : "never";
256
+ }
38
257
  function formatUnionStringType(list) {
39
- return list.length > 0 ? list.map((i) => JSON.stringify(i)).join(" | ") : "never";
258
+ return formatUnionType(list.map((i) => JSON.stringify(i)));
259
+ }
260
+ function formatAutocompleteUnion(literals, patterns) {
261
+ return formatUnionType([...Array.from(literals, (value) => JSON.stringify(value)), ...patterns]);
262
+ }
263
+ function formatAutocompleteValueMap(keys, entries, patternEntries, formatValue) {
264
+ const mergedKeys = new Set(keys);
265
+ for (const key of entries.keys()) mergedKeys.add(key);
266
+ for (const key of patternEntries.keys()) mergedKeys.add(key);
267
+ return mergedKeys.size > 0 ? `{ ${Array.from(mergedKeys, (key) => `${JSON.stringify(key)}: ${formatValue(entries.get(key) || [], patternEntries.get(key) || [])}`).join(", ")} }` : "never";
40
268
  }
41
269
  function generateAutocomplete(ctx) {
42
270
  const autocomplete = ctx.engine.config.autocomplete;
271
+ const patterns = autocomplete.patterns ?? {
272
+ selectors: /* @__PURE__ */ new Set(),
273
+ shortcuts: /* @__PURE__ */ new Set(),
274
+ properties: /* @__PURE__ */ new Map(),
275
+ cssProperties: /* @__PURE__ */ new Map()
276
+ };
43
277
  const { layers } = ctx.engine.config;
44
278
  const layerNames = sortLayerNames(layers);
45
279
  return [
46
280
  "export type Autocomplete = DefineAutocomplete<{",
47
- ` Selector: ${formatUnionStringType([...autocomplete.selectors])}`,
48
- ` StyleItemString: ${formatUnionStringType([...autocomplete.styleItemStrings])}`,
49
- ` ExtraProperty: ${formatUnionStringType([...autocomplete.extraProperties])}`,
50
- ` ExtraCssProperty: ${formatUnionStringType([...autocomplete.extraCssProperties])}`,
51
- ` PropertiesValue: { ${Array.from(autocomplete.properties.entries(), ([k, v]) => `${JSON.stringify(k)}: ${v.length > 0 ? v.join(" | ") : "never"}`).join(", ")} }`,
52
- ` CssPropertiesValue: { ${Array.from(autocomplete.cssProperties.entries(), ([k, v]) => `${JSON.stringify(k)}: ${formatUnionStringType(v)}`).join(", ")} }`,
281
+ ` Selector: ${formatAutocompleteUnion(autocomplete.selectors, patterns.selectors)}`,
282
+ ` Shortcut: ${formatAutocompleteUnion(autocomplete.shortcuts, patterns.shortcuts)}`,
283
+ ` PropertyValue: ${formatAutocompleteValueMap(autocomplete.extraProperties, autocomplete.properties, patterns.properties, (values, patterns) => formatUnionType([...values, ...patterns]))}`,
284
+ ` CSSPropertyValue: ${formatAutocompleteValueMap(autocomplete.extraCssProperties, autocomplete.cssProperties, patterns.cssProperties, (values, patterns) => formatAutocompleteUnion(values, patterns))}`,
53
285
  ` Layer: ${formatUnionStringType(layerNames)}`,
54
286
  "}>",
55
287
  ""
@@ -117,7 +349,7 @@ async function generateOverloadContent(ctx) {
117
349
  ...(await ctx.engine.renderAtomicStyles(true, {
118
350
  atomicStyleIds: usage.atomicStyleIds,
119
351
  isPreview: true
120
- })).trim().split("\n").map((line) => ` * ‎${line.replace(/^(\s*)/, "$1‎")}`),
352
+ })).trim().split("\n").map((line) => ` * ‎${line.replace(RE_LEADING_INDENT, "$1‎")}`),
121
353
  " * ```",
122
354
  " */",
123
355
  ` fn(...params: [${usage.params.map((_, index) => `p${index}: P${i}_${index}`).join(", ")}]): ReturnType<StyleFn>`
@@ -138,6 +370,19 @@ async function generateOverloadContent(ctx) {
138
370
  ...paramsLines
139
371
  ];
140
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
+ */
141
386
  async function generateTsCodegenContent(ctx) {
142
387
  log.debug("Generating TypeScript code generation content");
143
388
  const lines = [
@@ -148,7 +393,7 @@ async function generateTsCodegenContent(ctx) {
148
393
  " interface PikaAugment {",
149
394
  " Autocomplete: Autocomplete",
150
395
  " Selector: Autocomplete['Selector'] | CSSSelector",
151
- " CSSProperty: Autocomplete['ExtraCssProperty'] | CSSProperty",
396
+ " CSSProperty: ([Autocomplete['CSSPropertyValue']] extends [never] ? never : Extract<keyof Autocomplete['CSSPropertyValue'], string>) | CSSProperty",
152
397
  " Properties: Properties",
153
398
  " StyleDefinition: StyleDefinition",
154
399
  " StyleItem: StyleItem",
@@ -164,9 +409,39 @@ async function generateTsCodegenContent(ctx) {
164
409
  log.debug("TypeScript code generation content completed");
165
410
  return lines.join("\n");
166
411
  }
167
-
168
412
  //#endregion
169
413
  //#region src/ctx.ts
414
+ const RE_VALID_CONFIG_EXT = /\.(?:js|cjs|mjs|ts|cts|mts)$/;
415
+ function createConfigScaffoldContent({ currentPackageName, resolvedConfigPath, tsCodegenFilepath }) {
416
+ const relativeTsCodegenFilepath = tsCodegenFilepath == null ? null : `./${relative(dirname(resolvedConfigPath), tsCodegenFilepath)}`;
417
+ return [
418
+ ...relativeTsCodegenFilepath == null ? [] : [`/// <reference path="${relativeTsCodegenFilepath}" />`],
419
+ `import { defineEngineConfig } from '${currentPackageName}'`,
420
+ "",
421
+ "export default defineEngineConfig({",
422
+ " // Add your PikaCSS engine config here",
423
+ "})"
424
+ ].join("\n");
425
+ }
426
+ async function writeGeneratedFile(filepath, content) {
427
+ await mkdir(dirname(filepath), { recursive: true }).catch(() => {});
428
+ await writeFile(filepath, content);
429
+ }
430
+ async function evaluateConfigModule(resolvedConfigPath) {
431
+ log.info(`Using config file: ${resolvedConfigPath}`);
432
+ const { createJiti } = await import("jiti");
433
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
434
+ const content = await readFile(resolvedConfigPath, "utf-8");
435
+ const config = (await jiti.evalModule(content, {
436
+ id: resolvedConfigPath,
437
+ forceTranspile: true
438
+ })).default;
439
+ return {
440
+ config: klona(config),
441
+ file: resolvedConfigPath,
442
+ content
443
+ };
444
+ }
170
445
  function usePaths({ cwd: _cwd, cssCodegen, tsCodegen }) {
171
446
  const cwd = signal(_cwd);
172
447
  return {
@@ -176,7 +451,6 @@ function usePaths({ cwd: _cwd, cssCodegen, tsCodegen }) {
176
451
  };
177
452
  }
178
453
  function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfig, configOrPath, scan }) {
179
- const RE_VALID_CONFIG_EXT = /\.(?:js|cjs|mjs|ts|cts|mts)$/;
180
454
  const specificConfigPath = computed(() => {
181
455
  if (typeof configOrPath === "string" && RE_VALID_CONFIG_EXT.test(configOrPath)) return isAbsolute(configOrPath) ? configOrPath : join(cwd(), configOrPath);
182
456
  return null;
@@ -185,10 +459,27 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
185
459
  const _cwd = cwd();
186
460
  const _specificConfigPath = specificConfigPath();
187
461
  if (_specificConfigPath != null && statSync(_specificConfigPath, { throwIfNoEntry: false })?.isFile()) return _specificConfigPath;
188
- 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
+ });
189
466
  for await (const entry of stream) return join(_cwd, entry);
190
467
  return null;
191
468
  }
469
+ async function ensureConfigPath(candidatePath) {
470
+ if (candidatePath != null) return candidatePath;
471
+ if (autoCreateConfig === false) {
472
+ log.warn("Config file not found and autoCreateConfig is false");
473
+ return null;
474
+ }
475
+ const resolvedConfigPath = specificConfigPath() ?? join(cwd(), "pika.config.js");
476
+ await writeGeneratedFile(resolvedConfigPath, createConfigScaffoldContent({
477
+ currentPackageName,
478
+ resolvedConfigPath,
479
+ tsCodegenFilepath: tsCodegenFilepath()
480
+ }));
481
+ return resolvedConfigPath;
482
+ }
192
483
  const inlineConfig = typeof configOrPath === "object" ? configOrPath : null;
193
484
  async function _loadConfig() {
194
485
  try {
@@ -201,43 +492,13 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
201
492
  content: null
202
493
  };
203
494
  }
204
- let resolvedConfigPath = await findFirstExistingConfigPath();
205
- const _cwd = cwd();
206
- if (resolvedConfigPath == null) {
207
- if (autoCreateConfig === false) {
208
- log.warn("Config file not found and autoCreateConfig is false");
209
- return {
210
- config: null,
211
- file: null,
212
- content: null
213
- };
214
- }
215
- resolvedConfigPath = join(_cwd, specificConfigPath() ?? "pika.config.js");
216
- await mkdir(dirname(resolvedConfigPath), { recursive: true }).catch(() => {});
217
- const _tsCodegenFilepath = tsCodegenFilepath();
218
- const relativeTsCodegenFilepath = _tsCodegenFilepath == null ? null : `./${relative(dirname(resolvedConfigPath), _tsCodegenFilepath)}`;
219
- await writeFile(resolvedConfigPath, [
220
- ...relativeTsCodegenFilepath == null ? [] : [`/// <reference path="${relativeTsCodegenFilepath}" />`],
221
- `import { defineEngineConfig } from '${currentPackageName}'`,
222
- "",
223
- "export default defineEngineConfig({",
224
- " // Add your PikaCSS engine config here",
225
- "})"
226
- ].join("\n"));
227
- }
228
- log.info(`Using config file: ${resolvedConfigPath}`);
229
- const { createJiti } = await import("jiti");
230
- const jiti = createJiti(import.meta.url, { interopDefault: true });
231
- const content = await readFile(resolvedConfigPath, "utf-8");
232
- const config = (await jiti.evalModule(content, {
233
- id: resolvedConfigPath,
234
- forceTranspile: true
235
- })).default;
236
- return {
237
- config: klona(config),
238
- file: resolvedConfigPath,
239
- content
495
+ const resolvedConfigPath = await ensureConfigPath(await findFirstExistingConfigPath());
496
+ if (resolvedConfigPath == null) return {
497
+ config: null,
498
+ file: null,
499
+ content: null
240
500
  };
501
+ return await evaluateConfigModule(resolvedConfigPath);
241
502
  } catch (error) {
242
503
  log.error(`Failed to load config file: ${error.message}`, error);
243
504
  return {
@@ -265,117 +526,14 @@ function useConfig({ cwd, tsCodegenFilepath, currentPackageName, autoCreateConfi
265
526
  };
266
527
  }
267
528
  function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName, usages, engine, transformedFormat, triggerStyleUpdated, triggerTsCodegenUpdated }) {
268
- const ESCAPE_REPLACE_RE = /[.*+?^${}()|[\]\\/]/g;
269
- function createFnUtils(fnName) {
270
- const available = {
271
- normal: new Set([fnName]),
272
- forceString: new Set([
273
- `${fnName}.str`,
274
- `${fnName}['str']`,
275
- `${fnName}["str"]`,
276
- `${fnName}[\`str\`]`
277
- ]),
278
- forceArray: new Set([
279
- `${fnName}.arr`,
280
- `${fnName}['arr']`,
281
- `${fnName}["arr"]`,
282
- `${fnName}[\`arr\`]`
283
- ]),
284
- normalPreview: new Set([`${fnName}p`]),
285
- forceStringPreview: new Set([
286
- `${fnName}p.str`,
287
- `${fnName}p['str']`,
288
- `${fnName}p["str"]`,
289
- `${fnName}p[\`str\`]`
290
- ]),
291
- forceArrayPreview: new Set([
292
- `${fnName}p.arr`,
293
- `${fnName}p['arr']`,
294
- `${fnName}p["arr"]`,
295
- `${fnName}p[\`arr\`]`
296
- ])
297
- };
298
- return {
299
- isNormal: (fnName) => available.normal.has(fnName) || available.normalPreview.has(fnName),
300
- isForceString: (fnName) => available.forceString.has(fnName) || available.forceStringPreview.has(fnName),
301
- isForceArray: (fnName) => available.forceArray.has(fnName) || available.forceArrayPreview.has(fnName),
302
- isPreview: (fnName) => available.normalPreview.has(fnName) || available.forceStringPreview.has(fnName) || available.forceArrayPreview.has(fnName),
303
- RE: new RegExp(`\\b(${Object.values(available).flatMap((s) => [...s].map((f) => `(${f.replace(ESCAPE_REPLACE_RE, "\\$&")})`)).join("|")})\\(`, "g")
304
- };
305
- }
306
529
  const fnUtils = createFnUtils(fnName);
307
- function findFunctionCalls(code) {
308
- const RE = fnUtils.RE;
309
- const result = [];
310
- let matched = RE.exec(code);
311
- while (matched != null) {
312
- const fnName = matched[1];
313
- const start = matched.index;
314
- let end = start + fnName.length;
315
- let depth = 1;
316
- let inString = false;
317
- let isEscaped = false;
318
- while (depth > 0 && end < code.length) {
319
- end++;
320
- const char = code[end];
321
- if (isEscaped) {
322
- isEscaped = false;
323
- continue;
324
- }
325
- if (char === "\\") {
326
- isEscaped = true;
327
- continue;
328
- }
329
- if (inString !== false) {
330
- if (char === inString) inString = false;
331
- else if (inString === "`" && char === "$" && code[end + 1] === "{") {
332
- end++;
333
- depth++;
334
- }
335
- continue;
336
- }
337
- if (char === "(") depth++;
338
- else if (char === ")") depth--;
339
- else if (char === "'" || char === "\"" || char === "`") inString = char;
340
- else if (char === "/" && code[end + 1] === "/") {
341
- const lineEnd = code.indexOf("\n", end);
342
- if (lineEnd === -1) {
343
- log.warn(`Unclosed function call at position ${start}`);
344
- break;
345
- }
346
- end = lineEnd;
347
- } else if (char === "/" && code[end + 1] === "*") {
348
- const commentEnd = code.indexOf("*/", end + 2);
349
- if (commentEnd === -1) {
350
- log.warn(`Unclosed comment in function call at position ${start}`);
351
- break;
352
- }
353
- end = commentEnd + 1;
354
- }
355
- }
356
- if (depth !== 0) {
357
- log.warn(`Malformed function call at position ${start}, skipping`);
358
- matched = RE.exec(code);
359
- continue;
360
- }
361
- const snippet = code.slice(start, end + 1);
362
- result.push({
363
- fnName,
364
- start,
365
- end,
366
- snippet
367
- });
368
- matched = RE.exec(code);
369
- }
370
- return result;
371
- }
372
530
  async function transform(code, id) {
373
531
  const _engine = engine();
374
532
  if (_engine == null) return null;
375
533
  try {
376
534
  log.debug(`Transforming file: ${id}`);
377
535
  usages.delete(id);
378
- const functionCalls = findFunctionCalls(code);
536
+ const functionCalls = findFunctionCalls(code, fnUtils);
379
537
  if (functionCalls.length === 0) return;
380
538
  log.debug(`Found ${functionCalls.length} style function calls in ${id}`);
381
539
  const usageList = [];
@@ -405,7 +563,7 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
405
563
  map: transformed.generateMap({ hires: true })
406
564
  };
407
565
  } catch (error) {
408
- 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);
409
567
  return;
410
568
  }
411
569
  }
@@ -414,13 +572,25 @@ function useTransform({ cwd, cssCodegenFilepath, tsCodegenFilepath, scan, fnName
414
572
  include: scan.include,
415
573
  exclude: [
416
574
  ...scan.exclude,
417
- cssCodegenFilepath(),
418
- ...tsCodegenFilepath() ? [tsCodegenFilepath()] : []
575
+ relative(cwd(), cssCodegenFilepath()),
576
+ ...tsCodegenFilepath() ? [relative(cwd(), tsCodegenFilepath())] : []
419
577
  ]
420
578
  },
421
579
  transform
422
580
  };
423
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
+ */
424
594
  function createCtx(options) {
425
595
  const { cwd, cssCodegenFilepath, tsCodegenFilepath } = usePaths(options);
426
596
  const { resolvedConfig, resolvedConfigPath, resolvedConfigContent, loadConfig } = useConfig({
@@ -509,28 +679,30 @@ function createCtx(options) {
509
679
  await ctx.setupPromise;
510
680
  const content = await ctx.getCssCodegenContent();
511
681
  if (content == null) return;
512
- await mkdir(dirname(ctx.cssCodegenFilepath), { recursive: true }).catch(() => {});
513
682
  log.debug(`Writing CSS code generation file: ${ctx.cssCodegenFilepath}`);
514
- await writeFile(ctx.cssCodegenFilepath, content);
683
+ await writeGeneratedFile(ctx.cssCodegenFilepath, content);
515
684
  },
516
685
  writeTsCodegenFile: async () => {
517
686
  await ctx.setupPromise;
518
687
  if (ctx.tsCodegenFilepath == null) return;
519
688
  const content = await ctx.getTsCodegenContent();
520
689
  if (content == null) return;
521
- await mkdir(dirname(ctx.tsCodegenFilepath), { recursive: true }).catch(() => {});
522
690
  log.debug(`Writing TypeScript code generation file: ${ctx.tsCodegenFilepath}`);
523
- await writeFile(ctx.tsCodegenFilepath, content);
691
+ await writeGeneratedFile(ctx.tsCodegenFilepath, content);
524
692
  },
525
693
  fullyCssCodegen: async () => {
526
694
  await ctx.setupPromise;
527
695
  log.debug("Starting full CSS code generation scan");
528
- const stream = globbyStream(options.scan.include, { ignore: options.scan.exclude });
529
- let fileCount = 0;
530
696
  const _cwd = cwd();
697
+ const stream = globbyStream(options.scan.include, {
698
+ cwd: _cwd,
699
+ ignore: options.scan.exclude
700
+ });
701
+ let fileCount = 0;
531
702
  for await (const entry of stream) {
532
- const code = await readFile(join(_cwd, entry), "utf-8");
533
- 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);
534
706
  fileCount++;
535
707
  }
536
708
  log.debug(`Scanned ${fileCount} files for style collection`);
@@ -573,6 +745,5 @@ function createCtx(options) {
573
745
  }
574
746
  return ctx;
575
747
  }
576
-
577
748
  //#endregion
578
- 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.46",
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.46"
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
  }