pulse-js-framework 1.7.23 → 1.7.25
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/release.js +173 -25
- package/compiler/transformer/expressions.js +10 -1
- package/compiler/transformer/view.js +123 -15
- package/loader/vite-plugin.js +62 -17
- package/package.json +1 -1
package/cli/release.js
CHANGED
|
@@ -508,6 +508,46 @@ function buildCommitMessage(newVersion, title, changes) {
|
|
|
508
508
|
return message;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Verify that a git tag exists locally
|
|
513
|
+
*/
|
|
514
|
+
function verifyTagExists(version) {
|
|
515
|
+
try {
|
|
516
|
+
const tags = execSync('git tag -l', { cwd: root, encoding: 'utf-8' });
|
|
517
|
+
return tags.split('\n').includes(`v${version}`);
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Verify that a git tag exists on remote
|
|
525
|
+
*/
|
|
526
|
+
function verifyTagOnRemote(version) {
|
|
527
|
+
try {
|
|
528
|
+
const remoteTags = execSync('git ls-remote --tags origin', { cwd: root, encoding: 'utf-8' });
|
|
529
|
+
return remoteTags.includes(`refs/tags/v${version}`);
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Execute a git command with error handling
|
|
537
|
+
*/
|
|
538
|
+
function execGitCommand(command, description) {
|
|
539
|
+
log.info(` Running: ${description}...`);
|
|
540
|
+
try {
|
|
541
|
+
execSync(command, { cwd: root, stdio: 'inherit' });
|
|
542
|
+
return { success: true };
|
|
543
|
+
} catch (error) {
|
|
544
|
+
log.error(` Failed: ${description}`);
|
|
545
|
+
log.error(` Command: ${command}`);
|
|
546
|
+
log.error(` Error: ${error.message}`);
|
|
547
|
+
return { success: false, error };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
511
551
|
/**
|
|
512
552
|
* Execute git commands
|
|
513
553
|
*/
|
|
@@ -521,33 +561,142 @@ function gitCommitTagPush(newVersion, title, changes, dryRun = false) {
|
|
|
521
561
|
log.info(` [dry-run] git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
522
562
|
log.info(' [dry-run] git push');
|
|
523
563
|
log.info(' [dry-run] git push --tags');
|
|
524
|
-
return;
|
|
564
|
+
return { success: true };
|
|
525
565
|
}
|
|
526
566
|
|
|
527
567
|
// git add
|
|
528
|
-
|
|
529
|
-
|
|
568
|
+
let result = execGitCommand('git add -A', 'git add -A');
|
|
569
|
+
if (!result.success) {
|
|
570
|
+
return { success: false, stage: 'add', error: result.error };
|
|
571
|
+
}
|
|
530
572
|
|
|
531
573
|
// git commit using temp file for cross-platform compatibility
|
|
532
|
-
log.info(' Running: git commit...');
|
|
533
574
|
const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
|
|
534
575
|
writeFileSync(tempFile, commitMessage, 'utf-8');
|
|
535
576
|
try {
|
|
536
|
-
|
|
577
|
+
result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
|
|
578
|
+
if (!result.success) {
|
|
579
|
+
return { success: false, stage: 'commit', error: result.error };
|
|
580
|
+
}
|
|
537
581
|
} finally {
|
|
538
|
-
|
|
582
|
+
try {
|
|
583
|
+
unlinkSync(tempFile);
|
|
584
|
+
} catch {
|
|
585
|
+
// Ignore cleanup errors
|
|
586
|
+
}
|
|
539
587
|
}
|
|
540
588
|
|
|
541
589
|
// git tag
|
|
542
|
-
|
|
543
|
-
|
|
590
|
+
result = execGitCommand(
|
|
591
|
+
`git tag -a v${newVersion} -m "Release v${newVersion}"`,
|
|
592
|
+
`git tag v${newVersion}`
|
|
593
|
+
);
|
|
594
|
+
if (!result.success) {
|
|
595
|
+
log.error('');
|
|
596
|
+
log.error('Tag creation failed. The commit was created but not tagged.');
|
|
597
|
+
log.error('You can manually create the tag with:');
|
|
598
|
+
log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
599
|
+
return { success: false, stage: 'tag', error: result.error };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Verify tag was created
|
|
603
|
+
if (!verifyTagExists(newVersion)) {
|
|
604
|
+
log.error('');
|
|
605
|
+
log.error('Tag creation appeared to succeed but tag not found locally.');
|
|
606
|
+
log.error('You can manually create the tag with:');
|
|
607
|
+
log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
608
|
+
return { success: false, stage: 'tag-verify', error: new Error('Tag not found after creation') };
|
|
609
|
+
}
|
|
610
|
+
log.info(` Verified: tag v${newVersion} exists locally`);
|
|
544
611
|
|
|
545
612
|
// git push
|
|
546
|
-
|
|
547
|
-
|
|
613
|
+
result = execGitCommand('git push', 'git push');
|
|
614
|
+
if (!result.success) {
|
|
615
|
+
log.error('');
|
|
616
|
+
log.error('Push failed. Commit and tag were created locally.');
|
|
617
|
+
log.error('You can manually push with:');
|
|
618
|
+
log.error(' git push && git push --tags');
|
|
619
|
+
return { success: false, stage: 'push', error: result.error };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// git push --tags
|
|
623
|
+
result = execGitCommand('git push --tags', 'git push --tags');
|
|
624
|
+
if (!result.success) {
|
|
625
|
+
log.error('');
|
|
626
|
+
log.error('Tag push failed. The tag exists locally but not on remote.');
|
|
627
|
+
log.error('You can manually push tags with:');
|
|
628
|
+
log.error(' git push --tags');
|
|
629
|
+
return { success: false, stage: 'push-tags', error: result.error };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Verify tag was pushed to remote
|
|
633
|
+
if (!verifyTagOnRemote(newVersion)) {
|
|
634
|
+
log.warn('');
|
|
635
|
+
log.warn('Tag push appeared to succeed but tag not found on remote.');
|
|
636
|
+
log.warn('You may need to manually verify or push with:');
|
|
637
|
+
log.warn(' git push --tags');
|
|
638
|
+
} else {
|
|
639
|
+
log.info(` Verified: tag v${newVersion} exists on remote`);
|
|
640
|
+
}
|
|
548
641
|
|
|
549
|
-
|
|
550
|
-
|
|
642
|
+
return { success: true };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Execute git commands without pushing (--no-push mode)
|
|
647
|
+
*/
|
|
648
|
+
function gitCommitTagNoPush(newVersion, title, changes) {
|
|
649
|
+
const commitMessage = buildCommitMessage(newVersion, title, changes);
|
|
650
|
+
|
|
651
|
+
// git add
|
|
652
|
+
let result = execGitCommand('git add -A', 'git add -A');
|
|
653
|
+
if (!result.success) {
|
|
654
|
+
return { success: false, stage: 'add', error: result.error };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// git commit using temp file for cross-platform compatibility
|
|
658
|
+
const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
|
|
659
|
+
writeFileSync(tempFile, commitMessage, 'utf-8');
|
|
660
|
+
try {
|
|
661
|
+
result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
|
|
662
|
+
if (!result.success) {
|
|
663
|
+
return { success: false, stage: 'commit', error: result.error };
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
try {
|
|
667
|
+
unlinkSync(tempFile);
|
|
668
|
+
} catch {
|
|
669
|
+
// Ignore cleanup errors
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// git tag
|
|
674
|
+
result = execGitCommand(
|
|
675
|
+
`git tag -a v${newVersion} -m "Release v${newVersion}"`,
|
|
676
|
+
`git tag v${newVersion}`
|
|
677
|
+
);
|
|
678
|
+
if (!result.success) {
|
|
679
|
+
log.error('');
|
|
680
|
+
log.error('Tag creation failed. The commit was created but not tagged.');
|
|
681
|
+
log.error('You can manually create the tag with:');
|
|
682
|
+
log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
683
|
+
return { success: false, stage: 'tag', error: result.error };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Verify tag was created
|
|
687
|
+
if (!verifyTagExists(newVersion)) {
|
|
688
|
+
log.error('');
|
|
689
|
+
log.error('Tag creation appeared to succeed but tag not found locally.');
|
|
690
|
+
log.error('You can manually create the tag with:');
|
|
691
|
+
log.error(` git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
692
|
+
return { success: false, stage: 'tag-verify', error: new Error('Tag not found after creation') };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
log.info(` Verified: tag v${newVersion} exists locally`);
|
|
696
|
+
log.info(' Created commit and tag (--no-push specified)');
|
|
697
|
+
log.info(' To push later, run: git push && git push --tags');
|
|
698
|
+
|
|
699
|
+
return { success: true };
|
|
551
700
|
}
|
|
552
701
|
|
|
553
702
|
/**
|
|
@@ -811,25 +960,24 @@ export async function runRelease(args) {
|
|
|
811
960
|
log.info('');
|
|
812
961
|
log.info('Git operations...');
|
|
813
962
|
|
|
963
|
+
let gitResult = { success: true };
|
|
964
|
+
|
|
814
965
|
if (!dryRun) {
|
|
815
966
|
if (noPush) {
|
|
816
967
|
// Only commit and tag, no push
|
|
817
|
-
|
|
818
|
-
execSync('git add -A', { cwd: root, stdio: 'inherit' });
|
|
819
|
-
const tempFile = join(tmpdir(), `pulse-release-${Date.now()}.txt`);
|
|
820
|
-
writeFileSync(tempFile, commitMessage, 'utf-8');
|
|
821
|
-
try {
|
|
822
|
-
execSync(`git commit -F "${tempFile}"`, { cwd: root, stdio: 'inherit' });
|
|
823
|
-
} finally {
|
|
824
|
-
unlinkSync(tempFile);
|
|
825
|
-
}
|
|
826
|
-
execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
|
|
827
|
-
log.info(' Created commit and tag (--no-push specified)');
|
|
968
|
+
gitResult = gitCommitTagNoPush(newVersion, title, changes);
|
|
828
969
|
} else {
|
|
829
|
-
gitCommitTagPush(newVersion, title, changes, false);
|
|
970
|
+
gitResult = gitCommitTagPush(newVersion, title, changes, false);
|
|
830
971
|
}
|
|
831
972
|
} else {
|
|
832
|
-
gitCommitTagPush(newVersion, title, changes, true);
|
|
973
|
+
gitResult = gitCommitTagPush(newVersion, title, changes, true);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (!gitResult.success) {
|
|
977
|
+
log.error('');
|
|
978
|
+
log.error(`Release failed at stage: ${gitResult.stage}`);
|
|
979
|
+
log.error('Please fix the issue and retry, or complete the release manually.');
|
|
980
|
+
process.exit(1);
|
|
833
981
|
}
|
|
834
982
|
|
|
835
983
|
log.info('');
|
|
@@ -25,10 +25,14 @@ export function transformExpression(transformer, node) {
|
|
|
25
25
|
|
|
26
26
|
switch (node.type) {
|
|
27
27
|
case NodeType.Identifier:
|
|
28
|
+
// Props take precedence over state (props are destructured in render scope)
|
|
29
|
+
if (transformer.propVars.has(node.name)) {
|
|
30
|
+
return node.name;
|
|
31
|
+
}
|
|
28
32
|
if (transformer.stateVars.has(node.name)) {
|
|
29
33
|
return `${node.name}.get()`;
|
|
30
34
|
}
|
|
31
|
-
//
|
|
35
|
+
// Other identifiers (actions, imports, etc.) accessed directly
|
|
32
36
|
return node.name;
|
|
33
37
|
|
|
34
38
|
case NodeType.Literal:
|
|
@@ -153,8 +157,13 @@ export function transformExpression(transformer, node) {
|
|
|
153
157
|
*/
|
|
154
158
|
export function transformExpressionString(transformer, exprStr) {
|
|
155
159
|
// Simple transformation: wrap state vars with .get()
|
|
160
|
+
// Props take precedence - don't wrap props with .get()
|
|
156
161
|
let result = exprStr;
|
|
157
162
|
for (const stateVar of transformer.stateVars) {
|
|
163
|
+
// Skip if this var name is also a prop (props shadow state in render scope)
|
|
164
|
+
if (transformer.propVars.has(stateVar)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
158
167
|
result = result.replace(
|
|
159
168
|
new RegExp(`\\b${stateVar}\\b`, 'g'),
|
|
160
169
|
`${stateVar}.get()`
|
|
@@ -298,32 +298,127 @@ export function transformFocusTrapDirective(transformer, node, indent) {
|
|
|
298
298
|
return `{ ${optionsCode} }`;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Parse a balanced expression starting from an opening brace
|
|
303
|
+
* Handles nested braces and string literals correctly
|
|
304
|
+
* @param {string} str - The string to parse
|
|
305
|
+
* @param {number} start - Index of the opening brace
|
|
306
|
+
* @returns {Object} { expr: string, end: number } or null if invalid
|
|
307
|
+
*/
|
|
308
|
+
function parseBalancedExpression(str, start) {
|
|
309
|
+
if (str[start] !== '{') return null;
|
|
310
|
+
|
|
311
|
+
let depth = 0;
|
|
312
|
+
let inString = false;
|
|
313
|
+
let stringChar = '';
|
|
314
|
+
let i = start;
|
|
315
|
+
|
|
316
|
+
while (i < str.length) {
|
|
317
|
+
const char = str[i];
|
|
318
|
+
const prevChar = i > 0 ? str[i - 1] : '';
|
|
319
|
+
|
|
320
|
+
// Handle string literals
|
|
321
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
322
|
+
inString = true;
|
|
323
|
+
stringChar = char;
|
|
324
|
+
} else if (inString && char === stringChar && prevChar !== '\\') {
|
|
325
|
+
inString = false;
|
|
326
|
+
stringChar = '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Count braces only outside strings
|
|
330
|
+
if (!inString) {
|
|
331
|
+
if (char === '{') {
|
|
332
|
+
depth++;
|
|
333
|
+
} else if (char === '}') {
|
|
334
|
+
depth--;
|
|
335
|
+
if (depth === 0) {
|
|
336
|
+
// Found the matching closing brace
|
|
337
|
+
return {
|
|
338
|
+
expr: str.slice(start + 1, i),
|
|
339
|
+
end: i
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
i++;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null; // Unbalanced braces
|
|
349
|
+
}
|
|
350
|
+
|
|
301
351
|
/**
|
|
302
352
|
* Extract dynamic attributes from a selector
|
|
303
353
|
* Returns { cleanSelector, dynamicAttrs } where dynamicAttrs is an array of { name, expr }
|
|
354
|
+
* Handles complex expressions including ternaries, nested braces, and string literals
|
|
304
355
|
* @param {string} selector - CSS selector with potential dynamic attributes
|
|
305
356
|
* @returns {Object} { cleanSelector, dynamicAttrs }
|
|
306
357
|
*/
|
|
307
358
|
function extractDynamicAttributes(selector) {
|
|
308
359
|
const dynamicAttrs = [];
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
360
|
+
let cleanSelector = '';
|
|
361
|
+
let i = 0;
|
|
362
|
+
|
|
363
|
+
while (i < selector.length) {
|
|
364
|
+
// Look for attribute start: [
|
|
365
|
+
if (selector[i] === '[') {
|
|
366
|
+
i++; // Skip [
|
|
367
|
+
|
|
368
|
+
// Parse attribute name
|
|
369
|
+
let attrName = '';
|
|
370
|
+
while (i < selector.length && /[a-zA-Z0-9-]/.test(selector[i])) {
|
|
371
|
+
attrName += selector[i];
|
|
372
|
+
i++;
|
|
373
|
+
}
|
|
312
374
|
|
|
313
|
-
|
|
375
|
+
// Skip whitespace
|
|
376
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
314
377
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
378
|
+
// Check for =
|
|
379
|
+
if (i < selector.length && selector[i] === '=') {
|
|
380
|
+
i++; // Skip =
|
|
381
|
+
|
|
382
|
+
// Skip whitespace
|
|
383
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
321
384
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
385
|
+
// Check for optional quote
|
|
386
|
+
const hasQuote = selector[i] === '"';
|
|
387
|
+
if (hasQuote) i++;
|
|
388
|
+
|
|
389
|
+
// Check for dynamic expression {
|
|
390
|
+
if (selector[i] === '{') {
|
|
391
|
+
const result = parseBalancedExpression(selector, i);
|
|
392
|
+
if (result) {
|
|
393
|
+
dynamicAttrs.push({ name: attrName, expr: result.expr });
|
|
394
|
+
i = result.end + 1; // Skip past closing }
|
|
395
|
+
|
|
396
|
+
// Skip optional closing quote
|
|
397
|
+
if (hasQuote && selector[i] === '"') i++;
|
|
398
|
+
|
|
399
|
+
// Skip closing ]
|
|
400
|
+
if (selector[i] === ']') i++;
|
|
401
|
+
|
|
402
|
+
// Don't add this attribute to cleanSelector
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Not a dynamic attribute, copy everything from [ to ]
|
|
409
|
+
let bracketDepth = 1;
|
|
410
|
+
cleanSelector += '[';
|
|
411
|
+
while (i < selector.length && bracketDepth > 0) {
|
|
412
|
+
if (selector[i] === '[') bracketDepth++;
|
|
413
|
+
else if (selector[i] === ']') bracketDepth--;
|
|
414
|
+
cleanSelector += selector[i];
|
|
415
|
+
i++;
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
cleanSelector += selector[i];
|
|
419
|
+
i++;
|
|
420
|
+
}
|
|
325
421
|
}
|
|
326
|
-
cleanSelector = cleanSelector.replace(attrPatternQuoted, '');
|
|
327
422
|
|
|
328
423
|
return { cleanSelector, dynamicAttrs };
|
|
329
424
|
}
|
|
@@ -577,6 +672,18 @@ export function transformComponentCall(transformer, node, indent) {
|
|
|
577
672
|
* @param {number} indent - Indentation level
|
|
578
673
|
* @returns {string} JavaScript code
|
|
579
674
|
*/
|
|
675
|
+
/**
|
|
676
|
+
* Escape a string for use in a template literal
|
|
677
|
+
* @param {string} str - String to escape
|
|
678
|
+
* @returns {string} Escaped string
|
|
679
|
+
*/
|
|
680
|
+
function escapeTemplateString(str) {
|
|
681
|
+
return str
|
|
682
|
+
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
683
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
684
|
+
.replace(/\$/g, '\\$'); // Escape dollar signs to prevent ${} interpretation
|
|
685
|
+
}
|
|
686
|
+
|
|
580
687
|
export function transformTextNode(transformer, node, indent) {
|
|
581
688
|
const pad = ' '.repeat(indent);
|
|
582
689
|
const parts = node.parts;
|
|
@@ -589,7 +696,8 @@ export function transformTextNode(transformer, node, indent) {
|
|
|
589
696
|
// Has interpolations - use text() with a function
|
|
590
697
|
const textParts = parts.map(part => {
|
|
591
698
|
if (typeof part === 'string') {
|
|
592
|
-
|
|
699
|
+
// Escape for template literal (not JSON.stringify which adds quotes)
|
|
700
|
+
return escapeTemplateString(part);
|
|
593
701
|
}
|
|
594
702
|
// Interpolation
|
|
595
703
|
const expr = transformExpressionString(transformer, part.expression);
|
package/loader/vite-plugin.js
CHANGED
|
@@ -2,32 +2,42 @@
|
|
|
2
2
|
* Pulse Vite Plugin
|
|
3
3
|
*
|
|
4
4
|
* Enables .pulse file support in Vite projects
|
|
5
|
+
* Extracts CSS to virtual .css modules so Vite's CSS pipeline handles them
|
|
6
|
+
* (prevents JS minifier from corrupting CSS in template literals)
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { compile } from '../compiler/index.js';
|
|
8
10
|
import { existsSync } from 'fs';
|
|
9
11
|
import { resolve, dirname } from 'path';
|
|
10
12
|
|
|
13
|
+
// Virtual module ID for extracted CSS (uses .css extension so Vite treats it as CSS)
|
|
14
|
+
const VIRTUAL_CSS_SUFFIX = '.pulse.css';
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Create Pulse Vite plugin
|
|
13
18
|
*/
|
|
14
19
|
export default function pulsePlugin(options = {}) {
|
|
15
20
|
const {
|
|
16
|
-
include = /\.pulse$/,
|
|
17
21
|
exclude = /node_modules/,
|
|
18
22
|
sourceMap = true
|
|
19
23
|
} = options;
|
|
20
24
|
|
|
25
|
+
// Store extracted CSS for each .pulse module
|
|
26
|
+
const cssMap = new Map();
|
|
27
|
+
|
|
21
28
|
return {
|
|
22
29
|
name: 'vite-plugin-pulse',
|
|
23
30
|
enforce: 'pre',
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
* Resolve .pulse files and
|
|
27
|
-
* The compiler transforms .pulse imports to .js, so we need to
|
|
28
|
-
* resolve them back to .pulse for Vite to process them
|
|
33
|
+
* Resolve .pulse files and virtual CSS modules
|
|
29
34
|
*/
|
|
30
35
|
resolveId(id, importer) {
|
|
36
|
+
// Handle virtual CSS module resolution
|
|
37
|
+
if (id.endsWith(VIRTUAL_CSS_SUFFIX)) {
|
|
38
|
+
return '\0' + id;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Direct .pulse imports - resolve to absolute path
|
|
32
42
|
if (id.endsWith('.pulse') && importer) {
|
|
33
43
|
const importerDir = dirname(importer);
|
|
@@ -38,7 +48,6 @@ export default function pulsePlugin(options = {}) {
|
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
// Check if a .js import has a corresponding .pulse file
|
|
41
|
-
// This handles the compiler's transformation of .pulse -> .js imports
|
|
42
51
|
if (id.endsWith('.js') && importer) {
|
|
43
52
|
const pulseId = id.replace(/\.js$/, '.pulse');
|
|
44
53
|
const importerDir = dirname(importer);
|
|
@@ -52,8 +61,22 @@ export default function pulsePlugin(options = {}) {
|
|
|
52
61
|
return null;
|
|
53
62
|
},
|
|
54
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Load virtual CSS modules
|
|
66
|
+
*/
|
|
67
|
+
load(id) {
|
|
68
|
+
// Virtual modules start with \0
|
|
69
|
+
if (id.startsWith('\0') && id.endsWith(VIRTUAL_CSS_SUFFIX)) {
|
|
70
|
+
const pulseId = id.slice(1, -VIRTUAL_CSS_SUFFIX.length + '.pulse'.length);
|
|
71
|
+
const css = cssMap.get(pulseId);
|
|
72
|
+
return css || '';
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
|
|
55
77
|
/**
|
|
56
78
|
* Transform .pulse files to JavaScript
|
|
79
|
+
* CSS is extracted to a virtual .css module that Vite processes separately
|
|
57
80
|
*/
|
|
58
81
|
transform(code, id) {
|
|
59
82
|
if (!id.endsWith('.pulse')) {
|
|
@@ -79,8 +102,27 @@ export default function pulsePlugin(options = {}) {
|
|
|
79
102
|
return null;
|
|
80
103
|
}
|
|
81
104
|
|
|
105
|
+
let outputCode = result.code;
|
|
106
|
+
|
|
107
|
+
// Extract CSS from compiled output and move to virtual CSS module
|
|
108
|
+
const stylesMatch = outputCode.match(/const styles = `([\s\S]*?)`;/);
|
|
109
|
+
if (stylesMatch) {
|
|
110
|
+
const css = stylesMatch[1];
|
|
111
|
+
const virtualCssId = id + '.css';
|
|
112
|
+
|
|
113
|
+
// Store CSS for the virtual module loader
|
|
114
|
+
cssMap.set(id, css);
|
|
115
|
+
|
|
116
|
+
// Replace inline style injection with CSS import
|
|
117
|
+
// Vite will process this through its CSS pipeline (not JS minifier)
|
|
118
|
+
outputCode = outputCode.replace(
|
|
119
|
+
/\/\/ Styles\nconst styles = `[\s\S]*?`;\n\/\/ Inject styles\nconst styleEl = document\.createElement\("style"\);\nstyleEl\.textContent = styles;\ndocument\.head\.appendChild\(styleEl\);/,
|
|
120
|
+
`// Styles extracted to virtual CSS module\nimport "${virtualCssId}";`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
return {
|
|
83
|
-
code:
|
|
125
|
+
code: outputCode,
|
|
84
126
|
map: result.map || null
|
|
85
127
|
};
|
|
86
128
|
} catch (error) {
|
|
@@ -92,7 +134,7 @@ export default function pulsePlugin(options = {}) {
|
|
|
92
134
|
/**
|
|
93
135
|
* Handle hot module replacement
|
|
94
136
|
*/
|
|
95
|
-
handleHotUpdate({ file, server
|
|
137
|
+
handleHotUpdate({ file, server }) {
|
|
96
138
|
if (file.endsWith('.pulse')) {
|
|
97
139
|
console.log(`[Pulse] HMR update: ${file}`);
|
|
98
140
|
|
|
@@ -102,8 +144,14 @@ export default function pulsePlugin(options = {}) {
|
|
|
102
144
|
server.moduleGraph.invalidateModule(module);
|
|
103
145
|
}
|
|
104
146
|
|
|
147
|
+
// Also invalidate the associated virtual CSS module
|
|
148
|
+
const virtualCssId = '\0' + file + '.css';
|
|
149
|
+
const cssModule = server.moduleGraph.getModuleById(virtualCssId);
|
|
150
|
+
if (cssModule) {
|
|
151
|
+
server.moduleGraph.invalidateModule(cssModule);
|
|
152
|
+
}
|
|
153
|
+
|
|
105
154
|
// Send HMR update instead of full reload
|
|
106
|
-
// The module will handle its own state preservation via hmrRuntime
|
|
107
155
|
server.ws.send({
|
|
108
156
|
type: 'update',
|
|
109
157
|
updates: [{
|
|
@@ -123,7 +171,7 @@ export default function pulsePlugin(options = {}) {
|
|
|
123
171
|
* Configure dev server
|
|
124
172
|
*/
|
|
125
173
|
configureServer(server) {
|
|
126
|
-
server.middlewares.use((
|
|
174
|
+
server.middlewares.use((_req, _res, next) => {
|
|
127
175
|
// Add any custom middleware here
|
|
128
176
|
next();
|
|
129
177
|
});
|
|
@@ -133,11 +181,8 @@ export default function pulsePlugin(options = {}) {
|
|
|
133
181
|
* Build hooks
|
|
134
182
|
*/
|
|
135
183
|
buildStart() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
buildEnd() {
|
|
140
|
-
console.log('[Pulse] Build completed');
|
|
184
|
+
// Clear CSS map on new build
|
|
185
|
+
cssMap.clear();
|
|
141
186
|
}
|
|
142
187
|
};
|
|
143
188
|
}
|
|
@@ -191,9 +236,9 @@ export const utils = {
|
|
|
191
236
|
},
|
|
192
237
|
|
|
193
238
|
/**
|
|
194
|
-
*
|
|
239
|
+
* Get the virtual CSS module ID for a Pulse file
|
|
195
240
|
*/
|
|
196
|
-
|
|
197
|
-
return
|
|
241
|
+
getVirtualCssId(id) {
|
|
242
|
+
return id + '.css';
|
|
198
243
|
}
|
|
199
244
|
};
|