@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 +27 -0
- package/dist/index.d.mts +145 -12
- package/dist/index.mjs +340 -169
- package/package.json +12 -11
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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.
|
|
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: ${
|
|
48
|
-
`
|
|
49
|
-
`
|
|
50
|
-
`
|
|
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(
|
|
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['
|
|
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}", {
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
|
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
|
|
533
|
-
await
|
|
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.
|
|
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.
|
|
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.
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/micromatch": "^4.0.10"
|
|
51
|
+
"@pikacss/core": "0.0.48"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
|
-
"build": "tsdown
|
|
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
|
}
|