@rebasepro/core 0.0.1-canary.f81da60 → 0.1.2

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.
@@ -0,0 +1,157 @@
1
+ import React, { lazy, useMemo } from "react";
2
+ import type { ComponentRef, LazyComponentRef } from "@rebasepro/types";
3
+ import { isLazyComponentRef } from "@rebasepro/types";
4
+
5
+ /**
6
+ * Internal cache for resolved lazy components.
7
+ *
8
+ * `React.lazy()` must return the SAME wrapper across renders — if we call
9
+ * `lazy(loader)` on every render, React will treat each result as a new
10
+ * component type and unmount/remount on each render cycle.
11
+ *
12
+ * This WeakMap keeps a stable mapping from a `ComponentRef` (object/function)
13
+ * to the `React.lazy()` wrapper it produced. Strings are keyed by a separate
14
+ * plain Map since they can't be WeakMap keys.
15
+ */
16
+ const lazyCache = new WeakMap<object | Function, React.ComponentType<any>>();
17
+
18
+ /**
19
+ * Resolves a `ComponentRef` into a renderable `React.ComponentType`.
20
+ *
21
+ * This hook handles all three forms of `ComponentRef`:
22
+ *
23
+ * 1. **`LazyComponentRef`** (produced by the Vite plugin from string paths):
24
+ * Wraps the lazy loader with `React.lazy()` for automatic code-splitting.
25
+ *
26
+ * 2. **`() => Promise<{ default: ComponentType }>`** (manual lazy import):
27
+ * Also wraps with `React.lazy()`. Distinguished from regular components
28
+ * by checking that the function has no parameters and is not a known
29
+ * React internal (no `$$typeof`).
30
+ *
31
+ * 3. **Direct `React.ComponentType`**:
32
+ * Returned as-is.
33
+ *
34
+ * If the ref is `undefined` or a raw string (which should never happen at
35
+ * runtime in the browser since the Vite plugin transforms strings), returns `undefined`.
36
+ *
37
+ * The returned component is stable across re-renders as long as the `ref`
38
+ * is referentially stable.
39
+ *
40
+ * **Usage:** Wrap the rendered component in `<Suspense>` to handle the
41
+ * loading state of lazy components.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * const ResolvedField = useResolvedComponent(property.ui?.Field);
46
+ * if (!ResolvedField) return null;
47
+ * return (
48
+ * <Suspense fallback={<CircularProgress />}>
49
+ * <ResolvedField {...fieldProps} />
50
+ * </Suspense>
51
+ * );
52
+ * ```
53
+ */
54
+ export function useResolvedComponent<P = unknown>(
55
+ ref: ComponentRef<P> | undefined
56
+ ): React.ComponentType<P> | undefined {
57
+ return useMemo(() => resolveComponentRef(ref), [ref]);
58
+ }
59
+
60
+ /**
61
+ * Wraps a loader function with `React.lazy()`, caching the result so the
62
+ * same loader always returns the same lazy component identity.
63
+ */
64
+ function getOrCreateLazy<P>(
65
+ key: object | Function,
66
+ loader: () => Promise<{ default: React.ComponentType<P> }>
67
+ ): React.ComponentType<P> {
68
+ const cached = lazyCache.get(key);
69
+ if (cached) return cached as React.ComponentType<P>;
70
+
71
+ const LazyComponent = lazy(loader) as unknown as React.ComponentType<P>;
72
+ lazyCache.set(key, LazyComponent);
73
+ return LazyComponent;
74
+ }
75
+
76
+ /**
77
+ * Pure function version of the resolver, for use outside React components.
78
+ * Same resolution logic as `useResolvedComponent`.
79
+ *
80
+ * Results are cached per reference identity — calling this multiple times
81
+ * with the same `ref` object returns the same `React.ComponentType`.
82
+ */
83
+ export function resolveComponentRef<P = unknown>(
84
+ ref: ComponentRef<P> | undefined
85
+ ): React.ComponentType<P> | undefined {
86
+ if (ref == null) return undefined;
87
+
88
+ // 1. String — should not happen at runtime (Vite transforms them).
89
+ // Log a warning and bail out.
90
+ if (typeof ref === "string") {
91
+ console.warn(
92
+ `[Rebase] Encountered a raw string ComponentRef ("${ref}") at runtime. ` +
93
+ "This usually means the Vite transform plugin did not process this file. " +
94
+ "Ensure the file is inside the configured collectionsDir."
95
+ );
96
+ return undefined;
97
+ }
98
+
99
+ // 2. LazyComponentRef — produced by the Vite plugin from string paths.
100
+ // The object has { __rebaseLazy: true, load: () => import(...) }.
101
+ if (isLazyComponentRef(ref)) {
102
+ return getOrCreateLazy<P>(
103
+ ref as unknown as object,
104
+ () => (ref as LazyComponentRef<P>).load()
105
+ );
106
+ }
107
+
108
+ // 3. Function — either a React component or a lazy import loader.
109
+ if (typeof ref === "function") {
110
+ const fn = ref as Function;
111
+
112
+ // Class components (React.Component / PureComponent) have this flag
113
+ if (fn.prototype?.isReactComponent) {
114
+ return ref as React.ComponentType<P>;
115
+ }
116
+
117
+ // React internals (forwardRef, memo, etc.) carry $$typeof
118
+ if ("$$typeof" in fn) {
119
+ return ref as React.ComponentType<P>;
120
+ }
121
+
122
+ // A function with declared parameters (fn.length > 0) is a regular
123
+ // component that accepts props — return as-is.
124
+ if (fn.length > 0) {
125
+ return ref as React.ComponentType<P>;
126
+ }
127
+
128
+ // Zero-parameter function — could be a React component with no props
129
+ // OR a lazy loader `() => import(...)`. We distinguish by checking
130
+ // if `fn.name` looks like a component (starts with uppercase) or is
131
+ // an anonymous arrow.
132
+ //
133
+ // Convention: named function components always start with an
134
+ // uppercase letter. Dynamic import wrappers are typically anonymous
135
+ // arrows or have lowercase names.
136
+ const name = fn.name;
137
+ if (name && /^[A-Z]/.test(name)) {
138
+ // Named component (e.g. `function MyField() { ... }`) — return as-is
139
+ return ref as React.ComponentType<P>;
140
+ }
141
+
142
+ // Treat as a lazy loader — wrap with React.lazy, cached by identity.
143
+ return getOrCreateLazy<P>(
144
+ fn,
145
+ ref as () => Promise<{ default: React.ComponentType<P> }>
146
+ );
147
+ }
148
+
149
+ // 4. React wrapper objects (React.forwardRef, React.memo) —
150
+ // These are objects with $$typeof but are not functions.
151
+ if (typeof ref === "object" && "$$typeof" in (ref as object)) {
152
+ return ref as unknown as React.ComponentType<P>;
153
+ }
154
+
155
+ // 5. Unknown shape — return undefined to be safe
156
+ return undefined;
157
+ }
package/src/locales/de.ts CHANGED
@@ -44,6 +44,7 @@ export const de: RebaseTranslations = {
44
44
  all_entries_loaded: "Alle {{count}} Einträge geladen",
45
45
  create_your_first_entry: "Erstellen Sie Ihren ersten Eintrag",
46
46
  no_results_filter_sort: "Keine Ergebnisse mit angewendetem Filter/Sortierung",
47
+ no_results_search: "Keine Ergebnisse gefunden für \"{{search}}\"",
47
48
  add: "Hinzufügen",
48
49
  remove: "Entfernen",
49
50
  multiple_entities: "Mehrere Entitäten",
package/src/locales/en.ts CHANGED
@@ -52,6 +52,7 @@ export const en: RebaseTranslations = {
52
52
  all_entries_loaded: "All {{count}} entries loaded",
53
53
  create_your_first_entry: "Create your first entry",
54
54
  no_results_filter_sort: "No results with the applied filter/sort",
55
+ no_results_search: "No results found for \"{{search}}\"",
55
56
  add: "Add",
56
57
  remove: "Remove",
57
58
  multiple_entities: "Multiple entities",
package/src/locales/es.ts CHANGED
@@ -52,6 +52,7 @@ export const es: RebaseTranslations = {
52
52
  all_entries_loaded: "Todas las {{count}} entradas cargadas",
53
53
  create_your_first_entry: "Crea tu primera entrada",
54
54
  no_results_filter_sort: "No hay resultados con el filtro/orden aplicado",
55
+ no_results_search: "No se encontraron resultados para \"{{search}}\"",
55
56
  add: "Añadir",
56
57
  remove: "Quitar",
57
58
  multiple_entities: "Múltiples entidades",
package/src/locales/fr.ts CHANGED
@@ -44,6 +44,7 @@ export const fr: RebaseTranslations = {
44
44
  all_entries_loaded: "Toutes les {{count}} entrées chargées",
45
45
  create_your_first_entry: "Créez votre première entrée",
46
46
  no_results_filter_sort: "Aucun résultat avec le filtre/tri appliqué",
47
+ no_results_search: "Aucun résultat trouvé pour \"{{search}}\"",
47
48
  add: "Ajouter",
48
49
  remove: "Supprimer",
49
50
  multiple_entities: "Entités multiples",
package/src/locales/hi.ts CHANGED
@@ -44,6 +44,7 @@ export const hi: RebaseTranslations = {
44
44
  all_entries_loaded: "सभी {{count}} प्रविष्टियाँ लोड हो गईं",
45
45
  create_your_first_entry: "अपनी पहली प्रविष्टि बनाएं",
46
46
  no_results_filter_sort: "लागू किए गए फ़िल्टर/सॉर्ट के साथ कोई परिणाम नहीं",
47
+ no_results_search: "\"{{search}}\" के लिए कोई परिणाम नहीं मिला",
47
48
  add: "जोड़ें",
48
49
  remove: "हटाएं",
49
50
  multiple_entities: "एकाधिक इकाइयां",
package/src/locales/it.ts CHANGED
@@ -44,6 +44,7 @@ export const it: RebaseTranslations = {
44
44
  all_entries_loaded: "Tutte le {{count}} voci caricate",
45
45
  create_your_first_entry: "Crea la tua prima voce",
46
46
  no_results_filter_sort: "Nessun risultato con il filtro/ordinamento applicato",
47
+ no_results_search: "Nessun risultato trovato per \"{{search}}\"",
47
48
  add: "Aggiungi",
48
49
  remove: "Rimuovi",
49
50
  multiple_entities: "Entità multiple",
package/src/locales/pt.ts CHANGED
@@ -49,6 +49,7 @@ export const pt: RebaseTranslations = {
49
49
  all_entries_loaded: "Todos os {{count}} registos carregados",
50
50
  create_your_first_entry: "Crie o seu primeiro registo",
51
51
  no_results_filter_sort: "Sem resultados com o filtro/ordenação aplicado",
52
+ no_results_search: "Nenhum resultado encontrado para \"{{search}}\"",
52
53
  add: "Adicionar",
53
54
  remove: "Remover",
54
55
  multiple_entities: "Múltiplas entidades",
@@ -36,7 +36,7 @@ export function getEntityPreviewKeys(
36
36
  if (listProperties && listProperties.length > 0) {
37
37
  return listProperties;
38
38
  } else {
39
- listProperties = allProperties;
39
+ listProperties = (targetCollection.propertiesOrder as string[]) || allProperties;
40
40
  return listProperties
41
41
  .filter(key => {
42
42
  const prop = targetCollection.properties[key];
@@ -54,16 +54,25 @@ export function getEntityTitlePropertyKey<M extends Record<string, any>>(collect
54
54
  if (collection.titleProperty) {
55
55
  return collection.titleProperty as string;
56
56
  }
57
- // find first text field property
58
- for (const key in collection.properties) {
57
+
58
+ const orderToSearch = (collection.propertiesOrder as string[]) || Object.keys(collection.properties);
59
+ let firstStringCandidate: string | undefined;
60
+
61
+ for (const key of orderToSearch) {
59
62
  const property = collection.properties[key];
60
- if (!isPropertyBuilder(property)) {
63
+ if (property && !isPropertyBuilder(property)) {
61
64
  const prop = property as Property;
62
65
  if (prop.type === "string" && !prop.ui?.multiline && !prop.ui?.markdown && !prop.storage && !prop.isId) {
63
- return key;
66
+ if (!firstStringCandidate) {
67
+ firstStringCandidate = key;
68
+ }
69
+ const lowerKey = key.toLowerCase();
70
+ if (["name", "title", "label", "displayname", "username"].includes(lowerKey)) {
71
+ return key; // Immediate return if it's a strong title candidate
72
+ }
64
73
  }
65
74
  }
66
75
  }
67
- return undefined;
76
+ return firstStringCandidate;
68
77
  }
69
78
 
package/src/vitePlugin.ts CHANGED
@@ -1,4 +1,5 @@
1
1
 
2
+ import path from "path";
2
3
 
3
4
  export interface RebaseCollectionsPluginOptions {
4
5
  /**
@@ -8,22 +9,70 @@ export interface RebaseCollectionsPluginOptions {
8
9
  collectionsDir: string;
9
10
  }
10
11
 
12
+ /**
13
+ * Properties on collection objects that accept `ComponentRef` values.
14
+ * When a string literal is found for any of these keys in a collection file,
15
+ * the transform plugin replaces it with a `LazyComponentRef` object so the
16
+ * component is loaded lazily and never evaluated by the backend.
17
+ */
18
+ const LAZY_COMPONENT_KEYS = ["Field", "Preview", "Builder"];
19
+
20
+ /**
21
+ * Regex that matches `Key: "relative/path"` or `Key: 'relative/path'`
22
+ * for each key listed in LAZY_COMPONENT_KEYS.
23
+ *
24
+ * It captures:
25
+ * $1 — everything before the quote (e.g. `Field: `)
26
+ * $2 — the quote character (' or ")
27
+ * $3 — the path (must start with ./ or ../)
28
+ *
29
+ * The lookbehind-free pattern avoids issues with older runtimes.
30
+ */
31
+ function buildTransformRegex(): RegExp {
32
+ const keys = LAZY_COMPONENT_KEYS.join("|");
33
+ // Match property key, colon, optional whitespace, then a string starting with a dot-path
34
+ return new RegExp(
35
+ `((?:${keys})\\s*:\\s*)(['"])(\\.\\.?\\/[^'"]+)\\2`,
36
+ "g"
37
+ );
38
+ }
39
+
11
40
  /**
12
41
  * A Vite plugin that dynamically loads and automatically wires Rebase collections.
13
- * It provides a virtual module "virtual:rebase-collections" that statically exports the resolved collections array.
42
+ *
43
+ * It provides two capabilities:
44
+ * 1. A **virtual module** `"virtual:rebase-collections"` that statically exports
45
+ * the resolved collections array.
46
+ * 2. A **transform hook** that converts string-based component references
47
+ * (e.g. `Field: "../../components/MyField"`) into `LazyComponentRef` objects
48
+ * (`{ __rebaseLazy: true, load: () => import(...) }`), enabling code-splitting
49
+ * and preventing the backend from loading React-dependent modules.
14
50
  */
15
51
  export function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions) {
16
52
  const virtualModuleId = "virtual:rebase-collections";
17
53
  const resolvedVirtualModuleId = "\0" + virtualModuleId;
18
54
 
55
+ let resolvedCollectionsDir: string;
56
+ const transformRegex = buildTransformRegex();
57
+
19
58
  return {
20
59
  name: "rebase-collections-plugin",
60
+
61
+ configResolved(config: { root: string }) {
62
+ // Resolve the collections directory to an absolute path
63
+ // so the `transform` hook can match files reliably.
64
+ resolvedCollectionsDir = path.isAbsolute(options.collectionsDir)
65
+ ? options.collectionsDir
66
+ : path.resolve(config.root, options.collectionsDir);
67
+ },
68
+
21
69
  resolveId(id: string) {
22
70
  if (id === virtualModuleId) {
23
71
  return resolvedVirtualModuleId;
24
72
  }
25
73
  return null;
26
74
  },
75
+
27
76
  load(id: string) {
28
77
  if (id === resolvedVirtualModuleId) {
29
78
  // Vite evaluates `import.meta.glob` relative to the project root.
@@ -41,6 +90,43 @@ export function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions)
41
90
  `;
42
91
  }
43
92
  return null;
93
+ },
94
+
95
+ /**
96
+ * Transform collection files to convert string component references
97
+ * into lazy-loading `LazyComponentRef` objects.
98
+ *
99
+ * Example transform:
100
+ * ```
101
+ * // Input
102
+ * Field: "../../frontend/src/components/MyField"
103
+ *
104
+ * // Output
105
+ * Field: { __rebaseLazy: true, load: () => import("../../frontend/src/components/MyField") }
106
+ * ```
107
+ */
108
+ transform(code: string, id: string) {
109
+ // Only process .ts/.tsx files inside the collections directory
110
+ if (!resolvedCollectionsDir) return null;
111
+ if (!id.startsWith(resolvedCollectionsDir)) return null;
112
+ if (!/\.tsx?$/.test(id)) return null;
113
+
114
+ // Reset the regex state (global flag means lastIndex persists)
115
+ transformRegex.lastIndex = 0;
116
+
117
+ if (!transformRegex.test(code)) return null;
118
+
119
+ // Reset again after the test consumed the regex
120
+ transformRegex.lastIndex = 0;
121
+
122
+ const transformed = code.replace(
123
+ transformRegex,
124
+ (_match, prefix: string, quote: string, importPath: string) => {
125
+ return `${prefix}{ __rebaseLazy: true, load: () => import(${quote}${importPath}${quote}) }`;
126
+ }
127
+ );
128
+
129
+ return { code: transformed, map: null };
44
130
  }
45
131
  };
46
132
  }