ripple 0.2.175 → 0.2.177

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.175",
6
+ "version": "0.2.177",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -77,11 +77,12 @@
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/estree": "^1.0.8",
80
+ "@types/estree-jsx": "^1.0.5",
80
81
  "@types/node": "^24.3.0",
81
82
  "typescript": "^5.9.2",
82
83
  "@volar/language-core": "~2.4.23"
83
84
  },
84
85
  "peerDependencies": {
85
- "ripple": "0.2.175"
86
+ "ripple": "0.2.177"
86
87
  }
87
88
  }
@@ -0,0 +1,2 @@
1
+ // Re-export Rollup module types WITHOUT globals
2
+ export * from "rollup/dist/rollup.js";
@@ -1,4 +1,8 @@
1
1
  import type { Program } from 'estree';
2
+ import type {
3
+ CodeInformation as VolarCodeInformation,
4
+ Mapping as VolarMapping,
5
+ } from '@volar/language-core';
2
6
 
3
7
  // ============================================================================
4
8
  // Compiler API Exports
@@ -18,12 +22,26 @@ export interface CompileResult {
18
22
  css: string;
19
23
  }
20
24
 
25
+ export interface CustomMappingData {
26
+ generatedLengths: number[];
27
+ }
28
+
29
+ export interface MappingData extends VolarCodeInformation {
30
+ customData: CustomMappingData;
31
+ }
32
+
33
+ export interface CodeMapping extends VolarMapping<MappingData> {
34
+ data: MappingData;
35
+ }
36
+
21
37
  /**
22
38
  * Result of Volar mappings compilation
23
39
  */
24
40
  export interface VolarMappingsResult {
25
- /** Array of code mappings for Volar integration */
26
- [key: string]: any;
41
+ code: string;
42
+ mappings: CodeMapping[];
43
+ cssMappings: CodeMapping[];
44
+ cssSources: string[];
27
45
  }
28
46
 
29
47
  /**
@@ -34,12 +52,18 @@ export interface CompileOptions {
34
52
  mode?: 'client' | 'server';
35
53
  }
36
54
 
55
+ export interface ParseOptions {
56
+ /** Enable loose mode */
57
+ loose?: boolean;
58
+ }
59
+
37
60
  /**
38
61
  * Parse Ripple source code to ESTree AST
39
62
  * @param source - The Ripple source code to parse
63
+ * @param options - Parse options
40
64
  * @returns The parsed ESTree Program AST
41
65
  */
42
- export function parse(source: string): Program;
66
+ export function parse(source: string, options?: ParseOptions): Program;
43
67
 
44
68
  /**
45
69
  * Compile Ripple source code to JS/CSS output
@@ -48,19 +72,17 @@ export function parse(source: string): Program;
48
72
  * @param options - Compilation options (mode: 'client' or 'server')
49
73
  * @returns The compilation result with AST, JS, and CSS
50
74
  */
51
- export function compile(
52
- source: string,
53
- filename: string,
54
- options?: CompileOptions,
55
- ): CompileResult;
75
+ export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
56
76
 
57
77
  /**
58
78
  * Compile Ripple source to Volar mappings for editor integration
59
79
  * @param source - The Ripple source code
60
80
  * @param filename - The filename for source map generation
81
+ * @param options - Parse options
61
82
  * @returns Volar mappings object for editor integration
62
83
  */
63
84
  export function compile_to_volar_mappings(
64
85
  source: string,
65
86
  filename: string,
87
+ options?: ParseOptions,
66
88
  ): VolarMappingsResult;
@@ -1,4 +1,5 @@
1
1
  /** @import { Program } from 'estree' */
2
+ /** @import { ParseOptions } from 'ripple/compiler' */
2
3
 
3
4
  import { parse as parse_module } from './phases/1-parse/index.js';
4
5
  import { analyze } from './phases/2-analyze/index.js';
@@ -12,7 +13,7 @@ import { convert_source_map_to_mappings } from './phases/3-transform/segments.js
12
13
  * @returns {Program}
13
14
  */
14
15
  export function parse(source) {
15
- return parse_module(source);
16
+ return parse_module(source, undefined);
16
17
  }
17
18
 
18
19
  /**
@@ -23,7 +24,7 @@ export function parse(source) {
23
24
  * @returns {object}
24
25
  */
25
26
  export function compile(source, filename, options = {}) {
26
- const ast = parse_module(source);
27
+ const ast = parse_module(source, undefined);
27
28
  const analysis = analyze(ast, filename, options);
28
29
  const result =
29
30
  options.mode === 'server'
@@ -34,22 +35,24 @@ export function compile(source, filename, options = {}) {
34
35
  }
35
36
 
36
37
  /** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
37
- /** @import { MappingsResult } from './phases/3-transform/segments.js' */
38
+ /** @import { VolarMappingsResult } from 'ripple/compiler' */
38
39
 
39
40
  /**
40
41
  * Compile Ripple component to Volar virtual code with TypeScript mappings
41
42
  * @param {string} source
42
43
  * @param {string} filename
43
- * @returns {MappingsResult} Volar mappings object
44
+ * @param {ParseOptions} [options] - Compiler options
45
+ * @returns {VolarMappingsResult} Volar mappings object
44
46
  */
45
- export function compile_to_volar_mappings(source, filename) {
46
- const ast = parse_module(source);
47
- const analysis = analyze(ast, filename, { to_ts: true });
47
+ export function compile_to_volar_mappings(source, filename, options) {
48
+ const ast = parse_module(source, options);
49
+ const analysis = analyze(ast, filename, { to_ts: true, loose: !!options?.loose });
48
50
  const transformed = transform_client(filename, source, analysis, true);
49
51
 
50
52
  // Create volar mappings with esrap source map for accurate positioning
51
53
  return convert_source_map_to_mappings(
52
54
  transformed.ast,
55
+ ast,
53
56
  source,
54
57
  transformed.js.code,
55
58
  transformed.js.map,
@@ -1,10 +1,13 @@
1
1
  // @ts-nocheck
2
2
  /** @import { Program } from 'estree' */
3
- /** @import {
3
+ /**
4
+ * @import {
4
5
  * CommentWithLocation,
5
6
  * RipplePluginConfig
6
7
  * } from '#compiler' */
7
8
 
9
+ /** @import { ParseOptions } from 'ripple/compiler' */
10
+
8
11
  import * as acorn from 'acorn';
9
12
  import { tsPlugin } from '@sveltejs/acorn-typescript';
10
13
  import { parse_style } from './style.js';
@@ -74,7 +77,7 @@ function isWhitespaceTextNode(node) {
74
77
  * @returns {function(any): any} Parser extension function
75
78
  */
76
79
  function RipplePlugin(config) {
77
- return (/** @type {any} */ Parser) => {
80
+ return (/** @type {typeof acorn.Parser} */ Parser) => {
78
81
  const original = acorn.Parser.prototype;
79
82
  const tt = Parser.tokTypes || acorn.tokTypes;
80
83
  const tc = Parser.tokContexts || acorn.tokContexts;
@@ -85,6 +88,12 @@ function RipplePlugin(config) {
85
88
  /** @type {any[]} */
86
89
  #path = [];
87
90
  #commentContextId = 0;
91
+ #loose = false;
92
+
93
+ constructor(options, input) {
94
+ super(options, input);
95
+ this.#loose = options?.rippleOptions.loose === true;
96
+ }
88
97
 
89
98
  #createCommentMetadata() {
90
99
  if (this.#path.length === 0) {
@@ -1419,6 +1428,9 @@ function RipplePlugin(config) {
1419
1428
 
1420
1429
  // Position after the '>'
1421
1430
  this.pos = tagEndPos + 1;
1431
+
1432
+ // Add opening and closing for easier location tracking
1433
+ // TODO: we should also parse attributes inside the opening tag
1422
1434
  } else {
1423
1435
  open = this.jsx_parseOpeningElementAt();
1424
1436
  }
@@ -1468,6 +1480,41 @@ function RipplePlugin(config) {
1468
1480
  element.attributes = open.attributes;
1469
1481
  element.metadata ??= {};
1470
1482
  element.metadata.commentContainerId = ++this.#commentContextId;
1483
+ // Store opening tag's end position for use in loose mode when element is unclosed
1484
+ element.metadata.openingTagEnd = open.end;
1485
+ element.metadata.openingTagEndLoc = open.loc.end;
1486
+
1487
+ function addOpeningAndClosing() {
1488
+ //TODO: once parse attributes of style and script
1489
+ // we should the open element loc properly
1490
+ const name = open.name.name;
1491
+ open.loc = {
1492
+ start: {
1493
+ line: element.loc.start.line,
1494
+ column: element.loc.start.column,
1495
+ },
1496
+ end: {
1497
+ line: element.loc.start.line,
1498
+ column: element.loc.start.column + `${name}>`.length,
1499
+ },
1500
+ };
1501
+
1502
+ element.openingElement = open;
1503
+ element.closingElement = {
1504
+ type: 'JSXClosingElement',
1505
+ name: open.name,
1506
+ loc: {
1507
+ start: {
1508
+ line: element.loc.end.line,
1509
+ column: element.loc.end.column - `</${name}>`.length,
1510
+ },
1511
+ end: {
1512
+ line: element.loc.end.line,
1513
+ column: element.loc.end.column,
1514
+ },
1515
+ },
1516
+ };
1517
+ }
1471
1518
 
1472
1519
  if (element.selfClosing) {
1473
1520
  this.#path.pop();
@@ -1506,6 +1553,7 @@ function RipplePlugin(config) {
1506
1553
 
1507
1554
  element.content = content;
1508
1555
  this.finishNode(element, 'Element');
1556
+ addOpeningAndClosing(element, open);
1509
1557
  } else if (open.name.name === 'style') {
1510
1558
  // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
1511
1559
  // So backtrack to the end of the <style> tag to make sure everything is included
@@ -1515,7 +1563,7 @@ function RipplePlugin(config) {
1515
1563
  const content = input.slice(0, end);
1516
1564
 
1517
1565
  const component = this.#path.findLast((n) => n.type === 'Component');
1518
- const parsed_css = parse_style(content);
1566
+ const parsed_css = parse_style(content, { loose: this.#loose });
1519
1567
 
1520
1568
  if (!inside_head) {
1521
1569
  if (component.css !== null) {
@@ -1553,6 +1601,7 @@ function RipplePlugin(config) {
1553
1601
 
1554
1602
  element.css = content;
1555
1603
  this.finishNode(element, 'Element');
1604
+ addOpeningAndClosing(element, open);
1556
1605
  return element;
1557
1606
  } else {
1558
1607
  this.enterScope(0);
@@ -1590,15 +1639,24 @@ function RipplePlugin(config) {
1590
1639
  this.next();
1591
1640
  } else if (this.#path[this.#path.length - 1] === element) {
1592
1641
  // Check if this element was properly closed
1593
- // If we reach here and this element is still in the path, it means it was never closed
1594
- const tagName = this.getElementName(element.id);
1595
-
1596
- this.raise(
1597
- this.start,
1598
- `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
1599
- );
1642
+ if (!this.#loose) {
1643
+ const tagName = this.getElementName(element.id);
1644
+ this.raise(
1645
+ this.start,
1646
+ `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
1647
+ );
1648
+ } else {
1649
+ element.unclosed = true;
1650
+ const position = this.curPosition();
1651
+ position.line = element.metadata.openingTagEndLoc.line;
1652
+ position.column = element.metadata.openingTagEndLoc.column;
1653
+ element.loc.end = position;
1654
+ element.end = element.metadata.openingTagEnd;
1655
+ this.#path.pop();
1656
+ }
1600
1657
  }
1601
1658
  }
1659
+
1602
1660
  // Ensure we escape JSX <tag></tag> context
1603
1661
  const curContext = this.curContext();
1604
1662
 
@@ -1678,8 +1736,12 @@ function RipplePlugin(config) {
1678
1736
  }
1679
1737
  if (this.type === tt.braceL) {
1680
1738
  const node = this.jsx_parseExpressionContainer();
1681
- node.type = node.html ? 'Html' : 'Text';
1682
- delete node.html;
1739
+ // Keep JSXEmptyExpression as-is (for prettier to handle comments)
1740
+ // but convert other expressions to Text/Html nodes
1741
+ if (node.expression.type !== 'JSXEmptyExpression') {
1742
+ node.type = node.html ? 'Html' : 'Text';
1743
+ delete node.html;
1744
+ }
1683
1745
  body.push(node);
1684
1746
  } else if (this.type === tt.braceR) {
1685
1747
  return;
@@ -1720,10 +1782,40 @@ function RipplePlugin(config) {
1720
1782
  }
1721
1783
 
1722
1784
  if (openingTagName !== closingTagName) {
1723
- this.raise(
1724
- this.start,
1725
- `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
1726
- );
1785
+ if (!this.#loose) {
1786
+ this.raise(
1787
+ this.start,
1788
+ `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
1789
+ );
1790
+ } else {
1791
+ // Loop through all unclosed elements on the stack
1792
+ while (this.#path.length > 0) {
1793
+ const elem = this.#path[this.#path.length - 1];
1794
+
1795
+ // Stop at non-Element boundaries (Component, etc.)
1796
+ if (elem.type !== 'Element' && elem.type !== 'TsxCompat') {
1797
+ break;
1798
+ }
1799
+
1800
+ const elemName =
1801
+ elem.type === 'TsxCompat' ? 'tsx:' + elem.kind : this.getElementName(elem.id);
1802
+
1803
+ // Found matching opening tag
1804
+ if (elemName === closingTagName) {
1805
+ break;
1806
+ }
1807
+
1808
+ // Mark as unclosed and adjust location
1809
+ elem.unclosed = true;
1810
+ const position = this.curPosition();
1811
+ position.line = elem.metadata.openingTagEndLoc.line;
1812
+ position.column = elem.metadata.openingTagEndLoc.column;
1813
+ elem.loc.end = position;
1814
+ elem.end = elem.metadata.openingTagEnd;
1815
+
1816
+ this.#path.pop(); // Remove from stack
1817
+ }
1818
+ }
1727
1819
  }
1728
1820
 
1729
1821
  this.#path.pop();
@@ -1759,7 +1851,10 @@ function RipplePlugin(config) {
1759
1851
  ) {
1760
1852
  this.next();
1761
1853
  const node = this.jsx_parseExpressionContainer();
1762
- node.type = 'Text';
1854
+ // Keep JSXEmptyExpression as-is (don't convert to Text)
1855
+ if (node.expression.type !== 'JSXEmptyExpression') {
1856
+ node.type = 'Text';
1857
+ }
1763
1858
  this.next();
1764
1859
  this.context.pop();
1765
1860
  this.context.pop();
@@ -2256,9 +2351,10 @@ function get_comment_handlers(source, comments, index = 0) {
2256
2351
  /**
2257
2352
  * Parse Ripple source code into an AST
2258
2353
  * @param {string} source
2354
+ * @param {ParseOptions} [options]
2259
2355
  * @returns {Program}
2260
2356
  */
2261
- export function parse(source) {
2357
+ export function parse(source, options) {
2262
2358
  /** @type {CommentWithLocation[]} */
2263
2359
  const comments = [];
2264
2360
 
@@ -2315,6 +2411,9 @@ export function parse(source) {
2315
2411
  ecmaVersion: 13,
2316
2412
  locations: true,
2317
2413
  onComment: /** @type {any} */ (onComment),
2414
+ rippleOptions: {
2415
+ loose: options?.loose || false,
2416
+ },
2318
2417
  });
2319
2418
  } catch (e) {
2320
2419
  throw e;
@@ -88,8 +88,8 @@ class Parser {
88
88
  }
89
89
  }
90
90
 
91
- export function parse_style(content) {
92
- const parser = new Parser(content, false);
91
+ export function parse_style(content, options) {
92
+ const parser = new Parser(content, options.loose || false);
93
93
 
94
94
  return {
95
95
  source: content,
@@ -232,8 +232,8 @@ function read_declaration(parser) {
232
232
 
233
233
  const value = read_value(parser);
234
234
 
235
- if (!value && !property.startsWith('--')) {
236
- e.css_empty_declaration({ start, end: index });
235
+ if (!value && !property.startsWith('--') && !parser.loose) {
236
+ throw new Error('CSS Declaration cannot be empty');
237
237
  }
238
238
 
239
239
  const end = parser.index;
@@ -878,6 +878,7 @@ export function analyze(ast, filename, options = {}) {
878
878
  inside_head: false,
879
879
  inside_server_block: options.mode === 'server',
880
880
  to_ts: options.to_ts ?? false,
881
+ loose: options.loose ?? false,
881
882
  },
882
883
  visitors,
883
884
  );