@squaredr/fieldcraft-supabase 1.0.0 → 1.1.1
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 +8 -0
- package/dist/index.d.mts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +100 -0
- package/dist/index.mjs +99 -0
- package/package.json +21 -11
package/README.md
CHANGED
|
@@ -84,6 +84,14 @@ const draftAdapter = createSupabaseDraftAdapter({
|
|
|
84
84
|
|
|
85
85
|
The adapter is compatible with Supabase RLS policies. Configure your table's RLS rules to control which users can read/write responses.
|
|
86
86
|
|
|
87
|
+
## Community
|
|
88
|
+
|
|
89
|
+
[](https://discord.gg/YOUR_INVITE_LINK)
|
|
90
|
+
|
|
91
|
+
- [Discord](https://discord.gg/YOUR_INVITE_LINK) — Get help, share projects, request features
|
|
92
|
+
- [Docs](https://squaredr.tech/products/fieldcraft/docs/adapters) — Adapter documentation
|
|
93
|
+
- [Pro Tools](https://squaredr.tech/products/fieldcraft/admin-pro) — Visual FormBuilder, SchemaEditor, ResponseViewer, ThemeEditor
|
|
94
|
+
|
|
87
95
|
## License
|
|
88
96
|
|
|
89
97
|
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormResponse, SubmitAdapter, DraftAdapter } from '@squaredr/fieldcraft-core';
|
|
1
|
+
import { FormResponse, SubmitAdapter, DraftAdapter, SchemaAdapter } from '@squaredr/fieldcraft-core';
|
|
2
2
|
|
|
3
3
|
type SupabaseAdapterConfig = {
|
|
4
4
|
/** Supabase client instance from @supabase/supabase-js */
|
|
@@ -34,9 +34,22 @@ type SupabaseQueryBuilder = {
|
|
|
34
34
|
}): SupabaseFilterBuilder;
|
|
35
35
|
delete(): SupabaseFilterBuilder;
|
|
36
36
|
};
|
|
37
|
+
type SupabaseSchemaAdapterConfig = {
|
|
38
|
+
/** Supabase client instance */
|
|
39
|
+
client: SupabaseClient;
|
|
40
|
+
/** Table name for schemas (default: "formengine_schemas") */
|
|
41
|
+
table?: string;
|
|
42
|
+
/** Called on error */
|
|
43
|
+
onError?: (error: Error) => void;
|
|
44
|
+
};
|
|
37
45
|
type SupabaseFilterBuilder = {
|
|
38
46
|
eq(column: string, value: unknown): SupabaseFilterBuilder;
|
|
39
47
|
gt(column: string, value: unknown): SupabaseFilterBuilder;
|
|
48
|
+
ilike(column: string, pattern: string): SupabaseFilterBuilder;
|
|
49
|
+
order(column: string, options?: {
|
|
50
|
+
ascending?: boolean;
|
|
51
|
+
}): SupabaseFilterBuilder;
|
|
52
|
+
range(from: number, to: number): SupabaseFilterBuilder;
|
|
40
53
|
single(): Promise<{
|
|
41
54
|
data: Record<string, unknown> | null;
|
|
42
55
|
error: Error | null;
|
|
@@ -54,9 +67,28 @@ declare function createSupabaseAdapter(config: SupabaseAdapterConfig): SubmitAda
|
|
|
54
67
|
/** Create a Supabase draft adapter. */
|
|
55
68
|
declare function createSupabaseDraftAdapter(config: SupabaseDraftAdapterConfig): DraftAdapter;
|
|
56
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Create a Supabase schema adapter for persisting FormEngineSchema documents.
|
|
72
|
+
*
|
|
73
|
+
* Requires a `formengine_schemas` table (or custom name) with the following columns:
|
|
74
|
+
*
|
|
75
|
+
* ```sql
|
|
76
|
+
* create table formengine_schemas (
|
|
77
|
+
* id text primary key,
|
|
78
|
+
* title text not null default '',
|
|
79
|
+
* version text not null default '1.0.0',
|
|
80
|
+
* status text not null default 'draft',
|
|
81
|
+
* definition jsonb not null,
|
|
82
|
+
* created_at timestamptz not null default now(),
|
|
83
|
+
* updated_at timestamptz not null default now()
|
|
84
|
+
* );
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
declare function createSupabaseSchemaAdapter(config: SupabaseSchemaAdapterConfig): SchemaAdapter;
|
|
88
|
+
|
|
57
89
|
/** Encrypt specified fields in a values object. */
|
|
58
90
|
declare function encryptFields(values: Record<string, unknown>, fieldIds: string[], keyBase64: string): Record<string, unknown>;
|
|
59
91
|
/** Decrypt specified fields in a values object. */
|
|
60
92
|
declare function decryptFields(values: Record<string, unknown>, fieldIds: string[], keyBase64: string): Record<string, unknown>;
|
|
61
93
|
|
|
62
|
-
export { type SupabaseAdapterConfig, type SupabaseClient, type SupabaseDraftAdapterConfig, createSupabaseAdapter, createSupabaseDraftAdapter, decryptFields, encryptFields };
|
|
94
|
+
export { type SupabaseAdapterConfig, type SupabaseClient, type SupabaseDraftAdapterConfig, type SupabaseSchemaAdapterConfig, createSupabaseAdapter, createSupabaseDraftAdapter, createSupabaseSchemaAdapter, decryptFields, encryptFields };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormResponse, SubmitAdapter, DraftAdapter } from '@squaredr/fieldcraft-core';
|
|
1
|
+
import { FormResponse, SubmitAdapter, DraftAdapter, SchemaAdapter } from '@squaredr/fieldcraft-core';
|
|
2
2
|
|
|
3
3
|
type SupabaseAdapterConfig = {
|
|
4
4
|
/** Supabase client instance from @supabase/supabase-js */
|
|
@@ -34,9 +34,22 @@ type SupabaseQueryBuilder = {
|
|
|
34
34
|
}): SupabaseFilterBuilder;
|
|
35
35
|
delete(): SupabaseFilterBuilder;
|
|
36
36
|
};
|
|
37
|
+
type SupabaseSchemaAdapterConfig = {
|
|
38
|
+
/** Supabase client instance */
|
|
39
|
+
client: SupabaseClient;
|
|
40
|
+
/** Table name for schemas (default: "formengine_schemas") */
|
|
41
|
+
table?: string;
|
|
42
|
+
/** Called on error */
|
|
43
|
+
onError?: (error: Error) => void;
|
|
44
|
+
};
|
|
37
45
|
type SupabaseFilterBuilder = {
|
|
38
46
|
eq(column: string, value: unknown): SupabaseFilterBuilder;
|
|
39
47
|
gt(column: string, value: unknown): SupabaseFilterBuilder;
|
|
48
|
+
ilike(column: string, pattern: string): SupabaseFilterBuilder;
|
|
49
|
+
order(column: string, options?: {
|
|
50
|
+
ascending?: boolean;
|
|
51
|
+
}): SupabaseFilterBuilder;
|
|
52
|
+
range(from: number, to: number): SupabaseFilterBuilder;
|
|
40
53
|
single(): Promise<{
|
|
41
54
|
data: Record<string, unknown> | null;
|
|
42
55
|
error: Error | null;
|
|
@@ -54,9 +67,28 @@ declare function createSupabaseAdapter(config: SupabaseAdapterConfig): SubmitAda
|
|
|
54
67
|
/** Create a Supabase draft adapter. */
|
|
55
68
|
declare function createSupabaseDraftAdapter(config: SupabaseDraftAdapterConfig): DraftAdapter;
|
|
56
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Create a Supabase schema adapter for persisting FormEngineSchema documents.
|
|
72
|
+
*
|
|
73
|
+
* Requires a `formengine_schemas` table (or custom name) with the following columns:
|
|
74
|
+
*
|
|
75
|
+
* ```sql
|
|
76
|
+
* create table formengine_schemas (
|
|
77
|
+
* id text primary key,
|
|
78
|
+
* title text not null default '',
|
|
79
|
+
* version text not null default '1.0.0',
|
|
80
|
+
* status text not null default 'draft',
|
|
81
|
+
* definition jsonb not null,
|
|
82
|
+
* created_at timestamptz not null default now(),
|
|
83
|
+
* updated_at timestamptz not null default now()
|
|
84
|
+
* );
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
declare function createSupabaseSchemaAdapter(config: SupabaseSchemaAdapterConfig): SchemaAdapter;
|
|
88
|
+
|
|
57
89
|
/** Encrypt specified fields in a values object. */
|
|
58
90
|
declare function encryptFields(values: Record<string, unknown>, fieldIds: string[], keyBase64: string): Record<string, unknown>;
|
|
59
91
|
/** Decrypt specified fields in a values object. */
|
|
60
92
|
declare function decryptFields(values: Record<string, unknown>, fieldIds: string[], keyBase64: string): Record<string, unknown>;
|
|
61
93
|
|
|
62
|
-
export { type SupabaseAdapterConfig, type SupabaseClient, type SupabaseDraftAdapterConfig, createSupabaseAdapter, createSupabaseDraftAdapter, decryptFields, encryptFields };
|
|
94
|
+
export { type SupabaseAdapterConfig, type SupabaseClient, type SupabaseDraftAdapterConfig, type SupabaseSchemaAdapterConfig, createSupabaseAdapter, createSupabaseDraftAdapter, createSupabaseSchemaAdapter, decryptFields, encryptFields };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
createSupabaseAdapter: () => createSupabaseAdapter,
|
|
34
34
|
createSupabaseDraftAdapter: () => createSupabaseDraftAdapter,
|
|
35
|
+
createSupabaseSchemaAdapter: () => createSupabaseSchemaAdapter,
|
|
35
36
|
decryptFields: () => decryptFields,
|
|
36
37
|
encryptFields: () => encryptFields
|
|
37
38
|
});
|
|
@@ -166,10 +167,109 @@ function createSupabaseDraftAdapter(config) {
|
|
|
166
167
|
}
|
|
167
168
|
};
|
|
168
169
|
}
|
|
170
|
+
|
|
171
|
+
// src/supabase-schema-adapter.ts
|
|
172
|
+
function createSupabaseSchemaAdapter(config) {
|
|
173
|
+
const tableName = config.table ?? "formengine_schemas";
|
|
174
|
+
return {
|
|
175
|
+
name: "supabase-schema",
|
|
176
|
+
async save(schema) {
|
|
177
|
+
const { error } = await config.client.from(tableName).upsert(
|
|
178
|
+
{
|
|
179
|
+
id: schema.id,
|
|
180
|
+
title: schema.title ?? "",
|
|
181
|
+
version: schema.version ?? "1.0.0",
|
|
182
|
+
status: "draft",
|
|
183
|
+
definition: schema,
|
|
184
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
185
|
+
},
|
|
186
|
+
{ onConflict: "id" }
|
|
187
|
+
);
|
|
188
|
+
if (error) {
|
|
189
|
+
const err = new Error(`Supabase schema save failed: ${error.message}`);
|
|
190
|
+
config.onError?.(err);
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async load(schemaId) {
|
|
195
|
+
const { data, error } = await config.client.from(tableName).select("definition").eq("id", schemaId).single();
|
|
196
|
+
if (error || !data) return null;
|
|
197
|
+
return data.definition;
|
|
198
|
+
},
|
|
199
|
+
async delete(schemaId) {
|
|
200
|
+
const { error } = await config.client.from(tableName).delete().eq("id", schemaId);
|
|
201
|
+
if (error) {
|
|
202
|
+
const err = new Error(`Supabase schema delete failed: ${error.message}`);
|
|
203
|
+
config.onError?.(err);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
async list(params) {
|
|
208
|
+
const page = params?.page ?? 1;
|
|
209
|
+
const pageSize = params?.pageSize ?? 20;
|
|
210
|
+
const from = (page - 1) * pageSize;
|
|
211
|
+
const to = from + pageSize - 1;
|
|
212
|
+
let query = config.client.from(tableName).select("id, title, version, status, created_at, updated_at");
|
|
213
|
+
if (params?.search) {
|
|
214
|
+
query = query.ilike("title", `%${params.search}%`);
|
|
215
|
+
}
|
|
216
|
+
if (params?.status) {
|
|
217
|
+
query = query.eq("status", params.status);
|
|
218
|
+
}
|
|
219
|
+
const sortBy = params?.sortBy ?? "updatedAt";
|
|
220
|
+
const sortOrder = params?.sortOrder ?? "desc";
|
|
221
|
+
const columnMap = {
|
|
222
|
+
title: "title",
|
|
223
|
+
updatedAt: "updated_at",
|
|
224
|
+
createdAt: "created_at"
|
|
225
|
+
};
|
|
226
|
+
const sortColumn = columnMap[sortBy] ?? "updated_at";
|
|
227
|
+
query = query.order(sortColumn, { ascending: sortOrder === "asc" }).range(from, to);
|
|
228
|
+
const { data, error } = await query.then((res) => res);
|
|
229
|
+
if (error || !data) {
|
|
230
|
+
return { items: [], total: 0, page, pageSize };
|
|
231
|
+
}
|
|
232
|
+
const items = data.map((row) => ({
|
|
233
|
+
id: row.id,
|
|
234
|
+
title: row.title,
|
|
235
|
+
version: row.version,
|
|
236
|
+
updatedAt: row.updated_at,
|
|
237
|
+
createdAt: row.created_at,
|
|
238
|
+
status: row.status
|
|
239
|
+
}));
|
|
240
|
+
return {
|
|
241
|
+
items,
|
|
242
|
+
total: items.length < pageSize ? from + items.length : from + pageSize + 1,
|
|
243
|
+
page,
|
|
244
|
+
pageSize
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
onError: config.onError
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/index.ts
|
|
252
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
253
|
+
const _fc_banner = `
|
|
254
|
+
%c FieldCraft Supabase %c v1.1.1
|
|
255
|
+
|
|
256
|
+
%cField-level encryption \xB7 RLS \xB7 Schema CRUD
|
|
257
|
+
|
|
258
|
+
Docs \u2192 https://squaredr.tech/products/fieldcraft/docs/adapters
|
|
259
|
+
Discord \u2192 https://discord.gg/YOUR_INVITE_LINK
|
|
260
|
+
`;
|
|
261
|
+
console.log(
|
|
262
|
+
_fc_banner,
|
|
263
|
+
"background:#2563eb;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px 0 0 3px",
|
|
264
|
+
"background:#1e40af;color:#fff;padding:2px 6px;border-radius:0 3px 3px 0",
|
|
265
|
+
"color:#6b7280"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
169
268
|
// Annotate the CommonJS export names for ESM import in node:
|
|
170
269
|
0 && (module.exports = {
|
|
171
270
|
createSupabaseAdapter,
|
|
172
271
|
createSupabaseDraftAdapter,
|
|
272
|
+
createSupabaseSchemaAdapter,
|
|
173
273
|
decryptFields,
|
|
174
274
|
encryptFields
|
|
175
275
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -127,9 +127,108 @@ function createSupabaseDraftAdapter(config) {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
// src/supabase-schema-adapter.ts
|
|
132
|
+
function createSupabaseSchemaAdapter(config) {
|
|
133
|
+
const tableName = config.table ?? "formengine_schemas";
|
|
134
|
+
return {
|
|
135
|
+
name: "supabase-schema",
|
|
136
|
+
async save(schema) {
|
|
137
|
+
const { error } = await config.client.from(tableName).upsert(
|
|
138
|
+
{
|
|
139
|
+
id: schema.id,
|
|
140
|
+
title: schema.title ?? "",
|
|
141
|
+
version: schema.version ?? "1.0.0",
|
|
142
|
+
status: "draft",
|
|
143
|
+
definition: schema,
|
|
144
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
145
|
+
},
|
|
146
|
+
{ onConflict: "id" }
|
|
147
|
+
);
|
|
148
|
+
if (error) {
|
|
149
|
+
const err = new Error(`Supabase schema save failed: ${error.message}`);
|
|
150
|
+
config.onError?.(err);
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
async load(schemaId) {
|
|
155
|
+
const { data, error } = await config.client.from(tableName).select("definition").eq("id", schemaId).single();
|
|
156
|
+
if (error || !data) return null;
|
|
157
|
+
return data.definition;
|
|
158
|
+
},
|
|
159
|
+
async delete(schemaId) {
|
|
160
|
+
const { error } = await config.client.from(tableName).delete().eq("id", schemaId);
|
|
161
|
+
if (error) {
|
|
162
|
+
const err = new Error(`Supabase schema delete failed: ${error.message}`);
|
|
163
|
+
config.onError?.(err);
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
async list(params) {
|
|
168
|
+
const page = params?.page ?? 1;
|
|
169
|
+
const pageSize = params?.pageSize ?? 20;
|
|
170
|
+
const from = (page - 1) * pageSize;
|
|
171
|
+
const to = from + pageSize - 1;
|
|
172
|
+
let query = config.client.from(tableName).select("id, title, version, status, created_at, updated_at");
|
|
173
|
+
if (params?.search) {
|
|
174
|
+
query = query.ilike("title", `%${params.search}%`);
|
|
175
|
+
}
|
|
176
|
+
if (params?.status) {
|
|
177
|
+
query = query.eq("status", params.status);
|
|
178
|
+
}
|
|
179
|
+
const sortBy = params?.sortBy ?? "updatedAt";
|
|
180
|
+
const sortOrder = params?.sortOrder ?? "desc";
|
|
181
|
+
const columnMap = {
|
|
182
|
+
title: "title",
|
|
183
|
+
updatedAt: "updated_at",
|
|
184
|
+
createdAt: "created_at"
|
|
185
|
+
};
|
|
186
|
+
const sortColumn = columnMap[sortBy] ?? "updated_at";
|
|
187
|
+
query = query.order(sortColumn, { ascending: sortOrder === "asc" }).range(from, to);
|
|
188
|
+
const { data, error } = await query.then((res) => res);
|
|
189
|
+
if (error || !data) {
|
|
190
|
+
return { items: [], total: 0, page, pageSize };
|
|
191
|
+
}
|
|
192
|
+
const items = data.map((row) => ({
|
|
193
|
+
id: row.id,
|
|
194
|
+
title: row.title,
|
|
195
|
+
version: row.version,
|
|
196
|
+
updatedAt: row.updated_at,
|
|
197
|
+
createdAt: row.created_at,
|
|
198
|
+
status: row.status
|
|
199
|
+
}));
|
|
200
|
+
return {
|
|
201
|
+
items,
|
|
202
|
+
total: items.length < pageSize ? from + items.length : from + pageSize + 1,
|
|
203
|
+
page,
|
|
204
|
+
pageSize
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
onError: config.onError
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/index.ts
|
|
212
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
213
|
+
const _fc_banner = `
|
|
214
|
+
%c FieldCraft Supabase %c v1.1.1
|
|
215
|
+
|
|
216
|
+
%cField-level encryption \xB7 RLS \xB7 Schema CRUD
|
|
217
|
+
|
|
218
|
+
Docs \u2192 https://squaredr.tech/products/fieldcraft/docs/adapters
|
|
219
|
+
Discord \u2192 https://discord.gg/YOUR_INVITE_LINK
|
|
220
|
+
`;
|
|
221
|
+
console.log(
|
|
222
|
+
_fc_banner,
|
|
223
|
+
"background:#2563eb;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px 0 0 3px",
|
|
224
|
+
"background:#1e40af;color:#fff;padding:2px 6px;border-radius:0 3px 3px 0",
|
|
225
|
+
"color:#6b7280"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
130
228
|
export {
|
|
131
229
|
createSupabaseAdapter,
|
|
132
230
|
createSupabaseDraftAdapter,
|
|
231
|
+
createSupabaseSchemaAdapter,
|
|
133
232
|
decryptFields,
|
|
134
233
|
encryptFields
|
|
135
234
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squaredr/fieldcraft-supabase",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "FieldCraft Supabase storage adapter with field-level AES-256-GCM encryption",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,16 +12,13 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
16
19
|
"sideEffects": false,
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"typecheck": "tsc --noEmit",
|
|
21
|
-
"clean": "rm -rf dist"
|
|
22
|
-
},
|
|
23
20
|
"dependencies": {
|
|
24
|
-
"@squaredr/fieldcraft-core": "^1.
|
|
21
|
+
"@squaredr/fieldcraft-core": "^1.2.0"
|
|
25
22
|
},
|
|
26
23
|
"peerDependencies": {
|
|
27
24
|
"@supabase/supabase-js": "^2.0"
|
|
@@ -43,8 +40,21 @@
|
|
|
43
40
|
"url": "git+https://github.com/SquaredR98/fieldcraft.git",
|
|
44
41
|
"directory": "packages/adapters/supabase"
|
|
45
42
|
},
|
|
46
|
-
"keywords": [
|
|
43
|
+
"keywords": [
|
|
44
|
+
"fieldcraft",
|
|
45
|
+
"form-engine",
|
|
46
|
+
"supabase",
|
|
47
|
+
"adapter",
|
|
48
|
+
"storage",
|
|
49
|
+
"encryption"
|
|
50
|
+
],
|
|
47
51
|
"engines": {
|
|
48
52
|
"node": ">=18"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"clean": "rm -rf dist"
|
|
49
59
|
}
|
|
50
|
-
}
|
|
60
|
+
}
|