Skip to content

Commit 3ee7821

Browse files
authored
feat: swr plugin (#419)
1 parent 53716c9 commit 3ee7821

File tree

14 files changed

+981
-20
lines changed

14 files changed

+981
-20
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"root": true,
3+
"parser": "@typescript-eslint/parser",
4+
"parserOptions": {
5+
"ecmaVersion": 6,
6+
"sourceType": "module"
7+
},
8+
"plugins": ["@typescript-eslint"],
9+
"extends": [
10+
"eslint:recommended",
11+
"plugin:@typescript-eslint/eslint-recommended",
12+
"plugin:@typescript-eslint/recommended"
13+
]
14+
}

packages/plugins/swr/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../LICENSE

packages/plugins/swr/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ZenStack React plugin & runtime
2+
3+
This package contains ZenStack plugin and runtime for ReactJS.
4+
5+
Visit [Homepage](https://zenstack.dev) for more details.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
export default {
7+
// Automatically clear mock calls, instances, contexts and results before every test
8+
clearMocks: true,
9+
10+
// Indicates whether the coverage information should be collected while executing the test
11+
collectCoverage: true,
12+
13+
// The directory where Jest should output its coverage files
14+
coverageDirectory: 'tests/coverage',
15+
16+
// An array of regexp pattern strings used to skip coverage collection
17+
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
18+
19+
// Indicates which provider should be used to instrument code for coverage
20+
coverageProvider: 'v8',
21+
22+
// A list of reporter names that Jest uses when writing coverage reports
23+
coverageReporters: ['json', 'text', 'lcov', 'clover'],
24+
25+
// A map from regular expressions to paths to transformers
26+
transform: { '^.+\\.tsx?$': 'ts-jest' },
27+
28+
testTimeout: 300000,
29+
};

packages/plugins/swr/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@zenstackhq/swr",
3+
"displayName": "ZenStack plugin for generating SWR hooks",
4+
"version": "1.0.0-alpha.116",
5+
"description": "ZenStack plugin for generating SWR hooks",
6+
"main": "index.js",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/zenstackhq/zenstack"
10+
},
11+
"scripts": {
12+
"clean": "rimraf dist",
13+
"build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist",
14+
"watch": "tsc --watch",
15+
"lint": "eslint src --ext ts",
16+
"prepublishOnly": "pnpm build",
17+
"publish-dev": "pnpm publish --tag dev"
18+
},
19+
"publishConfig": {
20+
"directory": "dist",
21+
"linkDirectory": true
22+
},
23+
"keywords": [],
24+
"author": "ZenStack Team",
25+
"license": "MIT",
26+
"dependencies": {
27+
"@prisma/generator-helper": "^4.7.1",
28+
"@zenstackhq/sdk": "workspace:*",
29+
"change-case": "^4.1.2",
30+
"decimal.js": "^10.4.2",
31+
"lower-case-first": "^2.0.2",
32+
"superjson": "^1.11.0",
33+
"ts-morph": "^16.0.0",
34+
"upper-case-first": "^2.0.2"
35+
},
36+
"devDependencies": {
37+
"@tanstack/react-query": "^4.28.0",
38+
"@types/jest": "^29.5.0",
39+
"@types/lower-case-first": "^1.0.1",
40+
"@types/react": "^18.0.26",
41+
"@types/tmp": "^0.2.3",
42+
"@types/upper-case-first": "^1.1.2",
43+
"@zenstackhq/testtools": "workspace:*",
44+
"copyfiles": "^2.4.1",
45+
"jest": "^29.5.0",
46+
"react": "^17.0.2 || ^18",
47+
"react-dom": "^17.0.2 || ^18",
48+
"rimraf": "^3.0.2",
49+
"swr": "^2.0.3",
50+
"ts-jest": "^29.0.5",
51+
"typescript": "^4.9.4"
52+
}
53+
}

packages/plugins/swr/res/helper.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { createContext } from 'react';
2+
import type { MutatorCallback, MutatorOptions, SWRResponse } from 'swr';
3+
import useSWR, { useSWRConfig } from 'swr';
4+
5+
/**
6+
* Context type for configuring react hooks.
7+
*/
8+
export type RequestHandlerContext = {
9+
endpoint: string;
10+
};
11+
12+
/**
13+
* Context for configuring react hooks.
14+
*/
15+
export const RequestHandlerContext = createContext<RequestHandlerContext>({
16+
endpoint: '/api/model',
17+
});
18+
19+
/**
20+
* Context provider.
21+
*/
22+
export const Provider = RequestHandlerContext.Provider;
23+
24+
/**
25+
* Client request options
26+
*/
27+
export type RequestOptions<T> = {
28+
// disable data fetching
29+
disabled?: boolean;
30+
initialData?: T;
31+
};
32+
33+
/**
34+
* Makes a GET request with SWR.
35+
*
36+
* @param url The request URL.
37+
* @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter
38+
* @returns SWR response
39+
*/
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
export function get<Data, Error = any>(
42+
url: string | null,
43+
args?: unknown,
44+
options?: RequestOptions<Data>
45+
): SWRResponse<Data, Error> {
46+
const reqUrl = options?.disabled ? null : url ? makeUrl(url, args) : null;
47+
return useSWR<Data, Error>(reqUrl, fetcher, {
48+
fallbackData: options?.initialData,
49+
});
50+
}
51+
52+
/**
53+
* Makes a POST request.
54+
*
55+
* @param url The request URL.
56+
* @param data The request data.
57+
* @param mutate Mutator for invalidating cache.
58+
*/
59+
export async function post<Data, Result>(url: string, data: Data, mutate: Mutator): Promise<Result> {
60+
const r: Result = await fetcher(url, {
61+
method: 'POST',
62+
headers: {
63+
'content-type': 'application/json',
64+
},
65+
body: marshal(data),
66+
});
67+
mutate();
68+
return r;
69+
}
70+
71+
/**
72+
* Makes a PUT request.
73+
*
74+
* @param url The request URL.
75+
* @param data The request data.
76+
* @param mutate Mutator for invalidating cache.
77+
*/
78+
export async function put<Data, Result>(url: string, data: Data, mutate: Mutator): Promise<Result> {
79+
const r: Result = await fetcher(url, {
80+
method: 'PUT',
81+
headers: {
82+
'content-type': 'application/json',
83+
},
84+
body: marshal(data),
85+
});
86+
mutate();
87+
return r;
88+
}
89+
90+
/**
91+
* Makes a DELETE request.
92+
*
93+
* @param url The request URL.
94+
* @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter
95+
* @param mutate Mutator for invalidating cache.
96+
*/
97+
export async function del<Result>(url: string, args: unknown, mutate: Mutator): Promise<Result> {
98+
const reqUrl = makeUrl(url, args);
99+
const r: Result = await fetcher(reqUrl, {
100+
method: 'DELETE',
101+
});
102+
const path = url.split('/');
103+
path.pop();
104+
mutate();
105+
return r;
106+
}
107+
108+
type Mutator = (
109+
data?: unknown | Promise<unknown> | MutatorCallback,
110+
opts?: boolean | MutatorOptions
111+
) => Promise<unknown[]>;
112+
113+
export function getMutate(prefixes: string[]): Mutator {
114+
// https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex
115+
const { cache, mutate } = useSWRConfig();
116+
return (data?: unknown | Promise<unknown> | MutatorCallback, opts?: boolean | MutatorOptions) => {
117+
if (!(cache instanceof Map)) {
118+
throw new Error('mutate requires the cache provider to be a Map instance');
119+
}
120+
121+
const keys = Array.from(cache.keys()).filter(
122+
(k) => typeof k === 'string' && prefixes.some((prefix) => k.startsWith(prefix))
123+
) as string[];
124+
const mutations = keys.map((key) => mutate(key, data, opts));
125+
return Promise.all(mutations);
126+
};
127+
}
128+
129+
export async function fetcher<R>(url: string, options?: RequestInit) {
130+
const res = await fetch(url, options);
131+
if (!res.ok) {
132+
const error: Error & { info?: unknown; status?: number } = new Error(
133+
'An error occurred while fetching the data.'
134+
);
135+
error.info = unmarshal(await res.text());
136+
error.status = res.status;
137+
throw error;
138+
}
139+
140+
const textResult = await res.text();
141+
try {
142+
return unmarshal(textResult) as R;
143+
} catch (err) {
144+
console.error(`Unable to deserialize data:`, textResult);
145+
throw err;
146+
}
147+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function marshal(value: unknown) {
2+
return JSON.stringify(value);
3+
}
4+
5+
function unmarshal(value: string) {
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
return JSON.parse(value) as any;
8+
}
9+
10+
function makeUrl(url: string, args: unknown) {
11+
return args ? url + `?q=${encodeURIComponent(JSON.stringify(args))}` : url;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import superjson from 'superjson';
2+
3+
function marshal(value: unknown) {
4+
return superjson.stringify(value);
5+
}
6+
7+
function unmarshal(value: string) {
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
const j = JSON.parse(value) as any;
10+
if (j?.json) {
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
return superjson.parse<any>(value);
13+
} else {
14+
return j;
15+
}
16+
}
17+
18+
function makeUrl(url: string, args: unknown) {
19+
return args ? url + `?q=${encodeURIComponent(superjson.stringify(args))}` : url;
20+
}

0 commit comments

Comments
 (0)