@ethisyscore/vite-plugin 1.6.2 → 1.7.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.js CHANGED
@@ -318,7 +318,20 @@ var CONTRACT_B_SEMANTIC_PRIMITIVES = [
318
318
  "Drawer",
319
319
  "Modal",
320
320
  "CanvasSurface",
321
- "WebGLSurface"
321
+ "WebGLSurface",
322
+ // Phase 1 additions (per Contract B + PlatformReact plan §Q6, frequency
323
+ // survey of coreconnect-web Timesheets feature). Card / Tabs / Select /
324
+ // Alert close the four most-common gaps in the v1 primitive set:
325
+ // - Card: universal layout container, ~20 imports across surveyed pages.
326
+ // - Tabs: settings / configuration surfaces (6 tabs in TimesheetSettings).
327
+ // - Select: filter + form input, ~10 imports; Form's text input doesn't
328
+ // cover dropdowns.
329
+ // - Alert: lock/unlock warnings, validation banners, non-modal system
330
+ // messages; Card doesn't carry severity semantics.
331
+ "Card",
332
+ "Tabs",
333
+ "Select",
334
+ "Alert"
322
335
  ];
323
336
  var CONTRACT_B_RUNTIME_IMPORTS = [
324
337
  "react",
@@ -577,9 +590,313 @@ function ethisysContractBPlugin(options = {}) {
577
590
  }
578
591
  };
579
592
  }
593
+ var ID_REGEX = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
594
+ function assertHostOriginRelativePath2(value, label) {
595
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
596
+ throw new Error(
597
+ `[ethisys-platform-react] ${label} "${value}" must be a host-origin relative path, not a remote URL.`
598
+ );
599
+ }
600
+ if (/^(data|blob|javascript):/i.test(value)) {
601
+ throw new Error(
602
+ `[ethisys-platform-react] ${label} "${value}" must be a host-origin relative path, not a ${value.split(":")[0]}: URL.`
603
+ );
604
+ }
605
+ const normalized = normalize(value);
606
+ if (isAbsolute(value) || isAbsolute(normalized)) {
607
+ throw new Error(
608
+ `[ethisys-platform-react] ${label} "${value}" must be relative, not absolute.`
609
+ );
610
+ }
611
+ if (normalized.split(/[\\/]/).includes("..")) {
612
+ throw new Error(
613
+ `[ethisys-platform-react] ${label} "${value}" must not contain path traversal.`
614
+ );
615
+ }
616
+ }
617
+ function slash2(p) {
618
+ return sep === "\\" ? p.replace(/\\/g, "/") : p;
619
+ }
620
+ function ethisysPlatformReactPlugin(options = {}) {
621
+ const explicitRoot = options.root;
622
+ const manifestRel = options.manifestPath ?? "feature.manifest.json";
623
+ const outputPrefix = (options.outputPrefix ?? "platform-react/").replace(/\/+$/, "/").replace(/^\/+/, "");
624
+ let resolvedRoot;
625
+ let manifestAbsPath;
626
+ let pages = [];
627
+ function resolveRoot() {
628
+ if (resolvedRoot) {
629
+ return resolvedRoot;
630
+ }
631
+ resolvedRoot = explicitRoot ?? process.cwd();
632
+ manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
633
+ return resolvedRoot;
634
+ }
635
+ function readManifest() {
636
+ resolveRoot();
637
+ if (!existsSync(manifestAbsPath)) {
638
+ return null;
639
+ }
640
+ const raw = readFileSync(manifestAbsPath, "utf-8");
641
+ try {
642
+ return JSON.parse(raw);
643
+ } catch (e) {
644
+ throw new Error(
645
+ `[ethisys-platform-react] Failed to parse manifest at "${manifestAbsPath}": ${e.message}`
646
+ );
647
+ }
648
+ }
649
+ function validate() {
650
+ pages = [];
651
+ const manifest = readManifest();
652
+ if (manifest === null) {
653
+ return;
654
+ }
655
+ const declared = manifest.ui?.platformReactPages;
656
+ if (!Array.isArray(declared) || declared.length === 0) {
657
+ return;
658
+ }
659
+ const seenIds = /* @__PURE__ */ new Set();
660
+ for (let i = 0; i < declared.length; i++) {
661
+ const decl = declared[i];
662
+ const where = `ui.platformReactPages[${i}]`;
663
+ if (typeof decl?.id !== "string" || decl.id.length === 0) {
664
+ throw new Error(
665
+ `[ethisys-platform-react] ${where}.id is required (non-empty string).`
666
+ );
667
+ }
668
+ if (!ID_REGEX.test(decl.id)) {
669
+ throw new Error(
670
+ `[ethisys-platform-react] ${where}.id "${decl.id}" must match [a-z0-9_-]+ (URL-safe; case-insensitive).`
671
+ );
672
+ }
673
+ if (seenIds.has(decl.id)) {
674
+ throw new Error(
675
+ `[ethisys-platform-react] Duplicate page id "${decl.id}".`
676
+ );
677
+ }
678
+ seenIds.add(decl.id);
679
+ if (typeof decl.moduleSpecifier !== "string" || decl.moduleSpecifier.length === 0) {
680
+ throw new Error(
681
+ `[ethisys-platform-react] ${where}.moduleSpecifier is required (non-empty relative path).`
682
+ );
683
+ }
684
+ assertHostOriginRelativePath2(decl.moduleSpecifier, `${where}.moduleSpecifier`);
685
+ const absPath = resolve(resolvedRoot, decl.moduleSpecifier);
686
+ if (!existsSync(absPath)) {
687
+ throw new Error(
688
+ `[ethisys-platform-react] ${where}.moduleSpecifier "${decl.moduleSpecifier}" does not exist on disk (resolved: ${absPath}).`
689
+ );
690
+ }
691
+ const exportName = typeof decl.exportName === "string" && decl.exportName.length > 0 ? decl.exportName : "default";
692
+ pages.push({
693
+ id: decl.id,
694
+ exportName,
695
+ title: typeof decl.title === "string" && decl.title.length > 0 ? decl.title : null,
696
+ moduleSpecifierRel: decl.moduleSpecifier,
697
+ moduleSpecifierAbs: absPath
698
+ });
699
+ }
700
+ }
701
+ let isBuild = false;
702
+ return {
703
+ name: "ethisys-platform-react",
704
+ // `pre` ordering: validation should fire before user-supplied plugins.
705
+ enforce: "pre",
706
+ /**
707
+ * Force ESM single-module output per page. Output options don't depend on
708
+ * the resolved root, so they're safe to declare here — `config()` runs
709
+ * before `configResolved()`. The Rollup input table is added later in
710
+ * `configResolved()` once Vite has populated `resolvedRoot` correctly.
711
+ */
712
+ config(_config, { command }) {
713
+ if (command !== "build") {
714
+ return;
715
+ }
716
+ return {
717
+ build: {
718
+ rollupOptions: {
719
+ // CRITICAL: single-file ESM output per page. The host loads each
720
+ // page via `import(url)` at mount time and reads the declared
721
+ // `exportName` from the resulting module namespace. Chunk-split
722
+ // output would break that contract because the host has no
723
+ // import-map surface to resolve sibling chunks.
724
+ output: {
725
+ format: "es",
726
+ inlineDynamicImports: true,
727
+ entryFileNames: `${outputPrefix}[name].js`,
728
+ chunkFileNames: `${outputPrefix}[name]-[hash].js`,
729
+ assetFileNames: `${outputPrefix}[name][extname]`
730
+ }
731
+ }
732
+ }
733
+ };
734
+ },
735
+ /**
736
+ * Resolve the actual root Vite is using (which may differ from
737
+ * `process.cwd()`), validate the manifest against it, and mutate the
738
+ * resolved config's Rollup input table so each declared page becomes a
739
+ * build entry. Mutation in `configResolved` is the standard Vite-plugin
740
+ * pattern for input contributions that depend on the resolved root —
741
+ * `config()` runs too early to know the real value.
742
+ */
743
+ configResolved(config) {
744
+ isBuild = config.command === "build";
745
+ if (!explicitRoot) {
746
+ resolvedRoot = config.root;
747
+ manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
748
+ }
749
+ if (!isBuild) {
750
+ return;
751
+ }
752
+ validate();
753
+ if (pages.length === 0) {
754
+ return;
755
+ }
756
+ const input = {};
757
+ for (const page of pages) {
758
+ input[page.id] = slash2(page.moduleSpecifierAbs);
759
+ }
760
+ const writable = config;
761
+ writable.build ??= {};
762
+ writable.build.rollupOptions ??= {};
763
+ const rollupOptions = writable.build.rollupOptions;
764
+ const existing = rollupOptions.input;
765
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
766
+ rollupOptions.input = { ...existing, ...input };
767
+ } else if (typeof existing === "string") {
768
+ rollupOptions.input = { _entry: existing, ...input };
769
+ } else if (Array.isArray(existing)) {
770
+ const indexed = {};
771
+ existing.forEach((entry, idx) => {
772
+ indexed[`_entry${idx}`] = entry;
773
+ });
774
+ rollupOptions.input = { ...indexed, ...input };
775
+ } else {
776
+ rollupOptions.input = input;
777
+ }
778
+ },
779
+ buildStart() {
780
+ validate();
781
+ },
782
+ /**
783
+ * Emit the companion `platform-react-pages.json` listing every declared
784
+ * page. The `.ccpkg` packaging step (and the host's runtime loader) read
785
+ * this file to discover which page modules ship in the bundle without
786
+ * having to re-parse the manifest.
787
+ */
788
+ generateBundle() {
789
+ if (pages.length === 0) {
790
+ return;
791
+ }
792
+ const summary = {
793
+ pages: pages.map((p) => ({
794
+ id: p.id,
795
+ exportName: p.exportName,
796
+ title: p.title,
797
+ bundlePath: `${outputPrefix}${p.id}.js`,
798
+ source: p.moduleSpecifierRel
799
+ }))
800
+ };
801
+ this.emitFile({
802
+ type: "asset",
803
+ fileName: `${outputPrefix}platform-react-pages.json`,
804
+ source: JSON.stringify(summary, null, 2)
805
+ });
806
+ }
807
+ };
808
+ }
809
+ var ID_REGEX2 = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
810
+ function assertHostOriginRelativePath3(value, label) {
811
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
812
+ throw new Error(
813
+ `[ethisys-platform-react] ${label} "${value}" must be a host-origin relative path, not a remote URL.`
814
+ );
815
+ }
816
+ if (/^(data|blob|javascript):/i.test(value)) {
817
+ throw new Error(
818
+ `[ethisys-platform-react] ${label} "${value}" must be a host-origin relative path, not a ${value.split(":")[0]}: URL.`
819
+ );
820
+ }
821
+ const normalized = normalize(value);
822
+ if (isAbsolute(value) || isAbsolute(normalized)) {
823
+ throw new Error(
824
+ `[ethisys-platform-react] ${label} "${value}" must be relative, not absolute.`
825
+ );
826
+ }
827
+ if (normalized.split(/[\\/]/).includes("..")) {
828
+ throw new Error(
829
+ `[ethisys-platform-react] ${label} "${value}" must not contain path traversal.`
830
+ );
831
+ }
832
+ }
833
+ function parsePlatformReactPages(manifestPath, options = {}) {
834
+ const root = options.root ?? process.cwd();
835
+ const verifyOnDisk = options.verifyOnDisk ?? false;
836
+ const manifestAbsPath = isAbsolute(manifestPath) ? manifestPath : resolve(root, manifestPath);
837
+ if (!existsSync(manifestAbsPath)) {
838
+ return [];
839
+ }
840
+ let manifest;
841
+ try {
842
+ const raw = readFileSync(manifestAbsPath, "utf-8");
843
+ manifest = JSON.parse(raw);
844
+ } catch (e) {
845
+ throw new Error(
846
+ `[ethisys-platform-react] Failed to parse manifest at "${manifestAbsPath}": ${e.message}`
847
+ );
848
+ }
849
+ const declared = manifest.ui?.platformReactPages;
850
+ if (!Array.isArray(declared) || declared.length === 0) {
851
+ return [];
852
+ }
853
+ const result = [];
854
+ const seenIds = /* @__PURE__ */ new Set();
855
+ for (let i = 0; i < declared.length; i++) {
856
+ const decl = declared[i];
857
+ const where = `ui.platformReactPages[${i}]`;
858
+ if (typeof decl?.id !== "string" || decl.id.length === 0) {
859
+ throw new Error(
860
+ `[ethisys-platform-react] ${where}.id is required (non-empty string).`
861
+ );
862
+ }
863
+ if (!ID_REGEX2.test(decl.id)) {
864
+ throw new Error(
865
+ `[ethisys-platform-react] ${where}.id "${decl.id}" must match [a-z0-9_-]+ (URL-safe; case-insensitive).`
866
+ );
867
+ }
868
+ if (seenIds.has(decl.id)) {
869
+ throw new Error(
870
+ `[ethisys-platform-react] Duplicate page id "${decl.id}".`
871
+ );
872
+ }
873
+ seenIds.add(decl.id);
874
+ if (typeof decl.moduleSpecifier !== "string" || decl.moduleSpecifier.length === 0) {
875
+ throw new Error(
876
+ `[ethisys-platform-react] ${where}.moduleSpecifier is required (non-empty relative path).`
877
+ );
878
+ }
879
+ assertHostOriginRelativePath3(decl.moduleSpecifier, `${where}.moduleSpecifier`);
880
+ if (verifyOnDisk) {
881
+ const absPath = resolve(root, decl.moduleSpecifier);
882
+ if (!existsSync(absPath)) {
883
+ throw new Error(
884
+ `[ethisys-platform-react] ${where}.moduleSpecifier "${decl.moduleSpecifier}" does not exist on disk (resolved: ${absPath}).`
885
+ );
886
+ }
887
+ }
888
+ result.push({
889
+ id: decl.id,
890
+ moduleSpecifier: decl.moduleSpecifier,
891
+ exportName: typeof decl.exportName === "string" && decl.exportName.length > 0 ? decl.exportName : "default",
892
+ title: typeof decl.title === "string" && decl.title.length > 0 ? decl.title : null
893
+ });
894
+ }
895
+ return result;
896
+ }
580
897
 
581
898
  // src/index.ts
582
- function slash2(p) {
899
+ function slash3(p) {
583
900
  return sep === "\\" ? p.replace(/\\/g, "/") : p;
584
901
  }
585
902
  function escapeHtml(str) {
@@ -704,7 +1021,7 @@ function ethisysManifestPlugin(options = {}) {
704
1021
  }
705
1022
  seen.add(entry.entrypoint);
706
1023
  const name = entry.entrypoint.replace(/\.html$/, "");
707
- input[name] = slash2(resolve(rootDir, entry.entrypoint));
1024
+ input[name] = slash3(resolve(rootDir, entry.entrypoint));
708
1025
  }
709
1026
  return {
710
1027
  build: {
@@ -769,7 +1086,7 @@ function ethisysManifestPlugin(options = {}) {
769
1086
  // Entire handler is wrapped in try/catch so JSON syntax errors in the
770
1087
  // manifest don't crash the dev server (common during active editing).
771
1088
  handleHotUpdate({ file, server }) {
772
- if (slash2(file) === slash2(manifestAbsPath)) {
1089
+ if (slash3(file) === slash3(manifestAbsPath)) {
773
1090
  try {
774
1091
  entries = readManifest();
775
1092
  validateEntries();
@@ -787,9 +1104,9 @@ function ethisysManifestPlugin(options = {}) {
787
1104
  },
788
1105
  // Build: resolve virtual HTML module IDs (no physical files exist on disk)
789
1106
  resolveId(id) {
790
- const normalizedId = slash2(id);
1107
+ const normalizedId = slash3(id);
791
1108
  const match = entries.find(
792
- (e) => slash2(resolve(rootDir, e.entrypoint)) === normalizedId
1109
+ (e) => slash3(resolve(rootDir, e.entrypoint)) === normalizedId
793
1110
  );
794
1111
  if (match) {
795
1112
  return id;
@@ -797,9 +1114,9 @@ function ethisysManifestPlugin(options = {}) {
797
1114
  },
798
1115
  // Build: provide virtual HTML content for Rollup
799
1116
  load(id) {
800
- const normalizedId = slash2(id);
1117
+ const normalizedId = slash3(id);
801
1118
  const match = entries.find(
802
- (e) => slash2(resolve(rootDir, e.entrypoint)) === normalizedId
1119
+ (e) => slash3(resolve(rootDir, e.entrypoint)) === normalizedId
803
1120
  );
804
1121
  if (match) {
805
1122
  return generateHtml(match);
@@ -808,6 +1125,6 @@ function ethisysManifestPlugin(options = {}) {
808
1125
  };
809
1126
  }
810
1127
 
811
- export { CONTRACT_B_IMPORT_MAP_ALLOWLIST, CONTRACT_B_RUNTIME_IMPORTS, CONTRACT_B_SEMANTIC_PRIMITIVES, ethisysContractAPlugin, ethisysContractBPlugin, ethisysManifestPlugin, validateDeclarativeResource, validateReactiveRule };
1128
+ export { CONTRACT_B_IMPORT_MAP_ALLOWLIST, CONTRACT_B_RUNTIME_IMPORTS, CONTRACT_B_SEMANTIC_PRIMITIVES, ethisysContractAPlugin, ethisysContractBPlugin, ethisysManifestPlugin, ethisysPlatformReactPlugin, parsePlatformReactPages, validateDeclarativeResource, validateReactiveRule };
812
1129
  //# sourceMappingURL=index.js.map
813
1130
  //# sourceMappingURL=index.js.map