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