pulse-js-framework 1.7.31 → 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
+ }
@@ -4,11 +4,17 @@
4
4
  * Enables .pulse file support in Vite projects
5
5
  * Extracts CSS to virtual .css modules so Vite's CSS pipeline handles them
6
6
  * (prevents JS minifier from corrupting CSS in template literals)
7
+ *
8
+ * SASS/SCSS Support:
9
+ * - If `sass` is installed in the user's project, SCSS syntax in style blocks
10
+ * is automatically compiled before being passed to Vite's CSS pipeline
11
+ * - No configuration needed - just install sass: `npm install -D sass`
7
12
  */
8
13
 
9
14
  import { compile } from '../compiler/index.js';
10
15
  import { existsSync } from 'fs';
11
16
  import { resolve, dirname } from 'path';
17
+ import { preprocessStylesSync, isSassAvailable, getSassVersion } from '../compiler/preprocessor.js';
12
18
 
13
19
  // Virtual module ID for extracted CSS (uses .css extension so Vite treats it as CSS)
14
20
  const VIRTUAL_CSS_SUFFIX = '.pulse.css';
@@ -19,16 +25,37 @@ const VIRTUAL_CSS_SUFFIX = '.pulse.css';
19
25
  export default function pulsePlugin(options = {}) {
20
26
  const {
21
27
  exclude = /node_modules/,
22
- sourceMap = true
28
+ sourceMap = true,
29
+ // SASS options
30
+ sass: sassOptions = {}
23
31
  } = options;
24
32
 
25
33
  // Store extracted CSS for each .pulse module
26
34
  const cssMap = new Map();
27
35
 
36
+ // Check for sass availability once at startup
37
+ let sassAvailable = false;
38
+ let sassVersion = null;
39
+
28
40
  return {
29
41
  name: 'vite-plugin-pulse',
30
42
  enforce: 'pre',
31
43
 
44
+ /**
45
+ * Log sass availability on build start
46
+ */
47
+ buildStart() {
48
+ // Clear CSS map on new build
49
+ cssMap.clear();
50
+
51
+ // Check sass availability
52
+ sassAvailable = isSassAvailable();
53
+ if (sassAvailable) {
54
+ sassVersion = getSassVersion();
55
+ console.log(`[Pulse] SASS support enabled (sass ${sassVersion || 'unknown'})`);
56
+ }
57
+ },
58
+
32
59
  /**
33
60
  * Resolve .pulse files and virtual CSS modules
34
61
  */
@@ -107,9 +134,27 @@ export default function pulsePlugin(options = {}) {
107
134
  // Extract CSS from compiled output and move to virtual CSS module
108
135
  const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
109
136
  if (stylesMatch) {
110
- const css = stylesMatch[1];
137
+ let css = stylesMatch[1];
111
138
  const virtualCssId = id + '.css';
112
139
 
140
+ // Preprocess SASS/SCSS if detected and sass is available
141
+ if (sassAvailable) {
142
+ try {
143
+ const preprocessed = preprocessStylesSync(css, {
144
+ filename: id,
145
+ loadPaths: [dirname(id), ...(sassOptions.loadPaths || [])],
146
+ compressed: sassOptions.compressed || false
147
+ });
148
+
149
+ if (preprocessed.wasSass) {
150
+ css = preprocessed.css;
151
+ }
152
+ } catch (sassError) {
153
+ this.warn(`SASS compilation warning in ${id}: ${sassError.message}`);
154
+ // Continue with original CSS if SASS fails
155
+ }
156
+ }
157
+
113
158
  // Store CSS for the virtual module loader
114
159
  cssMap.set(id, css);
115
160
 
@@ -168,21 +213,21 @@ export default function pulsePlugin(options = {}) {
168
213
  },
169
214
 
170
215
  /**
171
- * Configure dev server
216
+ * Configure dev server - log sass status on start
172
217
  */
173
218
  configureServer(server) {
219
+ // Check sass on server start if not already checked
220
+ if (!sassAvailable) {
221
+ sassAvailable = isSassAvailable();
222
+ if (sassAvailable) {
223
+ sassVersion = getSassVersion();
224
+ console.log(`[Pulse] SASS support enabled (sass ${sassVersion || 'unknown'})`);
225
+ }
226
+ }
227
+
174
228
  server.middlewares.use((_req, _res, next) => {
175
- // Add any custom middleware here
176
229
  next();
177
230
  });
178
- },
179
-
180
- /**
181
- * Build hooks
182
- */
183
- buildStart() {
184
- // Clear CSS map on new build
185
- cssMap.clear();
186
231
  }
187
232
  };
188
233
  }