@gadgetinc/ggt 0.1.17 → 0.2.0
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 +6 -6
- package/bin/dev.js +24 -0
- package/bin/run.js +11 -0
- package/lib/__generated__/graphql.js +6 -9
- package/lib/__generated__/graphql.js.map +1 -1
- package/lib/commands/help.js +22 -37
- package/lib/commands/help.js.map +1 -1
- package/lib/commands/list.js +30 -56
- package/lib/commands/list.js.map +1 -1
- package/lib/commands/login.js +13 -29
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/logout.js +17 -34
- package/lib/commands/logout.js.map +1 -1
- package/lib/commands/sync.js +536 -502
- package/lib/commands/sync.js.map +1 -1
- package/lib/commands/whoami.js +17 -34
- package/lib/commands/whoami.js.map +1 -1
- package/lib/index.js +2 -5
- package/lib/index.js.map +1 -1
- package/lib/utils/base-command.js +106 -147
- package/lib/utils/base-command.js.map +1 -1
- package/lib/utils/client.js +104 -113
- package/lib/utils/client.js.map +1 -1
- package/lib/utils/context.js +63 -119
- package/lib/utils/context.js.map +1 -1
- package/lib/utils/errors.js +161 -242
- package/lib/utils/errors.js.map +1 -1
- package/lib/utils/flags.js +23 -26
- package/lib/utils/flags.js.map +1 -1
- package/lib/utils/fs-utils.js +46 -73
- package/lib/utils/fs-utils.js.map +1 -1
- package/lib/utils/help.js +19 -26
- package/lib/utils/help.js.map +1 -1
- package/lib/utils/promise.js +32 -78
- package/lib/utils/promise.js.map +1 -1
- package/lib/utils/sleep.js +6 -11
- package/lib/utils/sleep.js.map +1 -1
- package/npm-shrinkwrap.json +7609 -6954
- package/oclif.manifest.json +1 -1
- package/package.json +45 -45
- package/bin/dev +0 -20
- package/bin/run +0 -7
- package/lib/__generated__/graphql.d.ts +0 -294
- package/lib/commands/help.d.ts +0 -14
- package/lib/commands/list.d.ts +0 -18
- package/lib/commands/login.d.ts +0 -7
- package/lib/commands/logout.d.ts +0 -7
- package/lib/commands/sync.d.ts +0 -139
- package/lib/commands/whoami.d.ts +0 -7
- package/lib/index.d.ts +0 -1
- package/lib/utils/base-command.d.ts +0 -64
- package/lib/utils/client.d.ts +0 -42
- package/lib/utils/context.d.ts +0 -57
- package/lib/utils/errors.d.ts +0 -100
- package/lib/utils/flags.d.ts +0 -1
- package/lib/utils/fs-utils.d.ts +0 -21
- package/lib/utils/help.d.ts +0 -19
- package/lib/utils/promise.d.ts +0 -35
- package/lib/utils/sleep.d.ts +0 -5
package/lib/commands/sync.js
CHANGED
|
@@ -1,346 +1,256 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
enumerable: true,
|
|
35
|
-
configurable: true,
|
|
36
|
-
writable: true,
|
|
37
|
-
value: true
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* The current status of the sync process.
|
|
41
|
-
*/
|
|
42
|
-
Object.defineProperty(this, "status", {
|
|
43
|
-
enumerable: true,
|
|
44
|
-
configurable: true,
|
|
45
|
-
writable: true,
|
|
46
|
-
value: SyncStatus.STARTING
|
|
47
|
-
});
|
|
48
|
-
/**
|
|
49
|
-
* The absolute path to the directory to sync files to.
|
|
50
|
-
*/
|
|
51
|
-
Object.defineProperty(this, "dir", {
|
|
52
|
-
enumerable: true,
|
|
53
|
-
configurable: true,
|
|
54
|
-
writable: true,
|
|
55
|
-
value: void 0
|
|
56
|
-
});
|
|
57
|
-
/**
|
|
58
|
-
* A list of filepaths that have changed because of a remote file-sync event. This is used to avoid sending files that
|
|
59
|
-
* we recently received from a remote file-sync event.
|
|
60
|
-
*/
|
|
61
|
-
Object.defineProperty(this, "recentRemoteChanges", {
|
|
62
|
-
enumerable: true,
|
|
63
|
-
configurable: true,
|
|
64
|
-
writable: true,
|
|
65
|
-
value: new Set()
|
|
66
|
-
});
|
|
67
|
-
/**
|
|
68
|
-
* A FIFO async callback queue that ensures we process file-sync events in the order they occurred.
|
|
69
|
-
*/
|
|
70
|
-
Object.defineProperty(this, "queue", {
|
|
71
|
-
enumerable: true,
|
|
72
|
-
configurable: true,
|
|
73
|
-
writable: true,
|
|
74
|
-
value: new p_queue_1.default({ concurrency: 1 })
|
|
75
|
-
});
|
|
76
|
-
/**
|
|
77
|
-
* A GraphQL client connected to the app's /edit/api/graphql-ws endpoint
|
|
78
|
-
*/
|
|
79
|
-
Object.defineProperty(this, "client", {
|
|
80
|
-
enumerable: true,
|
|
81
|
-
configurable: true,
|
|
82
|
-
writable: true,
|
|
83
|
-
value: void 0
|
|
84
|
-
});
|
|
85
|
-
/**
|
|
86
|
-
* Loads the .ignore file and provides methods for checking if a file should be ignored.
|
|
87
|
-
*/
|
|
88
|
-
Object.defineProperty(this, "ignorer", {
|
|
89
|
-
enumerable: true,
|
|
90
|
-
configurable: true,
|
|
91
|
-
writable: true,
|
|
92
|
-
value: void 0
|
|
93
|
-
});
|
|
94
|
-
/**
|
|
95
|
-
* Watches the local filesystem for changes.
|
|
96
|
-
*/
|
|
97
|
-
Object.defineProperty(this, "watcher", {
|
|
98
|
-
enumerable: true,
|
|
99
|
-
configurable: true,
|
|
100
|
-
writable: true,
|
|
101
|
-
value: void 0
|
|
102
|
-
});
|
|
103
|
-
/**
|
|
104
|
-
* Holds information about the state of the local filesystem. It's persisted to `.gadget/sync.json`.
|
|
105
|
-
*/
|
|
106
|
-
Object.defineProperty(this, "metadata", {
|
|
107
|
-
enumerable: true,
|
|
108
|
-
configurable: true,
|
|
109
|
-
writable: true,
|
|
110
|
-
value: {
|
|
111
|
-
/**
|
|
112
|
-
* The app this filesystem is synced to.
|
|
113
|
-
*/
|
|
114
|
-
app: "",
|
|
115
|
-
/**
|
|
116
|
-
* The last filesVersion that was successfully written to the filesystem. This is used to determine if the remote
|
|
117
|
-
* filesystem is ahead of the local one.
|
|
118
|
-
*/
|
|
119
|
-
filesVersion: "0",
|
|
120
|
-
/**
|
|
121
|
-
* The largest mtime that was seen on the local filesystem before `ggt sync` stopped. This is used to determine if
|
|
122
|
-
* the local filesystem has changed since the last sync.
|
|
123
|
-
*
|
|
124
|
-
* Note: This does not include the mtime of files that are ignored.
|
|
125
|
-
*/
|
|
126
|
-
mtime: 0,
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
/**
|
|
130
|
-
* A debounced function that enqueue's local file changes to be sent to Gadget.
|
|
131
|
-
*/
|
|
132
|
-
Object.defineProperty(this, "publish", {
|
|
133
|
-
enumerable: true,
|
|
134
|
-
configurable: true,
|
|
135
|
-
writable: true,
|
|
136
|
-
value: void 0
|
|
137
|
-
});
|
|
138
|
-
/**
|
|
139
|
-
* Gracefully stops the sync.
|
|
140
|
-
*/
|
|
141
|
-
Object.defineProperty(this, "stop", {
|
|
142
|
-
enumerable: true,
|
|
143
|
-
configurable: true,
|
|
144
|
-
writable: true,
|
|
145
|
-
value: void 0
|
|
146
|
-
});
|
|
1
|
+
import { _ as _class_private_field_get } from "@swc/helpers/_/_class_private_field_get";
|
|
2
|
+
import { _ as _class_private_field_init } from "@swc/helpers/_/_class_private_field_init";
|
|
3
|
+
import { _ as _class_private_field_set } from "@swc/helpers/_/_class_private_field_set";
|
|
4
|
+
import { _ as _define_property } from "@swc/helpers/_/_define_property";
|
|
5
|
+
import { Args, Flags } from "@oclif/core";
|
|
6
|
+
import assert from "assert";
|
|
7
|
+
import chalkTemplate from "chalk-template";
|
|
8
|
+
import { FSWatcher } from "chokidar";
|
|
9
|
+
import { format } from "date-fns";
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
import fs from "fs-extra";
|
|
12
|
+
import inquirer from "inquirer";
|
|
13
|
+
import _ from "lodash";
|
|
14
|
+
import normalizePath from "normalize-path";
|
|
15
|
+
import pMap from "p-map";
|
|
16
|
+
import PQueue from "p-queue";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import pluralize from "pluralize";
|
|
19
|
+
import { dedent } from "ts-dedent";
|
|
20
|
+
import which from "which";
|
|
21
|
+
import { FileSyncEncoding } from "../__generated__/graphql.js";
|
|
22
|
+
import { BaseCommand } from "../utils/base-command.js";
|
|
23
|
+
import { Client } from "../utils/client.js";
|
|
24
|
+
import { context } from "../utils/context.js";
|
|
25
|
+
import { InvalidSyncAppFlagError, InvalidSyncFileError, YarnNotFoundError } from "../utils/errors.js";
|
|
26
|
+
import { app } from "../utils/flags.js";
|
|
27
|
+
import { FSIgnorer, ignoreEnoent, isEmptyDir, walkDir } from "../utils/fs-utils.js";
|
|
28
|
+
import { PromiseSignal } from "../utils/promise.js";
|
|
29
|
+
class Sync extends BaseCommand {
|
|
30
|
+
/**
|
|
31
|
+
* Turns an absolute filepath into a relative one from {@linkcode dir}.
|
|
32
|
+
*/ relative(to) {
|
|
33
|
+
return path.relative(this.dir, to);
|
|
147
34
|
}
|
|
148
35
|
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return path_1.default.relative(this.dir, to);
|
|
36
|
+
* Combines path segments into an absolute filepath that starts at {@linkcode dir}.
|
|
37
|
+
*/ absolute(...pathSegments) {
|
|
38
|
+
return path.resolve(this.dir, ...pathSegments);
|
|
153
39
|
}
|
|
154
40
|
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
41
|
+
* Similar to {@linkcode relative} in that it turns a filepath into a relative one from {@linkcode dir}. However, it
|
|
42
|
+
* also changes any slashes to be posix/unix-like forward slashes, condenses repeated slashes into a single slash, and
|
|
43
|
+
* adds a trailing slash if the filepath is a directory.
|
|
44
|
+
*
|
|
45
|
+
* This is used when sending file-sync events to Gadget to ensure that the paths are consistent across platforms.
|
|
46
|
+
*
|
|
47
|
+
* @see https://www.npmjs.com/package/normalize-path
|
|
48
|
+
*/ normalize(filepath, isDirectory) {
|
|
49
|
+
return normalizePath(path.isAbsolute(filepath) ? this.relative(filepath) : filepath) + (isDirectory ? "/" : "");
|
|
159
50
|
}
|
|
160
51
|
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
52
|
+
* Instead of deleting files, we move them to .gadget/backup so that users can recover them if something goes wrong.
|
|
53
|
+
*/ async softDelete(normalizedPath) {
|
|
54
|
+
try {
|
|
55
|
+
await fs.move(this.absolute(normalizedPath), this.absolute(".gadget/backup", normalizedPath), {
|
|
56
|
+
overwrite: true
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// replicate the behavior of `rm -rf` and ignore ENOENT
|
|
60
|
+
ignoreEnoent(error);
|
|
61
|
+
}
|
|
170
62
|
}
|
|
171
63
|
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
...
|
|
182
|
-
|
|
183
|
-
], (line) => line.slice(line.indexOf(" ") + 1));
|
|
64
|
+
* Pretty-prints changed and deleted filepaths to the console.
|
|
65
|
+
*
|
|
66
|
+
* @param prefix The prefix to print before each line.
|
|
67
|
+
* @param changed The normalized paths that have changed.
|
|
68
|
+
* @param deleted The normalized paths that have been deleted.
|
|
69
|
+
* @param options.limit The maximum number of lines to print. Defaults to 10. If debug is enabled, this is ignored.
|
|
70
|
+
*/ logPaths(prefix, changed, deleted, { limit =10 } = {}) {
|
|
71
|
+
const lines = _.sortBy([
|
|
72
|
+
...changed.map((normalizedPath)=>chalkTemplate`{green ${prefix}} ${normalizedPath} {gray (changed)}`),
|
|
73
|
+
...deleted.map((normalizedPath)=>chalkTemplate`{red ${prefix}} ${normalizedPath} {gray (deleted)}`)
|
|
74
|
+
], (line)=>line.slice(line.indexOf(" ") + 1));
|
|
184
75
|
let logged = 0;
|
|
185
|
-
for (const line of lines)
|
|
76
|
+
for (const line of lines){
|
|
186
77
|
this.log(line);
|
|
187
|
-
if (++logged == limit && !this.debugEnabled)
|
|
188
|
-
break;
|
|
78
|
+
if (++logged == limit && !this.debugEnabled) break;
|
|
189
79
|
}
|
|
190
80
|
if (lines.length > logged) {
|
|
191
|
-
this.log(
|
|
81
|
+
this.log(chalkTemplate`{gray … ${lines.length - logged} more}`);
|
|
192
82
|
}
|
|
193
|
-
this.log(
|
|
83
|
+
this.log(chalkTemplate`{gray ${pluralize("file", lines.length, true)} in total. ${changed.length} changed, ${deleted.length} deleted.}`);
|
|
194
84
|
this.log();
|
|
195
85
|
}
|
|
196
86
|
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
async init() {
|
|
87
|
+
* Initializes the sync process.
|
|
88
|
+
* - Ensures the directory exists.
|
|
89
|
+
* - Ensures the directory is empty or contains a `.gadget/sync.json` file.
|
|
90
|
+
* - Ensures an app is selected and that it matches the app the directory was previously synced to.
|
|
91
|
+
* - Ensures yarn v1 is installed.
|
|
92
|
+
* - Prompts the user how to resolve conflicts if the local filesystem has changed since the last sync.
|
|
93
|
+
*/ async init() {
|
|
205
94
|
await super.init();
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return this.flags.app;
|
|
213
|
-
if (this.metadata.app)
|
|
214
|
-
return this.metadata.app;
|
|
215
|
-
const selected = await (0, inquirer_1.prompt)({
|
|
95
|
+
assert(_.isString(this.args["directory"]));
|
|
96
|
+
this.dir = this.config.windows && this.args["directory"].startsWith("~/") ? path.join(this.config.home, this.args["directory"].slice(2)) : path.resolve(this.args["directory"]);
|
|
97
|
+
const getApp = async ()=>{
|
|
98
|
+
if (this.flags.app) return this.flags.app;
|
|
99
|
+
if (this.state?.app) return this.state.app;
|
|
100
|
+
const selected = await inquirer.prompt({
|
|
216
101
|
type: "list",
|
|
217
102
|
name: "app",
|
|
218
103
|
message: "Please select the app to sync to.",
|
|
219
|
-
choices: await
|
|
104
|
+
choices: await context.getAvailableApps().then((apps)=>apps.map((app)=>app.slug))
|
|
220
105
|
});
|
|
221
106
|
return selected.app;
|
|
222
107
|
};
|
|
223
|
-
if (await
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
108
|
+
if (await isEmptyDir(this.dir)) {
|
|
109
|
+
const app = await getApp();
|
|
110
|
+
this.state = SyncState.create(this.dir, {
|
|
111
|
+
app
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
227
114
|
try {
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
this.metadata.app = await getApp();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
115
|
+
this.state = SyncState.load(this.dir);
|
|
116
|
+
} catch (error) {
|
|
234
117
|
if (!this.flags.force) {
|
|
235
|
-
throw new
|
|
118
|
+
throw new InvalidSyncFileError(error, this, this.flags.app);
|
|
236
119
|
}
|
|
237
|
-
|
|
120
|
+
const app = await getApp();
|
|
121
|
+
this.state = SyncState.create(this.dir, {
|
|
122
|
+
app
|
|
123
|
+
});
|
|
238
124
|
}
|
|
239
125
|
}
|
|
240
|
-
if (this.flags.app && this.flags.app !== this.
|
|
241
|
-
throw new
|
|
126
|
+
if (this.flags.app && this.flags.app !== this.state.app && !this.flags.force) {
|
|
127
|
+
throw new InvalidSyncAppFlagError(this);
|
|
242
128
|
}
|
|
243
|
-
await
|
|
244
|
-
this.client = new
|
|
129
|
+
await context.setApp(this.state.app);
|
|
130
|
+
this.client = new Client();
|
|
245
131
|
// local files/folders that should never be published
|
|
246
|
-
this.ignorer = new
|
|
247
|
-
|
|
248
|
-
|
|
132
|
+
this.ignorer = new FSIgnorer(this.dir, [
|
|
133
|
+
"node_modules",
|
|
134
|
+
".gadget",
|
|
135
|
+
".git"
|
|
136
|
+
]);
|
|
137
|
+
this.watcher = new FSWatcher({
|
|
138
|
+
ignored: (filepath)=>this.ignorer.ignores(filepath),
|
|
249
139
|
// don't emit an event for every watched file on boot
|
|
250
140
|
ignoreInitial: true,
|
|
251
141
|
// make sure stats are always present on add/change events
|
|
252
142
|
alwaysStat: true,
|
|
253
143
|
// wait for the entire file to be written before emitting add/change events
|
|
254
|
-
awaitWriteFinish: {
|
|
144
|
+
awaitWriteFinish: {
|
|
145
|
+
pollInterval: this.flags["file-poll-interval"],
|
|
146
|
+
stabilityThreshold: this.flags["file-stability-threshold"]
|
|
147
|
+
}
|
|
255
148
|
});
|
|
256
149
|
this.debug("starting");
|
|
257
|
-
if (!
|
|
258
|
-
|
|
150
|
+
if (!which.sync("yarn", {
|
|
151
|
+
nothrow: true
|
|
152
|
+
})) {
|
|
153
|
+
throw new YarnNotFoundError();
|
|
259
154
|
}
|
|
260
|
-
await
|
|
261
|
-
const { remoteFilesVersion
|
|
262
|
-
|
|
263
|
-
|
|
155
|
+
await fs.ensureDir(this.dir);
|
|
156
|
+
const { remoteFilesVersion } = await this.client.queryUnwrap({
|
|
157
|
+
query: REMOTE_FILES_VERSION_QUERY
|
|
158
|
+
});
|
|
159
|
+
const hasRemoteChanges = BigInt(remoteFilesVersion) > BigInt(this.state.filesVersion);
|
|
160
|
+
const getChangedFiles = async ()=>{
|
|
264
161
|
const files = new Map();
|
|
265
|
-
for await (const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
162
|
+
for await (const absolutePath of walkDir(this.dir, {
|
|
163
|
+
ignorer: this.ignorer
|
|
164
|
+
})){
|
|
165
|
+
const stats = await fs.stat(absolutePath);
|
|
166
|
+
if (stats.mtime.getTime() > this.state.mtime) {
|
|
167
|
+
files.set(this.normalize(absolutePath, stats.isDirectory()), stats);
|
|
269
168
|
}
|
|
270
169
|
}
|
|
271
170
|
// never include the root directory
|
|
272
|
-
files.delete(
|
|
171
|
+
files.delete("/");
|
|
273
172
|
return files;
|
|
274
173
|
};
|
|
275
174
|
const changedFiles = await getChangedFiles();
|
|
276
175
|
const hasLocalChanges = changedFiles.size > 0;
|
|
277
176
|
if (hasLocalChanges) {
|
|
278
177
|
this.log("Local files have changed since you last synced");
|
|
279
|
-
this.logPaths("-", Array.from(changedFiles.keys()), [], {
|
|
178
|
+
this.logPaths("-", Array.from(changedFiles.keys()), [], {
|
|
179
|
+
limit: changedFiles.size
|
|
180
|
+
});
|
|
280
181
|
this.log();
|
|
281
182
|
}
|
|
282
|
-
this.debug("init %O", {
|
|
183
|
+
this.debug("init %O", {
|
|
184
|
+
metadata: this.state,
|
|
185
|
+
remoteFilesVersion,
|
|
186
|
+
hasRemoteChanges,
|
|
187
|
+
hasLocalChanges
|
|
188
|
+
});
|
|
283
189
|
let action;
|
|
284
190
|
if (hasLocalChanges) {
|
|
285
|
-
({ action
|
|
191
|
+
({ action } = await inquirer.prompt({
|
|
286
192
|
type: "list",
|
|
287
193
|
name: "action",
|
|
288
|
-
choices: [
|
|
289
|
-
|
|
194
|
+
choices: [
|
|
195
|
+
Action.CANCEL,
|
|
196
|
+
Action.MERGE,
|
|
197
|
+
Action.RESET
|
|
198
|
+
],
|
|
199
|
+
message: hasRemoteChanges ? "Remote files have also changed. How would you like to proceed?" : "How would you like to proceed?"
|
|
290
200
|
}));
|
|
291
201
|
}
|
|
292
|
-
switch
|
|
293
|
-
case Action.MERGE:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
case Action.RESET:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
202
|
+
switch(action){
|
|
203
|
+
case Action.MERGE:
|
|
204
|
+
{
|
|
205
|
+
// get all the changed files again in case more changed
|
|
206
|
+
const files = await getChangedFiles();
|
|
207
|
+
// We purposefully don't set the returned remoteFilesVersion here because we haven't received the remote changes
|
|
208
|
+
// yet. This will cause us to receive the local files that we just published + the remote files that were
|
|
209
|
+
// changed since the last sync.
|
|
210
|
+
await this.client.queryUnwrap({
|
|
211
|
+
query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
|
|
212
|
+
variables: {
|
|
213
|
+
input: {
|
|
214
|
+
expectedRemoteFilesVersion: remoteFilesVersion,
|
|
215
|
+
changed: await pMap(files, async ([normalizedPath, stats])=>{
|
|
216
|
+
if (stats.mtime.getTime() > this.state.mtime) {
|
|
217
|
+
this.state.mtime = stats.mtime.getTime();
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
path: normalizedPath,
|
|
221
|
+
mode: stats.mode,
|
|
222
|
+
content: stats.isDirectory() ? "" : await fs.readFile(this.absolute(normalizedPath), "base64"),
|
|
223
|
+
encoding: FileSyncEncoding.Base64
|
|
224
|
+
};
|
|
225
|
+
}),
|
|
226
|
+
deleted: []
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case Action.RESET:
|
|
233
|
+
{
|
|
234
|
+
// delete all the local files that have changed since the last sync and set the files version to 0 so we receive
|
|
235
|
+
// all the remote files again, including any files that we just deleted that still exist
|
|
236
|
+
await pMap(changedFiles.keys(), (normalizedPath)=>this.softDelete(normalizedPath));
|
|
237
|
+
this.state.filesVersion = 0n;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case Action.CANCEL:
|
|
241
|
+
{
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
332
244
|
}
|
|
333
245
|
this.debug("started");
|
|
334
246
|
}
|
|
335
247
|
/**
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
async run() {
|
|
248
|
+
* Runs the sync process until it is stopped or an error occurs.
|
|
249
|
+
*/ async run() {
|
|
339
250
|
let error;
|
|
340
|
-
const stopped = new
|
|
341
|
-
this.stop = async (e)
|
|
342
|
-
if (this.status != SyncStatus.RUNNING)
|
|
343
|
-
return;
|
|
251
|
+
const stopped = new PromiseSignal();
|
|
252
|
+
this.stop = async (e)=>{
|
|
253
|
+
if (this.status != SyncStatus.RUNNING) return;
|
|
344
254
|
error = e;
|
|
345
255
|
this.debug("stopping");
|
|
346
256
|
this.status = SyncStatus.STOPPING;
|
|
@@ -349,25 +259,29 @@ class Sync extends base_command_1.BaseCommand {
|
|
|
349
259
|
this.watcher.removeAllListeners();
|
|
350
260
|
this.publish.flush();
|
|
351
261
|
await this.queue.onIdle();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
await
|
|
355
|
-
|
|
262
|
+
} finally{
|
|
263
|
+
this.state.flush();
|
|
264
|
+
await Promise.allSettled([
|
|
265
|
+
this.watcher.close(),
|
|
266
|
+
this.client.dispose()
|
|
267
|
+
]);
|
|
356
268
|
this.debug("stopped");
|
|
357
269
|
this.status = SyncStatus.STOPPED;
|
|
358
270
|
stopped.resolve();
|
|
359
271
|
}
|
|
360
272
|
};
|
|
361
|
-
for (const signal of [
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
273
|
+
for (const signal of [
|
|
274
|
+
"SIGINT",
|
|
275
|
+
"SIGTERM"
|
|
276
|
+
]){
|
|
277
|
+
process.on(signal, ()=>{
|
|
278
|
+
if (this.status != SyncStatus.RUNNING) return;
|
|
279
|
+
this.log(chalkTemplate` Stopping... {gray (press Ctrl+C again to force)}`);
|
|
366
280
|
void this.stop();
|
|
367
281
|
// When ggt is run via npx, and the user presses Ctrl+C, npx sends SIGINT twice in quick succession. In order to prevent the second
|
|
368
282
|
// SIGINT from triggering the force exit listener, we wait a bit before registering it. This is a bit of a hack, but it works.
|
|
369
|
-
setTimeout(()
|
|
370
|
-
process.once(signal, ()
|
|
283
|
+
setTimeout(()=>{
|
|
284
|
+
process.once(signal, ()=>{
|
|
371
285
|
this.log(" Exiting immediately. Note that files may not have finished syncing.");
|
|
372
286
|
process.exit(1);
|
|
373
287
|
});
|
|
@@ -375,210 +289,258 @@ class Sync extends base_command_1.BaseCommand {
|
|
|
375
289
|
});
|
|
376
290
|
}
|
|
377
291
|
const unsubscribe = this.client.subscribeUnwrap({
|
|
378
|
-
query:
|
|
379
|
-
variables: ()
|
|
292
|
+
query: REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION,
|
|
293
|
+
variables: ()=>({
|
|
294
|
+
localFilesVersion: String(this.state.filesVersion)
|
|
295
|
+
})
|
|
380
296
|
}, {
|
|
381
|
-
error: (error)
|
|
382
|
-
next: ({ remoteFileSyncEvents
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
297
|
+
error: (error)=>void this.stop(error),
|
|
298
|
+
next: ({ remoteFileSyncEvents })=>{
|
|
299
|
+
const remoteFilesVersion = remoteFileSyncEvents.remoteFilesVersion;
|
|
300
|
+
// we always ignore .gadget/ files so that we don't publish them (they're managed by gadget), but we still want to receive them
|
|
301
|
+
const filter = (event)=>event.path.startsWith(".gadget/") || !this.ignorer.ignores(event.path);
|
|
302
|
+
const changed = remoteFileSyncEvents.changed.filter(filter);
|
|
303
|
+
const deleted = remoteFileSyncEvents.deleted.filter(filter);
|
|
304
|
+
this._enqueue(async ()=>{
|
|
305
|
+
if (!changed.length && !deleted.length) {
|
|
306
|
+
if (BigInt(remoteFilesVersion) > this.state.filesVersion) {
|
|
390
307
|
// we still need to update filesVersion, otherwise our expectedFilesVersion will be behind the next time we publish
|
|
391
|
-
this.debug("updated local files version from %s to %s", this.
|
|
392
|
-
this.
|
|
308
|
+
this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
|
|
309
|
+
this.state.filesVersion = remoteFilesVersion;
|
|
393
310
|
}
|
|
394
311
|
return;
|
|
395
312
|
}
|
|
396
|
-
this.log(
|
|
397
|
-
this.logPaths("←", changed.map((x)
|
|
398
|
-
|
|
399
|
-
|
|
313
|
+
this.log(chalkTemplate`Received {gray ${format(new Date(), "pp")}}`);
|
|
314
|
+
this.logPaths("←", changed.map((x)=>x.path), deleted.map((x)=>x.path));
|
|
315
|
+
// we need to processed deleted files first as we may delete an empty directory after a file has been put
|
|
316
|
+
// into it. if processed out of order the new file will be deleted as well
|
|
317
|
+
await pMap(deleted, async (file)=>{
|
|
318
|
+
this.recentRemoteChanges.add(file.path);
|
|
319
|
+
await this.softDelete(file.path);
|
|
320
|
+
});
|
|
321
|
+
await pMap(changed, async (file)=>{
|
|
322
|
+
this.recentRemoteChanges.add(file.path);
|
|
323
|
+
const absolutePath = this.absolute(file.path);
|
|
324
|
+
if (file.path.endsWith("/")) {
|
|
325
|
+
await fs.ensureDir(absolutePath, {
|
|
326
|
+
mode: 0o755
|
|
327
|
+
});
|
|
400
328
|
return;
|
|
401
329
|
}
|
|
402
|
-
|
|
403
|
-
this.recentRemoteChanges.add(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
await fs_extra_1.default.remove(filepath);
|
|
330
|
+
// we need to add the parent directory to recentRemoteChanges so that we don't try to publish it
|
|
331
|
+
this.recentRemoteChanges.add(path.dirname(file.path) + "/");
|
|
332
|
+
await fs.ensureDir(path.dirname(absolutePath), {
|
|
333
|
+
mode: 0o755
|
|
334
|
+
});
|
|
335
|
+
await fs.writeFile(absolutePath, Buffer.from(file.content, file.encoding), {
|
|
336
|
+
mode: file.mode
|
|
337
|
+
});
|
|
338
|
+
if (absolutePath == this.absolute("yarn.lock")) {
|
|
339
|
+
await execa("yarn", [
|
|
340
|
+
"install"
|
|
341
|
+
], {
|
|
342
|
+
cwd: this.dir
|
|
343
|
+
}).catch((err)=>{
|
|
344
|
+
this.debug("yarn install failed");
|
|
345
|
+
this.debug(err.message);
|
|
346
|
+
});
|
|
424
347
|
}
|
|
425
|
-
if (
|
|
348
|
+
if (absolutePath == this.ignorer.filepath) {
|
|
426
349
|
this.ignorer.reload();
|
|
427
350
|
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
this.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
351
|
+
});
|
|
352
|
+
this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
|
|
353
|
+
this.state.filesVersion = remoteFilesVersion;
|
|
354
|
+
// always remove the root directory from recentRemoteChanges
|
|
355
|
+
this.recentRemoteChanges.delete("./");
|
|
356
|
+
// remove any files in recentRemoteChanges that are ignored (e.g. .gadget/ files)
|
|
357
|
+
for (const filepath of this.recentRemoteChanges){
|
|
358
|
+
if (this.ignorer.ignores(filepath)) {
|
|
359
|
+
this.recentRemoteChanges.delete(filepath);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
437
364
|
});
|
|
438
365
|
const localFilesBuffer = new Map();
|
|
439
|
-
this.publish =
|
|
366
|
+
this.publish = _.debounce(()=>{
|
|
440
367
|
const localFiles = new Map(localFilesBuffer.entries());
|
|
441
368
|
localFilesBuffer.clear();
|
|
442
|
-
|
|
443
|
-
.add(async () => {
|
|
369
|
+
this._enqueue(async ()=>{
|
|
444
370
|
const changed = [];
|
|
445
371
|
const deleted = [];
|
|
446
|
-
await (
|
|
372
|
+
await pMap(localFiles, async ([normalizedPath, file])=>{
|
|
447
373
|
if ("isDeleted" in file) {
|
|
448
|
-
deleted.push({
|
|
374
|
+
deleted.push({
|
|
375
|
+
path: normalizedPath
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
449
378
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// above will raise an ENOENT. This is normal operation, so just ignore this event.
|
|
462
|
-
(0, fs_utils_1.ignoreEnoent)(error);
|
|
463
|
-
}
|
|
379
|
+
try {
|
|
380
|
+
changed.push({
|
|
381
|
+
path: normalizedPath,
|
|
382
|
+
mode: file.mode,
|
|
383
|
+
content: file.isDirectory ? "" : await fs.readFile(this.absolute(normalizedPath), "base64"),
|
|
384
|
+
encoding: FileSyncEncoding.Base64
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// A file could have been changed and then deleted before we process the change event, so the readFile
|
|
388
|
+
// above will raise an ENOENT. This is normal operation, so just ignore this event.
|
|
389
|
+
ignoreEnoent(error);
|
|
464
390
|
}
|
|
465
|
-
}
|
|
466
|
-
if (changed.length
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.metadata.filesVersion = remoteFilesVersion;
|
|
391
|
+
});
|
|
392
|
+
if (!changed.length && !deleted.length) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const data = await this.client.queryUnwrap({
|
|
396
|
+
query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
|
|
397
|
+
variables: {
|
|
398
|
+
input: {
|
|
399
|
+
expectedRemoteFilesVersion: String(this.state.filesVersion),
|
|
400
|
+
changed,
|
|
401
|
+
deleted
|
|
402
|
+
}
|
|
478
403
|
}
|
|
404
|
+
});
|
|
405
|
+
this.log(chalkTemplate`Sent {gray ${format(new Date(), "pp")}}`);
|
|
406
|
+
this.logPaths("→", changed.map((x)=>x.path), deleted.map((x)=>x.path));
|
|
407
|
+
const { remoteFilesVersion } = data.publishFileSyncEvents;
|
|
408
|
+
this.debug("remote files version after publishing %s", remoteFilesVersion);
|
|
409
|
+
if (BigInt(remoteFilesVersion) > this.state.filesVersion) {
|
|
410
|
+
this.debug("updated local files version from %s to %s", this.state.filesVersion, remoteFilesVersion);
|
|
411
|
+
this.state.filesVersion = remoteFilesVersion;
|
|
479
412
|
}
|
|
480
|
-
})
|
|
481
|
-
.catch(this.stop);
|
|
413
|
+
});
|
|
482
414
|
}, this.flags["file-push-delay"]);
|
|
483
|
-
this.watcher
|
|
484
|
-
.
|
|
485
|
-
.on("error", (error) => void this.stop(error))
|
|
486
|
-
.on("all", (event, filepath, stats) => {
|
|
487
|
-
const relativePath = this.relative(filepath);
|
|
415
|
+
this.watcher.add(`${this.dir}/**/*`).on("error", (error)=>void this.stop(error)).on("all", (event, absolutePath, stats)=>{
|
|
416
|
+
const normalizedPath = this.normalize(absolutePath, event == "addDir" || event == "unlinkDir");
|
|
488
417
|
if (stats?.isSymbolicLink?.()) {
|
|
489
|
-
this.debug("skipping event caused by symlink %s",
|
|
418
|
+
this.debug("skipping event caused by symlink %s", normalizedPath);
|
|
490
419
|
return;
|
|
491
420
|
}
|
|
492
|
-
if (
|
|
421
|
+
if (absolutePath == this.ignorer.filepath) {
|
|
493
422
|
this.ignorer.reload();
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
this.debug("skipping event caused by ignored file %s", relativePath);
|
|
423
|
+
} else if (this.ignorer.ignores(absolutePath)) {
|
|
424
|
+
this.debug("skipping event caused by ignored file %s", normalizedPath);
|
|
497
425
|
return;
|
|
498
426
|
}
|
|
499
427
|
// we only update the mtime if the file is not ignored, because if we restart and the mtime is set to an ignored
|
|
500
428
|
// file, then it could be greater than the mtime of all non ignored files and we'll think that local files have
|
|
501
429
|
// changed when only an ignored one has
|
|
502
|
-
if (stats && stats.mtime.getTime() > this.
|
|
503
|
-
this.
|
|
430
|
+
if (stats && stats.mtime.getTime() > this.state.mtime) {
|
|
431
|
+
this.state.mtime = stats.mtime.getTime();
|
|
504
432
|
}
|
|
505
|
-
if (this.recentRemoteChanges.delete(
|
|
506
|
-
this.debug("skipping event caused by recent write %s",
|
|
433
|
+
if (this.recentRemoteChanges.delete(normalizedPath)) {
|
|
434
|
+
this.debug("skipping event caused by recent write %s", normalizedPath);
|
|
507
435
|
return;
|
|
508
436
|
}
|
|
509
|
-
this.debug("file changed %s",
|
|
510
|
-
switch
|
|
437
|
+
this.debug("file changed %s", normalizedPath, event);
|
|
438
|
+
switch(event){
|
|
511
439
|
case "add":
|
|
512
440
|
case "change":
|
|
513
|
-
(
|
|
514
|
-
localFilesBuffer.set(
|
|
441
|
+
assert(stats, "missing stats on add/change event");
|
|
442
|
+
localFilesBuffer.set(normalizedPath, {
|
|
443
|
+
mode: stats.mode,
|
|
444
|
+
isDirectory: false
|
|
445
|
+
});
|
|
515
446
|
break;
|
|
516
447
|
case "addDir":
|
|
517
|
-
(
|
|
518
|
-
localFilesBuffer.set(
|
|
448
|
+
assert(stats, "missing stats on addDir event");
|
|
449
|
+
localFilesBuffer.set(normalizedPath, {
|
|
450
|
+
mode: stats.mode,
|
|
451
|
+
isDirectory: true
|
|
452
|
+
});
|
|
519
453
|
break;
|
|
520
454
|
case "unlinkDir":
|
|
521
455
|
case "unlink":
|
|
522
|
-
localFilesBuffer.set(
|
|
456
|
+
localFilesBuffer.set(normalizedPath, {
|
|
457
|
+
isDeleted: true,
|
|
458
|
+
isDirectory: event === "unlinkDir"
|
|
459
|
+
});
|
|
523
460
|
break;
|
|
524
461
|
}
|
|
525
462
|
this.publish();
|
|
526
463
|
});
|
|
527
464
|
this.status = SyncStatus.RUNNING;
|
|
528
465
|
// app should be defined at this point
|
|
529
|
-
(
|
|
466
|
+
assert(context.app);
|
|
530
467
|
this.log();
|
|
531
|
-
this.log((
|
|
468
|
+
this.log(dedent(chalkTemplate`
|
|
532
469
|
{bold ggt v${this.config.version}}
|
|
533
470
|
|
|
534
|
-
App ${
|
|
535
|
-
Editor https://${
|
|
536
|
-
Playground https://${
|
|
537
|
-
Docs https://docs.gadget.dev/api/${
|
|
471
|
+
App ${context.app.slug}
|
|
472
|
+
Editor https://${context.app.slug}.gadget.app/edit
|
|
473
|
+
Playground https://${context.app.slug}.gadget.app/api/graphql/playground
|
|
474
|
+
Docs https://docs.gadget.dev/api/${context.app.slug}
|
|
538
475
|
|
|
539
|
-
{underline Endpoints} ${
|
|
540
|
-
|
|
541
|
-
- https://${
|
|
542
|
-
- https://${
|
|
543
|
-
: `
|
|
544
|
-
- https://${context_1.context.app.primaryDomain}`}
|
|
476
|
+
{underline Endpoints} ${context.app.hasSplitEnvironments ? `
|
|
477
|
+
- https://${context.app.primaryDomain}
|
|
478
|
+
- https://${context.app.slug}--development.gadget.app` : `
|
|
479
|
+
- https://${context.app.primaryDomain}`}
|
|
545
480
|
|
|
546
481
|
Watching for file changes... {gray Press Ctrl+C to stop}
|
|
547
482
|
`));
|
|
548
483
|
this.log();
|
|
549
484
|
await stopped;
|
|
550
485
|
if (error) {
|
|
551
|
-
this.notify({
|
|
486
|
+
this.notify({
|
|
487
|
+
subtitle: "Uh oh!",
|
|
488
|
+
message: "An error occurred while syncing files"
|
|
489
|
+
});
|
|
552
490
|
throw error;
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
491
|
+
} else {
|
|
555
492
|
this.log("Goodbye!");
|
|
556
493
|
}
|
|
557
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Enqueues a function that handles file-sync events onto the {@linkcode queue}.
|
|
497
|
+
*
|
|
498
|
+
* @param fn The function to enqueue.
|
|
499
|
+
*/ _enqueue(fn) {
|
|
500
|
+
void this.queue.add(fn).catch(this.stop);
|
|
501
|
+
}
|
|
502
|
+
constructor(...args){
|
|
503
|
+
super(...args);
|
|
504
|
+
_define_property(this, "requireUser", true);
|
|
505
|
+
/**
|
|
506
|
+
* The current status of the sync process.
|
|
507
|
+
*/ _define_property(this, "status", SyncStatus.STARTING);
|
|
508
|
+
/**
|
|
509
|
+
* The absolute path to the directory to sync files to.
|
|
510
|
+
*/ _define_property(this, "dir", void 0);
|
|
511
|
+
/**
|
|
512
|
+
* A list of filepaths that have changed because of a remote file-sync event. This is used to avoid sending files that
|
|
513
|
+
* we recently received from a remote file-sync event.
|
|
514
|
+
*/ _define_property(this, "recentRemoteChanges", new Set());
|
|
515
|
+
/**
|
|
516
|
+
* A FIFO async callback queue that ensures we process file-sync events in the order they occurred.
|
|
517
|
+
*/ _define_property(this, "queue", new PQueue({
|
|
518
|
+
concurrency: 1
|
|
519
|
+
}));
|
|
520
|
+
/**
|
|
521
|
+
* A GraphQL client connected to the app's /edit/api/graphql-ws endpoint
|
|
522
|
+
*/ _define_property(this, "client", void 0);
|
|
523
|
+
/**
|
|
524
|
+
* Loads the .ignore file and provides methods for checking if a file should be ignored.
|
|
525
|
+
*/ _define_property(this, "ignorer", void 0);
|
|
526
|
+
/**
|
|
527
|
+
* Watches the local filesystem for changes.
|
|
528
|
+
*/ _define_property(this, "watcher", void 0);
|
|
529
|
+
/**
|
|
530
|
+
* The state of the local filesystem.
|
|
531
|
+
*/ _define_property(this, "state", void 0);
|
|
532
|
+
/**
|
|
533
|
+
* A debounced function that enqueue's local file changes to be sent to Gadget.
|
|
534
|
+
*/ _define_property(this, "publish", void 0);
|
|
535
|
+
/**
|
|
536
|
+
* Gracefully stops the sync.
|
|
537
|
+
*/ _define_property(this, "stop", void 0);
|
|
538
|
+
}
|
|
558
539
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
value: 1
|
|
564
|
-
});
|
|
565
|
-
Object.defineProperty(Sync, "summary", {
|
|
566
|
-
enumerable: true,
|
|
567
|
-
configurable: true,
|
|
568
|
-
writable: true,
|
|
569
|
-
value: "Sync your Gadget application's source code to and from your local filesystem."
|
|
570
|
-
});
|
|
571
|
-
Object.defineProperty(Sync, "usage", {
|
|
572
|
-
enumerable: true,
|
|
573
|
-
configurable: true,
|
|
574
|
-
writable: true,
|
|
575
|
-
value: "sync [DIRECTORY] [--app <name>]"
|
|
576
|
-
});
|
|
577
|
-
Object.defineProperty(Sync, "description", {
|
|
578
|
-
enumerable: true,
|
|
579
|
-
configurable: true,
|
|
580
|
-
writable: true,
|
|
581
|
-
value: (0, ts_dedent_1.default)((0, chalk_1.default) `
|
|
540
|
+
_define_property(Sync, "priority", 1);
|
|
541
|
+
_define_property(Sync, "summary", "Sync your Gadget application's source code to and from your local filesystem.");
|
|
542
|
+
_define_property(Sync, "usage", "sync [DIRECTORY] [--app <name>]");
|
|
543
|
+
_define_property(Sync, "description", dedent(chalkTemplate`
|
|
582
544
|
Sync provides the ability to sync your Gadget application's source code to and from your local
|
|
583
545
|
filesystem. While {gray ggt sync} is running, local file changes are immediately reflected within
|
|
584
546
|
Gadget, while files that are changed remotely are immediately saved to your local filesystem.
|
|
@@ -602,62 +564,47 @@ Object.defineProperty(Sync, "description", {
|
|
|
602
564
|
- Since file changes are immediately reflected in Gadget, avoid the following while {gray ggt sync} is running:
|
|
603
565
|
- Deleting all your files
|
|
604
566
|
- Moving all your files to a different directory
|
|
605
|
-
`)
|
|
567
|
+
`));
|
|
568
|
+
_define_property(Sync, "args", {
|
|
569
|
+
directory: Args.string({
|
|
570
|
+
description: "The directory to sync files to. If the directory doesn't exist, it will be created.",
|
|
571
|
+
default: "."
|
|
572
|
+
})
|
|
606
573
|
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
name: "file-stability-threshold",
|
|
639
|
-
summary: "Time in milliseconds a file's size must remain the same.",
|
|
640
|
-
helpGroup: "file",
|
|
641
|
-
helpValue: "ms",
|
|
642
|
-
default: 500,
|
|
643
|
-
hidden: true,
|
|
644
|
-
}),
|
|
645
|
-
"file-poll-interval": core_1.Flags.integer({
|
|
646
|
-
name: "file-poll-interval",
|
|
647
|
-
description: "Interval in milliseconds between polling a file's size.",
|
|
648
|
-
helpGroup: "file",
|
|
649
|
-
helpValue: "ms",
|
|
650
|
-
default: 100,
|
|
651
|
-
hidden: true,
|
|
652
|
-
}),
|
|
653
|
-
}
|
|
574
|
+
_define_property(Sync, "flags", {
|
|
575
|
+
app: app({
|
|
576
|
+
summary: "The Gadget application to sync files to."
|
|
577
|
+
}),
|
|
578
|
+
force: Flags.boolean({
|
|
579
|
+
summary: "Whether to sync even if we can't determine the state of your local files relative to your remote ones.",
|
|
580
|
+
default: false
|
|
581
|
+
}),
|
|
582
|
+
"file-push-delay": Flags.integer({
|
|
583
|
+
summary: "Delay in milliseconds before pushing files to your app.",
|
|
584
|
+
helpGroup: "file",
|
|
585
|
+
helpValue: "ms",
|
|
586
|
+
default: 100,
|
|
587
|
+
hidden: true
|
|
588
|
+
}),
|
|
589
|
+
"file-stability-threshold": Flags.integer({
|
|
590
|
+
name: "file-stability-threshold",
|
|
591
|
+
summary: "Time in milliseconds a file's size must remain the same.",
|
|
592
|
+
helpGroup: "file",
|
|
593
|
+
helpValue: "ms",
|
|
594
|
+
default: 500,
|
|
595
|
+
hidden: true
|
|
596
|
+
}),
|
|
597
|
+
"file-poll-interval": Flags.integer({
|
|
598
|
+
name: "file-poll-interval",
|
|
599
|
+
description: "Interval in milliseconds between polling a file's size.",
|
|
600
|
+
helpGroup: "file",
|
|
601
|
+
helpValue: "ms",
|
|
602
|
+
default: 100,
|
|
603
|
+
hidden: true
|
|
604
|
+
})
|
|
654
605
|
});
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
configurable: true,
|
|
658
|
-
writable: true,
|
|
659
|
-
value: [
|
|
660
|
-
(0, ts_dedent_1.default)((0, chalk_1.default) `
|
|
606
|
+
_define_property(Sync, "examples", [
|
|
607
|
+
dedent(chalkTemplate`
|
|
661
608
|
{gray $ ggt sync --app my-app ~/gadget/my-app}
|
|
662
609
|
|
|
663
610
|
App my-app
|
|
@@ -682,24 +629,110 @@ Object.defineProperty(Sync, "examples", {
|
|
|
682
629
|
|
|
683
630
|
^C Stopping... {gray (press Ctrl+C again to force)}
|
|
684
631
|
Goodbye!
|
|
685
|
-
`)
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
632
|
+
`)
|
|
633
|
+
]);
|
|
634
|
+
export { Sync as default };
|
|
635
|
+
var /**
|
|
636
|
+
* Saves the current state of the filesystem to `.gadget/sync.json`.
|
|
637
|
+
*/ _save = /*#__PURE__*/ new WeakMap();
|
|
638
|
+
/**
|
|
639
|
+
* Holds information about the state of the local filesystem. It's persisted to `.gadget/sync.json`.
|
|
640
|
+
*/ export class SyncState {
|
|
641
|
+
/**
|
|
642
|
+
* The app this filesystem is synced to.
|
|
643
|
+
*/ get app() {
|
|
644
|
+
return this._inner.app;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* The last filesVersion that was successfully written to the filesystem. This is used to determine if the remote
|
|
648
|
+
* filesystem is ahead of the local one.
|
|
649
|
+
*/ get filesVersion() {
|
|
650
|
+
return BigInt(this._inner.filesVersion);
|
|
651
|
+
}
|
|
652
|
+
set filesVersion(value) {
|
|
653
|
+
this._inner.filesVersion = String(value);
|
|
654
|
+
_class_private_field_get(this, _save).call(this);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* The largest mtime that was seen on the local filesystem before `ggt sync` stopped. This is used to determine if
|
|
658
|
+
* the local filesystem has changed since the last sync.
|
|
659
|
+
*
|
|
660
|
+
* Note: This does not include the mtime of files that are ignored.
|
|
661
|
+
*/ // eslint-disable-next-line @typescript-eslint/member-ordering
|
|
662
|
+
get mtime() {
|
|
663
|
+
return this._inner.mtime;
|
|
664
|
+
}
|
|
665
|
+
set mtime(value) {
|
|
666
|
+
this._inner.mtime = value;
|
|
667
|
+
_class_private_field_get(this, _save).call(this);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Creates a new SyncFile instance and saves it to the filesystem.
|
|
671
|
+
*
|
|
672
|
+
* @param rootDir The root directory of the app.
|
|
673
|
+
* @param app The app slug.
|
|
674
|
+
* @returns A new SyncFile instance.
|
|
675
|
+
*/ static create(rootDir, opts) {
|
|
676
|
+
const state = new SyncState(rootDir, {
|
|
677
|
+
filesVersion: "0",
|
|
678
|
+
mtime: 0,
|
|
679
|
+
...opts
|
|
680
|
+
});
|
|
681
|
+
_class_private_field_get(state, _save).call(state);
|
|
682
|
+
state.flush();
|
|
683
|
+
return state;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Loads a SyncFile instance from the filesystem.
|
|
687
|
+
*
|
|
688
|
+
* @param rootDir The root directory of the app.
|
|
689
|
+
* @returns The SyncFile instance.
|
|
690
|
+
*/ static load(rootDir) {
|
|
691
|
+
const state = fs.readJsonSync(path.join(rootDir, ".gadget/sync.json"));
|
|
692
|
+
assert(_.isString(state.app), "missing or invalid app");
|
|
693
|
+
assert(_.isString(state.filesVersion), "missing or invalid filesVersion");
|
|
694
|
+
assert(_.isNumber(state.mtime), "missing or invalid mtime");
|
|
695
|
+
return new SyncState(rootDir, {
|
|
696
|
+
app: state.app,
|
|
697
|
+
filesVersion: state.filesVersion,
|
|
698
|
+
mtime: state.mtime
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Flushes any pending writes to the filesystem.
|
|
703
|
+
*/ flush() {
|
|
704
|
+
_class_private_field_get(this, _save).flush();
|
|
705
|
+
}
|
|
706
|
+
constructor(_rootDir, inner){
|
|
707
|
+
_define_property(this, "_rootDir", void 0);
|
|
708
|
+
_define_property(this, "_inner", void 0);
|
|
709
|
+
_class_private_field_init(this, _save, {
|
|
710
|
+
writable: true,
|
|
711
|
+
value: void 0
|
|
712
|
+
});
|
|
713
|
+
this._rootDir = _rootDir;
|
|
714
|
+
_class_private_field_set(this, _save, _.debounce(()=>{
|
|
715
|
+
fs.outputJSONSync(path.join(this._rootDir, ".gadget/sync.json"), this._inner, {
|
|
716
|
+
spaces: 2
|
|
717
|
+
});
|
|
718
|
+
}, 100));
|
|
719
|
+
this._inner = inner;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
export var SyncStatus;
|
|
723
|
+
(function(SyncStatus) {
|
|
691
724
|
SyncStatus[SyncStatus["STARTING"] = 0] = "STARTING";
|
|
692
725
|
SyncStatus[SyncStatus["RUNNING"] = 1] = "RUNNING";
|
|
693
726
|
SyncStatus[SyncStatus["STOPPING"] = 2] = "STOPPING";
|
|
694
727
|
SyncStatus[SyncStatus["STOPPED"] = 3] = "STOPPED";
|
|
695
|
-
})(SyncStatus
|
|
696
|
-
var Action;
|
|
697
|
-
(function
|
|
728
|
+
})(SyncStatus || (SyncStatus = {}));
|
|
729
|
+
export var Action;
|
|
730
|
+
(function(Action) {
|
|
698
731
|
Action["CANCEL"] = "Cancel (Ctrl+C)";
|
|
699
732
|
Action["MERGE"] = "Merge local files with remote ones";
|
|
700
733
|
Action["RESET"] = "Reset local files to remote ones";
|
|
701
|
-
})(Action
|
|
702
|
-
|
|
734
|
+
})(Action || (Action = {}));
|
|
735
|
+
export const REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION = /* GraphQL */ `
|
|
703
736
|
subscription RemoteFileSyncEvents($localFilesVersion: String!) {
|
|
704
737
|
remoteFileSyncEvents(localFilesVersion: $localFilesVersion, encoding: base64) {
|
|
705
738
|
remoteFilesVersion
|
|
@@ -715,16 +748,17 @@ exports.REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION = `
|
|
|
715
748
|
}
|
|
716
749
|
}
|
|
717
750
|
`;
|
|
718
|
-
|
|
751
|
+
export const REMOTE_FILES_VERSION_QUERY = /* GraphQL */ `
|
|
719
752
|
query RemoteFilesVersion {
|
|
720
753
|
remoteFilesVersion
|
|
721
754
|
}
|
|
722
755
|
`;
|
|
723
|
-
|
|
756
|
+
export const PUBLISH_FILE_SYNC_EVENTS_MUTATION = /* GraphQL */ `
|
|
724
757
|
mutation PublishFileSyncEvents($input: PublishFileSyncEventsInput!) {
|
|
725
758
|
publishFileSyncEvents(input: $input) {
|
|
726
759
|
remoteFilesVersion
|
|
727
760
|
}
|
|
728
761
|
}
|
|
729
762
|
`;
|
|
763
|
+
|
|
730
764
|
//# sourceMappingURL=sync.js.map
|