@ethisyscore/vite-plugin 1.85.0 → 1.87.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/README.md CHANGED
@@ -24,6 +24,7 @@ match your render mode into `vite.config.ts`.
24
24
  | `ethisysPlatformReactPlugin` | **PlatformReact** pass — builds one ESM bundle per declared page |
25
25
  | `buildPlatformReactPages`, `rewriteAliasedExternalImports`, `virtualPageEntry` | Multi-bundle orchestrator: one isolated Vite sub-build per page, with host-externals dedupe (`PLATFORM_REACT_EXTERNALS`, `PLATFORM_REACT_DEDUPE`) and host-loader import rewriting |
26
26
  | `generatePlatformReactManifest`, `computePlatformReactPages` | Build-step codegen — derive `feature.manifest.json` + the `extension.manifest.json` overlay from `routeMeta.json` |
27
+ | `parseExportedNames` | The runtime-named exports of one module's source text — how the codegen resolves a view whose exported name differs from its filename |
27
28
  | `parsePlatformReactPages` | Test helper for snapshot-testing PlatformReact page declarations |
28
29
  | `ethisysIframeSandboxPlugin`, `buildIframeSandboxPages` | **Tier U** iframe-sandbox pass — cross-origin sandboxed pages with SHA-256 digest emission |
29
30
  | `ethisysManifestPlugin` | Legacy manifest-driven pass — auto-generates `index.html` for pages/surfaces that declare a `source` field |
@@ -44,6 +45,43 @@ Most plugins never call these directly — `cc package` drives the correct pass
44
45
  for the plugin's declared render mode. Reach for the factories when you maintain
45
46
  a bespoke build or need the validators/codegen in tests.
46
47
 
48
+ ### How `generatePlatformReactManifest` resolves a `useView` name
49
+
50
+ Each page's `useView("<module>", "<Name>")` call becomes one entry of that page's
51
+ `views` map, and `build-pages` wires it with a **named** import
52
+ (`import { <Name> } from "<adapter module>"`). `<Name>` is resolved against the
53
+ adapter tree in this order:
54
+
55
+ 1. **By filename** — an adapter file whose basename is `<Name>` (e.g.
56
+ `CompanyCostsView` → `src/adapter/finance/CompanyCostsView.tsx`). This is the
57
+ original and still the winning rule, so a plugin that follows the filename
58
+ convention resolves exactly as it always has.
59
+ 2. **By exported name** — otherwise, the single adapter file that *exports*
60
+ `<Name>`. An adapter file may legitimately export its component under a name
61
+ other than its filename:
62
+
63
+ ```tsx
64
+ // src/adapter/chat/ChatChannelCreateFormView.tsx
65
+ export function ChatChannelCreateFormView(/* … */) { /* … */ }
66
+ export { ChatChannelCreateFormView as ChatChannelCreateForm };
67
+ ```
68
+
69
+ `useView("chat", "ChatChannelCreateForm")` is correct at runtime, and now
70
+ generates. `export function|const|let|var|class X`, `export { X }`,
71
+ `export { X as Y }` and `export { X } from "…"` all count.
72
+
73
+ `export default …` deliberately does **not** — a default export cannot satisfy
74
+ a named import — and neither do type-only exports (`export type` /
75
+ `export interface`), which are erased at runtime. `export * from "…"` is out of
76
+ reach of the scan, so a view reachable only that way still reports as
77
+ unresolvable rather than being mis-resolved.
78
+ 3. **Two files export the same name** → the generator fails, naming every
79
+ candidate. Picking one would compile and then render the wrong component. Give
80
+ the intended file that basename (rule 1 always wins) or rename the view.
81
+ 4. **Nothing matches** → the generator fails (unless `strict: false`). This error
82
+ is the point of the pass: `useView` is a manifest lookup, not an import, so an
83
+ unsatisfiable call renders "View not available" at runtime behind a green build.
84
+
47
85
  ## Build and test
48
86
 
49
87
  ```bash
package/dist/index.cjs CHANGED
@@ -1639,6 +1639,31 @@ function parsePlatformReactPages(manifestPath, options = {}) {
1639
1639
  return result;
1640
1640
  }
1641
1641
  var DEFAULT_SCHEMA = "https://ethisys.dev/schemas/feature-manifest.json";
1642
+ var EXPORT_DECL_RE = /^[ \t]*export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm;
1643
+ var EXPORT_LIST_RE = /^[ \t]*export\s*\{([^}]*)\}/gm;
1644
+ var EXPORT_SPECIFIER_RE = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/;
1645
+ function parseExportedNames(content) {
1646
+ const names = /* @__PURE__ */ new Set();
1647
+ EXPORT_DECL_RE.lastIndex = 0;
1648
+ let m;
1649
+ while ((m = EXPORT_DECL_RE.exec(content)) !== null) {
1650
+ names.add(m[1]);
1651
+ }
1652
+ EXPORT_LIST_RE.lastIndex = 0;
1653
+ while ((m = EXPORT_LIST_RE.exec(content)) !== null) {
1654
+ for (const raw of m[1].split(",")) {
1655
+ const item = raw.trim();
1656
+ if (item.length === 0) continue;
1657
+ if (/^type\s/.test(item)) continue;
1658
+ const spec = EXPORT_SPECIFIER_RE.exec(item);
1659
+ if (spec === null) continue;
1660
+ const exported = spec[2] ?? spec[1];
1661
+ if (exported === "default") continue;
1662
+ names.add(exported);
1663
+ }
1664
+ }
1665
+ return [...names];
1666
+ }
1642
1667
  function computePlatformReactPages(input) {
1643
1668
  const routePrefix = `/extensions/${input.slug}/`;
1644
1669
  const routeRoot = `/extensions/${input.slug}`;
@@ -1655,6 +1680,7 @@ function computePlatformReactPages(input) {
1655
1680
  const entries = [];
1656
1681
  const missingViews = [];
1657
1682
  const unregisteredViews = [];
1683
+ const ambiguousViews = [];
1658
1684
  for (const { file, content, moduleSpecifier } of input.pages) {
1659
1685
  const name = path.basename(file, ".tsx");
1660
1686
  const override = input.pageOverrides?.[file] ?? {};
@@ -1671,6 +1697,15 @@ function computePlatformReactPages(input) {
1671
1697
  }
1672
1698
  if (input.viewFiles[v] !== void 0) {
1673
1699
  views[v] = input.viewFiles[v];
1700
+ continue;
1701
+ }
1702
+ const byExport = input.viewExports?.[v] ?? [];
1703
+ if (byExport.length === 1) {
1704
+ views[v] = byExport[0];
1705
+ } else if (byExport.length > 1) {
1706
+ ambiguousViews.push(
1707
+ `${file}: view "${v}" matches no adapter filename and is exported by ${byExport.length} adapter files, so the binding is ambiguous: ${byExport.join(", ")}. Rename the view, or give the intended file that basename (a filename match always wins)`
1708
+ );
1674
1709
  } else {
1675
1710
  missingViews.push(`${file}: view "${v}" has no adapter file`);
1676
1711
  }
@@ -1686,7 +1721,7 @@ function computePlatformReactPages(input) {
1686
1721
  views
1687
1722
  });
1688
1723
  }
1689
- return { entries, missingViews, unregisteredViews };
1724
+ return { entries, missingViews, unregisteredViews, ambiguousViews };
1690
1725
  }
1691
1726
  function walk2(dir) {
1692
1727
  const out = [];
@@ -1713,13 +1748,26 @@ function generatePlatformReactManifest(config) {
1713
1748
  const pageDirs = (Array.isArray(pagesCfg) ? pagesCfg : [pagesCfg]).map((p) => path.resolve(root, p));
1714
1749
  const exclude = new Set(config.paths?.pageExclude ?? []);
1715
1750
  const viewFiles = {};
1751
+ const viewExportsByName = /* @__PURE__ */ new Map();
1716
1752
  if (fs.existsSync(adapterDir)) {
1717
1753
  for (const f of walk2(adapterDir)) {
1718
1754
  if (f.endsWith(".tsx")) {
1719
- viewFiles[path.basename(f, ".tsx")] = slash2(path.relative(root, f));
1755
+ const rel = slash2(path.relative(root, f));
1756
+ viewFiles[path.basename(f, ".tsx")] = rel;
1757
+ for (const name of parseExportedNames(fs.readFileSync(f, "utf8"))) {
1758
+ let paths = viewExportsByName.get(name);
1759
+ if (paths === void 0) {
1760
+ paths = /* @__PURE__ */ new Set();
1761
+ viewExportsByName.set(name, paths);
1762
+ }
1763
+ paths.add(rel);
1764
+ }
1720
1765
  }
1721
1766
  }
1722
1767
  }
1768
+ const viewExports = Object.fromEntries(
1769
+ [...viewExportsByName].map(([name, paths]) => [name, [...paths].sort()])
1770
+ );
1723
1771
  const seen = /* @__PURE__ */ new Map();
1724
1772
  const pages = [];
1725
1773
  for (const dir of pageDirs) {
@@ -1743,13 +1791,14 @@ function generatePlatformReactManifest(config) {
1743
1791
  });
1744
1792
  }
1745
1793
  }
1746
- const { entries, missingViews, unregisteredViews } = computePlatformReactPages({
1794
+ const { entries, missingViews, unregisteredViews, ambiguousViews } = computePlatformReactPages({
1747
1795
  module,
1748
1796
  moduleKeys: config.moduleKeys,
1749
1797
  slug,
1750
1798
  routeMeta,
1751
1799
  pages,
1752
1800
  viewFiles,
1801
+ viewExports,
1753
1802
  pageOverrides: config.pageOverrides
1754
1803
  });
1755
1804
  const schema = config.manifest.schema ?? DEFAULT_SCHEMA;
@@ -1795,13 +1844,15 @@ function generatePlatformReactManifest(config) {
1795
1844
  pagesWithoutView,
1796
1845
  missingViews,
1797
1846
  unregisteredViews,
1847
+ ambiguousViews,
1798
1848
  featureManifestPath,
1799
1849
  overlayPath
1800
1850
  };
1801
- if ((config.strict ?? true) && (missingViews.length > 0 || unregisteredViews.length > 0)) {
1851
+ const diagnostics = [...unregisteredViews, ...ambiguousViews, ...missingViews];
1852
+ if ((config.strict ?? true) && diagnostics.length > 0) {
1802
1853
  throw new Error(
1803
- `generate-manifest: ${missingViews.length + unregisteredViews.length} unresolvable useView call(s). Each renders "View not available" at runtime behind a green build:
1804
- ` + [...unregisteredViews, ...missingViews].map((d) => ` - ${d}`).join("\n")
1854
+ `generate-manifest: ${diagnostics.length} unresolvable useView call(s). Each renders "View not available" at runtime behind a green build:
1855
+ ` + diagnostics.map((d) => ` - ${d}`).join("\n")
1805
1856
  );
1806
1857
  }
1807
1858
  return result;
@@ -2166,6 +2217,7 @@ exports.findUnscopedSelectors = findUnscopedSelectors;
2166
2217
  exports.formatUnscopedSelectorError = formatUnscopedSelectorError;
2167
2218
  exports.generatePlatformReactManifest = generatePlatformReactManifest;
2168
2219
  exports.isScopableCssId = isScopableCssId;
2220
+ exports.parseExportedNames = parseExportedNames;
2169
2221
  exports.parsePlatformReactPages = parsePlatformReactPages;
2170
2222
  exports.rewriteAliasedExternalImports = rewriteAliasedExternalImports;
2171
2223
  exports.scopePluginCss = scopePluginCss;