@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.
- package/README.md +178 -110
- package/dist/components/BootstrapAdminBanner.d.ts +4 -0
- package/dist/components/LoginView/LoginView.d.ts +22 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useCollapsedGroups.d.ts +16 -1
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/index.es.js +214 -42
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +212 -40
- package/dist/index.umd.js.map +1 -1
- package/dist/vitePlugin.d.ts +28 -1
- package/dist/vitePlugin.js +42 -0
- package/package.json +6 -6
- package/src/components/BootstrapAdminBanner.tsx +66 -0
- package/src/components/LoginView/LoginView.tsx +48 -20
- package/src/components/common/useDataTableController.tsx +15 -3
- package/src/components/index.tsx +1 -1
- package/src/core/Rebase.tsx +20 -14
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useCollapsedGroups.ts +48 -6
- package/src/hooks/useRebaseContext.tsx +11 -6
- package/src/hooks/useResolvedComponent.tsx +157 -0
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +1 -0
- package/src/locales/it.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/util/previews.ts +15 -6
- package/src/vitePlugin.ts +87 -1
|
@@ -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",
|
package/src/util/previews.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
}
|