pulse-js-framework 1.7.32 → 1.7.33

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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Pulse Rollup Plugin
3
+ *
4
+ * Enables .pulse file support in Rollup projects
5
+ * Extracts CSS and compiles to JavaScript with source maps
6
+ *
7
+ * CSS Preprocessor Support:
8
+ * - If `sass`, `less`, or `stylus` is installed in the user's project,
9
+ * preprocessor syntax in style blocks is automatically compiled
10
+ * - No configuration needed - just install the preprocessor package
11
+ *
12
+ * Usage in rollup.config.js:
13
+ * ```js
14
+ * import pulsePlugin from 'pulse-js-framework/rollup';
15
+ *
16
+ * export default {
17
+ * input: 'src/main.js',
18
+ * output: { file: 'dist/bundle.js', format: 'es' },
19
+ * plugins: [
20
+ * pulsePlugin({
21
+ * sourceMap: true,
22
+ * extractCss: 'dist/bundle.css',
23
+ * sass: { loadPaths: ['src/styles'] }
24
+ * })
25
+ * ]
26
+ * };
27
+ * ```
28
+ */
29
+
30
+ import { compile } from '../compiler/index.js';
31
+ import {
32
+ preprocessStylesSync,
33
+ isSassAvailable,
34
+ isLessAvailable,
35
+ isStylusAvailable,
36
+ getSassVersion,
37
+ getLessVersion,
38
+ getStylusVersion,
39
+ detectPreprocessor
40
+ } from '../compiler/preprocessor.js';
41
+ import { dirname } from 'path';
42
+
43
+ // Cache for preprocessor availability checks
44
+ let preprocessorCache = null;
45
+
46
+ /**
47
+ * Check available preprocessors once
48
+ */
49
+ function checkPreprocessors() {
50
+ if (preprocessorCache) return preprocessorCache;
51
+
52
+ preprocessorCache = {
53
+ sass: isSassAvailable(),
54
+ less: isLessAvailable(),
55
+ stylus: isStylusAvailable()
56
+ };
57
+
58
+ return preprocessorCache;
59
+ }
60
+
61
+ /**
62
+ * Create Pulse Rollup plugin
63
+ */
64
+ export default function pulsePlugin(options = {}) {
65
+ const {
66
+ include = /\.pulse$/,
67
+ exclude = /node_modules/,
68
+ sourceMap = true,
69
+ extractCss = null, // Path to output CSS file, or null for inline
70
+ sass: sassOptions = {},
71
+ less: lessOptions = {},
72
+ stylus: stylusOptions = {}
73
+ } = options;
74
+
75
+ const filter = createFilter(include, exclude);
76
+
77
+ // Accumulated CSS from all .pulse files
78
+ let accumulatedCss = '';
79
+ let cssEmitted = false;
80
+
81
+ return {
82
+ name: 'pulse',
83
+
84
+ /**
85
+ * Log preprocessor availability on build start
86
+ */
87
+ buildStart() {
88
+ // Reset accumulated CSS
89
+ accumulatedCss = '';
90
+ cssEmitted = false;
91
+
92
+ // Check preprocessor availability
93
+ const available = checkPreprocessors();
94
+ const preprocessors = [];
95
+
96
+ if (available.sass) {
97
+ preprocessors.push(`SASS ${getSassVersion() || 'unknown'}`);
98
+ }
99
+ if (available.less) {
100
+ preprocessors.push(`LESS ${getLessVersion() || 'unknown'}`);
101
+ }
102
+ if (available.stylus) {
103
+ preprocessors.push(`Stylus ${getStylusVersion() || 'unknown'}`);
104
+ }
105
+
106
+ if (preprocessors.length > 0) {
107
+ console.log(`[Pulse Rollup] Preprocessor support: ${preprocessors.join(', ')}`);
108
+ }
109
+ },
110
+
111
+ /**
112
+ * Resolve .pulse files
113
+ */
114
+ resolveId(id, importer) {
115
+ // Handle .pulse imports
116
+ if (id.endsWith('.pulse')) {
117
+ return null; // Let Rollup handle it
118
+ }
119
+
120
+ // Check if a .js import has a corresponding .pulse file
121
+ if (id.endsWith('.js') && importer) {
122
+ const pulseId = id.replace(/\.js$/, '.pulse');
123
+ // Let Rollup's resolution handle this
124
+ return null;
125
+ }
126
+
127
+ return null;
128
+ },
129
+
130
+ /**
131
+ * Transform .pulse files to JavaScript
132
+ */
133
+ transform(source, id) {
134
+ if (!filter(id)) return null;
135
+ if (!id.endsWith('.pulse')) return null;
136
+
137
+ try {
138
+ // Compile .pulse to JavaScript
139
+ const result = compile(source, {
140
+ runtime: 'pulse-js-framework/runtime',
141
+ sourceMap,
142
+ filename: id
143
+ });
144
+
145
+ if (!result.success) {
146
+ const errors = result.errors.map(e =>
147
+ `${e.message}${e.line ? ` at line ${e.line}` : ''}`
148
+ ).join('\n');
149
+
150
+ this.error(`Pulse compilation failed:\n${errors}`);
151
+ return null;
152
+ }
153
+
154
+ let outputCode = result.code;
155
+ let outputMap = result.map;
156
+
157
+ // Extract CSS from compiled output
158
+ const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
159
+
160
+ if (stylesMatch) {
161
+ let css = stylesMatch[1];
162
+
163
+ // Check available preprocessors
164
+ const available = checkPreprocessors();
165
+ const preprocessor = detectPreprocessor(css);
166
+
167
+ // Preprocess if preprocessor detected and available
168
+ if (preprocessor !== 'none' && available[preprocessor]) {
169
+ try {
170
+ const preprocessorOptions = {
171
+ sass: sassOptions,
172
+ less: lessOptions,
173
+ stylus: stylusOptions
174
+ }[preprocessor];
175
+
176
+ const preprocessed = preprocessStylesSync(css, {
177
+ filename: id,
178
+ loadPaths: [dirname(id), ...(preprocessorOptions.loadPaths || [])],
179
+ compressed: preprocessorOptions.compressed || false,
180
+ preprocessor // Force detected preprocessor
181
+ });
182
+
183
+ css = preprocessed.css;
184
+
185
+ // Log preprocessor usage in verbose mode
186
+ if (preprocessorOptions.verbose) {
187
+ console.log(`[Pulse] Compiled ${preprocessor.toUpperCase()} in ${id}`);
188
+ }
189
+ } catch (preprocessorError) {
190
+ // Emit warning but continue with original CSS
191
+ this.warn(`${preprocessor.toUpperCase()} compilation warning: ${preprocessorError.message}`);
192
+ }
193
+ }
194
+
195
+ if (extractCss) {
196
+ // Accumulate CSS for later emission
197
+ accumulatedCss += `/* ${id} */\n${css}\n\n`;
198
+
199
+ // Remove inline CSS injection from output
200
+ outputCode = outputCode.replace(
201
+ /\/\/ Styles\nconst styles = `[\s\S]*?`;\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\nstyleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/,
202
+ '// Styles extracted to CSS file'
203
+ );
204
+ }
205
+ // else: keep inline CSS injection
206
+ }
207
+
208
+ return {
209
+ code: outputCode,
210
+ map: sourceMap ? outputMap : null
211
+ };
212
+ } catch (error) {
213
+ this.error(`Pulse plugin error: ${error.message}`);
214
+ return null;
215
+ }
216
+ },
217
+
218
+ /**
219
+ * Emit accumulated CSS as asset
220
+ */
221
+ generateBundle() {
222
+ if (extractCss && accumulatedCss && !cssEmitted) {
223
+ this.emitFile({
224
+ type: 'asset',
225
+ fileName: extractCss,
226
+ source: accumulatedCss
227
+ });
228
+ cssEmitted = true;
229
+ console.log(`[Pulse] Emitted CSS to ${extractCss}`);
230
+ }
231
+ }
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Create filter function for include/exclude patterns
237
+ * Simple implementation to avoid dependency on @rollup/pluginutils
238
+ */
239
+ function createFilter(include, exclude) {
240
+ return (id) => {
241
+ // Check exclude first
242
+ if (exclude) {
243
+ if (exclude instanceof RegExp && exclude.test(id)) return false;
244
+ if (Array.isArray(exclude) && exclude.some(pattern =>
245
+ pattern instanceof RegExp ? pattern.test(id) : id.includes(pattern)
246
+ )) return false;
247
+ }
248
+
249
+ // Check include
250
+ if (include) {
251
+ if (include instanceof RegExp) return include.test(id);
252
+ if (Array.isArray(include)) return include.some(pattern =>
253
+ pattern instanceof RegExp ? pattern.test(id) : id.includes(pattern)
254
+ );
255
+ }
256
+
257
+ return true;
258
+ };
259
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Pulse SWC Plugin
3
+ *
4
+ * Enables .pulse file support in SWC-based build pipelines
5
+ * Provides transform functions and a plugin interface for custom build scripts
6
+ *
7
+ * CSS Preprocessor Support:
8
+ * - If `sass`, `less`, or `stylus` is installed in the user's project,
9
+ * preprocessor syntax in style blocks is automatically compiled
10
+ * - No configuration needed - just install the preprocessor package
11
+ *
12
+ * Usage with direct transform API:
13
+ * ```js
14
+ * import { transformPulseFile } from 'pulse-js-framework/swc';
15
+ *
16
+ * const result = transformPulseFile('src/App.pulse', {
17
+ * sourceMap: true,
18
+ * sass: { loadPaths: ['src/styles'] }
19
+ * });
20
+ * console.log(result.code); // Compiled JavaScript
21
+ * console.log(result.css); // Extracted CSS (or null)
22
+ * ```
23
+ *
24
+ * Usage with plugin interface:
25
+ * ```js
26
+ * import pulsePlugin from 'pulse-js-framework/swc';
27
+ *
28
+ * const plugin = pulsePlugin({ extractCss: 'dist/bundle.css' });
29
+ * plugin.buildStart();
30
+ * const result = plugin.transform(source, 'src/App.pulse');
31
+ * plugin.buildEnd(); // Writes accumulated CSS
32
+ * ```
33
+ *
34
+ * Batch processing:
35
+ * ```js
36
+ * import { buildPulseFiles } from 'pulse-js-framework/swc';
37
+ *
38
+ * const results = buildPulseFiles(['src/App.pulse', 'src/Home.pulse'], {
39
+ * extractCss: 'dist/bundle.css'
40
+ * });
41
+ * ```
42
+ */
43
+
44
+ import { compile } from '../compiler/index.js';
45
+ import {
46
+ preprocessStylesSync,
47
+ isSassAvailable,
48
+ isLessAvailable,
49
+ isStylusAvailable,
50
+ getSassVersion,
51
+ getLessVersion,
52
+ getStylusVersion,
53
+ detectPreprocessor
54
+ } from '../compiler/preprocessor.js';
55
+ import { dirname, resolve } from 'path';
56
+ 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
+ }
75
+
76
+ /**
77
+ * Create Pulse SWC plugin
78
+ */
79
+ export default function pulsePlugin(options = {}) {
80
+ const {
81
+ sourceMap = true,
82
+ extractCss = null, // Path to output CSS file, or null for inline
83
+ sass: sassOptions = {},
84
+ less: lessOptions = {},
85
+ stylus: stylusOptions = {}
86
+ } = options;
87
+
88
+ // Accumulated CSS from all .pulse files
89
+ let accumulatedCss = '';
90
+ let buildStarted = false;
91
+
92
+ return {
93
+ name: 'pulse',
94
+
95
+ /**
96
+ * Reset accumulated CSS and log preprocessor availability
97
+ */
98
+ buildStart() {
99
+ accumulatedCss = '';
100
+
101
+ if (!buildStarted) {
102
+ const available = checkPreprocessors();
103
+ const preprocessors = [];
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'}`);
113
+ }
114
+
115
+ if (preprocessors.length > 0) {
116
+ console.log(`[Pulse SWC] Preprocessor support: ${preprocessors.join(', ')}`);
117
+ }
118
+
119
+ buildStarted = true;
120
+ }
121
+ },
122
+
123
+ /**
124
+ * Transform a .pulse file source to JavaScript
125
+ * @param {string} source - The .pulse file source code
126
+ * @param {string} filePath - The file path for error reporting and preprocessor resolution
127
+ * @returns {{ code: string|null, map: object|null, css: string|null, error: string|null }}
128
+ */
129
+ transform(source, filePath) {
130
+ try {
131
+ // Compile .pulse to JavaScript
132
+ const result = compile(source, {
133
+ runtime: 'pulse-js-framework/runtime',
134
+ sourceMap,
135
+ filename: filePath
136
+ });
137
+
138
+ if (!result.success) {
139
+ const errors = result.errors.map(e =>
140
+ `${e.message}${e.line ? ` at line ${e.line}` : ''}`
141
+ ).join('\n');
142
+
143
+ return {
144
+ code: null,
145
+ map: null,
146
+ css: null,
147
+ error: `Pulse compilation failed:\n${errors}`
148
+ };
149
+ }
150
+
151
+ let outputCode = result.code;
152
+ let extractedCss = null;
153
+
154
+ // Extract CSS from compiled output
155
+ const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
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];
172
+
173
+ const preprocessed = preprocessStylesSync(css, {
174
+ filename: filePath,
175
+ loadPaths: [dirname(filePath), ...(preprocessorOptions.loadPaths || [])],
176
+ compressed: preprocessorOptions.compressed || false,
177
+ preprocessor // Force detected preprocessor
178
+ });
179
+
180
+ css = preprocessed.css;
181
+
182
+ // Log preprocessor usage in verbose mode
183
+ if (preprocessorOptions.verbose) {
184
+ console.log(`[Pulse] Compiled ${preprocessor.toUpperCase()} in ${filePath}`);
185
+ }
186
+ } catch (preprocessorError) {
187
+ // Emit warning but continue with original CSS
188
+ console.warn(`[Pulse SWC] ${preprocessor.toUpperCase()} compilation warning: ${preprocessorError.message}`);
189
+ }
190
+ }
191
+
192
+ extractedCss = css;
193
+
194
+ if (extractCss) {
195
+ // Accumulate CSS for later emission
196
+ accumulatedCss += `/* ${filePath} */\n${css}\n\n`;
197
+
198
+ // Remove inline CSS injection from output
199
+ outputCode = outputCode.replace(
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
+ );
203
+ }
204
+ // else: keep inline CSS injection
205
+ }
206
+
207
+ return {
208
+ code: outputCode,
209
+ map: result.sourceMap || null,
210
+ css: extractedCss,
211
+ error: null
212
+ };
213
+ } catch (error) {
214
+ return {
215
+ code: null,
216
+ map: null,
217
+ css: null,
218
+ error: `Pulse plugin error: ${error.message}`
219
+ };
220
+ }
221
+ },
222
+
223
+ /**
224
+ * Write accumulated CSS to disk
225
+ * Call after all transforms are complete
226
+ */
227
+ buildEnd() {
228
+ if (extractCss && accumulatedCss) {
229
+ try {
230
+ const cssPath = resolve(typeof extractCss === 'string' ? extractCss : 'dist/bundle.css');
231
+ const cssDir = dirname(cssPath);
232
+ mkdirSync(cssDir, { recursive: true });
233
+ writeFileSync(cssPath, accumulatedCss, 'utf8');
234
+ console.log(`[Pulse] Emitted CSS to ${extractCss}`);
235
+ } catch (writeError) {
236
+ console.error(`[Pulse] Failed to write CSS file: ${writeError.message}`);
237
+ }
238
+ }
239
+ }
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Transform a .pulse file to JavaScript (standalone function)
245
+ * @param {string} filePath - Path to the .pulse file
246
+ * @param {object} options - Plugin options
247
+ * @returns {{ code: string|null, map: object|null, css: string|null, error: string|null }}
248
+ */
249
+ export function transformPulseFile(filePath, options = {}) {
250
+ const resolvedPath = resolve(filePath);
251
+ const source = readFileSync(resolvedPath, 'utf8');
252
+ return transformPulseCode(source, { ...options, filename: resolvedPath });
253
+ }
254
+
255
+ /**
256
+ * Transform .pulse source code to JavaScript (standalone function)
257
+ * @param {string} source - The .pulse source code
258
+ * @param {object} options - Plugin options (plus optional `filename`)
259
+ * @returns {{ code: string|null, map: object|null, css: string|null, error: string|null }}
260
+ */
261
+ export function transformPulseCode(source, options = {}) {
262
+ const { filename = 'unknown.pulse', ...pluginOptions } = options;
263
+ const plugin = pulsePlugin({ ...pluginOptions, extractCss: null });
264
+ return plugin.transform(source, filename);
265
+ }
266
+
267
+ /**
268
+ * Batch process multiple .pulse files
269
+ * @param {string[]} files - Array of .pulse file paths
270
+ * @param {object} options - Plugin options
271
+ * @returns {Array<{ file: string, code: string|null, map: object|null, css: string|null, error: string|null }>}
272
+ */
273
+ export function buildPulseFiles(files, options = {}) {
274
+ const plugin = pulsePlugin(options);
275
+ plugin.buildStart();
276
+
277
+ const results = files.map(filePath => {
278
+ const resolvedPath = resolve(filePath);
279
+ const source = readFileSync(resolvedPath, 'utf8');
280
+ const result = plugin.transform(source, resolvedPath);
281
+ return { file: filePath, ...result };
282
+ });
283
+
284
+ plugin.buildEnd();
285
+ return results;
286
+ }