instant-cli 0.22.95-experimental.surgical.20386947966.1 → 0.22.95

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > instant-cli@0.22.95-experimental.surgical.20386947966.1 build /home/runner/work/instant/instant/client/packages/cli
2
+ > instant-cli@0.22.95 build /home/runner/work/instant/instant/client/packages/cli
3
3
  > rm -rf dist; tsc -p tsconfig.json
4
4
 
@@ -0,0 +1,197 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { mergeSchema } from '../src/util/mergeSchema';
3
+
4
+ test('preserves type annotations', () => {
5
+ const oldFile = `
6
+ import { i } from '@instantdb/core';
7
+ import { Label } from './types';
8
+
9
+ const _schema = i.schema({
10
+ entities: {
11
+ $users: i.entity({
12
+ email: i.string().unique().indexed(),
13
+ }),
14
+ todos: i.entity({
15
+ title: i.string(),
16
+ status: i.string<'todo' | 'in_progress' | 'done'>(),
17
+ priority: i.number<1 | 2 | 3>(),
18
+ labels: i.json<Label[]>().optional(),
19
+ }),
20
+ projects: i.entity({
21
+ name: i.string(),
22
+ }),
23
+ },
24
+ links: {
25
+ todoProject: {
26
+ forward: { on: 'todos', has: 'one', label: 'project' },
27
+ reverse: { on: 'projects', has: 'many', label: 'todos' },
28
+ },
29
+ projectOwner: {
30
+ forward: { on: 'projects', has: 'one', label: 'owner' },
31
+ reverse: { on: '$users', has: 'many', label: 'projects' },
32
+ },
33
+ },
34
+ rooms: {
35
+ projectRoom: {
36
+ presence: i.entity({
37
+ cursor: i.json<{ x: number; y: number }>(),
38
+ }),
39
+ },
40
+ },
41
+ });
42
+
43
+ export default _schema;
44
+ `;
45
+
46
+ const newFile = `
47
+ import { i } from '@instantdb/core';
48
+
49
+ const _schema = i.schema({
50
+ entities: {
51
+ $users: i.entity({
52
+ email: i.string().unique().indexed(),
53
+ }),
54
+ todos: i.entity({
55
+ title: i.string(),
56
+ status: i.string(),
57
+ priority: i.number(),
58
+ labels: i.json().optional(),
59
+ }),
60
+ projects: i.entity({
61
+ name: i.string(),
62
+ }),
63
+ },
64
+ links: {
65
+ todoProject: {
66
+ forward: { on: 'todos', has: 'one', label: 'project' },
67
+ reverse: { on: 'projects', has: 'many', label: 'todos' },
68
+ },
69
+ projectOwner: {
70
+ forward: { on: 'projects', has: 'one', label: 'owner' },
71
+ reverse: { on: '$users', has: 'many', label: 'projects' },
72
+ },
73
+ },
74
+ rooms: {
75
+ projectRoom: {
76
+ presence: i.entity({
77
+ cursor: i.json(),
78
+ }),
79
+ },
80
+ },
81
+ });
82
+
83
+ export default _schema;
84
+ `;
85
+
86
+ const result = mergeSchema(oldFile, newFile);
87
+
88
+ // Type annotations preserved
89
+ expect(result).toContain("i.string<'todo' | 'in_progress' | 'done'>()");
90
+ expect(result).toContain('i.number<1 | 2 | 3>()');
91
+ expect(result).toContain('i.json<Label[]>()');
92
+ expect(result).toContain('i.json<{ x: number; y: number }>()');
93
+
94
+ // Import preserved
95
+ expect(result).toContain("import { Label } from './types';");
96
+ });
97
+
98
+ test('preserves different import styles', () => {
99
+ const oldFile = `
100
+ import { i } from '@instantdb/core';
101
+ import { Label } from './types';
102
+ import { Tag as MyTag } from './types';
103
+ import Priority from './Priority';
104
+ import * as Models from './models';
105
+ import type { Meta } from './meta';
106
+
107
+ const _schema = i.schema({
108
+ entities: {
109
+ a: i.entity({ f: i.json<Label>() }),
110
+ b: i.entity({ f: i.json<MyTag>() }),
111
+ c: i.entity({ f: i.json<Priority>() }),
112
+ d: i.entity({ f: i.json<Models.Status>() }),
113
+ e: i.entity({ f: i.json<Meta>() }),
114
+ },
115
+ });
116
+ `;
117
+
118
+ const newFile = `
119
+ import { i } from '@instantdb/core';
120
+
121
+ const _schema = i.schema({
122
+ entities: {
123
+ a: i.entity({ f: i.json() }),
124
+ b: i.entity({ f: i.json() }),
125
+ c: i.entity({ f: i.json() }),
126
+ d: i.entity({ f: i.json() }),
127
+ e: i.entity({ f: i.json() }),
128
+ },
129
+ });
130
+ `;
131
+
132
+ const result = mergeSchema(oldFile, newFile);
133
+
134
+ // All import styles preserved (named imports from same module are combined)
135
+ expect(result).toContain('Label');
136
+ expect(result).toContain('Tag as MyTag');
137
+ expect(result).toContain("import Priority from './Priority';");
138
+ expect(result).toContain("import * as Models from './models';");
139
+ expect(result).toContain("import type { Meta } from './meta';");
140
+
141
+ // Type annotations preserved
142
+ expect(result).toContain('i.json<Label>()');
143
+ expect(result).toContain('i.json<MyTag>()');
144
+ expect(result).toContain('i.json<Priority>()');
145
+ expect(result).toContain('i.json<Models.Status>()');
146
+ expect(result).toContain('i.json<Meta>()');
147
+ });
148
+
149
+ test('handles entity additions and removals', () => {
150
+ const oldFile = `
151
+ import { i } from '@instantdb/core';
152
+ import { Label } from './types';
153
+
154
+ const _schema = i.schema({
155
+ entities: {
156
+ todos: i.entity({
157
+ labels: i.json<Label[]>(),
158
+ }),
159
+ oldEntity: i.entity({
160
+ data: i.json<{ removed: true }>(),
161
+ }),
162
+ },
163
+ });
164
+ `;
165
+
166
+ const newFile = `
167
+ import { i } from '@instantdb/core';
168
+ import { Label } from './types';
169
+
170
+ const _schema = i.schema({
171
+ entities: {
172
+ todos: i.entity({
173
+ labels: i.json(),
174
+ }),
175
+ newEntity: i.entity({
176
+ data: i.json(),
177
+ }),
178
+ },
179
+ });
180
+ `;
181
+
182
+ const result = mergeSchema(oldFile, newFile);
183
+
184
+ // Existing entity type preserved
185
+ expect(result).toContain('i.json<Label[]>()');
186
+
187
+ // Removed entity is gone
188
+ expect(result).not.toContain('oldEntity');
189
+
190
+ // New entity has no type annotation
191
+ expect(result).toContain('newEntity: i.entity({');
192
+ expect(result).not.toContain('i.json<{ removed: true }>()');
193
+
194
+ // Import not duplicated (was already in newFile)
195
+ const importMatches = result.match(/import { Label } from '\.\/types';/g);
196
+ expect(importMatches?.length).toBe(1);
197
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA68DA,8EAQC;AA+ED;;;;;EAKE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAo8DA,8EAQC;AA+ED;;;;;EAKE"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  // @ts-check
11
11
  import { generatePermsTypescriptFile, apiSchemaToInstantSchemaDef, generateSchemaTypescriptFile, diffSchemas, convertTxSteps, validateSchema, SchemaValidationError, PlatformApi, } from '@instantdb/platform';
12
12
  import version from './version.js';
13
+ import { existsSync } from 'fs';
13
14
  import { mkdir, writeFile, readFile, unlink } from 'fs/promises';
14
15
  import path, { join } from 'path';
15
16
  import { randomUUID } from 'crypto';
@@ -37,7 +38,7 @@ import { buildAutoRenameSelector } from './rename.js';
37
38
  import { loadEnv } from './util/loadEnv.js';
38
39
  import { isHeadlessEnvironment } from './util/isHeadlessEnvironment.js';
39
40
  import { getSchemaReadCandidates, getPermsReadCandidates, getSchemaPathToWrite, getPermsPathToWrite, } from './util/findConfigCandidates.js';
40
- import { updateSchemaFile } from './util/updateSchemaFile.js';
41
+ import { mergeSchema } from './util/mergeSchema.js';
41
42
  const execAsync = promisify(exec);
42
43
  loadEnv();
43
44
  const dev = Boolean(process.env.INSTANT_CLI_DEV);
@@ -353,7 +354,7 @@ program
353
354
  .argument('[schema|perms|all]', 'Which configuration to push. Defaults to `all`')
354
355
  .option('-a --app <app-id>', 'App ID to push to. Defaults to *_INSTANT_APP_ID in .env')
355
356
  .option('-p --package <react|react-native|core|admin>', 'Which package to automatically install if there is not one installed already.')
356
- .option('--experimental-type-preservation', '[Experimental] Preserve manual type changes and schema edits when pulling schema')
357
+ .option('--experimental-type-preservation', "[Experimental] Preserve manual type changes like `status: i.json<'online' | 'offline'>()` when doing `instant-cli pull schema`")
357
358
  .description('Pull schema and perm files from production.')
358
359
  .addHelpText('after', `
359
360
  Environment Variables:
@@ -1016,20 +1017,16 @@ function pullSchema(appId_1, _a) {
1016
1017
  }
1017
1018
  const shortSchemaPath = getSchemaPathToWrite(prev === null || prev === void 0 ? void 0 : prev.path);
1018
1019
  const schemaPath = join(pkgDir, shortSchemaPath);
1019
- const serverSchema = apiSchemaToInstantSchemaDef(pullRes.data.schema);
1020
- let newSchemaContent;
1020
+ let newSchemaContent = generateSchemaTypescriptFile(prev === null || prev === void 0 ? void 0 : prev.schema, apiSchemaToInstantSchemaDef(pullRes.data.schema), instantModuleName);
1021
1021
  if (prev && experimentalTypePreservation) {
1022
1022
  try {
1023
1023
  const oldSchemaContent = yield readFile(prev.path, 'utf-8');
1024
- newSchemaContent = yield updateSchemaFile(oldSchemaContent, prev.schema, serverSchema);
1024
+ newSchemaContent = mergeSchema(oldSchemaContent, newSchemaContent);
1025
1025
  }
1026
1026
  catch (e) {
1027
- warn('Failed to update schema with existing file. Overwriting instead.', e);
1027
+ warn('Failed to merge schema with existing file. Overwriting instead.', e);
1028
1028
  }
1029
1029
  }
1030
- if (!newSchemaContent) {
1031
- newSchemaContent = generateSchemaTypescriptFile(prev === null || prev === void 0 ? void 0 : prev.schema, serverSchema, instantModuleName);
1032
- }
1033
1030
  yield writeTypescript(schemaPath, newSchemaContent, 'utf-8');
1034
1031
  console.log('✅ Wrote schema to ' + shortSchemaPath);
1035
1032
  return { ok: true };