@cloudwerk/vite-plugin 0.6.9 → 0.8.0

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/dist/index.d.ts CHANGED
@@ -95,6 +95,8 @@ interface ClientComponentInfo {
95
95
  bundlePath: string;
96
96
  /** Absolute file path */
97
97
  absolutePath: string;
98
+ /** Named export name (undefined means default export) */
99
+ exportName?: string;
98
100
  }
99
101
  /**
100
102
  * Information about a CSS import detected in a layout or page.
package/dist/index.js CHANGED
@@ -1101,9 +1101,12 @@ function generateClientEntry(clientComponents, cssImports, options, manifest) {
1101
1101
  }
1102
1102
  function generateStaticImportsAndMap(clientComponents) {
1103
1103
  const components = Array.from(clientComponents.values());
1104
- const imports = components.map(
1105
- (info, index) => `import Component${index} from '${info.absolutePath}'`
1106
- ).join("\n");
1104
+ const imports = components.map((info, index) => {
1105
+ if (info.exportName) {
1106
+ return `import { ${info.exportName} as Component${index} } from '${info.absolutePath}'`;
1107
+ }
1108
+ return `import Component${index} from '${info.absolutePath}'`;
1109
+ }).join("\n");
1107
1110
  const componentMapEntries = components.map(
1108
1111
  (info, index) => ` '${info.componentId}': Component${index}`
1109
1112
  ).join(",\n");
@@ -1195,7 +1198,10 @@ import { jsx } from 'hono/jsx/jsx-runtime'`,
1195
1198
  });
1196
1199
  }
1197
1200
  const bundleMap = Object.fromEntries(
1198
- Array.from(clientComponents.values()).map((info) => [info.componentId, `/@fs${info.absolutePath}`])
1201
+ Array.from(clientComponents.values()).map((info) => [
1202
+ info.componentId,
1203
+ { path: `/@fs${info.absolutePath}`, exportName: info.exportName ?? null }
1204
+ ])
1199
1205
  );
1200
1206
  return `/**
1201
1207
  * Generated Cloudwerk Client Entry (Hono JSX)
@@ -1244,19 +1250,19 @@ async function hydrate() {
1244
1250
  continue
1245
1251
  }
1246
1252
 
1247
- const bundlePath = bundles[componentId]
1248
- if (!bundlePath) {
1253
+ const bundle = bundles[componentId]
1254
+ if (!bundle) {
1249
1255
  console.warn('[Cloudwerk] Unknown client component:', componentId)
1250
1256
  continue
1251
1257
  }
1252
1258
 
1253
1259
  try {
1254
1260
  const props = propsJson ? JSON.parse(propsJson) : {}
1255
- const module = await loadComponent(bundlePath)
1256
- const Component = module.default
1261
+ const module = await loadComponent(bundle.path)
1262
+ const Component = bundle.exportName ? module[bundle.exportName] : module.default
1257
1263
 
1258
1264
  if (!Component) {
1259
- console.error('[Cloudwerk] No default export in component:', componentId)
1265
+ console.error('[Cloudwerk] No export found in component:', componentId)
1260
1266
  continue
1261
1267
  }
1262
1268
 
@@ -1472,6 +1478,57 @@ function findDefaultExport(ast) {
1472
1478
  }
1473
1479
  return { type: null, name: null, index: -1 };
1474
1480
  }
1481
+ function findNamedExports(ast) {
1482
+ const results = [];
1483
+ for (let i = 0; i < ast.body.length; i++) {
1484
+ const node = ast.body[i];
1485
+ if (node.type === "ExportDeclaration") {
1486
+ const decl = node.declaration;
1487
+ if (decl.type === "FunctionDeclaration" && decl.identifier) {
1488
+ const name = decl.identifier.value;
1489
+ if (/^[A-Z]/.test(name)) {
1490
+ results.push({
1491
+ name,
1492
+ type: "function",
1493
+ index: i,
1494
+ spanStart: node.span.start,
1495
+ spanEnd: node.span.end,
1496
+ isAsync: decl.async
1497
+ });
1498
+ }
1499
+ }
1500
+ if (decl.type === "ClassDeclaration" && decl.identifier) {
1501
+ const name = decl.identifier.value;
1502
+ if (/^[A-Z]/.test(name)) {
1503
+ results.push({
1504
+ name,
1505
+ type: "class",
1506
+ index: i,
1507
+ spanStart: node.span.start,
1508
+ spanEnd: node.span.end
1509
+ });
1510
+ }
1511
+ }
1512
+ if (decl.type === "VariableDeclaration") {
1513
+ for (const declarator of decl.declarations) {
1514
+ if (declarator.id.type === "Identifier") {
1515
+ const name = declarator.id.value;
1516
+ if (/^[A-Z]/.test(name)) {
1517
+ results.push({
1518
+ name,
1519
+ type: "const",
1520
+ index: i,
1521
+ spanStart: node.span.start,
1522
+ spanEnd: node.span.end
1523
+ });
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ }
1530
+ return results;
1531
+ }
1475
1532
  function transformClientComponent(code, options) {
1476
1533
  const { componentId, bundlePath } = options;
1477
1534
  try {
@@ -1481,7 +1538,8 @@ function transformClientComponent(code, options) {
1481
1538
  comments: true
1482
1539
  });
1483
1540
  const exportInfo = findDefaultExport(ast);
1484
- if (exportInfo.type === null) {
1541
+ const namedExports = findNamedExports(ast);
1542
+ if (exportInfo.type === null && namedExports.length === 0) {
1485
1543
  return {
1486
1544
  code,
1487
1545
  success: false,
@@ -1492,97 +1550,134 @@ function transformClientComponent(code, options) {
1492
1550
  const wrapperImport = `import { createClientComponentWrapper as __createWrapper } from '@cloudwerk/ui/client'
1493
1551
  `;
1494
1552
  const metaObj = JSON.stringify({ componentId, bundlePath });
1495
- switch (exportInfo.type) {
1496
- case "function": {
1497
- if (exportInfo.name) {
1498
- const asyncPrefix = exportInfo.isAsync ? "async " : "";
1499
- transformed = transformed.replace(
1500
- new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s+${exportInfo.name}`),
1501
- `${asyncPrefix}function ${exportInfo.name}`
1502
- );
1503
- transformed = wrapperImport + transformed;
1504
- transformed += `
1553
+ const wrappedExports = [];
1554
+ if (namedExports.length > 0) {
1555
+ const sorted = [...namedExports].sort((a, b) => b.spanStart - a.spanStart);
1556
+ for (const namedExport of sorted) {
1557
+ const namedMetaObj = JSON.stringify({
1558
+ componentId: `${componentId}__${namedExport.name}`,
1559
+ bundlePath
1560
+ });
1561
+ const originalName = `__${namedExport.name}_original`;
1562
+ switch (namedExport.type) {
1563
+ case "function": {
1564
+ const asyncPrefix = namedExport.isAsync ? "async " : "";
1565
+ transformed = transformed.replace(
1566
+ new RegExp(`export\\s+${asyncPrefix}function\\s+${namedExport.name}\\s*\\(`),
1567
+ `${asyncPrefix}function ${originalName}(`
1568
+ );
1569
+ break;
1570
+ }
1571
+ case "class": {
1572
+ transformed = transformed.replace(
1573
+ new RegExp(`export\\s+class\\s+${namedExport.name}\\s`),
1574
+ `class ${originalName} `
1575
+ );
1576
+ break;
1577
+ }
1578
+ case "const": {
1579
+ transformed = transformed.replace(
1580
+ new RegExp(`export\\s+(const|let|var)\\s+${namedExport.name}\\s*=`),
1581
+ `const ${originalName} =`
1582
+ );
1583
+ break;
1584
+ }
1585
+ }
1586
+ transformed += `
1587
+ export const ${namedExport.name} = __createWrapper(${originalName}, ${namedMetaObj})
1588
+ `;
1589
+ wrappedExports.push(namedExport.name);
1590
+ }
1591
+ }
1592
+ if (exportInfo.type !== null) {
1593
+ switch (exportInfo.type) {
1594
+ case "function": {
1595
+ if (exportInfo.name) {
1596
+ const asyncPrefix = exportInfo.isAsync ? "async " : "";
1597
+ transformed = transformed.replace(
1598
+ new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s+${exportInfo.name}`),
1599
+ `${asyncPrefix}function ${exportInfo.name}`
1600
+ );
1601
+ transformed += `
1505
1602
  const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
1506
1603
  export default __WrappedComponent
1507
1604
  `;
1508
- } else {
1509
- const asyncPrefix = exportInfo.isAsync ? "async " : "";
1605
+ } else {
1606
+ const asyncPrefix = exportInfo.isAsync ? "async " : "";
1607
+ transformed = transformed.replace(
1608
+ new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s*\\(`),
1609
+ `const __OriginalComponent = ${asyncPrefix}function(`
1610
+ );
1611
+ transformed += `
1612
+ const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
1613
+ export default __WrappedComponent
1614
+ `;
1615
+ }
1616
+ break;
1617
+ }
1618
+ case "arrow": {
1510
1619
  transformed = transformed.replace(
1511
- new RegExp(`export\\s+default\\s+${asyncPrefix}function\\s*\\(`),
1512
- `const __OriginalComponent = ${asyncPrefix}function(`
1620
+ /export\s+default/,
1621
+ "const __OriginalComponent ="
1513
1622
  );
1514
- transformed = wrapperImport + transformed;
1515
1623
  transformed += `
1516
1624
  const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
1517
1625
  export default __WrappedComponent
1518
1626
  `;
1627
+ break;
1519
1628
  }
1520
- break;
1521
- }
1522
- case "arrow": {
1523
- transformed = transformed.replace(
1524
- /export\s+default/,
1525
- "const __OriginalComponent ="
1526
- );
1527
- transformed = wrapperImport + transformed;
1528
- transformed += `
1629
+ case "class": {
1630
+ if (exportInfo.name) {
1631
+ transformed = transformed.replace(
1632
+ new RegExp(`export\\s+default\\s+class\\s+${exportInfo.name}`),
1633
+ `class ${exportInfo.name}`
1634
+ );
1635
+ transformed += `
1636
+ const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
1637
+ export default __WrappedComponent
1638
+ `;
1639
+ } else {
1640
+ transformed = transformed.replace(
1641
+ /export\s+default\s+class\s*\{/,
1642
+ "const __OriginalComponent = class {"
1643
+ );
1644
+ transformed += `
1529
1645
  const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
1530
1646
  export default __WrappedComponent
1531
1647
  `;
1532
- break;
1533
- }
1534
- case "class": {
1535
- if (exportInfo.name) {
1648
+ }
1649
+ break;
1650
+ }
1651
+ case "identifier": {
1536
1652
  transformed = transformed.replace(
1537
- new RegExp(`export\\s+default\\s+class\\s+${exportInfo.name}`),
1538
- `class ${exportInfo.name}`
1653
+ /export\s+default\s+\w+\s*;?\s*$/m,
1654
+ ""
1539
1655
  );
1540
- transformed = wrapperImport + transformed;
1541
1656
  transformed += `
1542
1657
  const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
1543
1658
  export default __WrappedComponent
1544
1659
  `;
1545
- } else {
1660
+ break;
1661
+ }
1662
+ case "named-export": {
1546
1663
  transformed = transformed.replace(
1547
- /export\s+default\s+class\s*\{/,
1548
- "const __OriginalComponent = class {"
1664
+ /export\s*\{\s*\w+\s+as\s+default\s*\}\s*;?/,
1665
+ ""
1549
1666
  );
1550
- transformed = wrapperImport + transformed;
1551
1667
  transformed += `
1552
- const __WrappedComponent = __createWrapper(__OriginalComponent, ${metaObj})
1553
- export default __WrappedComponent
1554
- `;
1555
- }
1556
- break;
1557
- }
1558
- case "identifier": {
1559
- transformed = transformed.replace(
1560
- /export\s+default\s+\w+\s*;?\s*$/m,
1561
- ""
1562
- );
1563
- transformed = wrapperImport + transformed;
1564
- transformed += `
1565
1668
  const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
1566
1669
  export default __WrappedComponent
1567
1670
  `;
1568
- break;
1569
- }
1570
- case "named-export": {
1571
- transformed = transformed.replace(
1572
- /export\s*\{\s*\w+\s+as\s+default\s*\}\s*;?/,
1573
- ""
1574
- );
1575
- transformed = wrapperImport + transformed;
1576
- transformed += `
1577
- const __WrappedComponent = __createWrapper(${exportInfo.name}, ${metaObj})
1578
- export default __WrappedComponent
1579
- `;
1580
- break;
1671
+ break;
1672
+ }
1581
1673
  }
1582
1674
  }
1675
+ transformed = wrapperImport + transformed;
1583
1676
  return {
1584
1677
  code: transformed,
1585
- success: true
1678
+ success: true,
1679
+ wrappedExports: wrappedExports.length > 0 ? wrappedExports : void 0,
1680
+ hasDefaultExport: exportInfo.type !== null
1586
1681
  };
1587
1682
  } catch (error) {
1588
1683
  const message = error instanceof Error ? error.message : String(error);
@@ -1594,6 +1689,87 @@ export default __WrappedComponent
1594
1689
  }
1595
1690
  }
1596
1691
 
1692
+ // src/strip-server-exports.ts
1693
+ import { parseSync as parseSync2 } from "@swc/core";
1694
+ var SERVER_ONLY_EXPORTS = /* @__PURE__ */ new Set(["loader", "config", "generateStaticParams"]);
1695
+ function stripServerExports(code) {
1696
+ const ast = parseSync2(code, {
1697
+ syntax: "typescript",
1698
+ tsx: true,
1699
+ comments: true
1700
+ });
1701
+ const stripped = [];
1702
+ const removals = [];
1703
+ for (const node of ast.body) {
1704
+ if (node.type === "ExportDeclaration") {
1705
+ const decl = node.declaration;
1706
+ if (decl.type === "FunctionDeclaration" && SERVER_ONLY_EXPORTS.has(decl.identifier.value)) {
1707
+ stripped.push(decl.identifier.value);
1708
+ removals.push({ start: node.span.start, end: node.span.end });
1709
+ continue;
1710
+ }
1711
+ if (decl.type === "VariableDeclaration") {
1712
+ const names = decl.declarations.map((d) => d.id.type === "Identifier" ? d.id.value : null).filter(Boolean);
1713
+ const allServerOnly = names.length > 0 && names.every((n) => SERVER_ONLY_EXPORTS.has(n));
1714
+ if (allServerOnly) {
1715
+ stripped.push(...names);
1716
+ removals.push({ start: node.span.start, end: node.span.end });
1717
+ }
1718
+ }
1719
+ }
1720
+ if (node.type === "ExportNamedDeclaration") {
1721
+ const specifiers = node.specifiers;
1722
+ if (specifiers.length === 0) continue;
1723
+ const serverSpecifiers = [];
1724
+ const keptSpecifiers = [];
1725
+ for (const spec of specifiers) {
1726
+ if (spec.type === "ExportSpecifier") {
1727
+ const exportedName = spec.exported?.type === "Identifier" ? spec.exported.value : null;
1728
+ const origName = spec.orig.type === "Identifier" ? spec.orig.value : null;
1729
+ const effectiveName = exportedName ?? origName;
1730
+ if (effectiveName && SERVER_ONLY_EXPORTS.has(effectiveName)) {
1731
+ serverSpecifiers.push(effectiveName);
1732
+ } else {
1733
+ if (origName) {
1734
+ if (exportedName && exportedName !== origName) {
1735
+ keptSpecifiers.push(`${origName} as ${exportedName}`);
1736
+ } else {
1737
+ keptSpecifiers.push(origName);
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ if (serverSpecifiers.length === 0) continue;
1744
+ stripped.push(...serverSpecifiers);
1745
+ if (keptSpecifiers.length === 0) {
1746
+ removals.push({ start: node.span.start, end: node.span.end });
1747
+ } else {
1748
+ const source = node.source ? ` from ${code.slice(node.source.span.start - ast.span.start, node.source.span.end - ast.span.start)}` : "";
1749
+ const replacement = `export { ${keptSpecifiers.join(", ")} }${source}`;
1750
+ removals.push({
1751
+ start: node.span.start,
1752
+ end: node.span.end,
1753
+ replacement
1754
+ });
1755
+ }
1756
+ }
1757
+ }
1758
+ if (removals.length === 0) {
1759
+ return { code, stripped: [] };
1760
+ }
1761
+ const offset = ast.span.start;
1762
+ let result = code;
1763
+ removals.sort((a, b) => b.start - a.start);
1764
+ for (const removal of removals) {
1765
+ const start = removal.start - offset;
1766
+ const end = removal.end - offset;
1767
+ const replacement = removal.replacement ?? "";
1768
+ result = result.slice(0, start) + replacement + result.slice(end);
1769
+ }
1770
+ return { code: result, stripped };
1771
+ }
1772
+
1597
1773
  // src/wrangler-watcher.ts
1598
1774
  import * as fs from "fs";
1599
1775
  import * as path2 from "path";
@@ -2529,7 +2705,7 @@ function cloudwerkPlugin(options = {}) {
2529
2705
  * Transform hook to detect and wrap client components,
2530
2706
  * and rewrite binding imports to use the bindings proxy.
2531
2707
  */
2532
- transform(code, id) {
2708
+ transform(code, id, options2) {
2533
2709
  if (!state) return null;
2534
2710
  if (id.includes("node_modules")) return null;
2535
2711
  if (!id.endsWith(".tsx") && !id.endsWith(".ts")) return null;
@@ -2613,6 +2789,13 @@ function cloudwerkPlugin(options = {}) {
2613
2789
  }
2614
2790
  }
2615
2791
  }
2792
+ if ((isLayout || isPage) && !options2?.ssr) {
2793
+ const stripResult = stripServerExports(transformedCode);
2794
+ if (stripResult.stripped.length > 0) {
2795
+ transformedCode = stripResult.code;
2796
+ wasTransformed = true;
2797
+ }
2798
+ }
2616
2799
  if (hasUseClientDirective(transformedCode)) {
2617
2800
  const componentId = generateComponentId(id, state.options.root);
2618
2801
  const bundlePath = `${state.options.hydrationEndpoint}/${componentId}.js`;
@@ -2638,6 +2821,25 @@ function cloudwerkPlugin(options = {}) {
2638
2821
  map: null
2639
2822
  };
2640
2823
  }
2824
+ if (result.wrappedExports && result.wrappedExports.length > 0) {
2825
+ for (const exportName of result.wrappedExports) {
2826
+ const namedComponentId = `${componentId}__${exportName}`;
2827
+ const namedBundlePath = `${state.options.hydrationEndpoint}/${namedComponentId}.js`;
2828
+ const namedInfo = {
2829
+ componentId: namedComponentId,
2830
+ bundlePath: namedBundlePath,
2831
+ absolutePath: id,
2832
+ exportName
2833
+ };
2834
+ state.clientComponents.set(`${id}::${exportName}`, namedInfo);
2835
+ if (state.options.verbose) {
2836
+ console.log(`[cloudwerk] Detected named client component: ${namedComponentId}`);
2837
+ }
2838
+ }
2839
+ if (!result.hasDefaultExport) {
2840
+ state.clientComponents.delete(id);
2841
+ }
2842
+ }
2641
2843
  return {
2642
2844
  code: result.code,
2643
2845
  map: null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudwerk/vite-plugin",
3
- "version": "0.6.9",
3
+ "version": "0.8.0",
4
4
  "description": "Vite plugin for Cloudwerk file-based routing with virtual entry generation",
5
5
  "repository": {
6
6
  "type": "git",