@feltdb/core 0.6.2 → 0.6.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/dist/cli/commands.js +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +0 -4
- package/dist/workspace/workspace-connection.d.ts +8 -20
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +165 -39
- package/package.json +2 -1
package/dist/cli/commands.js
CHANGED
|
@@ -525,6 +525,7 @@ async function handleDev(args) {
|
|
|
525
525
|
if (workspace) {
|
|
526
526
|
const token = generatePairingToken();
|
|
527
527
|
token.workspaceId = workspace.workspaceId;
|
|
528
|
+
token.endpoint = process.env.VITE_FELTDB_URL;
|
|
528
529
|
persistPairingToken(process.cwd(), token);
|
|
529
530
|
pairingToken = token.token;
|
|
530
531
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as path from 'path';
|
|
|
23
23
|
import * as readline from 'readline';
|
|
24
24
|
import { getClient } from './api-client.js';
|
|
25
25
|
import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
|
|
26
|
-
const VERSION = '0.6.
|
|
26
|
+
const VERSION = '0.6.3';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.6.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.6.3';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -5856,10 +5856,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
5856
5856
|
axum::routing::patch(platform_update_application_member),
|
|
5857
5857
|
)
|
|
5858
5858
|
.route("/api/tenants/{tenant_id}", get(get_tenant))
|
|
5859
|
-
.route(
|
|
5860
|
-
"/api/certification/fixtures/{application_id}",
|
|
5861
|
-
axum::routing::delete(delete_certification_fixture),
|
|
5862
|
-
)
|
|
5863
5859
|
.route("/api/keys", get(list_keys).post(create_key))
|
|
5864
5860
|
.route("/api/keys/{id}", axum::routing::delete(revoke_key))
|
|
5865
5861
|
.route(
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* to a FeltDB Development Workspace.
|
|
6
6
|
*/
|
|
7
7
|
import type { ClientType } from './workspace-types.js';
|
|
8
|
+
import type { WorkspaceEventPayload } from './workspace-types.js';
|
|
8
9
|
export interface WorkspaceConnectionOptions {
|
|
9
10
|
pairingCode?: string;
|
|
10
11
|
workspaceId?: string;
|
|
@@ -26,6 +27,7 @@ export interface PairingTokenData {
|
|
|
26
27
|
token: string;
|
|
27
28
|
workspaceId: string;
|
|
28
29
|
expiresAt: number;
|
|
30
|
+
endpoint?: string;
|
|
29
31
|
}
|
|
30
32
|
export interface ClientInfo {
|
|
31
33
|
clientId: string;
|
|
@@ -101,8 +103,9 @@ export declare class DevelopmentWorkspaceConnection {
|
|
|
101
103
|
clientType: ClientType;
|
|
102
104
|
private options;
|
|
103
105
|
private subscriptions;
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
+
connectedAt: number;
|
|
107
|
+
private transport;
|
|
108
|
+
private unsubscribeEvents;
|
|
106
109
|
constructor(workspaceId: string, clientId: string, clientType: ClientType, options: WorkspaceConnectionOptions);
|
|
107
110
|
/**
|
|
108
111
|
* Establish connection to workspace
|
|
@@ -112,10 +115,6 @@ export declare class DevelopmentWorkspaceConnection {
|
|
|
112
115
|
* Close connection to workspace
|
|
113
116
|
*/
|
|
114
117
|
disconnect(): Promise<void>;
|
|
115
|
-
/**
|
|
116
|
-
* Register this client as connected
|
|
117
|
-
*/
|
|
118
|
-
private registerClient;
|
|
119
118
|
/**
|
|
120
119
|
* Get capabilities for this client type
|
|
121
120
|
*/
|
|
@@ -123,7 +122,7 @@ export declare class DevelopmentWorkspaceConnection {
|
|
|
123
122
|
/**
|
|
124
123
|
* List connected clients in workspace
|
|
125
124
|
*/
|
|
126
|
-
clients(): ClientInfo[]
|
|
125
|
+
clients(): Promise<ClientInfo[]>;
|
|
127
126
|
/**
|
|
128
127
|
* Subscribe to workspace entity changes
|
|
129
128
|
*
|
|
@@ -157,18 +156,7 @@ export declare class DevelopmentWorkspaceConnection {
|
|
|
157
156
|
* Notify subscribers of entity changes
|
|
158
157
|
*/
|
|
159
158
|
private notifySubscribers;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
* Event payload for workspace changes
|
|
163
|
-
*/
|
|
164
|
-
export interface WorkspaceEventPayload<T = unknown> {
|
|
165
|
-
id: string;
|
|
166
|
-
workspaceId: string;
|
|
167
|
-
collection: string;
|
|
168
|
-
entityId: string;
|
|
169
|
-
type: 'created' | 'updated' | 'deleted';
|
|
170
|
-
value?: T;
|
|
171
|
-
timestamp: number;
|
|
172
|
-
originClientId: string;
|
|
159
|
+
private requireTransport;
|
|
160
|
+
private resolveEndpoint;
|
|
173
161
|
}
|
|
174
162
|
//# sourceMappingURL=workspace-connection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { HttpJsDb } from '../http-db.js';
|
|
9
10
|
/**
|
|
10
11
|
* Discover workspace from .feltdb/workspace.json
|
|
11
12
|
*
|
|
@@ -109,7 +110,9 @@ export async function connectDevelopmentWorkspace(options) {
|
|
|
109
110
|
}
|
|
110
111
|
const clientId = options.clientId || generateClientId(options.clientType);
|
|
111
112
|
const clientType = options.clientType || 'other';
|
|
112
|
-
|
|
113
|
+
const connection = new DevelopmentWorkspaceConnection(workspaceId, clientId, clientType, options);
|
|
114
|
+
await connection.connect();
|
|
115
|
+
return connection;
|
|
113
116
|
}
|
|
114
117
|
/**
|
|
115
118
|
* Generate a unique client ID based on type
|
|
@@ -130,7 +133,8 @@ export class DevelopmentWorkspaceConnection {
|
|
|
130
133
|
constructor(workspaceId, clientId, clientType, options) {
|
|
131
134
|
this.subscriptions = new Map();
|
|
132
135
|
this.connectedAt = 0;
|
|
133
|
-
this.
|
|
136
|
+
this.transport = null;
|
|
137
|
+
this.unsubscribeEvents = null;
|
|
134
138
|
this.workspaceId = workspaceId;
|
|
135
139
|
this.clientId = clientId;
|
|
136
140
|
this.clientType = clientType;
|
|
@@ -140,26 +144,34 @@ export class DevelopmentWorkspaceConnection {
|
|
|
140
144
|
* Establish connection to workspace
|
|
141
145
|
*/
|
|
142
146
|
async connect() {
|
|
143
|
-
this.
|
|
144
|
-
|
|
147
|
+
if (this.transport)
|
|
148
|
+
return;
|
|
149
|
+
const endpoint = await this.resolveEndpoint();
|
|
150
|
+
const transport = new HttpWorkspaceTransport(endpoint, this.options.auth?.token ?? '');
|
|
151
|
+
await transport.assertReachable();
|
|
152
|
+
const connectedAt = Date.now();
|
|
153
|
+
await transport.registerClient(this.workspaceId, {
|
|
154
|
+
clientId: this.clientId,
|
|
155
|
+
clientType: this.clientType,
|
|
156
|
+
connectedAt,
|
|
157
|
+
capabilities: this.getCapabilities(),
|
|
158
|
+
});
|
|
159
|
+
this.transport = transport;
|
|
160
|
+
this.unsubscribeEvents = transport.subscribe(this.workspaceId, event => this.notifySubscribers(event));
|
|
161
|
+
this.connectedAt = connectedAt;
|
|
145
162
|
}
|
|
146
163
|
/**
|
|
147
164
|
* Close connection to workspace
|
|
148
165
|
*/
|
|
149
166
|
async disconnect() {
|
|
167
|
+
this.unsubscribeEvents?.();
|
|
168
|
+
this.unsubscribeEvents = null;
|
|
169
|
+
if (this.transport) {
|
|
170
|
+
await this.transport.unregisterClient(this.workspaceId, this.clientId);
|
|
171
|
+
}
|
|
150
172
|
this.subscriptions.clear();
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Register this client as connected
|
|
155
|
-
*/
|
|
156
|
-
registerClient() {
|
|
157
|
-
this.connectedClients.set(this.clientId, {
|
|
158
|
-
clientId: this.clientId,
|
|
159
|
-
clientType: this.clientType,
|
|
160
|
-
connectedAt: this.connectedAt,
|
|
161
|
-
capabilities: this.getCapabilities(),
|
|
162
|
-
});
|
|
173
|
+
this.transport = null;
|
|
174
|
+
this.connectedAt = 0;
|
|
163
175
|
}
|
|
164
176
|
/**
|
|
165
177
|
* Get capabilities for this client type
|
|
@@ -181,8 +193,8 @@ export class DevelopmentWorkspaceConnection {
|
|
|
181
193
|
/**
|
|
182
194
|
* List connected clients in workspace
|
|
183
195
|
*/
|
|
184
|
-
clients() {
|
|
185
|
-
return
|
|
196
|
+
async clients() {
|
|
197
|
+
return this.requireTransport().clients(this.workspaceId);
|
|
186
198
|
}
|
|
187
199
|
/**
|
|
188
200
|
* Subscribe to workspace entity changes
|
|
@@ -214,56 +226,44 @@ export class DevelopmentWorkspaceConnection {
|
|
|
214
226
|
*/
|
|
215
227
|
async publish(collection, entity) {
|
|
216
228
|
const id = generateEntityId();
|
|
217
|
-
|
|
218
|
-
id: generateEventId(),
|
|
229
|
+
await this.requireTransport().publish({
|
|
219
230
|
workspaceId: this.workspaceId,
|
|
220
231
|
collection,
|
|
221
232
|
entityId: id,
|
|
222
|
-
type: 'created',
|
|
223
233
|
value: entity,
|
|
224
|
-
timestamp: Date.now(),
|
|
225
234
|
originClientId: this.clientId,
|
|
226
|
-
};
|
|
227
|
-
this.notifySubscribers(collection, event);
|
|
235
|
+
});
|
|
228
236
|
return id;
|
|
229
237
|
}
|
|
230
238
|
/**
|
|
231
239
|
* Get entity from workspace
|
|
232
240
|
*/
|
|
233
241
|
async get(collection, entityId) {
|
|
234
|
-
|
|
235
|
-
// For now, returns null (placeholder)
|
|
236
|
-
return null;
|
|
242
|
+
return this.requireTransport().get({ workspaceId: this.workspaceId, collection, entityId });
|
|
237
243
|
}
|
|
238
244
|
/**
|
|
239
245
|
* Query entities in workspace collection
|
|
240
246
|
*/
|
|
241
247
|
async query(collection) {
|
|
242
|
-
|
|
243
|
-
// For now, returns empty array (placeholder)
|
|
244
|
-
return [];
|
|
248
|
+
return this.requireTransport().query({ workspaceId: this.workspaceId, collection });
|
|
245
249
|
}
|
|
246
250
|
/**
|
|
247
251
|
* Update entity in workspace
|
|
248
252
|
*/
|
|
249
253
|
async update(collection, entityId, updates) {
|
|
250
|
-
|
|
251
|
-
id: generateEventId(),
|
|
254
|
+
await this.requireTransport().update({
|
|
252
255
|
workspaceId: this.workspaceId,
|
|
253
256
|
collection,
|
|
254
257
|
entityId,
|
|
255
|
-
|
|
256
|
-
value: updates,
|
|
257
|
-
timestamp: Date.now(),
|
|
258
|
+
updates,
|
|
258
259
|
originClientId: this.clientId,
|
|
259
|
-
};
|
|
260
|
-
this.notifySubscribers(collection, event);
|
|
260
|
+
});
|
|
261
261
|
}
|
|
262
262
|
/**
|
|
263
263
|
* Notify subscribers of entity changes
|
|
264
264
|
*/
|
|
265
|
-
notifySubscribers(
|
|
266
|
-
const handlers = this.subscriptions.get(collection);
|
|
265
|
+
notifySubscribers(event) {
|
|
266
|
+
const handlers = this.subscriptions.get(event.collection);
|
|
267
267
|
if (handlers) {
|
|
268
268
|
for (const handler of handlers) {
|
|
269
269
|
try {
|
|
@@ -275,6 +275,27 @@ export class DevelopmentWorkspaceConnection {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
+
requireTransport() {
|
|
279
|
+
if (!this.transport)
|
|
280
|
+
throw new Error('Development Workspace is not connected');
|
|
281
|
+
return this.transport;
|
|
282
|
+
}
|
|
283
|
+
async resolveEndpoint() {
|
|
284
|
+
if (this.options.endpoint)
|
|
285
|
+
return this.options.endpoint;
|
|
286
|
+
const environmentEndpoint = typeof process !== 'undefined'
|
|
287
|
+
? process.env.FELTDB_WORKSPACE_ENDPOINT || process.env.VITE_FELTDB_URL
|
|
288
|
+
: undefined;
|
|
289
|
+
if (environmentEndpoint)
|
|
290
|
+
return environmentEndpoint;
|
|
291
|
+
if (this.options.pairingCode && this.options.projectDir) {
|
|
292
|
+
const pairingPath = path.join(this.options.projectDir, '.feltdb', 'pairing.json');
|
|
293
|
+
const pairing = JSON.parse(fs.readFileSync(pairingPath, 'utf8'));
|
|
294
|
+
if (pairing.endpoint)
|
|
295
|
+
return pairing.endpoint;
|
|
296
|
+
}
|
|
297
|
+
throw new Error('Cannot connect: workspace authority endpoint is required');
|
|
298
|
+
}
|
|
278
299
|
}
|
|
279
300
|
/**
|
|
280
301
|
* Generate unique entity ID
|
|
@@ -288,3 +309,108 @@ function generateEntityId() {
|
|
|
288
309
|
function generateEventId() {
|
|
289
310
|
return `event_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
290
311
|
}
|
|
312
|
+
const ENTITY_COLLECTION = '_development_workspace_entities';
|
|
313
|
+
const EVENT_COLLECTION = '_development_workspace_events';
|
|
314
|
+
const CLIENT_COLLECTION = '_development_workspace_clients';
|
|
315
|
+
class HttpWorkspaceTransport {
|
|
316
|
+
constructor(endpoint, token) {
|
|
317
|
+
this.seenEvents = new Set();
|
|
318
|
+
this.db = new HttpJsDb({ url: endpoint, token });
|
|
319
|
+
}
|
|
320
|
+
async assertReachable() {
|
|
321
|
+
const result = await this.db.query(ENTITY_COLLECTION);
|
|
322
|
+
if (!result.success)
|
|
323
|
+
throw new Error(`Workspace authority unavailable: ${result.error}`);
|
|
324
|
+
}
|
|
325
|
+
async registerClient(workspaceId, client) {
|
|
326
|
+
const result = await this.db.insert(`${CLIENT_COLLECTION}:${recordId(workspaceId, client.clientId)}`, JSON.stringify({ workspaceId, ...client }));
|
|
327
|
+
if (!result.success) {
|
|
328
|
+
const update = await this.db.update(`${CLIENT_COLLECTION}:${recordId(workspaceId, client.clientId)}`, JSON.stringify({ workspaceId, ...client }));
|
|
329
|
+
if (!update.success)
|
|
330
|
+
throw new Error(`Client registration failed: ${update.error}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async unregisterClient(workspaceId, clientId) {
|
|
334
|
+
const result = await this.db.delete(`${CLIENT_COLLECTION}:${recordId(workspaceId, clientId)}`);
|
|
335
|
+
if (!result.success && !String(result.error).includes('404'))
|
|
336
|
+
throw new Error(`Client cleanup failed: ${result.error}`);
|
|
337
|
+
}
|
|
338
|
+
async clients(workspaceId) {
|
|
339
|
+
return (await this.readCollection(CLIENT_COLLECTION))
|
|
340
|
+
.filter(client => client.workspaceId === workspaceId);
|
|
341
|
+
}
|
|
342
|
+
async publish(input) {
|
|
343
|
+
const envelope = { ...input, updatedAt: Date.now() };
|
|
344
|
+
const stored = await this.db.insert(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`, JSON.stringify(envelope));
|
|
345
|
+
if (!stored.success)
|
|
346
|
+
throw new Error(`Workspace publish failed: ${stored.error}`);
|
|
347
|
+
await this.appendEvent(input, 'created', input.value);
|
|
348
|
+
}
|
|
349
|
+
async get(input) {
|
|
350
|
+
const result = await this.db.get(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`);
|
|
351
|
+
if (!result.success)
|
|
352
|
+
throw new Error(`Workspace get failed: ${result.error}`);
|
|
353
|
+
if (!result.data)
|
|
354
|
+
return null;
|
|
355
|
+
const envelope = JSON.parse(result.data);
|
|
356
|
+
return envelope.workspaceId === input.workspaceId && envelope.collection === input.collection ? envelope.value : null;
|
|
357
|
+
}
|
|
358
|
+
async query(input) {
|
|
359
|
+
return (await this.readCollection(ENTITY_COLLECTION))
|
|
360
|
+
.filter(entity => entity.workspaceId === input.workspaceId && entity.collection === input.collection)
|
|
361
|
+
.map(entity => entity.value);
|
|
362
|
+
}
|
|
363
|
+
async update(input) {
|
|
364
|
+
const current = await this.get(input);
|
|
365
|
+
if (current === null)
|
|
366
|
+
throw new Error(`Workspace entity not found: ${input.entityId}`);
|
|
367
|
+
const value = { ...current, ...input.updates };
|
|
368
|
+
const envelope = { workspaceId: input.workspaceId, collection: input.collection, entityId: input.entityId, value, updatedAt: Date.now() };
|
|
369
|
+
const result = await this.db.update(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`, JSON.stringify(envelope));
|
|
370
|
+
if (!result.success)
|
|
371
|
+
throw new Error(`Workspace update failed: ${result.error}`);
|
|
372
|
+
await this.appendEvent(input, 'updated', value);
|
|
373
|
+
}
|
|
374
|
+
subscribe(workspaceId, handler) {
|
|
375
|
+
let stopped = false;
|
|
376
|
+
const subscribedAt = Date.now();
|
|
377
|
+
const unsubscribe = this.db.subscribe_changes(collection => {
|
|
378
|
+
if (collection !== EVENT_COLLECTION || stopped)
|
|
379
|
+
return;
|
|
380
|
+
void this.deliverEvents(workspaceId, subscribedAt, handler);
|
|
381
|
+
});
|
|
382
|
+
return () => { stopped = true; unsubscribe(); };
|
|
383
|
+
}
|
|
384
|
+
async appendEvent(input, type, value) {
|
|
385
|
+
const event = { id: generateEventId(), ...input, type, value, timestamp: Date.now() };
|
|
386
|
+
const result = await this.db.insert(`${EVENT_COLLECTION}:${event.id}`, JSON.stringify(event));
|
|
387
|
+
if (!result.success)
|
|
388
|
+
throw new Error(`Workspace event persistence failed: ${result.error}`);
|
|
389
|
+
}
|
|
390
|
+
async deliverEvents(workspaceId, subscribedAt, handler) {
|
|
391
|
+
for (const event of await this.readCollection(EVENT_COLLECTION)) {
|
|
392
|
+
if (event.workspaceId !== workspaceId || event.timestamp < subscribedAt || this.seenEvents.has(event.id))
|
|
393
|
+
continue;
|
|
394
|
+
this.seenEvents.add(event.id);
|
|
395
|
+
handler(event);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async readCollection(collection) {
|
|
399
|
+
const result = await this.db.query(collection);
|
|
400
|
+
if (!result.success)
|
|
401
|
+
throw new Error(`Workspace query failed: ${result.error}`);
|
|
402
|
+
return result.data ? JSON.parse(result.data) : [];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function recordId(...parts) {
|
|
406
|
+
const value = parts.join('\u0000');
|
|
407
|
+
const hash = (seed) => {
|
|
408
|
+
let current = seed;
|
|
409
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
410
|
+
current ^= BigInt(byte);
|
|
411
|
+
current = BigInt.asUintN(64, current * 0x100000001b3n);
|
|
412
|
+
}
|
|
413
|
+
return current.toString(16).padStart(16, '0');
|
|
414
|
+
};
|
|
415
|
+
return `workspace_${hash(0xcbf29ce484222325n)}${hash(0x84222325cbf29ce4n)}`;
|
|
416
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feltdb/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "FeltDB Core - Application-facing database with Browser, Local, and Server runtimes. Durable state, reactive APIs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "rm -rf dist && tsc && node ../../tools/stage-core-wasm.mjs && npm run build --workspace @feltdb/react && npm run build --workspace @feltdb/migration-sherpa && npm run build --workspace @feltdb/studio && npm run build --workspace @feltdb/cli && npm run build --workspace create-feltdb && node ../../tools/assemble-core-sdk.mjs",
|
|
58
58
|
"test": "node ../../test-runner.ts",
|
|
59
|
+
"test:workspace-e2e": "node --test test/workspace-connection.e2e.test.mjs",
|
|
59
60
|
"type-check": "tsc --noEmit"
|
|
60
61
|
},
|
|
61
62
|
"keywords": [
|