@tailorkit/app 0.1.0-beta.2 → 0.1.0-beta.3
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/package.json +4 -3
- package/src/builder/index.ts +116 -0
- package/src/builder/upload-manifest.ts +18 -0
- package/src/config/config.ts +24 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +35 -0
- package/src/index.ts +206 -0
- package/src/preact-version.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tailorkit/app",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"directory": "packages/app"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"src"
|
|
12
13
|
],
|
|
13
14
|
"type": "module",
|
|
14
15
|
"imports": {
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"access": "public"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"@tailorkit/core": "0.1.0-beta.
|
|
50
|
+
"@tailorkit/core": "0.1.0-beta.3",
|
|
50
51
|
"cosmiconfig": "^10.0.0",
|
|
51
52
|
"vite": "^8.2.2",
|
|
52
53
|
"zod": "^4.4.3"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { build as viteBuild } from "vite";
|
|
5
|
+
import { loadTailorKitConfig } from "../config/loader";
|
|
6
|
+
import { assertSupportedPreactVersion } from "../preact-version";
|
|
7
|
+
import { createTailorKitUploadManifest } from "./upload-manifest";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
createTailorKitUploadManifest,
|
|
11
|
+
tailorkitUploadManifestSchema,
|
|
12
|
+
type TailorKitUploadManifest,
|
|
13
|
+
} from "./upload-manifest";
|
|
14
|
+
|
|
15
|
+
const preactExternal = /^preact(?:\/.*)?$/u;
|
|
16
|
+
const preactPackageJson = "preact/package.json";
|
|
17
|
+
const preactPackageJsonModuleId = "\0tailorkit-preact-package-json";
|
|
18
|
+
|
|
19
|
+
export interface BuildAppOptions {
|
|
20
|
+
configPath?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
entry?: string;
|
|
23
|
+
mode?: string;
|
|
24
|
+
outDir?: string;
|
|
25
|
+
watch?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const buildApp = async (options: BuildAppOptions = {}): Promise<unknown> => {
|
|
29
|
+
const loaded = await loadTailorKitConfig(options.configPath, options.cwd);
|
|
30
|
+
const entry = options.entry ?? loaded.config.client?.entry ?? "./src/client.ts";
|
|
31
|
+
const outDir = options.outDir ?? loaded.config.build?.outDir ?? ".tailorkit";
|
|
32
|
+
const preactVersion = getInstalledPreactVersion(loaded.root);
|
|
33
|
+
|
|
34
|
+
assertSupportedPreactVersion(preactVersion);
|
|
35
|
+
|
|
36
|
+
const resolvedOutDir = path.resolve(loaded.root, outDir);
|
|
37
|
+
const result = await viteBuild({
|
|
38
|
+
build: {
|
|
39
|
+
emptyOutDir: true,
|
|
40
|
+
lib: {
|
|
41
|
+
entry: path.resolve(loaded.root, entry),
|
|
42
|
+
fileName: "client",
|
|
43
|
+
formats: ["es"],
|
|
44
|
+
},
|
|
45
|
+
outDir: resolvedOutDir,
|
|
46
|
+
watch: options.watch ? {} : null,
|
|
47
|
+
minify: "oxc",
|
|
48
|
+
rollupOptions: {
|
|
49
|
+
external: (id) =>
|
|
50
|
+
id !== preactPackageJson && id !== preactPackageJsonModuleId && preactExternal.test(id),
|
|
51
|
+
output: {
|
|
52
|
+
comments: {
|
|
53
|
+
annotation: false,
|
|
54
|
+
jsdoc: false,
|
|
55
|
+
legal: false,
|
|
56
|
+
},
|
|
57
|
+
minify: true,
|
|
58
|
+
minifyInternalExports: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
configFile: false,
|
|
63
|
+
mode: options.mode,
|
|
64
|
+
plugins: [
|
|
65
|
+
{
|
|
66
|
+
name: "tailorkit-preact-package-json",
|
|
67
|
+
enforce: "pre",
|
|
68
|
+
resolveId(id) {
|
|
69
|
+
if (id === preactPackageJson) {
|
|
70
|
+
return preactPackageJsonModuleId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
},
|
|
75
|
+
load(id) {
|
|
76
|
+
if (id === preactPackageJsonModuleId) {
|
|
77
|
+
const version = JSON.stringify(preactVersion);
|
|
78
|
+
|
|
79
|
+
return `export const version = ${version}; export default { version: ${version} };`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
root: loaded.root,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await writeFile(
|
|
90
|
+
path.join(resolvedOutDir, "tailorkit-upload.json"),
|
|
91
|
+
`${JSON.stringify(createTailorKitUploadManifest(), null, 2)}\n`,
|
|
92
|
+
"utf-8",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function getInstalledPreactVersion(root: string): string {
|
|
99
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const packageJsonPath = require.resolve("preact/package.json");
|
|
103
|
+
const packageJson = require(packageJsonPath) as { version?: unknown };
|
|
104
|
+
|
|
105
|
+
if (typeof packageJson.version === "string") {
|
|
106
|
+
return packageJson.version;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`TailorKit requires Preact to build an app. Install preact@^10 and try again.`,
|
|
111
|
+
{ cause: error },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error("Unable to read the installed Preact version.");
|
|
116
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const tailorkitUploadManifestSchema = z.object({
|
|
4
|
+
assets: z.object({
|
|
5
|
+
client: z.literal("client.js"),
|
|
6
|
+
}),
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type TailorKitUploadManifest = z.output<typeof tailorkitUploadManifestSchema>;
|
|
11
|
+
|
|
12
|
+
export const createTailorKitUploadManifest = (): TailorKitUploadManifest =>
|
|
13
|
+
tailorkitUploadManifestSchema.parse({
|
|
14
|
+
assets: {
|
|
15
|
+
client: "client.js",
|
|
16
|
+
},
|
|
17
|
+
version: 1,
|
|
18
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const clientConfigSchema = z.object({
|
|
4
|
+
entry: z.string().default("./src/client.ts"),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const buildConfigSchema = z.object({
|
|
8
|
+
outDir: z.string().default(".tailorkit"),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const tailorkitConfigSchema = z.object({
|
|
12
|
+
appId: z.string().min(1).optional(),
|
|
13
|
+
build: buildConfigSchema.optional(),
|
|
14
|
+
client: clientConfigSchema.optional(),
|
|
15
|
+
host: z.string().url(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type TailorKitConfig = z.input<typeof tailorkitConfigSchema>;
|
|
19
|
+
export type ResolvedTailorKitConfig = z.output<typeof tailorkitConfigSchema>;
|
|
20
|
+
|
|
21
|
+
export { tailorkitConfigSchema };
|
|
22
|
+
|
|
23
|
+
export const defineConfig = (config: TailorKitConfig): TailorKitConfig => config;
|
|
24
|
+
export const defineTailorKitConfig = defineConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineConfig, defineTailorKitConfig, type TailorKitConfig } from "./config";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ResolvedTailorKitConfig } from "./config";
|
|
4
|
+
import { tailorkitConfigSchema } from "./config";
|
|
5
|
+
|
|
6
|
+
export interface LoadedTailorKitConfig {
|
|
7
|
+
config: ResolvedTailorKitConfig;
|
|
8
|
+
filepath: string;
|
|
9
|
+
root: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const explorer = cosmiconfig("tailorkit", {
|
|
13
|
+
searchPlaces: ["tailorkit.config.ts", "tailorkit.config.mjs", "tailorkit.config.js"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const loadTailorKitConfig = async (
|
|
17
|
+
configPath: string | undefined,
|
|
18
|
+
cwd = process.cwd(),
|
|
19
|
+
): Promise<LoadedTailorKitConfig> => {
|
|
20
|
+
const searchFrom = path.resolve(cwd);
|
|
21
|
+
const result =
|
|
22
|
+
configPath === undefined
|
|
23
|
+
? await explorer.search(searchFrom)
|
|
24
|
+
: await explorer.load(path.resolve(searchFrom, configPath));
|
|
25
|
+
|
|
26
|
+
if (result === null) {
|
|
27
|
+
throw new Error(`Could not find tailorkit config from ${searchFrom}.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
config: tailorkitConfigSchema.parse(result.config),
|
|
32
|
+
filepath: result.filepath,
|
|
33
|
+
root: path.dirname(result.filepath),
|
|
34
|
+
};
|
|
35
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createContext, h, render } from "preact";
|
|
2
|
+
import type { ComponentChild, ComponentChildren, ComponentType, VNode } from "preact";
|
|
3
|
+
import { useContext } from "preact/hooks";
|
|
4
|
+
|
|
5
|
+
declare const __PREACT_VERSION__: string;
|
|
6
|
+
|
|
7
|
+
const preactVersion = __PREACT_VERSION__;
|
|
8
|
+
|
|
9
|
+
// oxlint-disable-next-line typescript-eslint/no-empty-interface, typescript-eslint/no-empty-object-type
|
|
10
|
+
export interface TailorKitScreens {}
|
|
11
|
+
|
|
12
|
+
export type ScreenPath = Extract<keyof TailorKitScreens & string, `/${string}`>;
|
|
13
|
+
export type AppScreenPath = ScreenPath;
|
|
14
|
+
|
|
15
|
+
export type ScreenProps<TPath extends ScreenPath> = TailorKitScreens[TPath] extends object
|
|
16
|
+
? TailorKitScreens[TPath]
|
|
17
|
+
: Record<string, never>;
|
|
18
|
+
|
|
19
|
+
export type ScreenPropsForPath<TPath extends AppScreenPath> = TPath extends ScreenPath
|
|
20
|
+
? ScreenProps<TPath>
|
|
21
|
+
: never;
|
|
22
|
+
|
|
23
|
+
export type ScreenContext<TPath extends AppScreenPath> =
|
|
24
|
+
ScreenPropsForPath<TPath> extends { context: infer TContext } ? TContext : Record<string, never>;
|
|
25
|
+
|
|
26
|
+
export type View<TProps extends object = Record<string, never>> = (props: TProps) => ComponentChild;
|
|
27
|
+
|
|
28
|
+
export type ScreenRuntimeProps<TPath extends AppScreenPath> =
|
|
29
|
+
| {
|
|
30
|
+
context: ScreenContext<TPath>;
|
|
31
|
+
screen: TPath;
|
|
32
|
+
status: "ready";
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
context?: never;
|
|
36
|
+
screen: TPath;
|
|
37
|
+
status: "loading";
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
context?: never;
|
|
41
|
+
screen: TPath;
|
|
42
|
+
status: "error";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ScreenDefinition<TPath extends AppScreenPath = AppScreenPath> {
|
|
46
|
+
component: View<ScreenRuntimeProps<TPath>>;
|
|
47
|
+
path: TPath;
|
|
48
|
+
useContext: () => ScreenContext<TPath>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type RegisteredScreens = {
|
|
52
|
+
[TPath in AppScreenPath]: ScreenDefinition<TPath>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type ScreenDefinitions = Partial<RegisteredScreens>;
|
|
56
|
+
|
|
57
|
+
type InvalidScreenPath<TScreens> = Exclude<keyof TScreens & string, `/${string}`>;
|
|
58
|
+
|
|
59
|
+
type RequireScreenPaths<TScreens> =
|
|
60
|
+
InvalidScreenPath<TScreens> extends never
|
|
61
|
+
? unknown
|
|
62
|
+
: {
|
|
63
|
+
readonly __tailorkit_error__: `Screen paths must start with "/". Invalid screen: ${InvalidScreenPath<TScreens>}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ScreenKeyPathMismatch<TScreens> = {
|
|
67
|
+
[TPath in keyof TScreens & string]: TScreens[TPath] extends ScreenDefinition<infer TScreenPath>
|
|
68
|
+
? TScreenPath extends TPath
|
|
69
|
+
? never
|
|
70
|
+
: TPath
|
|
71
|
+
: TPath;
|
|
72
|
+
}[keyof TScreens & string];
|
|
73
|
+
|
|
74
|
+
type RequireMatchingScreenKeys<TScreens> =
|
|
75
|
+
ScreenKeyPathMismatch<TScreens> extends never
|
|
76
|
+
? unknown
|
|
77
|
+
: {
|
|
78
|
+
readonly __tailorkit_error__: `Screen key must match createScreen path. Invalid screen: ${ScreenKeyPathMismatch<TScreens>}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface TailorKitClient<TScreens extends ScreenDefinitions = ScreenDefinitions> {
|
|
82
|
+
screens: TScreens;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TailorKitClientMeta {
|
|
86
|
+
preactVersion: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TailorKitClientRuntime {
|
|
90
|
+
h: typeof h;
|
|
91
|
+
render: (vnode: VNode, parent: Element | Document | ShadowRoot | DocumentFragment) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type TailorKitClientWithMeta<TScreens extends ScreenDefinitions = ScreenDefinitions> =
|
|
95
|
+
TailorKitClient<TScreens> & {
|
|
96
|
+
$meta: TailorKitClientMeta;
|
|
97
|
+
$runtime: TailorKitClientRuntime;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const createScreen = <const TPath extends AppScreenPath>(
|
|
101
|
+
path: TPath,
|
|
102
|
+
options: {
|
|
103
|
+
component: View<Record<string, never>>;
|
|
104
|
+
error?: View<Record<string, never>>;
|
|
105
|
+
loading?: View<Record<string, never>>;
|
|
106
|
+
},
|
|
107
|
+
): ScreenDefinition<TPath> => {
|
|
108
|
+
const Context = createContext<ScreenContext<TPath> | null>(null);
|
|
109
|
+
|
|
110
|
+
const Screen = (props: ScreenRuntimeProps<TPath>) => {
|
|
111
|
+
if (props.status === "loading") {
|
|
112
|
+
return options.loading ? h(options.loading as ComponentType<object>, {}) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (props.status === "error") {
|
|
116
|
+
return options.error ? h(options.error as ComponentType<object>, {}) : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return h(
|
|
120
|
+
Context.Provider,
|
|
121
|
+
{ value: props.context as ScreenContext<TPath> },
|
|
122
|
+
h(options.component as ComponentType<object>, {}),
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
component: Screen,
|
|
128
|
+
path,
|
|
129
|
+
useContext: () => {
|
|
130
|
+
const context = useContext(Context);
|
|
131
|
+
|
|
132
|
+
if (context === null) {
|
|
133
|
+
throw new Error(`Screen context is only available while rendering "${path}".`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return context;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const defineClient = <const TScreens extends ScreenDefinitions>(
|
|
142
|
+
client: TailorKitClient<TScreens> &
|
|
143
|
+
RequireScreenPaths<TScreens> &
|
|
144
|
+
RequireMatchingScreenKeys<TScreens>,
|
|
145
|
+
): TailorKitClientWithMeta<TScreens> => ({
|
|
146
|
+
...client,
|
|
147
|
+
$meta: {
|
|
148
|
+
preactVersion,
|
|
149
|
+
},
|
|
150
|
+
$runtime: {
|
|
151
|
+
h,
|
|
152
|
+
render,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const componentTagPrefix = "tailorkit-";
|
|
157
|
+
|
|
158
|
+
const toComponentTagName = (name: string): string =>
|
|
159
|
+
`${componentTagPrefix}${name
|
|
160
|
+
.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2")
|
|
161
|
+
.replaceAll(/[\s_]+/gu, "-")
|
|
162
|
+
.toLowerCase()}`;
|
|
163
|
+
|
|
164
|
+
const toCallbackEventName = (name: string): string =>
|
|
165
|
+
`tailorkitcallback${name.replaceAll(/[^A-Za-z0-9_$]/gu, "").toLowerCase()}`;
|
|
166
|
+
|
|
167
|
+
const toEventProp = (event: string): string => `on${event}`;
|
|
168
|
+
|
|
169
|
+
export const createRemoteComponent = <TProps extends object, TSlots extends readonly string[]>(
|
|
170
|
+
name: string,
|
|
171
|
+
options: { callbacks?: Record<string, number>; slots: TSlots },
|
|
172
|
+
): View<TProps & { children?: ComponentChildren }> => {
|
|
173
|
+
const tagName = toComponentTagName(name);
|
|
174
|
+
const callbacks = options.callbacks;
|
|
175
|
+
|
|
176
|
+
if (!callbacks || Object.keys(callbacks).length === 0) {
|
|
177
|
+
return function RemoteComponent({ children, ...props }) {
|
|
178
|
+
return h(tagName, props, children);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return function RemoteComponent({ children, ...props }) {
|
|
183
|
+
const nextProps = { ...props } as Record<string, unknown>;
|
|
184
|
+
const callbackMap: Record<string, { callback: string; inputCount: number }> = {};
|
|
185
|
+
|
|
186
|
+
for (const [key, inputCount] of Object.entries(callbacks)) {
|
|
187
|
+
const callback = nextProps[key];
|
|
188
|
+
Reflect.deleteProperty(nextProps, key);
|
|
189
|
+
if (typeof callback !== "function") {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const eventName = toCallbackEventName(key);
|
|
194
|
+
callbackMap[eventName] = { callback: key, inputCount };
|
|
195
|
+
nextProps[toEventProp(eventName)] = (event: { detail?: unknown[] }) => {
|
|
196
|
+
callback(...(event.detail ?? []).slice(0, inputCount));
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Object.keys(callbackMap).length > 0) {
|
|
201
|
+
nextProps["data-tailorkit-callbacks"] = JSON.stringify(callbackMap);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return h(tagName, nextProps, children);
|
|
205
|
+
};
|
|
206
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const MINIMUM_PREACT_MAJOR_VERSION = 10;
|
|
2
|
+
|
|
3
|
+
export interface PreactVersionCheck {
|
|
4
|
+
major: number;
|
|
5
|
+
version: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const parsePreactVersion = (version: string): PreactVersionCheck => {
|
|
9
|
+
const match = /^(\d+)\./u.exec(version);
|
|
10
|
+
|
|
11
|
+
if (!match?.[1]) {
|
|
12
|
+
throw new Error(`Unable to parse Preact version "${version}".`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
major: Number.parseInt(match[1], 10),
|
|
17
|
+
version,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const assertSupportedPreactVersion = (version: string): PreactVersionCheck => {
|
|
22
|
+
const check = parsePreactVersion(version);
|
|
23
|
+
|
|
24
|
+
if (check.major < MINIMUM_PREACT_MAJOR_VERSION) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`TailorKit requires Preact ${MINIMUM_PREACT_MAJOR_VERSION}.0.0 or newer, but found ${version}.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return check;
|
|
31
|
+
};
|