pulse-js-framework 1.11.3 → 1.11.4
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/cli/analyze.js +21 -8
- package/cli/build.js +83 -56
- package/cli/dev.js +108 -94
- package/cli/docs-test.js +52 -33
- package/cli/index.js +81 -51
- package/cli/mobile.js +92 -40
- package/cli/release.js +64 -46
- package/cli/scaffold.js +14 -13
- package/compiler/lexer.js +55 -54
- package/compiler/parser/core.js +1 -0
- package/compiler/parser/state.js +6 -12
- package/compiler/parser/style.js +17 -20
- package/compiler/parser/view.js +1 -3
- package/compiler/preprocessor.js +124 -262
- package/compiler/sourcemap.js +10 -4
- package/compiler/transformer/expressions.js +122 -106
- package/compiler/transformer/index.js +2 -4
- package/compiler/transformer/style.js +74 -7
- package/compiler/transformer/view.js +86 -36
- package/loader/esbuild-plugin-server-components.js +209 -0
- package/loader/esbuild-plugin.js +41 -93
- package/loader/parcel-plugin.js +37 -97
- package/loader/rollup-plugin-server-components.js +30 -169
- package/loader/rollup-plugin.js +27 -78
- package/loader/shared.js +362 -0
- package/loader/swc-plugin.js +65 -82
- package/loader/vite-plugin-server-components.js +30 -171
- package/loader/vite-plugin.js +25 -10
- package/loader/webpack-loader-server-components.js +21 -134
- package/loader/webpack-loader.js +25 -80
- package/package.json +52 -12
- package/runtime/dom-selector.js +2 -1
- package/runtime/form.js +4 -3
- package/runtime/http.js +6 -1
- package/runtime/logger.js +44 -24
- package/runtime/router/utils.js +14 -7
- package/runtime/security.js +13 -1
- package/runtime/server-components/actions-server.js +23 -19
- package/runtime/server-components/error-sanitizer.js +18 -18
- package/runtime/server-components/security.js +41 -24
- package/runtime/ssr-preload.js +5 -3
- package/runtime/testing.js +759 -0
- package/runtime/utils.js +3 -2
- package/server/utils.js +15 -9
- package/sw/index.js +2 -0
- package/types/loaders.d.ts +1043 -0
- package/compiler/parser/_extract.js +0 -393
- package/loader/README.md +0 -509
package/loader/shared.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Loader - Shared Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common helpers used across all build tool plugins (Vite, Webpack, Rollup,
|
|
5
|
+
* ESBuild, Parcel, SWC) and Server Components plugins.
|
|
6
|
+
*
|
|
7
|
+
* @module pulse-js-framework/loader/shared
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
preprocessStylesSync,
|
|
12
|
+
isSassAvailable,
|
|
13
|
+
isLessAvailable,
|
|
14
|
+
isStylusAvailable,
|
|
15
|
+
getSassVersion,
|
|
16
|
+
getLessVersion,
|
|
17
|
+
getStylusVersion,
|
|
18
|
+
detectPreprocessor
|
|
19
|
+
} from '../compiler/preprocessor.js';
|
|
20
|
+
import { dirname, relative, posix } from 'path';
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
22
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Server Components Constants
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** Regex to match `__directive: "use client"` in compiled output */
|
|
29
|
+
export const DIRECTIVE_REGEX = /__directive:\s*["']use client["']/;
|
|
30
|
+
|
|
31
|
+
/** Regex to extract component ID from `__componentId: "Name"` */
|
|
32
|
+
export const COMPONENT_ID_REGEX = /__componentId:\s*["'](\w+)["']/;
|
|
33
|
+
|
|
34
|
+
/** Regex to extract component name from `export const Name = {` */
|
|
35
|
+
export const EXPORT_CONST_REGEX = /export const (\w+) = \{/;
|
|
36
|
+
|
|
37
|
+
/** Chunk name prefix for Client Components */
|
|
38
|
+
export const CLIENT_CHUNK_PREFIX = 'client-';
|
|
39
|
+
|
|
40
|
+
/** Default manifest output path */
|
|
41
|
+
export const DEFAULT_MANIFEST_PATH = 'dist/.pulse-manifest.json';
|
|
42
|
+
|
|
43
|
+
/** Default manifest filename */
|
|
44
|
+
export const DEFAULT_MANIFEST_FILENAME = '.pulse-manifest.json';
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Preprocessor Cache
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
let preprocessorCache = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check available preprocessors (cached after first call)
|
|
54
|
+
* @returns {{ sass: boolean, less: boolean, stylus: boolean }}
|
|
55
|
+
*/
|
|
56
|
+
export function checkPreprocessors() {
|
|
57
|
+
if (preprocessorCache) return preprocessorCache;
|
|
58
|
+
|
|
59
|
+
preprocessorCache = {
|
|
60
|
+
sass: isSassAvailable(),
|
|
61
|
+
less: isLessAvailable(),
|
|
62
|
+
stylus: isStylusAvailable()
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return preprocessorCache;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reset preprocessor cache (for testing)
|
|
70
|
+
*/
|
|
71
|
+
export function resetPreprocessorCache() {
|
|
72
|
+
preprocessorCache = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Preprocessor Logging
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log available preprocessors on plugin startup.
|
|
81
|
+
* @param {string} pluginLabel - e.g. 'Pulse Rollup'
|
|
82
|
+
* @param {{ sassOnly?: boolean, logFn?: Function }} [options]
|
|
83
|
+
*/
|
|
84
|
+
export function logPreprocessorAvailability(pluginLabel, options = {}) {
|
|
85
|
+
const { sassOnly = false, logFn = console.log } = options;
|
|
86
|
+
const available = checkPreprocessors();
|
|
87
|
+
const preprocessors = [];
|
|
88
|
+
|
|
89
|
+
if (available.sass) {
|
|
90
|
+
preprocessors.push(`SASS ${getSassVersion() || 'unknown'}`);
|
|
91
|
+
}
|
|
92
|
+
if (!sassOnly) {
|
|
93
|
+
if (available.less) {
|
|
94
|
+
preprocessors.push(`LESS ${getLessVersion() || 'unknown'}`);
|
|
95
|
+
}
|
|
96
|
+
if (available.stylus) {
|
|
97
|
+
preprocessors.push(`Stylus ${getStylusVersion() || 'unknown'}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (preprocessors.length > 0) {
|
|
102
|
+
logFn(`[${pluginLabel}] Preprocessor support: ${preprocessors.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// CSS Regex Constants
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/** Regex to extract CSS from compiled .pulse output */
|
|
111
|
+
export const STYLES_MATCH_REGEX = /const styles = `([^`]*)`;/;
|
|
112
|
+
|
|
113
|
+
/** Regex to remove inline style injection block (handles optional SCOPE_ID line) */
|
|
114
|
+
export const INLINE_STYLES_REMOVAL_REGEX =
|
|
115
|
+
/\/\/ Styles\n(?:const SCOPE_ID = '[^']*';\n)?const styles = `[^`]*`;\n\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\n(?:styleEl\.setAttribute\([^)]*\);\n)?styleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/;
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// CSS Helpers
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract CSS from compiled .pulse output code.
|
|
123
|
+
* @param {string} outputCode
|
|
124
|
+
* @returns {{ css: string|null, found: boolean }}
|
|
125
|
+
*/
|
|
126
|
+
export function extractCssFromOutput(outputCode) {
|
|
127
|
+
const match = outputCode.match(STYLES_MATCH_REGEX);
|
|
128
|
+
if (!match) return { css: null, found: false };
|
|
129
|
+
return { css: match[1], found: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove inline style injection and replace with a custom string.
|
|
134
|
+
* @param {string} outputCode
|
|
135
|
+
* @param {string} replacement
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
export function removeInlineStyles(outputCode, replacement) {
|
|
139
|
+
return outputCode.replace(INLINE_STYLES_REMOVAL_REGEX, replacement);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Preprocessor Execution
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get preprocessor-specific options.
|
|
148
|
+
* @param {string} preprocessor - 'sass' | 'less' | 'stylus'
|
|
149
|
+
* @param {{ sassOptions?: object, lessOptions?: object, stylusOptions?: object }} allOptions
|
|
150
|
+
* @returns {object}
|
|
151
|
+
*/
|
|
152
|
+
export function getPreprocessorOptions(preprocessor, { sassOptions = {}, lessOptions = {}, stylusOptions = {} }) {
|
|
153
|
+
return { sass: sassOptions, less: lessOptions, stylus: stylusOptions }[preprocessor] || {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Detect preprocessor and run it synchronously if available.
|
|
158
|
+
* Returns the processed CSS and any warning (instead of throwing).
|
|
159
|
+
*
|
|
160
|
+
* @param {string} css - Raw CSS from compiled output
|
|
161
|
+
* @param {string} filePath - Source file path (for loadPaths)
|
|
162
|
+
* @param {{ sassOptions?: object, lessOptions?: object, stylusOptions?: object }} [allOptions]
|
|
163
|
+
* @returns {{ css: string, preprocessor: string, warning: string|null }}
|
|
164
|
+
*/
|
|
165
|
+
export function processStyles(css, filePath, allOptions = {}) {
|
|
166
|
+
const available = checkPreprocessors();
|
|
167
|
+
const preprocessor = detectPreprocessor(css);
|
|
168
|
+
|
|
169
|
+
if (preprocessor === 'none' || !available[preprocessor]) {
|
|
170
|
+
return { css, preprocessor: 'none', warning: null };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const opts = getPreprocessorOptions(preprocessor, allOptions);
|
|
175
|
+
const preprocessed = preprocessStylesSync(css, {
|
|
176
|
+
filename: filePath,
|
|
177
|
+
loadPaths: [dirname(filePath), ...(opts.loadPaths || [])],
|
|
178
|
+
compressed: opts.compressed || false,
|
|
179
|
+
preprocessor
|
|
180
|
+
});
|
|
181
|
+
return { css: preprocessed.css, preprocessor, warning: null };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return {
|
|
184
|
+
css,
|
|
185
|
+
preprocessor,
|
|
186
|
+
warning: `${preprocessor.toUpperCase()} compilation warning: ${err.message}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Server Components Helpers
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract import statements from source code.
|
|
197
|
+
* Handles: static imports (single/multi-line), namespace, dynamic imports,
|
|
198
|
+
* side-effect imports, re-exports, and TypeScript type-only imports.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} code
|
|
201
|
+
* @returns {string[]} Deduplicated array of import specifiers
|
|
202
|
+
*/
|
|
203
|
+
export function extractImports(code) {
|
|
204
|
+
const imports = [];
|
|
205
|
+
let match;
|
|
206
|
+
|
|
207
|
+
// 1. All static imports/re-exports: ... from '...'
|
|
208
|
+
// Covers: import X from, import { A } from, import * as ns from,
|
|
209
|
+
// export { x } from, export * from, type imports, multi-line imports
|
|
210
|
+
const fromRegex = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
211
|
+
while ((match = fromRegex.exec(code)) !== null) {
|
|
212
|
+
imports.push(match[1]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 2. Side-effect imports: import '...'
|
|
216
|
+
const sideEffectRegex = /\bimport\s+['"]([^'"]+)['"]/g;
|
|
217
|
+
while ((match = sideEffectRegex.exec(code)) !== null) {
|
|
218
|
+
imports.push(match[1]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 3. Dynamic imports: import('...')
|
|
222
|
+
const dynamicRegex = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
223
|
+
while ((match = dynamicRegex.exec(code)) !== null) {
|
|
224
|
+
imports.push(match[1]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [...new Set(imports)];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create import violation error message for Client → Server imports.
|
|
232
|
+
* @param {string} clientPath
|
|
233
|
+
* @param {string} serverPath
|
|
234
|
+
* @param {string} importSource
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
export function createImportViolationError(clientPath, serverPath, importSource) {
|
|
238
|
+
const clientRelative = relative(process.cwd(), clientPath);
|
|
239
|
+
const serverRelative = relative(process.cwd(), serverPath);
|
|
240
|
+
|
|
241
|
+
return `
|
|
242
|
+
[Pulse] Import Violation: Client Component cannot import Server Component
|
|
243
|
+
at ${clientRelative}
|
|
244
|
+
importing ${importSource}
|
|
245
|
+
resolved to ${serverRelative}
|
|
246
|
+
|
|
247
|
+
Client Components can only import:
|
|
248
|
+
\u2022 Other Client Components ('use client')
|
|
249
|
+
\u2022 Shared utilities (no directive)
|
|
250
|
+
\u2022 Third-party packages
|
|
251
|
+
|
|
252
|
+
\u2192 Move shared logic to a Client Component
|
|
253
|
+
\u2192 Use Server Actions for server-side operations
|
|
254
|
+
\u2192 Create a wrapper Client Component that calls Server Actions
|
|
255
|
+
|
|
256
|
+
See: https://pulse-js.fr/server-components#import-rules
|
|
257
|
+
`.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Build the manifest data object from a client components map.
|
|
262
|
+
* @param {Map<string, { file: string, chunk: string|null }>} clientComponents
|
|
263
|
+
* @param {{ base?: string }} config
|
|
264
|
+
* @returns {{ version: string, components: object }}
|
|
265
|
+
*/
|
|
266
|
+
export function buildManifest(clientComponents, config = {}) {
|
|
267
|
+
const manifest = { version: '1.0', components: {} };
|
|
268
|
+
|
|
269
|
+
for (const [componentId, info] of clientComponents.entries()) {
|
|
270
|
+
if (info.chunk) {
|
|
271
|
+
const base = config.base || '';
|
|
272
|
+
const chunkUrl = posix.join(base, info.chunk);
|
|
273
|
+
|
|
274
|
+
manifest.components[componentId] = {
|
|
275
|
+
id: componentId,
|
|
276
|
+
chunk: chunkUrl,
|
|
277
|
+
exports: ['default', componentId]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return manifest;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Write manifest JSON to disk synchronously (creates directories as needed).
|
|
287
|
+
* @param {object} manifest
|
|
288
|
+
* @param {{ manifestPath?: string, quiet?: boolean }} config
|
|
289
|
+
*/
|
|
290
|
+
export function writeManifestToDisk(manifest, config = {}) {
|
|
291
|
+
if (!config.manifestPath) return;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const manifestDir = dirname(config.manifestPath);
|
|
295
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
296
|
+
writeFileSync(config.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
297
|
+
if (!config.quiet) {
|
|
298
|
+
console.log(`[Pulse Server Components] Manifest written to ${config.manifestPath}`);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(`[Pulse Server Components] Failed to write manifest: ${error.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Write manifest JSON to disk asynchronously (creates directories as needed).
|
|
307
|
+
* Preferred for Vite/Rollup closeBundle hooks that support async.
|
|
308
|
+
* @param {object} manifest
|
|
309
|
+
* @param {{ manifestPath?: string, quiet?: boolean }} config
|
|
310
|
+
*/
|
|
311
|
+
export async function writeManifestToDiskAsync(manifest, config = {}) {
|
|
312
|
+
if (!config.manifestPath) return;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const manifestDir = dirname(config.manifestPath);
|
|
316
|
+
await mkdir(manifestDir, { recursive: true });
|
|
317
|
+
await writeFile(config.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
318
|
+
if (!config.quiet) {
|
|
319
|
+
console.log(`[Pulse Server Components] Manifest written to ${config.manifestPath}`);
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn(`[Pulse Server Components] Failed to write manifest: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Client Manifest Helpers (shared across all SC plugins)
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load client manifest from disk (for SSR).
|
|
332
|
+
* @param {string} manifestPath - Path to manifest file
|
|
333
|
+
* @returns {{ version: string, components: object }}
|
|
334
|
+
*/
|
|
335
|
+
export function loadClientManifest(manifestPath) {
|
|
336
|
+
try {
|
|
337
|
+
const content = readFileSync(manifestPath, 'utf-8');
|
|
338
|
+
return JSON.parse(content);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.warn(`Failed to load client manifest from ${manifestPath}:`, error.message);
|
|
341
|
+
return { version: '1.0', components: {} };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get client component chunk URL from manifest.
|
|
347
|
+
* @param {{ components: object }} manifest
|
|
348
|
+
* @param {string} componentId
|
|
349
|
+
* @returns {string|null}
|
|
350
|
+
*/
|
|
351
|
+
export function getComponentChunk(manifest, componentId) {
|
|
352
|
+
return manifest.components[componentId]?.chunk || null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get all client component IDs from manifest.
|
|
357
|
+
* @param {{ components: object }} manifest
|
|
358
|
+
* @returns {Set<string>}
|
|
359
|
+
*/
|
|
360
|
+
export function getClientComponentIds(manifest) {
|
|
361
|
+
return new Set(Object.keys(manifest.components));
|
|
362
|
+
}
|
package/loader/swc-plugin.js
CHANGED
|
@@ -43,35 +43,15 @@
|
|
|
43
43
|
|
|
44
44
|
import { compile } from '../compiler/index.js';
|
|
45
45
|
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
detectPreprocessor
|
|
54
|
-
} from '../compiler/preprocessor.js';
|
|
55
|
-
import { dirname, resolve } from 'path';
|
|
46
|
+
logPreprocessorAvailability,
|
|
47
|
+
extractCssFromOutput,
|
|
48
|
+
removeInlineStyles,
|
|
49
|
+
processStyles,
|
|
50
|
+
getPreprocessorOptions
|
|
51
|
+
} from './shared.js';
|
|
52
|
+
import { resolve, dirname } from 'path';
|
|
56
53
|
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
57
|
-
|
|
58
|
-
// Cache for preprocessor availability checks
|
|
59
|
-
let preprocessorCache = null;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check available preprocessors once
|
|
63
|
-
*/
|
|
64
|
-
function checkPreprocessors() {
|
|
65
|
-
if (preprocessorCache) return preprocessorCache;
|
|
66
|
-
|
|
67
|
-
preprocessorCache = {
|
|
68
|
-
sass: isSassAvailable(),
|
|
69
|
-
less: isLessAvailable(),
|
|
70
|
-
stylus: isStylusAvailable()
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
return preprocessorCache;
|
|
74
|
-
}
|
|
54
|
+
import { readFile } from 'fs/promises';
|
|
75
55
|
|
|
76
56
|
/**
|
|
77
57
|
* Create Pulse SWC plugin
|
|
@@ -80,6 +60,7 @@ export default function pulsePlugin(options = {}) {
|
|
|
80
60
|
const {
|
|
81
61
|
sourceMap = true,
|
|
82
62
|
extractCss = null, // Path to output CSS file, or null for inline
|
|
63
|
+
quiet = false,
|
|
83
64
|
sass: sassOptions = {},
|
|
84
65
|
less: lessOptions = {},
|
|
85
66
|
stylus: stylusOptions = {}
|
|
@@ -99,23 +80,9 @@ export default function pulsePlugin(options = {}) {
|
|
|
99
80
|
accumulatedCss = '';
|
|
100
81
|
|
|
101
82
|
if (!buildStarted) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (available.sass) {
|
|
106
|
-
preprocessors.push(`SASS ${getSassVersion() || 'unknown'}`);
|
|
107
|
-
}
|
|
108
|
-
if (available.less) {
|
|
109
|
-
preprocessors.push(`LESS ${getLessVersion() || 'unknown'}`);
|
|
110
|
-
}
|
|
111
|
-
if (available.stylus) {
|
|
112
|
-
preprocessors.push(`Stylus ${getStylusVersion() || 'unknown'}`);
|
|
83
|
+
if (!quiet) {
|
|
84
|
+
logPreprocessorAvailability('Pulse SWC');
|
|
113
85
|
}
|
|
114
|
-
|
|
115
|
-
if (preprocessors.length > 0) {
|
|
116
|
-
console.log(`[Pulse SWC] Preprocessor support: ${preprocessors.join(', ')}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
86
|
buildStarted = true;
|
|
120
87
|
}
|
|
121
88
|
},
|
|
@@ -152,54 +119,31 @@ export default function pulsePlugin(options = {}) {
|
|
|
152
119
|
let extractedCss = null;
|
|
153
120
|
|
|
154
121
|
// Extract CSS from compiled output
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if (stylesMatch) {
|
|
158
|
-
let css = stylesMatch[1];
|
|
159
|
-
|
|
160
|
-
// Check available preprocessors
|
|
161
|
-
const available = checkPreprocessors();
|
|
162
|
-
const preprocessor = detectPreprocessor(css);
|
|
163
|
-
|
|
164
|
-
// Preprocess if preprocessor detected and available
|
|
165
|
-
if (preprocessor !== 'none' && available[preprocessor]) {
|
|
166
|
-
try {
|
|
167
|
-
const preprocessorOptions = {
|
|
168
|
-
sass: sassOptions,
|
|
169
|
-
less: lessOptions,
|
|
170
|
-
stylus: stylusOptions
|
|
171
|
-
}[preprocessor];
|
|
122
|
+
const { css: rawCss, found } = extractCssFromOutput(outputCode);
|
|
172
123
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
loadPaths: [dirname(filePath), ...(preprocessorOptions.loadPaths || [])],
|
|
176
|
-
compressed: preprocessorOptions.compressed || false,
|
|
177
|
-
preprocessor // Force detected preprocessor
|
|
178
|
-
});
|
|
124
|
+
if (found) {
|
|
125
|
+
const styleResult = processStyles(rawCss, filePath, { sassOptions, lessOptions, stylusOptions });
|
|
179
126
|
|
|
180
|
-
|
|
127
|
+
if (styleResult.warning) {
|
|
128
|
+
console.warn(`[Pulse SWC] ${styleResult.warning}`);
|
|
129
|
+
}
|
|
181
130
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Emit warning but continue with original CSS
|
|
188
|
-
console.warn(`[Pulse SWC] ${preprocessor.toUpperCase()} compilation warning: ${preprocessorError.message}`);
|
|
131
|
+
// Log preprocessor usage in verbose mode
|
|
132
|
+
if (styleResult.preprocessor && styleResult.preprocessor !== 'none') {
|
|
133
|
+
const opts = getPreprocessorOptions(styleResult.preprocessor, { sassOptions, lessOptions, stylusOptions });
|
|
134
|
+
if (opts && opts.verbose) {
|
|
135
|
+
console.log(`[Pulse] Compiled ${styleResult.preprocessor.toUpperCase()} in ${filePath}`);
|
|
189
136
|
}
|
|
190
137
|
}
|
|
191
138
|
|
|
192
|
-
extractedCss = css;
|
|
139
|
+
extractedCss = styleResult.css;
|
|
193
140
|
|
|
194
141
|
if (extractCss) {
|
|
195
142
|
// Accumulate CSS for later emission
|
|
196
|
-
accumulatedCss += `/* ${filePath} */\n${css}\n\n`;
|
|
143
|
+
accumulatedCss += `/* ${filePath} */\n${styleResult.css}\n\n`;
|
|
197
144
|
|
|
198
145
|
// Remove inline CSS injection from output
|
|
199
|
-
outputCode = outputCode
|
|
200
|
-
/\/\/ Styles\nconst styles = `[\s\S]*?`;\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\nstyleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/,
|
|
201
|
-
'// Styles extracted to CSS file'
|
|
202
|
-
);
|
|
146
|
+
outputCode = removeInlineStyles(outputCode, '// Styles extracted to CSS file');
|
|
203
147
|
}
|
|
204
148
|
// else: keep inline CSS injection
|
|
205
149
|
}
|
|
@@ -231,7 +175,9 @@ export default function pulsePlugin(options = {}) {
|
|
|
231
175
|
const cssDir = dirname(cssPath);
|
|
232
176
|
mkdirSync(cssDir, { recursive: true });
|
|
233
177
|
writeFileSync(cssPath, accumulatedCss, 'utf8');
|
|
234
|
-
|
|
178
|
+
if (!quiet) {
|
|
179
|
+
console.log(`[Pulse] Emitted CSS to ${extractCss}`);
|
|
180
|
+
}
|
|
235
181
|
} catch (writeError) {
|
|
236
182
|
console.error(`[Pulse] Failed to write CSS file: ${writeError.message}`);
|
|
237
183
|
}
|
|
@@ -284,3 +230,40 @@ export function buildPulseFiles(files, options = {}) {
|
|
|
284
230
|
plugin.buildEnd();
|
|
285
231
|
return results;
|
|
286
232
|
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Async Variants
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Transform a .pulse file to JavaScript asynchronously
|
|
240
|
+
* @param {string} filePath - Path to the .pulse file
|
|
241
|
+
* @param {object} options - Plugin options
|
|
242
|
+
* @returns {Promise<{ code: string|null, map: object|null, css: string|null, error: string|null }>}
|
|
243
|
+
*/
|
|
244
|
+
export async function transformPulseFileAsync(filePath, options = {}) {
|
|
245
|
+
const resolvedPath = resolve(filePath);
|
|
246
|
+
const source = await readFile(resolvedPath, 'utf8');
|
|
247
|
+
return transformPulseCode(source, { ...options, filename: resolvedPath });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Batch process multiple .pulse files asynchronously
|
|
252
|
+
* @param {string[]} files - Array of .pulse file paths
|
|
253
|
+
* @param {object} options - Plugin options
|
|
254
|
+
* @returns {Promise<Array<{ file: string, code: string|null, map: object|null, css: string|null, error: string|null }>>}
|
|
255
|
+
*/
|
|
256
|
+
export async function buildPulseFilesAsync(files, options = {}) {
|
|
257
|
+
const plugin = pulsePlugin(options);
|
|
258
|
+
plugin.buildStart();
|
|
259
|
+
|
|
260
|
+
const results = await Promise.all(files.map(async (filePath) => {
|
|
261
|
+
const resolvedPath = resolve(filePath);
|
|
262
|
+
const source = await readFile(resolvedPath, 'utf8');
|
|
263
|
+
const result = plugin.transform(source, resolvedPath);
|
|
264
|
+
return { file: filePath, ...result };
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
plugin.buildEnd();
|
|
268
|
+
return results;
|
|
269
|
+
}
|