@rebasepro/app 0.22.0 → 0.24.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.
@@ -20,6 +20,17 @@ export declare function removeEntityFromMemoryCache(path: string): void;
20
20
  */
21
21
  export declare function saveEntityToMemoryCache(path: string, data: object): void;
22
22
  export declare function getEntityFromMemoryCache(path: string): object | undefined;
23
+ /**
24
+ * Drops every edit in flight. They were parked by a user who is no longer
25
+ * signed in, so nobody is taking them over.
26
+ */
27
+ export declare function clearEntityMemoryCache(): void;
28
+ /**
29
+ * Drops the whole local-changes backup: every draft in `sessionStorage`, for
30
+ * every record. For when the drafts belong to somebody other than the user now
31
+ * signed in.
32
+ */
33
+ export declare function clearEntityDrafts(): void;
23
34
  /**
24
35
  * Retrieves a entity from the local-changes backup in `sessionStorage`.
25
36
  *
@@ -7,3 +7,4 @@ export * from "./previews.js";
7
7
  export * from "./enums.js";
8
8
  export * from "./constants.js";
9
9
  export * from "./local_storage.js";
10
+ export * from "./realtime.js";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Whether an error is the realtime client saying the connection is down —
3
+ * not that the data is gone.
4
+ *
5
+ * Once the socket has been down for longer than a blip, the SDK tells every
6
+ * live subscription `CONNECTION_LOST` through its `onError`, once, and keeps
7
+ * the subscription: when the socket is back it is re-subscribed and its next
8
+ * update carries what changed meanwhile. A view that already shows data should
9
+ * keep showing it (the admin's connection banner says it may be stale) rather
10
+ * than replace it with an error — for an open record that would unmount the
11
+ * form and everything typed into it.
12
+ *
13
+ * Read by `code` rather than `instanceof RebaseApiError`: two copies of
14
+ * `@rebasepro/types` in one tree disagree about the class, never about the code.
15
+ *
16
+ * @group Hooks and utilities
17
+ */
18
+ export declare function isConnectionLostError(error: unknown): boolean;
@@ -50,6 +50,12 @@ export declare function transformCollectionSource(code: string, id: string): {
50
50
  * every call site.
51
51
  */
52
52
  export declare function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions): Plugin;
53
+ /**
54
+ * Whether a module id is a file inside `directory`, whichever separators
55
+ * either was written with. On a whole path segment, so `collections-old/` is
56
+ * not inside `collections`.
57
+ */
58
+ export declare function isInsideDirectory(id: string, directory: string): boolean;
53
59
  /**
54
60
  * How the admin's dependencies are split into cached chunks.
55
61
  *
@@ -2,12 +2,114 @@ import path from "path";
2
2
  import ts from "typescript";
3
3
  import MagicString from "magic-string";
4
4
  const LAZY_COMPONENT_KEYS = /* @__PURE__ */ new Set(["Field", "Preview", "Builder", "Filter", "Component"]);
5
- function isInsideCallbacksBlock(node) {
6
- const objectLiteral = node.parent;
7
- if (!objectLiteral || !ts.isObjectLiteralExpression(objectLiteral)) return false;
8
- const owner = objectLiteral.parent;
9
- if (!owner || !ts.isPropertyAssignment(owner)) return false;
10
- return getPropertyName(owner) === "callbacks";
5
+ function unwrapObjectLiteral(expr) {
6
+ let current = expr;
7
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isNonNullExpression(current)) {
8
+ current = current.expression;
9
+ }
10
+ return ts.isObjectLiteralExpression(current) ? current : void 0;
11
+ }
12
+ function callbackStripEdits(sourceFile) {
13
+ const edits = [];
14
+ const memberEdit = (member) => {
15
+ if (ts.isPropertyAssignment(member)) {
16
+ return { start: member.initializer.getStart(sourceFile), end: member.initializer.getEnd(), text: "undefined" };
17
+ }
18
+ if (ts.isSpreadAssignment(member)) {
19
+ return { start: member.getStart(sourceFile), end: member.getEnd(), text: "...undefined" };
20
+ }
21
+ const name = member.name;
22
+ return name ? { start: member.getStart(sourceFile), end: member.getEnd(), text: `${name.getText(sourceFile)}: undefined` } : void 0;
23
+ };
24
+ walkAST(sourceFile, (node) => {
25
+ if (ts.isShorthandPropertyAssignment(node) && node.name.text === "callbacks") {
26
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: "callbacks: undefined" });
27
+ return;
28
+ }
29
+ if (!ts.isPropertyAssignment(node) || getPropertyName(node) !== "callbacks") return;
30
+ const literal = unwrapObjectLiteral(node.initializer);
31
+ if (!literal) {
32
+ edits.push({ start: node.initializer.getStart(sourceFile), end: node.initializer.getEnd(), text: "undefined" });
33
+ return;
34
+ }
35
+ for (const member of literal.properties) {
36
+ const edit = memberEdit(member);
37
+ if (edit) edits.push(edit);
38
+ }
39
+ });
40
+ return edits.filter((edit) => !edits.some((outer) => outer !== edit && outer.start <= edit.start && edit.end <= outer.end));
41
+ }
42
+ function isReference(id) {
43
+ const parent = id.parent;
44
+ if (!parent) return false;
45
+ if (ts.isPropertyAccessExpression(parent) && parent.name === id) return false;
46
+ if (ts.isQualifiedName(parent) && parent.right === id) return false;
47
+ if ((ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) || ts.isMethodSignature(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isEnumMember(parent) || ts.isJsxAttribute(parent)) && parent.name === id) return false;
48
+ if ((ts.isVariableDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isClassDeclaration(parent) || ts.isParameter(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isBindingElement(parent) || ts.isFunctionExpression(parent) || ts.isClassExpression(parent) || ts.isTypeAliasDeclaration(parent) || ts.isInterfaceDeclaration(parent) || ts.isTypeParameterDeclaration(parent)) && parent.name === id) return false;
49
+ if (ts.isBindingElement(parent) && parent.propertyName === id) return false;
50
+ if (ts.isImportSpecifier(parent)) return false;
51
+ if (ts.isLabeledStatement(parent) || ts.isBreakOrContinueStatement(parent)) return false;
52
+ return true;
53
+ }
54
+ function deadTopLevelStatements(sourceFile, removed) {
55
+ const bindings = [];
56
+ for (const statement of sourceFile.statements) {
57
+ const exported = ts.canHaveModifiers(statement) && (ts.getModifiers(statement) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
58
+ if (exported) continue;
59
+ if (ts.isImportDeclaration(statement)) {
60
+ const clause = statement.importClause;
61
+ if (!clause || clause.isTypeOnly) continue;
62
+ const names = [];
63
+ if (clause.name) names.push(clause.name.text);
64
+ const named = clause.namedBindings;
65
+ if (named && ts.isNamespaceImport(named)) names.push(named.name.text);
66
+ if (named && ts.isNamedImports(named)) {
67
+ for (const el of named.elements) if (!el.isTypeOnly) names.push(el.name.text);
68
+ }
69
+ if (names.length > 0) bindings.push({ statement, names });
70
+ } else if (ts.isVariableStatement(statement)) {
71
+ const names = [];
72
+ const collect = (name) => {
73
+ if (ts.isIdentifier(name)) names.push(name.text);
74
+ else for (const el of name.elements) if (!ts.isOmittedExpression(el)) collect(el.name);
75
+ };
76
+ for (const decl of statement.declarationList.declarations) collect(decl.name);
77
+ bindings.push({ statement, names });
78
+ } else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) {
79
+ bindings.push({ statement, names: [statement.name.text] });
80
+ }
81
+ }
82
+ if (bindings.length === 0) return [];
83
+ const references = [];
84
+ walkAST(sourceFile, (node) => {
85
+ if (ts.isIdentifier(node) && isReference(node)) references.push(node);
86
+ });
87
+ const ranges = removed.map(({ start, end }) => ({ start, end }));
88
+ const within = (node) => {
89
+ const at = node.getStart(sourceFile);
90
+ return ranges.some((r) => r.start <= at && node.getEnd() <= r.end);
91
+ };
92
+ const dead = [];
93
+ for (let changed = true; changed; ) {
94
+ changed = false;
95
+ for (const binding of bindings) {
96
+ if (dead.includes(binding.statement)) continue;
97
+ let readByStripped = false;
98
+ let readLive = false;
99
+ for (const ref of references) {
100
+ if (!binding.names.includes(ref.text)) continue;
101
+ if (ref.getStart(sourceFile) >= binding.statement.getStart(sourceFile) && ref.getEnd() <= binding.statement.getEnd()) continue;
102
+ if (within(ref)) readByStripped = true;
103
+ else readLive = true;
104
+ }
105
+ if (readByStripped && !readLive) {
106
+ dead.push(binding.statement);
107
+ ranges.push({ start: binding.statement.getStart(sourceFile), end: binding.statement.getEnd() });
108
+ changed = true;
109
+ }
110
+ }
111
+ }
112
+ return dead;
11
113
  }
12
114
  function walkAST(node, visitor) {
13
115
  visitor(node);
@@ -30,15 +132,22 @@ function transformCollectionSource(code, id) {
30
132
  );
31
133
  const ms = new MagicString(code);
32
134
  let replaced = false;
135
+ const strips = callbackStripEdits(sourceFile);
136
+ for (const edit of strips) {
137
+ ms.overwrite(edit.start, edit.end, edit.text);
138
+ replaced = true;
139
+ }
140
+ const gone = [...strips];
141
+ for (const statement of deadTopLevelStatements(sourceFile, strips)) {
142
+ ms.remove(statement.getStart(sourceFile), statement.getEnd());
143
+ gone.push({ start: statement.getStart(sourceFile), end: statement.getEnd() });
144
+ replaced = true;
145
+ }
146
+ const stripped = (node) => gone.some((e) => e.start <= node.getStart(sourceFile) && node.getEnd() <= e.end);
33
147
  walkAST(sourceFile, (node) => {
34
148
  if (!ts.isPropertyAssignment(node)) return;
149
+ if (stripped(node)) return;
35
150
  const name = getPropertyName(node);
36
- if (name && isInsideCallbacksBlock(node)) {
37
- const value = node.initializer;
38
- ms.overwrite(value.getStart(sourceFile), value.getEnd(), "undefined");
39
- replaced = true;
40
- return;
41
- }
42
151
  const propName = name;
43
152
  if (!propName || !LAZY_COMPONENT_KEYS.has(propName)) return;
44
153
  const init = node.initializer;
@@ -61,10 +170,13 @@ function rebaseCollectionsPlugin(options) {
61
170
  const virtualModuleId = "virtual:rebase-collections";
62
171
  const resolvedVirtualModuleId = "\0" + virtualModuleId;
63
172
  let resolvedCollectionsDir;
173
+ let collectionsDirGlob;
64
174
  return {
65
175
  name: "rebase-collections-plugin",
66
176
  configResolved(config) {
67
177
  resolvedCollectionsDir = path.isAbsolute(options.collectionsDir) ? options.collectionsDir : path.resolve(config.root, options.collectionsDir);
178
+ const fromRoot = toModuleIdPath(path.relative(config.root, resolvedCollectionsDir));
179
+ collectionsDirGlob = fromRoot ? "/" + fromRoot : "";
68
180
  },
69
181
  /**
70
182
  * Watch the collections directory itself, not just the files in it.
@@ -103,10 +215,13 @@ function rebaseCollectionsPlugin(options) {
103
215
  },
104
216
  load(id) {
105
217
  if (id === resolvedVirtualModuleId) {
106
- const globPath = options.collectionsDir.startsWith("/") ? `${options.collectionsDir}/*.ts` : `/${options.collectionsDir}/*.ts`;
218
+ const globPatterns = [
219
+ `${collectionsDirGlob}/*.ts`,
220
+ `!${collectionsDirGlob}/*.test.*`
221
+ ];
107
222
  return `
108
223
  // GENERATED BY @rebasepro/app Vite Plugin
109
- const collectionModules = import.meta.glob('${globPath}', { eager: true });
224
+ const collectionModules = import.meta.glob(${JSON.stringify(globPatterns)}, { eager: true });
110
225
 
111
226
  // index exports the directory's defaults, not a collection \u2014
112
227
  // it has no default export, so it drops out of the list below.
@@ -159,12 +274,19 @@ function rebaseCollectionsPlugin(options) {
159
274
  */
160
275
  transform(code, id) {
161
276
  if (!resolvedCollectionsDir) return null;
162
- if (!id.startsWith(resolvedCollectionsDir)) return null;
277
+ if (!isInsideDirectory(id, resolvedCollectionsDir)) return null;
163
278
  if (!/\.tsx?$/.test(id)) return null;
164
279
  return transformCollectionSource(code, id);
165
280
  }
166
281
  };
167
282
  }
283
+ function toModuleIdPath(filePath) {
284
+ return filePath.replace(/\\/g, "/");
285
+ }
286
+ function isInsideDirectory(id, directory) {
287
+ const dir = toModuleIdPath(directory);
288
+ return toModuleIdPath(id).startsWith(dir.endsWith("/") ? dir : dir + "/");
289
+ }
168
290
  function rebaseManualChunks(id) {
169
291
  if (id.includes("commonjsHelpers") || id.includes("commonjs-dynamic-modules")) return "vendor-commonjs-helpers";
170
292
  if (id.includes("exceljs")) return "vendor-exceljs";
@@ -190,6 +312,7 @@ function rebaseManualChunks(id) {
190
312
  return void 0;
191
313
  }
192
314
  export {
315
+ isInsideDirectory,
193
316
  rebaseCollectionsPlugin,
194
317
  rebaseManualChunks,
195
318
  transformCollectionSource
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/app",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
5
5
  "keywords": [
6
6
  "rebase",
@@ -63,12 +63,12 @@
63
63
  "magic-string": "^0.30.0",
64
64
  "notistack": "^3.0.2",
65
65
  "react-i18next": "^17.0.11",
66
- "@rebasepro/cms-types": "0.22.0",
67
- "@rebasepro/forms": "0.22.0",
68
- "@rebasepro/utils": "0.22.0",
69
- "@rebasepro/common": "0.22.0",
70
- "@rebasepro/ui": "0.22.0",
71
- "@rebasepro/types": "0.22.0"
66
+ "@rebasepro/cms-types": "0.24.0",
67
+ "@rebasepro/common": "0.24.0",
68
+ "@rebasepro/types": "0.24.0",
69
+ "@rebasepro/ui": "0.24.0",
70
+ "@rebasepro/utils": "0.24.0",
71
+ "@rebasepro/forms": "0.24.0"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": "^19.2.7",