htmx-router 1.0.0-alpha.5 → 1.0.0-pre1

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.
Files changed (58) hide show
  1. package/example/eventdim-react/package.json +67 -0
  2. package/example/eventdim-react/server.js +90 -0
  3. package/example/island-react/global.d.ts +8 -0
  4. package/example/island-react/package.json +38 -0
  5. package/example/island-react/server.js +58 -0
  6. package/global.d.ts +7 -0
  7. package/package.json +10 -8
  8. package/readme.md +17 -212
  9. package/bin/cli/config.d.ts +0 -10
  10. package/bin/cli/config.js +0 -4
  11. package/bin/cli/index.d.ts +0 -2
  12. package/bin/cli/index.js +0 -66
  13. package/bin/client/entry.d.ts +0 -1
  14. package/bin/client/entry.js +0 -12
  15. package/bin/client/index.d.ts +0 -7
  16. package/bin/client/index.js +0 -132
  17. package/bin/client/mount.d.ts +0 -2
  18. package/bin/client/mount.js +0 -116
  19. package/bin/client/watch.d.ts +0 -1
  20. package/bin/client/watch.js +0 -11
  21. package/bin/helper.d.ts +0 -2
  22. package/bin/helper.js +0 -34
  23. package/bin/index.d.ts +0 -11
  24. package/bin/index.js +0 -18
  25. package/bin/request/http.d.ts +0 -10
  26. package/bin/request/http.js +0 -41
  27. package/bin/request/index.d.ts +0 -16
  28. package/bin/request/index.js +0 -6
  29. package/bin/request/native.d.ts +0 -9
  30. package/bin/request/native.js +0 -46
  31. package/bin/response.d.ts +0 -9
  32. package/bin/response.js +0 -46
  33. package/bin/router.d.ts +0 -49
  34. package/bin/router.js +0 -217
  35. package/bin/types.d.ts +0 -10
  36. package/bin/types.js +0 -1
  37. package/bin/util/cookies.d.ts +0 -25
  38. package/bin/util/cookies.js +0 -60
  39. package/bin/util/css.d.ts +0 -13
  40. package/bin/util/css.js +0 -55
  41. package/bin/util/dynamic.d.ts +0 -8
  42. package/bin/util/dynamic.js +0 -40
  43. package/bin/util/endpoint.d.ts +0 -13
  44. package/bin/util/endpoint.js +0 -32
  45. package/bin/util/event-source.d.ts +0 -16
  46. package/bin/util/event-source.js +0 -85
  47. package/bin/util/hash.d.ts +0 -4
  48. package/bin/util/hash.js +0 -10
  49. package/bin/util/index.d.ts +0 -1
  50. package/bin/util/index.js +0 -7
  51. package/bin/util/parameters.d.ts +0 -10
  52. package/bin/util/parameters.js +0 -17
  53. package/bin/util/path-builder.d.ts +0 -1
  54. package/bin/util/path-builder.js +0 -43
  55. package/bin/util/response.d.ts +0 -11
  56. package/bin/util/response.js +0 -46
  57. package/bin/util/shell.d.ts +0 -120
  58. package/bin/util/shell.js +0 -251
package/bin/router.js DELETED
@@ -1,217 +0,0 @@
1
- import * as endpoint from './util/endpoint.js';
2
- import * as dynamic from './util/dynamic.js';
3
- import * as mount from './client/mount.js';
4
- import * as css from './util/css.js';
5
- import { Parameterize } from './util/parameters.js';
6
- import { Cookies } from './util/cookies.js';
7
- export class GenericContext {
8
- request;
9
- headers; // response headers
10
- cookie;
11
- params;
12
- url;
13
- render;
14
- constructor(request, url, renderer) {
15
- this.cookie = new Cookies(request.headers);
16
- this.headers = new Headers();
17
- this.request = request;
18
- this.params = {};
19
- this.url = url;
20
- this.render = renderer;
21
- this.headers.set("x-powered-by", "htmx-router");
22
- }
23
- shape(shape) {
24
- return new RouteContext(this, shape);
25
- }
26
- }
27
- export class RouteContext {
28
- request;
29
- headers; // response headers
30
- cookie;
31
- params;
32
- url;
33
- render;
34
- constructor(base, shape) {
35
- this.params = Parameterize(base.params, shape);
36
- this.cookie = base.cookie;
37
- this.headers = base.headers;
38
- this.request = base.request;
39
- this.render = base.render;
40
- this.url = base.url;
41
- }
42
- }
43
- export class RouteLeaf {
44
- module;
45
- constructor(module) {
46
- this.module = module;
47
- }
48
- async resolve(ctx) {
49
- const res = await this.renderWrapper(ctx);
50
- if (res === null)
51
- return null;
52
- if (res instanceof Response)
53
- return res;
54
- return ctx.render(res);
55
- }
56
- async error(ctx, e) {
57
- if (!this.module.error)
58
- throw e;
59
- const res = await this.module.error(ctx, e);
60
- if (res instanceof Response)
61
- return res;
62
- return ctx.render(res);
63
- }
64
- async renderWrapper(ctx) {
65
- try {
66
- if (!this.module.loader && !this.module.action)
67
- return null;
68
- const context = ctx.shape(this.module.parameters || {});
69
- if (ctx.request.method === "HEAD" || ctx.request.method === "GET") {
70
- if (this.module.loader)
71
- return await this.module.loader(context);
72
- else
73
- return null;
74
- }
75
- if (this.module.action)
76
- return await this.module.action(context);
77
- throw new Response("Method not Allowed", { status: 405, statusText: "Method not Allowed", headers: ctx.headers });
78
- }
79
- catch (e) {
80
- return await this.error(ctx, e);
81
- }
82
- return null;
83
- }
84
- }
85
- export class RouteTree {
86
- root;
87
- nested;
88
- // Leaf nodes
89
- index; // about._index
90
- // Wild card route
91
- slug; // $
92
- wild; // e.g. $userID
93
- wildCard;
94
- constructor(root = true) {
95
- this.root = root;
96
- this.nested = new Map();
97
- this.wildCard = "";
98
- this.slug = null;
99
- this.wild = null;
100
- this.index = null;
101
- }
102
- ingest(path, module) {
103
- if (!Array.isArray(path))
104
- path = path.split("/");
105
- if (path.length === 0 || (path.length == 1 && path[0] === "_index")) {
106
- this.index = new RouteLeaf(module);
107
- return;
108
- }
109
- if (path[0] === "$") {
110
- this.slug = new RouteLeaf(module);
111
- return;
112
- }
113
- if (path[0][0] === "$") {
114
- const wildCard = path[0].slice(1);
115
- // Check wildcard isn't being changed
116
- if (!this.wild) {
117
- this.wildCard = wildCard;
118
- this.wild = new RouteTree(false);
119
- }
120
- else if (wildCard !== this.wildCard) {
121
- throw new Error(`Redefinition of wild card ${this.wildCard} to ${wildCard}`);
122
- }
123
- path.splice(0, 1);
124
- this.wild.ingest(path, module);
125
- return;
126
- }
127
- let next = this.nested.get(path[0]);
128
- if (!next) {
129
- next = new RouteTree(false);
130
- this.nested.set(path[0], next);
131
- }
132
- path.splice(0, 1);
133
- next.ingest(path, module);
134
- }
135
- async resolve(fragments, ctx) {
136
- if (!this.slug)
137
- return await this._resolve(fragments, ctx);
138
- try {
139
- return await this._resolve(fragments, ctx);
140
- }
141
- catch (e) {
142
- return this.unwrap(ctx, e);
143
- }
144
- }
145
- async _resolve(fragments, ctx) {
146
- let res = await this.resolveNative(fragments, ctx)
147
- || await this.resolveIndex(fragments, ctx)
148
- || await this.resolveNext(fragments, ctx)
149
- || await this.resolveWild(fragments, ctx)
150
- || await this.resolveSlug(fragments, ctx);
151
- if (res instanceof Response) {
152
- if (100 <= res.status && res.status <= 399)
153
- return res;
154
- if (res.headers.has("X-Caught"))
155
- return res;
156
- return this.unwrap(ctx, res);
157
- }
158
- return res;
159
- }
160
- async resolveIndex(fragments, ctx) {
161
- if (fragments.length > 0)
162
- return null;
163
- if (!this.index)
164
- return null;
165
- return await this.index.resolve(ctx);
166
- }
167
- async resolveNext(fragments, ctx) {
168
- if (fragments.length < 1)
169
- return null;
170
- const next = this.nested.get(fragments[0]);
171
- if (!next)
172
- return null;
173
- return await next.resolve(fragments.slice(1), ctx);
174
- }
175
- async resolveWild(fragments, ctx) {
176
- if (!this.wild)
177
- return null;
178
- if (fragments.length < 1)
179
- return null;
180
- ctx.params[this.wildCard] = fragments[0];
181
- return this.wild.resolve(fragments.slice(1), ctx);
182
- }
183
- async resolveSlug(fragments, ctx) {
184
- if (!this.slug)
185
- return null;
186
- ctx.params["$"] = fragments.join("/");
187
- const res = this.slug.resolve
188
- ? await this.slug.resolve(ctx)
189
- : null;
190
- return res;
191
- }
192
- async resolveNative(fragments, ctx) {
193
- if (!this.root)
194
- return null;
195
- if (fragments.length < 2)
196
- return null;
197
- if (fragments[0] != "_")
198
- return null;
199
- return await ResolveNatively(fragments, ctx);
200
- }
201
- async unwrap(ctx, res) {
202
- if (!this.slug)
203
- throw res;
204
- const caught = await this.slug.error(ctx, res);
205
- caught.headers.set("X-Caught", "true");
206
- return caught;
207
- }
208
- }
209
- async function ResolveNatively(fragments, ctx) {
210
- switch (fragments[1]) {
211
- case "dynamic": return dynamic._resolve(fragments, ctx);
212
- case "endpoint": return endpoint._resolve(fragments, ctx);
213
- case "mount": return mount._resolve(fragments);
214
- case "style": return css._resolve(fragments);
215
- }
216
- return null;
217
- }
package/bin/types.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import { ParameterShaper } from "./util/parameters.js";
2
- import { RouteContext } from "./router.js";
3
- export type CatchFunction<T> = (args: T, err: unknown) => Promise<Response | JSX.Element>;
4
- export type RenderFunction<T> = (args: T) => Promise<Response | JSX.Element | null>;
5
- export type RouteModule<T extends ParameterShaper> = {
6
- parameters?: T;
7
- loader?: RenderFunction<RouteContext<T>>;
8
- action?: RenderFunction<RouteContext<T>>;
9
- error?: CatchFunction<RouteContext<T>>;
10
- };
package/bin/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,25 +0,0 @@
1
- export interface CookieOptions {
2
- domain?: string | undefined;
3
- expires?: Date;
4
- httpOnly?: boolean;
5
- maxAge?: number;
6
- partitioned?: boolean;
7
- path?: string;
8
- priority?: "low" | "medium" | "high";
9
- sameSite?: "lax" | "strict" | "none";
10
- secure?: boolean;
11
- }
12
- /**
13
- * Helper provided in the Generic and RouteContext which provides reading and updating cookies
14
- */
15
- export declare class Cookies {
16
- private map;
17
- private config;
18
- constructor(headers: Headers);
19
- get(name: string): string | null;
20
- has(name: string): boolean;
21
- set(name: string, value: string, options?: CookieOptions): void;
22
- flash(name: string, value: string): void;
23
- unset(name: string): void;
24
- export(): string[];
25
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Helper provided in the Generic and RouteContext which provides reading and updating cookies
3
- */
4
- export class Cookies {
5
- map;
6
- config;
7
- constructor(headers) {
8
- this.config = {};
9
- this.map = {};
10
- const cookie = headers.get("Cookie");
11
- if (!cookie)
12
- return;
13
- for (const line of cookie.split("; ")) {
14
- const [name, value] = line.split("=");
15
- this.map[name] = value;
16
- }
17
- }
18
- get(name) {
19
- return this.map[name] || null;
20
- }
21
- has(name) {
22
- return name in this.map;
23
- }
24
- set(name, value, options = {}) {
25
- if (!options['path'])
26
- options['path'] = "/";
27
- this.config[name] = options;
28
- this.map[name] = value;
29
- }
30
- flash(name, value) {
31
- return this.set(name, value, { maxAge: 0 });
32
- }
33
- unset(name) {
34
- return this.set(name, "", { maxAge: 0 });
35
- }
36
- export() {
37
- const headers = new Array();
38
- for (const name in this.config) {
39
- let config = "";
40
- for (const opt in this.config[name]) {
41
- const prop = opt === "maxAge"
42
- ? "Max-Age"
43
- : opt[0].toUpperCase() + opt.slice(1);
44
- const raw = this.config[name][opt];
45
- if (raw === true) {
46
- config += `; ${prop}`;
47
- continue;
48
- }
49
- if (raw === false)
50
- continue;
51
- let value = String(raw);
52
- value = value[0].toUpperCase() + value.slice(1);
53
- config += `; ${prop}=${value}`;
54
- }
55
- const cookie = name + "=" + this.map[name] + config + ";";
56
- headers.push(cookie);
57
- }
58
- return headers;
59
- }
60
- }
package/bin/util/css.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Create a new css class to be included in the sheet
3
- * Use .this as your class name in the source, and it will be replaced with a unique name
4
- */
5
- export declare class StyleClass {
6
- readonly name: string;
7
- readonly style: string;
8
- readonly hash: string;
9
- constructor(name: string, style: string);
10
- toString(): string;
11
- }
12
- export declare function GetSheetUrl(): string;
13
- export declare function _resolve(fragments: string[]): Response | null;
package/bin/util/css.js DELETED
@@ -1,55 +0,0 @@
1
- import { QuickHash } from "../util/hash.js";
2
- const classNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
3
- const registry = new Map();
4
- let cache = null;
5
- /**
6
- * Create a new css class to be included in the sheet
7
- * Use .this as your class name in the source, and it will be replaced with a unique name
8
- */
9
- export class StyleClass {
10
- name; // unique name generated based on the original name and hash of the style
11
- style; // the mutated source
12
- hash;
13
- constructor(name, style) {
14
- if (!name.match(classNamePattern))
15
- throw new Error("Cannot use given name for CSS class");
16
- this.hash = QuickHash(style);
17
- this.name = `${name}-${this.hash}`;
18
- style = style.replaceAll(".this", "." + this.name);
19
- this.style = style;
20
- registry.set(this.name, this);
21
- cache = null;
22
- }
23
- toString() {
24
- return this.name;
25
- }
26
- }
27
- function GetSheet() {
28
- return cache || BuildSheet();
29
- }
30
- export function GetSheetUrl() {
31
- const sheet = GetSheet();
32
- return `/_/style/${sheet.hash}.css`;
33
- }
34
- export function _resolve(fragments) {
35
- if (!fragments[2])
36
- return null;
37
- const build = GetSheet();
38
- if (!fragments[2].startsWith(build.hash))
39
- return null;
40
- const headers = new Headers();
41
- headers.set("Content-Type", "text/css");
42
- headers.set("Cache-Control", "public, max-age=604800");
43
- return new Response(build.sheet, { headers });
44
- }
45
- function BuildSheet() {
46
- let composite = "";
47
- let sheet = "";
48
- for (const [key, def] of registry) {
49
- composite += key;
50
- sheet += def.style;
51
- }
52
- const hash = QuickHash(composite);
53
- cache = { hash, sheet };
54
- return cache;
55
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * This whole file is only for internal use but the generated router for the <Dynamic> component
3
- */
4
- import { GenericContext } from "../router.js";
5
- export declare function DynamicReference<T extends Record<string, string>>(loader: Loader<T>, params?: T): string;
6
- type Loader<T> = (ctx: GenericContext, params: T) => Promise<JSX.Element>;
7
- export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
8
- export {};
@@ -1,40 +0,0 @@
1
- /**
2
- * This whole file is only for internal use but the generated router for the <Dynamic> component
3
- */
4
- import { QuickHash } from "../util/hash.js";
5
- const registry = new Map();
6
- const index = new Map();
7
- function Register(load) {
8
- const existing = index.get(load);
9
- if (existing)
10
- return existing;
11
- const hash = QuickHash(String(load));
12
- const name = `${encodeURIComponent(load.name)}-${hash}`;
13
- registry.set(name, load);
14
- const url = `/_/dynamic/${name}`;
15
- index.set(load, url);
16
- return url;
17
- }
18
- export function DynamicReference(loader, params) {
19
- let url = Register(loader);
20
- if (params) {
21
- const query = new URLSearchParams();
22
- if (params)
23
- for (const key in params)
24
- query.set(key, params[key]);
25
- url += "?" + query.toString();
26
- }
27
- return url;
28
- }
29
- export async function _resolve(fragments, ctx) {
30
- if (!fragments[2])
31
- return null;
32
- const endpoint = registry.get(fragments[2]);
33
- if (!endpoint)
34
- return null;
35
- const props = {};
36
- for (const [key, value] of ctx.url.searchParams)
37
- props[key] = value;
38
- ctx.headers.set("X-Partial", "true");
39
- return ctx.render(await endpoint(ctx, props));
40
- }
@@ -1,13 +0,0 @@
1
- import { RenderFunction } from "../types.js";
2
- import { GenericContext } from "../router.js";
3
- /**
4
- * Create a route-less endpoint
5
- * The name is optional and will be inferred from the function if not given (helpful for network waterfalls)
6
- */
7
- export declare class Endpoint {
8
- readonly render: RenderFunction<GenericContext>;
9
- readonly name: string;
10
- readonly url: string;
11
- constructor(render: RenderFunction<GenericContext>, name?: string);
12
- }
13
- export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
@@ -1,32 +0,0 @@
1
- import { QuickHash } from "../util/hash.js";
2
- const registry = new Map();
3
- /**
4
- * Create a route-less endpoint
5
- * The name is optional and will be inferred from the function if not given (helpful for network waterfalls)
6
- */
7
- export class Endpoint {
8
- render;
9
- name;
10
- url;
11
- constructor(render, name) {
12
- this.render = render;
13
- name ||= render.constructor.name;
14
- const hash = QuickHash(String(render));
15
- this.name = name ? `${encodeURIComponent(name)}-${hash}` : hash;
16
- this.url = `/_/endpoint/${this.name}`;
17
- registry.set(this.name, this);
18
- }
19
- }
20
- export async function _resolve(fragments, ctx) {
21
- if (!fragments[2])
22
- return null;
23
- const endpoint = registry.get(fragments[2]);
24
- if (!endpoint)
25
- return null;
26
- const res = await endpoint.render(ctx);
27
- if (res === null)
28
- return null;
29
- if (res instanceof Response)
30
- return res;
31
- return ctx.render(res);
32
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
3
- * Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
4
- */
5
- export declare class EventSourceConnection {
6
- private controller;
7
- private stream;
8
- private timer;
9
- readonly createdAt: number;
10
- constructor(request: Request, keepAlive?: number);
11
- response(): Response;
12
- private keepAlive;
13
- send(type: string, data: string, timeStamp: number): boolean;
14
- isClosed(): boolean;
15
- close(unlink?: boolean): boolean;
16
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * Helper for Server-Sent-Events, with auto close on SIGTERM and SIGHUP messages
3
- * Includes a keep alive empty packet sent every 30sec (because Chrome implodes at 120sec, and can be unreliable at 60sec)
4
- */
5
- export class EventSourceConnection {
6
- controller;
7
- stream;
8
- timer;
9
- createdAt; // unix time
10
- constructor(request, keepAlive = 30_000) {
11
- this.createdAt = Date.now();
12
- this.controller = null;
13
- this.stream = new ReadableStream({
14
- start: (c) => { this.controller = c; },
15
- cancel: () => { this.close(); }
16
- });
17
- request.signal.addEventListener('abort', () => this.close());
18
- this.timer = setInterval(() => this.keepAlive(), keepAlive);
19
- register.push(this);
20
- }
21
- response() {
22
- const headers = new Headers();
23
- headers.set("Content-Type", "text/event-stream");
24
- headers.set("Transfer-Encoding", "chunked");
25
- headers.set("Connection", "keep-alive");
26
- headers.set("Keep-Alive", `timeout=120`);
27
- return new Response(this.stream, { headers });
28
- }
29
- keepAlive() {
30
- if (!this.controller)
31
- return;
32
- try {
33
- this.controller.enqueue("\n\n");
34
- }
35
- catch (e) {
36
- console.error(e);
37
- this.close(); // unbind on failure
38
- }
39
- }
40
- send(type, data, timeStamp) {
41
- if (!this.controller)
42
- return false;
43
- try {
44
- this.controller.enqueue(`event: ${type}\ndata: [${data},${timeStamp}]\n\n`);
45
- return true;
46
- }
47
- catch (e) {
48
- console.error(e);
49
- this.close(); // unbind on failure
50
- return false;
51
- }
52
- }
53
- isClosed() {
54
- return this.controller === null;
55
- }
56
- close(unlink = true) {
57
- clearInterval(this.timer);
58
- if (!this.controller)
59
- return false;
60
- if (unlink) {
61
- const i = register.indexOf(this);
62
- if (i !== -1)
63
- register.splice(i, 1);
64
- }
65
- try {
66
- this.controller?.close();
67
- }
68
- catch (e) {
69
- console.error(e);
70
- this.controller = null;
71
- return false;
72
- }
73
- this.controller = null;
74
- return true;
75
- }
76
- }
77
- // Auto close all SSE streams when shutdown requested
78
- // Without this graceful shutdowns will hang indefinitely
79
- const register = new Array();
80
- function CloseAll() {
81
- for (const connection of register)
82
- connection.close(false); // don't waste time unregistering
83
- }
84
- process.on('SIGTERM', CloseAll);
85
- process.on('SIGHUP', CloseAll);
@@ -1,4 +0,0 @@
1
- /**
2
- * Not the best hash in the world, but it's something really fast that will work on all JS runtimes
3
- */
4
- export declare function QuickHash(input: string): string;
package/bin/util/hash.js DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * Not the best hash in the world, but it's something really fast that will work on all JS runtimes
3
- */
4
- export function QuickHash(input) {
5
- let hash = 0;
6
- for (let i = 0; i < input.length; i++) {
7
- hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
8
- }
9
- return hash.toString(36).slice(0, 5);
10
- }
@@ -1 +0,0 @@
1
- export declare function MergeHeaders(base: Headers, extension: Headers, override: boolean): void;
package/bin/util/index.js DELETED
@@ -1,7 +0,0 @@
1
- export function MergeHeaders(base, extension, override) {
2
- extension.forEach((val, key) => {
3
- if (!override && base.has(key))
4
- return;
5
- base.set(key, val);
6
- });
7
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * This is used by GenericContext to convert itself to a RouteContext<T>
3
- */
4
- export type Parameterized<T extends ParameterShaper> = {
5
- [K in keyof T]: ReturnType<T[K]>;
6
- };
7
- export type ParameterShaper = Record<string, (val: string) => any>;
8
- export declare function Parameterize<T extends ParameterShaper>(params: {
9
- [key: string]: string;
10
- }, shape: T): Parameterized<T>;
@@ -1,17 +0,0 @@
1
- /**
2
- * This is used by GenericContext to convert itself to a RouteContext<T>
3
- */
4
- export function Parameterize(params, shape) {
5
- const out = {};
6
- for (const key in shape) {
7
- if (!(key in params))
8
- console.warn(`Parameter ${key} not present in route, but defined in parameters`);
9
- const func = shape[key];
10
- const val = func(params[key] || "");
11
- // NaN moment
12
- if (func === Number && typeof val === "number" && isNaN(val))
13
- throw new Error("Invalid Number");
14
- out[key] = val;
15
- }
16
- return out;
17
- }
@@ -1 +0,0 @@
1
- export {};