ripple 0.3.10 → 0.3.12

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/index.d.ts +3 -1
  5. package/src/compiler/phases/1-parse/index.js +170 -8
  6. package/src/compiler/phases/2-analyze/index.js +231 -20
  7. package/src/compiler/phases/3-transform/client/index.js +169 -77
  8. package/src/compiler/phases/3-transform/server/index.js +46 -3
  9. package/src/compiler/types/index.d.ts +19 -2
  10. package/src/compiler/types/parse.d.ts +1 -1
  11. package/src/compiler/utils.js +174 -0
  12. package/src/runtime/index-client.js +14 -4
  13. package/src/runtime/internal/client/composite.js +2 -2
  14. package/src/runtime/internal/client/expression.js +64 -2
  15. package/src/runtime/internal/client/portal.js +1 -1
  16. package/src/utils/builders.js +30 -0
  17. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  18. package/tests/client/basic/basic.rendering.test.ripple +4 -2
  19. package/tests/client/composite/composite.render.test.ripple +46 -0
  20. package/tests/client/return.test.ripple +101 -0
  21. package/tests/client/tsx.test.ripple +486 -0
  22. package/tests/hydration/compiled/client/basic.js +8 -24
  23. package/tests/hydration/compiled/client/composite.js +6 -24
  24. package/tests/hydration/compiled/client/events.js +9 -54
  25. package/tests/hydration/compiled/client/for.js +59 -152
  26. package/tests/hydration/compiled/client/head.js +5 -20
  27. package/tests/hydration/compiled/client/hmr.js +2 -8
  28. package/tests/hydration/compiled/client/html.js +59 -226
  29. package/tests/hydration/compiled/client/if-children.js +6 -22
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +18 -66
  31. package/tests/hydration/compiled/client/nested-control-flow.js +92 -368
  32. package/tests/hydration/compiled/client/portal.js +4 -16
  33. package/tests/hydration/compiled/client/reactivity.js +7 -40
  34. package/tests/hydration/compiled/client/return.js +1 -4
  35. package/tests/hydration/compiled/client/try.js +2 -2
  36. package/tests/utils/compiler-compat-config.test.js +38 -0
  37. package/tests/utils/vite-plugin-config.test.js +113 -0
  38. package/tsconfig.json +2 -0
  39. package/tsconfig.typecheck.json +2 -1
  40. package/types/index.d.ts +2 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.12
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.3.12
9
+
10
+ ## 0.3.11
11
+
12
+ ### Patch Changes
13
+
14
+ - [#853](https://github.com/Ripple-TS/ripple/pull/853)
15
+ [`6792c70`](https://github.com/Ripple-TS/ripple/commit/6792c700db30ec0c25077bf8892753f18eddc5cc)
16
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! -
17
+ fix(compiler): add `throw` statement support in `if` blocks
18
+
19
+ - [#858](https://github.com/Ripple-TS/ripple/pull/858)
20
+ [`f2624a6`](https://github.com/Ripple-TS/ripple/commit/f2624a6596479480c47317ea3030863214a6e2b3)
21
+ Thanks [@RazinShafayet2007](https://github.com/RazinShafayet2007)! - fix: scoped
22
+ styles apply correctly when child content is rendered through a parent component
23
+
24
+ - [#840](https://github.com/Ripple-TS/ripple/pull/840)
25
+ [`13323dd`](https://github.com/Ripple-TS/ripple/commit/13323dddbcb68e1e8e373142884a7c54fbb76cd7)
26
+ Thanks [@trueadm](https://github.com/trueadm)! - Remove the `compat` option from
27
+ `mount()` and `hydrate()`, and stop exporting the old public compat types from
28
+ `ripple`. Compat integrations are now expected to be provided by the Vite plugin
29
+ via `ripple.config.ts`, while direct runtime tests can seed the generated global
30
+ compat registry.
31
+
32
+ Also add the `reactCompat()` config-facing helper from `@ripple-ts/compat-react`
33
+ for use in `ripple.config.ts`.
34
+
35
+ - Updated dependencies
36
+ [[`6792c70`](https://github.com/Ripple-TS/ripple/commit/6792c700db30ec0c25077bf8892753f18eddc5cc),
37
+ [`f2624a6`](https://github.com/Ripple-TS/ripple/commit/f2624a6596479480c47317ea3030863214a6e2b3),
38
+ [`13323dd`](https://github.com/Ripple-TS/ripple/commit/13323dddbcb68e1e8e373142884a7c54fbb76cd7)]:
39
+ - ripple@0.3.11
40
+
3
41
  ## 0.3.10
4
42
 
5
43
  ### Patch Changes
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.3.10",
6
+ "version": "0.3.12",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -105,6 +105,6 @@
105
105
  "vscode-languageserver-types": "^3.17.5"
106
106
  },
107
107
  "peerDependencies": {
108
- "ripple": "0.3.10"
108
+ "ripple": "0.3.12"
109
109
  }
110
110
  }
@@ -59,7 +59,7 @@ function is_ripple_error_suppress_comment(comment) {
59
59
  }
60
60
 
61
61
  /**
62
- * @param {AST.Node} node
62
+ * @param {AST.Node | AST.NodeWithLocation} node
63
63
  * @param {AST.CommentWithLocation[]} comments
64
64
  */
65
65
  function is_ripple_error_suppressed(node, comments) {
@@ -98,6 +98,7 @@ interface SharedCompileOptions {
98
98
  export interface CompileOptions extends SharedCompileOptions {
99
99
  mode?: 'client' | 'server';
100
100
  hmr?: boolean;
101
+ compat_kinds?: string[];
101
102
  }
102
103
 
103
104
  export interface ParseOptions {
@@ -106,7 +107,8 @@ export interface ParseOptions {
106
107
  comments?: AST.CommentWithLocation[];
107
108
  }
108
109
 
109
- export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode'> {
110
+ export interface AnalyzeOptions
111
+ extends ParseOptions, Pick<CompileOptions, 'mode' | 'compat_kinds'> {
110
112
  errors?: RippleCompileError[];
111
113
  to_ts?: boolean;
112
114
  }
@@ -1358,7 +1358,9 @@ function RipplePlugin(config) {
1358
1358
 
1359
1359
  /** @type {Parse.Parser['jsx_readToken']} */
1360
1360
  jsx_readToken() {
1361
- const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
1361
+ const inside_tsx_compat = this.#path.findLast(
1362
+ (n) => n.type === 'TsxCompat' || n.type === 'Tsx',
1363
+ );
1362
1364
  if (inside_tsx_compat) {
1363
1365
  return super.jsx_readToken();
1364
1366
  }
@@ -1509,6 +1511,48 @@ function RipplePlugin(config) {
1509
1511
  }
1510
1512
  }
1511
1513
 
1514
+ /**
1515
+ * Override jsx_parseElement to intercept expression-level JSX.
1516
+ * This is called by acorn-jsx's parseExprAtom when it encounters <
1517
+ * in expression position. Only <tsx> and <tsx:*> are allowed.
1518
+ * @type {Parse.Parser['jsx_parseElement']}
1519
+ */
1520
+ jsx_parseElement() {
1521
+ const inside_tsx = this.#path.findLast((n) => n.type === 'TsxCompat' || n.type === 'Tsx');
1522
+ if (inside_tsx) {
1523
+ // Inside tsx/tsx:*, let acorn-jsx handle it normally
1524
+ return super.jsx_parseElement();
1525
+ }
1526
+
1527
+ // Check if the element being parsed IS a <tsx> or <tsx:*> tag
1528
+ // Current token is jsxTagStart, this.end is position after '<'
1529
+ const tag_name_start = this.end;
1530
+ const char_after_tsx = this.input.charCodeAt(tag_name_start + 3);
1531
+ const is_tsx_tag =
1532
+ this.input.startsWith('tsx', tag_name_start) &&
1533
+ (tag_name_start + 3 >= this.input.length ||
1534
+ char_after_tsx === 62 || // >
1535
+ char_after_tsx === 47 || // / (self-closing)
1536
+ char_after_tsx === 32 || // space
1537
+ char_after_tsx === 9 || // tab
1538
+ char_after_tsx === 10 || // newline
1539
+ char_after_tsx === 13 || // carriage return
1540
+ char_after_tsx === 58); // : (tsx:react)
1541
+
1542
+ if (is_tsx_tag) {
1543
+ // Use Ripple's parseElement to create a Tsx/TsxCompat node
1544
+ this.next();
1545
+ return /** @type {import('estree-jsx').JSXElement} */ (
1546
+ /** @type {unknown} */ (this.parseElement())
1547
+ );
1548
+ }
1549
+
1550
+ this.raise(
1551
+ this.start,
1552
+ 'JSX elements cannot be used as expressions. Wrap with `<tsx>...</tsx>` or use elements as statements within a component.',
1553
+ );
1554
+ }
1555
+
1512
1556
  /**
1513
1557
  * @type {Parse.Parser['parseElement']}
1514
1558
  */
@@ -1520,7 +1564,7 @@ function RipplePlugin(config) {
1520
1564
  const start = this.start - 1;
1521
1565
  const position = new acorn.Position(this.curLine, start - this.lineStart);
1522
1566
 
1523
- const element = /** @type {AST.Element | AST.TsxCompat} */ (this.startNode());
1567
+ const element = /** @type {AST.Element | AST.Tsx | AST.TsxCompat} */ (this.startNode());
1524
1568
  element.start = start;
1525
1569
  /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1526
1570
  element.metadata = { path: [] };
@@ -1535,6 +1579,8 @@ function RipplePlugin(config) {
1535
1579
 
1536
1580
  // Check if this is a namespaced element (tsx:react)
1537
1581
  const is_tsx_compat = open.name.type === 'JSXNamespacedName';
1582
+ const is_tsx =
1583
+ !is_tsx_compat && open.name.type === 'JSXIdentifier' && open.name.name === 'tsx';
1538
1584
 
1539
1585
  if (is_tsx_compat) {
1540
1586
  const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
@@ -1548,6 +1594,15 @@ function RipplePlugin(config) {
1548
1594
  `TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
1549
1595
  );
1550
1596
  }
1597
+ } else if (is_tsx) {
1598
+ /** @type {AST.Tsx} */ (element).type = 'Tsx';
1599
+
1600
+ if (open.selfClosing) {
1601
+ this.raise(
1602
+ open.start,
1603
+ `TSX elements cannot be self-closing. '<tsx />' must have a closing tag '</tsx>'.`,
1604
+ );
1605
+ }
1551
1606
  } else {
1552
1607
  element.type = 'Element';
1553
1608
  }
@@ -1575,7 +1630,7 @@ function RipplePlugin(config) {
1575
1630
  }
1576
1631
  }
1577
1632
 
1578
- if (!is_tsx_compat) {
1633
+ if (!is_tsx_compat && !is_tsx) {
1579
1634
  /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1580
1635
  convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1581
1636
  );
@@ -1745,6 +1800,7 @@ function RipplePlugin(config) {
1745
1800
  const insideTemplate =
1746
1801
  parent?.type === 'Component' ||
1747
1802
  parent?.type === 'Element' ||
1803
+ parent?.type === 'Tsx' ||
1748
1804
  parent?.type === 'TsxCompat';
1749
1805
 
1750
1806
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -1757,7 +1813,30 @@ function RipplePlugin(config) {
1757
1813
  this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
1758
1814
  this.exitScope();
1759
1815
 
1760
- if (element.type === 'TsxCompat') {
1816
+ if (element.type === 'Tsx') {
1817
+ this.#path.pop();
1818
+
1819
+ if (!element.unclosed) {
1820
+ const raise_error = () => {
1821
+ this.raise(this.start, `Expected closing tag '</tsx>'`);
1822
+ };
1823
+
1824
+ this.next();
1825
+ // we should expect to see </tsx>
1826
+ if (this.value !== '/') {
1827
+ raise_error();
1828
+ }
1829
+ this.next();
1830
+ if (this.value !== 'tsx') {
1831
+ raise_error();
1832
+ }
1833
+ this.next();
1834
+ if (this.type !== tstt.jsxTagEnd) {
1835
+ raise_error();
1836
+ }
1837
+ this.next();
1838
+ }
1839
+ } else if (element.type === 'TsxCompat') {
1761
1840
  this.#path.pop();
1762
1841
 
1763
1842
  if (!element.unclosed) {
@@ -1813,6 +1892,7 @@ function RipplePlugin(config) {
1813
1892
  const insideTemplate =
1814
1893
  parent?.type === 'Component' ||
1815
1894
  parent?.type === 'Element' ||
1895
+ parent?.type === 'Tsx' ||
1816
1896
  parent?.type === 'TsxCompat';
1817
1897
 
1818
1898
  if (curContext === tstc.tc_expr && !insideTemplate) {
@@ -1820,7 +1900,7 @@ function RipplePlugin(config) {
1820
1900
  }
1821
1901
  }
1822
1902
 
1823
- if (element.closingElement && !is_tsx_compat) {
1903
+ if (element.closingElement && !is_tsx_compat && !is_tsx) {
1824
1904
  /** @type {unknown} */ (element.closingElement.name) = convert_from_jsx(
1825
1905
  element.closingElement.name,
1826
1906
  );
@@ -1836,6 +1916,7 @@ function RipplePlugin(config) {
1836
1916
  parseTemplateBody(body) {
1837
1917
  const inside_func =
1838
1918
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
1919
+ const inside_tsx = this.#path.findLast((n) => n.type === 'Tsx');
1839
1920
  const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
1840
1921
 
1841
1922
  if (!inside_func) {
@@ -1847,6 +1928,75 @@ function RipplePlugin(config) {
1847
1928
  }
1848
1929
  }
1849
1930
 
1931
+ if (inside_tsx) {
1932
+ this.exprAllowed = true;
1933
+
1934
+ while (true) {
1935
+ if (this.type === tt.eof || this.pos >= this.input.length || this.type === tt.braceR) {
1936
+ if (!this.#loose) {
1937
+ this.raise(
1938
+ this.start,
1939
+ `Unclosed tag '<tsx>'. Expected '</tsx>' before end of component.`,
1940
+ );
1941
+ } else {
1942
+ inside_tsx.unclosed = true;
1943
+ /** @type {AST.NodeWithLocation} */ (inside_tsx).loc.end = {
1944
+ .../** @type {AST.SourceLocation} */ (inside_tsx.openingElement.loc).end,
1945
+ };
1946
+ inside_tsx.end = inside_tsx.openingElement.end;
1947
+ }
1948
+ return;
1949
+ }
1950
+
1951
+ if (this.input.slice(this.pos, this.pos + 4) === '/tsx') {
1952
+ const after = this.input.charCodeAt(this.pos + 4);
1953
+ // Make sure it's </tsx> and not </tsx:...>
1954
+ if (after === 62 /* > */) {
1955
+ return;
1956
+ }
1957
+ }
1958
+
1959
+ if (this.type === tt.braceL) {
1960
+ const node = this.jsx_parseExpressionContainer();
1961
+ body.push(node);
1962
+ } else if (this.type === tstt.jsxTagStart) {
1963
+ // Parse JSX element
1964
+ const node = super.parseExpression();
1965
+ body.push(node);
1966
+ } else {
1967
+ const start = this.start;
1968
+ this.pos = start;
1969
+ let text = '';
1970
+
1971
+ while (this.pos < this.input.length) {
1972
+ const ch = this.input.charCodeAt(this.pos);
1973
+
1974
+ // Stop at opening tag, expression, or the component-closing brace
1975
+ if (ch === 60 || ch === 123 || ch === 125) {
1976
+ // < or { or }
1977
+ break;
1978
+ }
1979
+
1980
+ text += this.input[this.pos];
1981
+ this.pos++;
1982
+ }
1983
+
1984
+ if (text) {
1985
+ const node = /** @type {ESTreeJSX.JSXText} */ ({
1986
+ type: 'JSXText',
1987
+ value: text,
1988
+ raw: text,
1989
+ start,
1990
+ end: this.pos,
1991
+ });
1992
+ body.push(node);
1993
+ }
1994
+
1995
+ // Always call next() to ensure parser makes progress
1996
+ this.next();
1997
+ }
1998
+ }
1999
+ }
1850
2000
  if (inside_tsx_compat) {
1851
2001
  this.exprAllowed = true;
1852
2002
 
@@ -1950,7 +2100,9 @@ function RipplePlugin(config) {
1950
2100
  const currentElement = this.#path[this.#path.length - 1];
1951
2101
  if (
1952
2102
  !currentElement ||
1953
- (currentElement.type !== 'Element' && currentElement.type !== 'TsxCompat')
2103
+ (currentElement.type !== 'Element' &&
2104
+ currentElement.type !== 'Tsx' &&
2105
+ currentElement.type !== 'TsxCompat')
1954
2106
  ) {
1955
2107
  this.raise(this.start, 'Unexpected closing tag');
1956
2108
  }
@@ -1966,6 +2118,12 @@ function RipplePlugin(config) {
1966
2118
  closingElement.name?.type === 'JSXNamespacedName'
1967
2119
  ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
1968
2120
  : this.getElementName(closingElement.name);
2121
+ } else if (currentElement.type === 'Tsx') {
2122
+ openingTagName = 'tsx';
2123
+ closingTagName =
2124
+ closingElement.name?.type === 'JSXNamespacedName'
2125
+ ? closingElement.name.namespace.name + ':' + closingElement.name.name.name
2126
+ : this.getElementName(closingElement.name);
1969
2127
  } else {
1970
2128
  // Regular Element node
1971
2129
  openingTagName = this.getElementName(currentElement.id);
@@ -1987,12 +2145,16 @@ function RipplePlugin(config) {
1987
2145
  const elem = this.#path[this.#path.length - 1];
1988
2146
 
1989
2147
  // Stop at non-Element boundaries (Component, etc.)
1990
- if (elem.type !== 'Element' && elem.type !== 'TsxCompat') {
2148
+ if (elem.type !== 'Element' && elem.type !== 'Tsx' && elem.type !== 'TsxCompat') {
1991
2149
  break;
1992
2150
  }
1993
2151
 
1994
2152
  const elemName =
1995
- elem.type === 'TsxCompat' ? 'tsx:' + elem.kind : this.getElementName(elem.id);
2153
+ elem.type === 'TsxCompat'
2154
+ ? 'tsx:' + elem.kind
2155
+ : elem.type === 'Tsx'
2156
+ ? 'tsx'
2157
+ : this.getElementName(elem.id);
1996
2158
 
1997
2159
  // Found matching opening tag
1998
2160
  if (elemName === closingTagName) {