@ethisyscore/vite-plugin 1.6.3 → 1.7.1

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