@mittwald/react-ghostmaker 1.5.3 → 1.6.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/README.md CHANGED
@@ -497,6 +497,27 @@ class Blog {
497
497
  - Name and ID can be set explicitly (for readable query keys)
498
498
  - Works for multiple models and inheritance
499
499
 
500
+ #### Framework-agnostic model code
501
+
502
+ If your models live in code that must not depend on React (a shared model
503
+ package, server-side or CLI code), import the decorator from the
504
+ `@mittwald/react-ghostmaker/model` entry point instead of the main entry:
505
+
506
+ ```ts
507
+ import { GhostMakerModel } from "@mittwald/react-ghostmaker/model";
508
+ ```
509
+
510
+ This entry point only contains what is needed to describe models —
511
+ `GhostMakerModel`, `ghostMakerModel`, `getModelName`, `getModelId` and the
512
+ `GhostMakerModelMeta` and `DynamicModel` types — and imports neither React nor
513
+ TanStack Query, not even for its type declarations. `react` and
514
+ `@tanstack/react-query` are therefore optional peer dependencies: you only need
515
+ them where you import from the main entry (`@mittwald/react-ghostmaker`), i.e.
516
+ where you actually create and use ghosts.
517
+
518
+ Both entry points share the same model registry, so models decorated via
519
+ `/model` are recognized by ghosts created via the main entry.
520
+
500
521
  #### Alternative: `registerModelIdentifier` (deprecated)
501
522
 
502
523
  The previous `registerModelIdentifier` function is still available but marked as deprecated. It can be used to centrally register IDs for models:
@@ -861,6 +882,9 @@ Creates a ghost proxy from any object or class instance.
861
882
  - TanStack Query ^5
862
883
  - TypeScript (recommended)
863
884
 
885
+ React and TanStack Query are only required for the main entry point, not for
886
+ [`@mittwald/react-ghostmaker/model`](#framework-agnostic-model-code).
887
+
864
888
  ## 📄 License
865
889
 
866
890
  MIT © [Mittwald CM Service GmbH & Co. KG](https://github.com/mittwald)
package/dist/esm/index.js CHANGED
@@ -3,5 +3,4 @@ export { registerModelIdentifier } from "./modelIdentifier.js";
3
3
  export { getQueryContext, forwardQueryContext } from "./context.js";
4
4
  export { invalidateGhosts } from "./invalidate.js";
5
5
  export * from "./maybeGhost/index.js";
6
- export { GhostMakerModel, ghostMakerModel } from "./metaData.js";
7
- export { getModelId, getModelName } from "./queries.js";
6
+ export { GhostMakerModel, ghostMakerModel, getModelId, getModelName, } from "./metaData.js";
@@ -40,3 +40,10 @@ test("asGhost() works with generics", () => {
40
40
  asGhost(prop).foo().use();
41
41
  }
42
42
  });
43
+ test("calling function on optional props works", () => {
44
+ const customerNameGhost = ProjectGhost.ofId("Project A")
45
+ .getDetailed()
46
+ .optionalCustomer?.getDetailed()
47
+ .getName();
48
+ expectTypeOf(customerNameGhost).toEqualTypeOf();
49
+ });
@@ -36,6 +36,17 @@ export const getMetaData = (something) => {
36
36
  return (getMetaDataRecursive(something, undefined, getClass(something)) ??
37
37
  getDynamicMetaData(something));
38
38
  };
39
+ export const getModelName = (something) => {
40
+ return getMetaData(something)?.name;
41
+ };
42
+ export const getModelId = (something) => {
43
+ if (is.object(something)) {
44
+ const getId = getMetaData(something)?.getId;
45
+ if (getId) {
46
+ return getId(something);
47
+ }
48
+ }
49
+ };
39
50
  const getMetaDataRecursive = (something, collectedMeta,
40
51
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
41
52
  klass) => {
@@ -0,0 +1 @@
1
+ export { GhostMakerModel, ghostMakerModel, getModelId, getModelName, } from "./metaData.js";
@@ -1,18 +1,7 @@
1
1
  import is from "@sindresorhus/is";
2
2
  import { hashObject } from "./hash.js";
3
- import { getMetaData } from "./metaData.js";
3
+ import { getModelId, getModelName } from "./metaData.js";
4
4
  import { modelIdentifiers } from "./modelIdentifier.js";
5
- export const getModelName = (something) => {
6
- return getMetaData(something)?.name;
7
- };
8
- export const getModelId = (something) => {
9
- if (is.object(something)) {
10
- const getId = getMetaData(something)?.getId;
11
- if (getId) {
12
- return getId(something);
13
- }
14
- }
15
- };
16
5
  const getObjectName = (something) => {
17
6
  const modelName = getModelName(something);
18
7
  if (modelName) {
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import { describe, expect, test } from "vitest";
5
+ const srcDir = __dirname;
6
+ const isReactPackage = (specifier) => /^(react|react-dom|@tanstack\/react-query)(\/|$)/.test(specifier);
7
+ const collectPackageImports = (entry) => {
8
+ const packageImports = new Map();
9
+ const visited = new Set();
10
+ const queue = [resolve(srcDir, entry)];
11
+ while (queue.length > 0) {
12
+ const file = queue.pop();
13
+ if (visited.has(file)) {
14
+ continue;
15
+ }
16
+ visited.add(file);
17
+ const { importedFiles } = ts.preProcessFile(readFileSync(file, "utf8"), true, true);
18
+ for (const { fileName: specifier } of importedFiles) {
19
+ if (specifier.startsWith(".")) {
20
+ queue.push(resolve(dirname(file), specifier));
21
+ }
22
+ else {
23
+ const importers = packageImports.get(specifier) ?? [];
24
+ importers.push(file.slice(srcDir.length + 1));
25
+ packageImports.set(specifier, importers);
26
+ }
27
+ }
28
+ }
29
+ return packageImports;
30
+ };
31
+ describe.each(["model.ts", "polytype.ts"])("entry point %s", (entry) => {
32
+ test("does not import React or TanStack Query, not even for types", () => {
33
+ const reactImports = Array.from(collectPackageImports(entry)).filter(([specifier]) => isReactPackage(specifier));
34
+ expect(Object.fromEntries(reactImports)).toEqual({});
35
+ });
36
+ });
37
+ test("the import walker detects React imports", () => {
38
+ expect(collectPackageImports("index.ts").has("react")).toBe(true);
39
+ });
@@ -186,6 +186,7 @@ let ProjectDetailed = (() => {
186
186
  id;
187
187
  name;
188
188
  customer;
189
+ optionalCustomer;
189
190
  constructor(id, name, customerId) {
190
191
  this.id = id;
191
192
  this.name = name;
@@ -4,5 +4,4 @@ export { type ReactGhost, UseGhostReturn } from "./types.ts";
4
4
  export { getQueryContext, forwardQueryContext } from "./context.ts";
5
5
  export { invalidateGhosts } from "./invalidate.ts";
6
6
  export * from "./maybeGhost/index.ts";
7
- export { GhostMakerModel, ghostMakerModel } from "./metaData.ts";
8
- export { getModelId, getModelName } from "./queries.ts";
7
+ export { GhostMakerModel, ghostMakerModel, getModelId, getModelName, type DynamicModel, type GhostMakerModelMeta, } from "./metaData.ts";
@@ -1,13 +1,15 @@
1
1
  import type { AbstractClass, Class } from "type-fest";
2
2
  type DecoratorTarget = Class<unknown> | AbstractClass<unknown>;
3
- interface GhostMakerModelMeta<T extends DecoratorTarget> {
3
+ export interface GhostMakerModelMeta<T extends DecoratorTarget> {
4
4
  getId?: (instance: InstanceType<T>) => string;
5
5
  name?: string;
6
6
  }
7
7
  export declare function GhostMakerModel<T extends DecoratorTarget>(options: GhostMakerModelMeta<T>): (target: T) => void;
8
- type DynamicModel = (something: unknown) => GhostMakerModelMeta<DecoratorTarget> | void | undefined;
8
+ export type DynamicModel = (something: unknown) => GhostMakerModelMeta<DecoratorTarget> | void | undefined;
9
9
  export declare function ghostMakerModel(modelIdentifier: DynamicModel): void;
10
10
  export declare const getMetaData: (something: unknown) => GhostMakerModelMeta<DecoratorTarget> | undefined;
11
+ export declare const getModelName: (something: unknown) => string | undefined;
12
+ export declare const getModelId: (something: unknown) => string | undefined;
11
13
  export declare const getProtoypes: {
12
14
  current: (something: unknown) => any[];
13
15
  };
@@ -0,0 +1 @@
1
+ export { GhostMakerModel, ghostMakerModel, getModelId, getModelName, type DynamicModel, type GhostMakerModelMeta, } from "./metaData.ts";
@@ -1,6 +1,4 @@
1
1
  import type { GhostChain, GhostChainItem, QueryKey } from "./types.ts";
2
- export declare const getModelName: (something: unknown) => string | undefined;
3
- export declare const getModelId: (something: unknown) => string | undefined;
4
2
  export declare const queries: {
5
3
  ghostmaker: () => string[];
6
4
  target: (target: unknown) => string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -37,6 +37,7 @@ export declare class ProjectDetailed {
37
37
  readonly id: string;
38
38
  readonly name: string;
39
39
  readonly customer: Customer;
40
+ readonly optionalCustomer?: Customer;
40
41
  constructor(id: string, name: string, customerId: string);
41
42
  getName(): string;
42
43
  getStatus(): Promise<string>;
@@ -22,7 +22,7 @@ export declare const transformFnProp: "___transformFn";
22
22
  export declare const isGhostMarker: "___ghostMarker";
23
23
  type GhostProp<V, TOriginal> = V extends AnyFunction ? (...args: Parameters<V>) => ReactGhost<Awaited<ReturnType<V>> | OrOptional<TOriginal>> : ReactGhost<V | OrOptional<TOriginal>>;
24
24
  export type ReactGhost<T> = ReactGhostMethods<T> & {
25
- [K in keyof NonNullable<T>]-?: GhostProp<NonNullable<T>[K], T>;
25
+ [K in keyof NonNullable<T>]-?: Exclude<GhostProp<NonNullable<T>[K], T>, ReactGhost<undefined>>;
26
26
  };
27
27
  export interface GhostChainItem {
28
28
  propName: string | typeof transformFnProp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/react-ghostmaker",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
5
5
  "contributors": [
6
6
  "Marco Falkenberg <m.falkenberg@mittwald.de>"
@@ -27,6 +27,10 @@
27
27
  "types": "./dist/types/index.d.ts",
28
28
  "import": "./dist/esm/index.js"
29
29
  },
30
+ "./model": {
31
+ "types": "./dist/types/model.d.ts",
32
+ "import": "./dist/esm/model.js"
33
+ },
30
34
  "./polytype": {
31
35
  "types": "./dist/types/polytype.d.ts",
32
36
  "import": "./dist/esm/polytype.js"
@@ -78,8 +82,14 @@
78
82
  "react": ">=19.2"
79
83
  },
80
84
  "peerDependenciesMeta": {
85
+ "@tanstack/react-query": {
86
+ "optional": true
87
+ },
81
88
  "polytype": {
82
89
  "optional": true
90
+ },
91
+ "react": {
92
+ "optional": true
83
93
  }
84
94
  },
85
95
  "scripts": {