@gadgetinc/ggt 0.3.3 → 0.4.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.
Files changed (148) hide show
  1. package/README.md +139 -76
  2. package/bin/dev.js +4 -7
  3. package/lib/__generated__/graphql.js.map +1 -1
  4. package/lib/commands/deploy.js +232 -0
  5. package/lib/commands/deploy.js.map +1 -0
  6. package/lib/commands/list.js +20 -16
  7. package/lib/commands/list.js.map +1 -1
  8. package/lib/commands/login.js +22 -20
  9. package/lib/commands/login.js.map +1 -1
  10. package/lib/commands/logout.js +13 -9
  11. package/lib/commands/logout.js.map +1 -1
  12. package/lib/commands/root.js +89 -56
  13. package/lib/commands/root.js.map +1 -1
  14. package/lib/commands/sync.js +253 -496
  15. package/lib/commands/sync.js.map +1 -1
  16. package/lib/commands/version.js +21 -0
  17. package/lib/commands/version.js.map +1 -0
  18. package/lib/commands/whoami.js +15 -11
  19. package/lib/commands/whoami.js.map +1 -1
  20. package/lib/main.js +4 -10
  21. package/lib/main.js.map +1 -1
  22. package/lib/services/{app.js → app/app.js} +8 -3
  23. package/lib/services/app/app.js.map +1 -0
  24. package/lib/services/app/arg.js +28 -0
  25. package/lib/services/app/arg.js.map +1 -0
  26. package/lib/services/app/edit-graphql.js +389 -0
  27. package/lib/services/app/edit-graphql.js.map +1 -0
  28. package/lib/services/command/arg.js +53 -0
  29. package/lib/services/command/arg.js.map +1 -0
  30. package/lib/services/command/command.js +27 -0
  31. package/lib/services/command/command.js.map +1 -0
  32. package/lib/services/command/context.js +60 -0
  33. package/lib/services/command/context.js.map +1 -0
  34. package/lib/services/{config.js → config/config.js} +29 -31
  35. package/lib/services/config/config.js.map +1 -0
  36. package/lib/services/config/env.js +22 -0
  37. package/lib/services/config/env.js.map +1 -0
  38. package/lib/services/config/package-json.js +9 -0
  39. package/lib/services/config/package-json.js.map +1 -0
  40. package/lib/services/filesync/changes.js +97 -0
  41. package/lib/services/filesync/changes.js.map +1 -0
  42. package/lib/services/filesync/conflicts.js +137 -0
  43. package/lib/services/filesync/conflicts.js.map +1 -0
  44. package/lib/services/filesync/directory.js +253 -0
  45. package/lib/services/filesync/directory.js.map +1 -0
  46. package/lib/services/filesync/error.js +67 -0
  47. package/lib/services/filesync/error.js.map +1 -0
  48. package/lib/services/filesync/file.js +3 -0
  49. package/lib/services/filesync/file.js.map +1 -0
  50. package/lib/services/filesync/filesync.js +673 -0
  51. package/lib/services/filesync/filesync.js.map +1 -0
  52. package/lib/services/filesync/hashes.js +150 -0
  53. package/lib/services/filesync/hashes.js.map +1 -0
  54. package/lib/services/http/auth.js +41 -0
  55. package/lib/services/http/auth.js.map +1 -0
  56. package/lib/services/http/http.js +64 -0
  57. package/lib/services/http/http.js.map +1 -0
  58. package/lib/services/output/log/field.js +3 -0
  59. package/lib/services/output/log/field.js.map +1 -0
  60. package/lib/services/output/log/format/format.js +8 -0
  61. package/lib/services/output/log/format/format.js.map +1 -0
  62. package/lib/services/output/log/format/json.js +45 -0
  63. package/lib/services/output/log/format/json.js.map +1 -0
  64. package/lib/services/output/log/format/pretty.js +147 -0
  65. package/lib/services/output/log/format/pretty.js.map +1 -0
  66. package/lib/services/output/log/level.js +41 -0
  67. package/lib/services/output/log/level.js.map +1 -0
  68. package/lib/services/output/log/logger.js +40 -0
  69. package/lib/services/output/log/logger.js.map +1 -0
  70. package/lib/services/output/log/printer.js +120 -0
  71. package/lib/services/output/log/printer.js.map +1 -0
  72. package/lib/services/output/log/structured.js +52 -0
  73. package/lib/services/output/log/structured.js.map +1 -0
  74. package/lib/services/{notify.js → output/notify.js} +7 -6
  75. package/lib/services/output/notify.js.map +1 -0
  76. package/lib/services/output/prompt.js +52 -0
  77. package/lib/services/output/prompt.js.map +1 -0
  78. package/lib/services/output/report.js +162 -0
  79. package/lib/services/output/report.js.map +1 -0
  80. package/lib/services/output/sprint.js +21 -0
  81. package/lib/services/output/sprint.js.map +1 -0
  82. package/lib/services/output/stream.js +54 -0
  83. package/lib/services/output/stream.js.map +1 -0
  84. package/lib/services/{version.js → output/update.js} +24 -16
  85. package/lib/services/output/update.js.map +1 -0
  86. package/lib/services/user/session.js +50 -0
  87. package/lib/services/user/session.js.map +1 -0
  88. package/lib/services/{user.js → user/user.js} +23 -14
  89. package/lib/services/user/user.js.map +1 -0
  90. package/lib/services/util/boolean.js +15 -0
  91. package/lib/services/util/boolean.js.map +1 -0
  92. package/lib/services/util/collection.js +38 -0
  93. package/lib/services/util/collection.js.map +1 -0
  94. package/lib/services/util/function.js +97 -0
  95. package/lib/services/util/function.js.map +1 -0
  96. package/lib/services/{is.js → util/is.js} +7 -0
  97. package/lib/services/util/is.js.map +1 -0
  98. package/lib/services/util/number.js +27 -0
  99. package/lib/services/util/number.js.map +1 -0
  100. package/lib/services/util/object.js +101 -0
  101. package/lib/services/util/object.js.map +1 -0
  102. package/lib/services/util/paths.js +36 -0
  103. package/lib/services/util/paths.js.map +1 -0
  104. package/lib/services/{promise.js → util/promise.js} +5 -7
  105. package/lib/services/util/promise.js.map +1 -0
  106. package/npm-shrinkwrap.json +2143 -1304
  107. package/package.json +50 -42
  108. package/lib/commands/index.js +0 -9
  109. package/lib/commands/index.js.map +0 -1
  110. package/lib/services/app.js.map +0 -1
  111. package/lib/services/args.js +0 -28
  112. package/lib/services/args.js.map +0 -1
  113. package/lib/services/collections.js +0 -17
  114. package/lib/services/collections.js.map +0 -1
  115. package/lib/services/config.js.map +0 -1
  116. package/lib/services/debounce.js +0 -21
  117. package/lib/services/debounce.js.map +0 -1
  118. package/lib/services/defaults.js +0 -8
  119. package/lib/services/defaults.js.map +0 -1
  120. package/lib/services/edit-graphql.js +0 -202
  121. package/lib/services/edit-graphql.js.map +0 -1
  122. package/lib/services/errors.js +0 -277
  123. package/lib/services/errors.js.map +0 -1
  124. package/lib/services/filesync.js +0 -404
  125. package/lib/services/filesync.js.map +0 -1
  126. package/lib/services/fs.js +0 -35
  127. package/lib/services/fs.js.map +0 -1
  128. package/lib/services/http.js +0 -53
  129. package/lib/services/http.js.map +0 -1
  130. package/lib/services/is.js.map +0 -1
  131. package/lib/services/log.js +0 -45
  132. package/lib/services/log.js.map +0 -1
  133. package/lib/services/noop.js +0 -4
  134. package/lib/services/noop.js.map +0 -1
  135. package/lib/services/notify.js.map +0 -1
  136. package/lib/services/output.js +0 -74
  137. package/lib/services/output.js.map +0 -1
  138. package/lib/services/promise.js.map +0 -1
  139. package/lib/services/prompt.js +0 -22
  140. package/lib/services/prompt.js.map +0 -1
  141. package/lib/services/session.js +0 -31
  142. package/lib/services/session.js.map +0 -1
  143. package/lib/services/sleep.js +0 -21
  144. package/lib/services/sleep.js.map +0 -1
  145. package/lib/services/timeout.js +0 -8
  146. package/lib/services/timeout.js.map +0 -1
  147. package/lib/services/user.js.map +0 -1
  148. package/lib/services/version.js.map +0 -1
@@ -1,556 +1,313 @@
1
- import { _ as _define_property } from "@swc/helpers/_/_define_property";
2
- import arg from "arg";
3
1
  import dayjs from "dayjs";
4
2
  import { execa } from "execa";
5
- import fs from "fs-extra";
6
3
  import ms from "ms";
7
4
  import path from "node:path";
8
- import pMap from "p-map";
9
- import PQueue from "p-queue";
10
- import FSWatcher from "watcher";
5
+ import Watcher from "watcher";
11
6
  import which from "which";
12
- import { FileSyncEncoding } from "../__generated__/graphql.js";
13
- import { AppArg } from "../services/args.js";
14
- import { config } from "../services/config.js";
15
- import { debounce } from "../services/debounce.js";
16
- import { defaults } from "../services/defaults.js";
17
- import { EditGraphQL } from "../services/edit-graphql.js";
18
- import { YarnNotFoundError } from "../services/errors.js";
19
- import { FileSync, PUBLISH_FILE_SYNC_EVENTS_MUTATION, REMOTE_FILES_VERSION_QUERY, REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION, printPaths } from "../services/filesync.js";
20
- import { swallowEnoent } from "../services/fs.js";
21
- import { createLogger } from "../services/log.js";
22
- import { noop } from "../services/noop.js";
23
- import { notify } from "../services/notify.js";
24
- import { println, sprint } from "../services/output.js";
25
- import { PromiseSignal } from "../services/promise.js";
26
- import { select } from "../services/prompt.js";
27
- import { getUserOrLogin } from "../services/user.js";
28
- export const usage = sprint`
29
- Sync your Gadget application's source code to and from
30
- your local filesystem.
7
+ import { AppArg } from "../services/app/arg.js";
8
+ import { config } from "../services/config/config.js";
9
+ import { Changes } from "../services/filesync/changes.js";
10
+ import { YarnNotFoundError } from "../services/filesync/error.js";
11
+ import { ConflictPreferenceArg, FileSync } from "../services/filesync/filesync.js";
12
+ import { notify } from "../services/output/notify.js";
13
+ import { reportErrorAndExit } from "../services/output/report.js";
14
+ import { sprint } from "../services/output/sprint.js";
15
+ import { getUserOrLogin } from "../services/user/user.js";
16
+ import { debounce } from "../services/util/function.js";
17
+ import { isAbortError } from "../services/util/is.js";
18
+ export const usage = ()=>sprint`
19
+ Sync your Gadget environment's source code with your local filesystem.
31
20
 
32
21
  {bold USAGE}
33
- $ ggt sync [DIRECTORY] [--app <name>]
22
+ ggt sync [DIRECTORY]
34
23
 
35
24
  {bold ARGUMENTS}
36
- DIRECTORY {dim [default: .] The directory to sync files to.
37
-
38
- If the directory doesn't exist, it will be created.}
25
+ DIRECTORY The directory to sync files to (default: ".")
39
26
 
40
27
  {bold FLAGS}
41
- -a, --app=<name> {dim The Gadget application to sync files to.}
42
-
43
- --force {dim Whether to sync even if we can't determine
44
- the state of your local files relative to
45
- your remote ones.}
28
+ -a, --app=<name> The Gadget application to sync files to
29
+ --prefer=<filesystem> Prefer "local" or "gadget" conflicting changes
30
+ --once Sync once and exit
31
+ --force Sync regardless of local filesystem state
46
32
 
47
33
  {bold DESCRIPTION}
48
- Sync provides the ability to sync your Gadget application's source
49
- code to and from your local filesystem.
34
+ Sync allows you to synchronize your Gadget application's source
35
+ code with your local filesystem.
50
36
 
51
37
  While ggt sync is running, local file changes are immediately
52
- reflected within Gadget, while files that are changed remotely are
38
+ reflected within Gadget, while files that are changed in Gadget are
53
39
  immediately saved to your local filesystem.
54
40
 
55
- Use cases for this include:
56
- Developing locally with your own editor like VSCode
57
- • Storing your source code in a Git repository like GitHub
41
+ Ideal for:
42
+ Local development with editors like VSCode
43
+ • Storing source code in a Git repository like GitHub
58
44
 
59
- Sync includes the concept of a {dim .ignore} file. This file may
60
- contain a list of files and directories that won't be received or
61
- sent to Gadget when syncing. The format of this file is identical
62
- to the one used by Git {dim (https://git-scm.com/docs/gitignore)}.
45
+ Sync looks for a ".ignore" file to exclude certain files/directories
46
+ from being synced. The format is identical to Git's.
63
47
 
64
- The following files and directories are always ignored:
48
+ These files are always ignored:
65
49
  • .DS_Store
66
50
  • .gadget
67
51
  • .git
68
52
  • node_modules
69
53
 
70
54
  Note:
71
- If you have separate development and production environments,
72
- {dim ggt sync} will only sync with your development environment
73
- • Gadget applications only support installing dependencies
74
- with Yarn 1 {dim (https://classic.yarnpkg.com/lang/en/)}
75
- Since file changes are immediately reflected in Gadget,
76
- avoid the following while {dim ggt sync} is running:
77
- • Deleting all your files
78
- • Moving all your files to a different directory
55
+ Sync only works with your development environment
56
+ Avoid deleting/moving all your files while sync is running
57
+ • Gadget only supports Yarn v1 for dependency installation
58
+
59
+ {bold EXAMPLE}
60
+ $ ggt sync ~/gadget/example --app example
79
61
 
80
- {bold EXAMPLES}
81
- {dim $ ggt sync --app my-app ~/gadget/my-app}
62
+ App example
63
+ Editor https://example.gadget.app/edit
64
+ Playground https://example.gadget.app/api/graphql/playground
65
+ Docs https://docs.gadget.dev/api/example
82
66
 
83
- App my-app
84
- Editor https://my-app.gadget.app/edit
85
- Playground https://my-app.gadget.app/api/graphql/playground
86
- Docs https://docs.gadget.dev/api/my-app
67
+ Endpoints
68
+ https://example.gadget.app
69
+ https://example--development.gadget.app
70
+
71
+ Watching for file changes... {gray Press Ctrl+C to stop}
87
72
 
88
- Endpoints
89
- https://my-app.gadget.app
90
- • https://my-app--development.gadget.app
73
+ → Sent {gray 09:06:25 AM}
74
+ {greenBright routes/GET-hello.js + created}
91
75
 
92
- Watching for file changes... {dim Press Ctrl+C to stop}
76
+ Sent {gray 09:06:49 AM}
77
+ {blueBright routes/GET-hello.js ± updated}
93
78
 
94
- Received {dim 12:00:00 PM}
95
- {green ←} routes/GET.js {dim (changed)}
96
- {green ←} user/signUp/signIn.js {dim (changed)}
97
- {dim 2 files in total. 2 changed, 0 deleted.}
79
+ Received {gray 09:06:54 AM}
80
+ {blueBright routes/GET-hello.js ± updated}
98
81
 
99
- Sent {dim 12:00:03 PM}
100
- {green →} routes/GET.ts {dim (changed)}
101
- {dim 1 file in total. 1 changed, 0 deleted.}
82
+ Received {gray 09:06:56 AM}
83
+ {redBright routes/GET-hello.js - deleted}
84
+ ^C Stopping... {gray press Ctrl+C again to force}
102
85
 
103
- ^C Stopping... {dim (press Ctrl+C again to force)}
104
- Goodbye!
86
+ Goodbye!
105
87
  `;
106
- export var SyncStatus;
107
- (function(SyncStatus) {
108
- SyncStatus[SyncStatus["STARTING"] = 0] = "STARTING";
109
- SyncStatus[SyncStatus["RUNNING"] = 1] = "RUNNING";
110
- SyncStatus[SyncStatus["STOPPING"] = 2] = "STOPPING";
111
- SyncStatus[SyncStatus["STOPPED"] = 3] = "STOPPED";
112
- })(SyncStatus || (SyncStatus = {}));
113
- export var Action;
114
- (function(Action) {
115
- Action["CANCEL"] = "Cancel (Ctrl+C)";
116
- Action["MERGE"] = "Merge local files with remote ones";
117
- Action["RESET"] = "Reset local files to remote ones";
118
- })(Action || (Action = {}));
119
- const argSpec = {
120
- "-a": "--app",
121
- "--app": AppArg,
88
+ export const args = {
89
+ "--app": {
90
+ type: AppArg,
91
+ alias: "-a"
92
+ },
122
93
  "--force": Boolean,
123
- "--file-push-delay": Number,
124
- "--file-watch-debounce": Number,
125
- "--file-watch-poll-interval": Number,
126
- "--file-watch-poll-timeout": Number,
127
- "--file-watch-rename-timeout": Number
94
+ "--once": Boolean,
95
+ "--prefer": ConflictPreferenceArg,
96
+ "--file-push-delay": {
97
+ type: Number,
98
+ default: ms("100ms")
99
+ },
100
+ "--file-watch-debounce": {
101
+ type: Number,
102
+ default: ms("300ms")
103
+ },
104
+ "--file-watch-poll-interval": {
105
+ type: Number,
106
+ default: ms("3s")
107
+ },
108
+ "--file-watch-poll-timeout": {
109
+ type: Number,
110
+ default: ms("20s")
111
+ },
112
+ "--file-watch-rename-timeout": {
113
+ type: Number,
114
+ default: ms("1.25s")
115
+ }
128
116
  };
129
- export class Sync {
117
+ /**
118
+ * Runs the sync process until it is stopped or an error occurs.
119
+ */ export const command = async (ctx)=>{
120
+ const filesync = await FileSync.init({
121
+ user: await getUserOrLogin(),
122
+ dir: ctx.args._[0],
123
+ app: ctx.args["--app"],
124
+ force: ctx.args["--force"]
125
+ });
126
+ await filesync.sync({
127
+ preference: ctx.args["--prefer"]
128
+ });
129
+ if (ctx.args["--once"]) {
130
+ ctx.log.println("Done!");
131
+ return;
132
+ }
133
+ if (!which.sync("yarn", {
134
+ nothrow: true
135
+ })) {
136
+ throw new YarnNotFoundError();
137
+ }
130
138
  /**
131
- * Initializes the sync process.
132
- * - Ensures the directory exists.
133
- * - Ensures the directory is empty or contains a `.gadget/sync.json` file.
134
- * - Ensures an app is selected and that it matches the app the directory was previously synced to.
135
- * - Ensures yarn v1 is installed.
136
- * - Prompts the user how to resolve conflicts if the local filesystem has changed since the last sync.
137
- */ async init(rootArgs) {
138
- this.args = defaults(arg(argSpec, {
139
- argv: rootArgs._
140
- }), {
141
- "--file-push-delay": 100,
142
- "--file-watch-debounce": 300,
143
- "--file-watch-poll-interval": 3_000,
144
- "--file-watch-poll-timeout": 20_000,
145
- "--file-watch-rename-timeout": 1_250
146
- });
147
- if (!which.sync("yarn", {
148
- nothrow: true
149
- })) {
150
- throw new YarnNotFoundError();
139
+ * A list of filepaths that have changed because we (this ggt process)
140
+ * modified them. This is used to avoid reacting to filesystem events
141
+ * that we caused, which would cause an infinite loop.
142
+ */ const recentWritesToLocalFilesystem = new Map();
143
+ const clearRecentWritesInterval = setInterval(()=>{
144
+ for (const [path, timestamp] of recentWritesToLocalFilesystem){
145
+ if (dayjs().isAfter(timestamp + ms("5s"))) {
146
+ // this change should have been seen by now
147
+ recentWritesToLocalFilesystem.delete(path);
148
+ }
151
149
  }
152
- const user = await getUserOrLogin();
153
- this.filesync = await FileSync.init(user, {
154
- dir: this.args._[0],
155
- app: this.args["--app"],
156
- force: this.args["--force"],
157
- extraIgnorePaths: [
158
- ".gadget"
159
- ]
160
- });
161
- this.graphql = new EditGraphQL(this.filesync.app);
162
- const { remoteFilesVersion } = await this.graphql.query({
163
- query: REMOTE_FILES_VERSION_QUERY
164
- });
165
- const hasRemoteChanges = BigInt(remoteFilesVersion) > this.filesync.filesVersion;
166
- const getChangedFiles = async ()=>{
167
- const files = new Map();
168
- for await (const [absolutePath, stats] of this.filesync.walkDir()){
169
- if (stats.mtime.getTime() > this.filesync.mtime) {
170
- files.set(this.filesync.normalize(absolutePath, stats.isDirectory()), stats);
150
+ }, ms("1s")).unref();
151
+ /**
152
+ * Subscribe to file changes on Gadget and apply them to the local
153
+ * filesystem.
154
+ */ const unsubscribeFromGadgetChanges = filesync.subscribeToGadgetChanges({
155
+ onError: (error)=>ctx.abort(error),
156
+ beforeChanges: ({ changed, deleted })=>{
157
+ // add all the files and directories we're about to touch to
158
+ // recentWritesToLocalFilesystem so that we don't send them back
159
+ // to Gadget
160
+ for (const filepath of [
161
+ ...changed,
162
+ ...deleted
163
+ ]){
164
+ recentWritesToLocalFilesystem.set(filepath, Date.now());
165
+ let dir = path.dirname(filepath);
166
+ while(dir !== "."){
167
+ recentWritesToLocalFilesystem.set(dir + "/", Date.now());
168
+ dir = path.dirname(dir);
171
169
  }
172
170
  }
173
- // never include the root directory
174
- files.delete("/");
175
- return files;
176
- };
177
- let changedFiles = await getChangedFiles();
178
- const hasLocalChanges = changedFiles.size > 0;
179
- if (hasLocalChanges) {
180
- this.log.info("local files have changed", {
181
- remoteFilesVersion,
182
- hasRemoteChanges,
183
- hasLocalChanges,
184
- changed: Array.from(changedFiles.keys())
185
- });
186
- println("Local files have changed since you last synced");
187
- printPaths("-", Array.from(changedFiles.keys()), [], {
188
- limit: changedFiles.size
189
- });
190
- println();
171
+ },
172
+ afterChanges: async ({ changes })=>{
173
+ if (changes.has("yarn.lock")) {
174
+ await execa("yarn", [
175
+ "install",
176
+ "--check-files"
177
+ ], {
178
+ cwd: filesync.directory.path
179
+ }).catch((error)=>{
180
+ ctx.log.error("yarn install failed", {
181
+ error
182
+ });
183
+ });
184
+ }
191
185
  }
192
- let action;
193
- if (hasLocalChanges) {
194
- action = await select({
195
- message: hasRemoteChanges ? "Remote files have also changed. How would you like to proceed?" : "How would you like to proceed?",
196
- choices: [
197
- "Cancel (Ctrl+C)",
198
- "Merge local files with remote ones",
199
- "Reset local files to remote ones"
200
- ]
186
+ });
187
+ /**
188
+ * A buffer of local file changes to send to Gadget.
189
+ */ const localChangesBuffer = new Changes();
190
+ /**
191
+ * A debounced function that sends the local file changes to Gadget.
192
+ */ const sendChangesToGadget = debounce(ctx.args["--file-push-delay"], ()=>{
193
+ const changes = new Changes(localChangesBuffer.entries());
194
+ localChangesBuffer.clear();
195
+ filesync.sendChangesToGadget({
196
+ changes
197
+ }).catch((error)=>ctx.abort(error));
198
+ });
199
+ ctx.log.debug("watching", {
200
+ path: filesync.directory.path
201
+ });
202
+ /**
203
+ * Watches the local filesystem for changes.
204
+ */ const fileWatcher = new Watcher(filesync.directory.path, {
205
+ // don't emit an event for every watched file on boot
206
+ ignoreInitial: true,
207
+ // don't emit changes to .gadget/ files because they're readonly (Gadget manages them)
208
+ ignore: (path)=>filesync.directory.relative(path).startsWith(".gadget") || filesync.directory.ignores(path),
209
+ renameDetection: true,
210
+ recursive: true,
211
+ debounce: ctx.args["--file-watch-debounce"],
212
+ pollingInterval: ctx.args["--file-watch-poll-interval"],
213
+ pollingTimeout: ctx.args["--file-watch-poll-timeout"],
214
+ renameTimeout: ctx.args["--file-watch-rename-timeout"]
215
+ }, (event, absolutePath, renamedPath)=>{
216
+ const filepath = event === "rename" || event === "renameDir" ? renamedPath : absolutePath;
217
+ const isDirectory = event === "renameDir" || event === "addDir" || event === "unlinkDir";
218
+ const normalizedPath = filesync.directory.normalize(filepath, isDirectory);
219
+ ctx.log.trace("file event", {
220
+ event,
221
+ isDirectory,
222
+ path: normalizedPath
223
+ });
224
+ if (filepath === filesync.directory.absolute(".ignore")) {
225
+ filesync.directory.loadIgnoreFile().catch((error)=>ctx.abort(error));
226
+ } else if (filesync.directory.ignores(filepath)) {
227
+ return;
228
+ }
229
+ if (recentWritesToLocalFilesystem.delete(normalizedPath)) {
230
+ ctx.log.trace("ignoring event because we caused it", {
231
+ event,
232
+ path: normalizedPath
201
233
  });
234
+ return;
202
235
  }
203
- // get all the changed files again in case more changed
204
- changedFiles = await getChangedFiles();
205
- switch(action){
206
- case "Merge local files with remote ones":
236
+ switch(event){
237
+ case "add":
238
+ case "addDir":
239
+ localChangesBuffer.set(normalizedPath, {
240
+ type: "create"
241
+ });
242
+ break;
243
+ case "rename":
244
+ case "renameDir":
207
245
  {
208
- this.log.info("merging local changes", {
209
- remoteFilesVersion,
210
- hasRemoteChanges,
211
- hasLocalChanges,
212
- changed: Array.from(changedFiles.keys())
213
- });
214
- // We purposefully don't write the returned files version here
215
- // because we haven't received its associated files yet. This
216
- // will cause us to receive the remote files that have changed
217
- // since the last sync (+ the local files that we just
218
- // published)
219
- await this.graphql.query({
220
- query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
221
- variables: {
222
- input: {
223
- expectedRemoteFilesVersion: remoteFilesVersion,
224
- changed: await pMap(changedFiles, async ([normalizedPath, stats])=>({
225
- path: normalizedPath,
226
- mode: stats.mode,
227
- content: stats.isDirectory() ? "" : await fs.readFile(this.filesync.absolute(normalizedPath), "base64"),
228
- encoding: FileSyncEncoding.Base64
229
- })),
230
- deleted: []
231
- }
232
- }
246
+ const oldNormalizedPath = filesync.directory.normalize(absolutePath, isDirectory);
247
+ localChangesBuffer.set(normalizedPath, {
248
+ type: "create",
249
+ oldPath: oldNormalizedPath
233
250
  });
234
251
  break;
235
252
  }
236
- case "Reset local files to remote ones":
253
+ case "change":
237
254
  {
238
- this.log.info("resetting local changes", {
239
- remoteFilesVersion,
240
- hasRemoteChanges,
241
- hasLocalChanges,
242
- changed: Array.from(changedFiles.keys())
255
+ localChangesBuffer.set(normalizedPath, {
256
+ type: "update"
243
257
  });
244
- // delete all the local files that have changed since the last
245
- // sync and set the files version to 0 so we receive all the
246
- // remote files again, including any files that we just deleted
247
- // that still exist
248
- await this.filesync.write(0n, [], changedFiles.keys(), true);
249
258
  break;
250
259
  }
251
- case "Cancel (Ctrl+C)":
260
+ case "unlink":
261
+ case "unlinkDir":
252
262
  {
253
- process.exit(0);
254
- }
255
- }
256
- }
257
- /**
258
- * Runs the sync process until it is stopped or an error occurs.
259
- */ async run() {
260
- let error;
261
- const stopped = new PromiseSignal();
262
- const recentRemoteChangesInterval = setInterval(()=>{
263
- for (const [path, timestamp] of this.recentRemoteChanges){
264
- if (dayjs().isAfter(timestamp + ms("5s"))) {
265
- // this change should have been seen by now, so remove it
266
- this.recentRemoteChanges.delete(path);
267
- }
268
- }
269
- }, ms("1s")).unref();
270
- this.stop = async (e)=>{
271
- if (this.status !== 1) {
272
- return;
273
- }
274
- this.status = 2;
275
- error = e;
276
- this.log.info("stopping", {
277
- error
278
- });
279
- try {
280
- clearInterval(recentRemoteChangesInterval);
281
- unsubscribe();
282
- this.watcher.removeAllListeners();
283
- this.publish.flush();
284
- await this.queue.onIdle();
285
- } finally{
286
- await Promise.allSettled([
287
- this.watcher.close(),
288
- this.graphql.dispose()
289
- ]);
290
- this.status = 3;
291
- stopped.resolve();
292
- this.log.info("stopped");
293
- }
294
- };
295
- for (const signal of [
296
- "SIGINT",
297
- "SIGTERM"
298
- ]){
299
- process.on(signal, ()=>{
300
- if (this.status !== 1) {
301
- return;
302
- }
303
- println` Stopping... {gray (press Ctrl+C again to force)}`;
304
- void this.stop();
305
- // 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
306
- // SIGINT from triggering the force exit listener, we wait a bit before registering it. This is a bit of a hack, but it works.
307
- setTimeout(()=>{
308
- process.once(signal, ()=>{
309
- println(" Exiting immediately. Note that files may not have finished syncing.");
310
- process.exit(1);
263
+ localChangesBuffer.set(normalizedPath, {
264
+ type: "delete"
311
265
  });
312
- }, 100).unref();
313
- });
314
- }
315
- const unsubscribe = this.graphql.subscribe({
316
- query: REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION,
317
- variables: ()=>({
318
- localFilesVersion: String(this.filesync.filesVersion)
319
- })
320
- }, {
321
- error: (error)=>void this.stop(error),
322
- next: ({ remoteFileSyncEvents })=>{
323
- const remoteFilesVersion = remoteFileSyncEvents.remoteFilesVersion;
324
- // we always ignore .gadget/ files so that we don't publish them (they're managed by gadget), but we still want to receive them
325
- const filterIgnored = (event)=>event.path.startsWith(".gadget/") || !this.filesync.ignores(event.path);
326
- const changed = remoteFileSyncEvents.changed.filter(filterIgnored);
327
- const deleted = remoteFileSyncEvents.deleted.filter(filterIgnored);
328
- this.log.info("received files", {
329
- remoteFilesVersion,
330
- changed: changed.map((x)=>x.path),
331
- deleted: deleted.map((x)=>x.path)
332
- });
333
- this._enqueue(async ()=>{
334
- // add all the non-ignored files and directories we're about
335
- // to touch to recentRemoteChanges so that we don't send
336
- // them back
337
- for (const file of [
338
- ...changed,
339
- ...deleted
340
- ].filter((file)=>!this.filesync.ignores(file.path))){
341
- this.recentRemoteChanges.set(file.path, Date.now());
342
- let dir = path.dirname(file.path);
343
- while(dir !== "."){
344
- this.recentRemoteChanges.set(dir + "/", Date.now());
345
- dir = path.dirname(dir);
346
- }
347
- }
348
- if (changed.length > 0 || deleted.length > 0) {
349
- println`Received {gray ${dayjs().format("hh:mm:ss A")}}`;
350
- printPaths("←", changed.map((x)=>x.path), deleted.map((x)=>x.path));
351
- }
352
- await this.filesync.write(remoteFilesVersion, changed, deleted.map((x)=>x.path));
353
- if (changed.some((x)=>x.path === "yarn.lock")) {
354
- await execa("yarn", [
355
- "install"
356
- ], {
357
- cwd: this.filesync.dir
358
- }).catch(noop);
359
- }
360
- });
361
- }
362
- });
363
- const localFilesBuffer = new Map();
364
- this.publish = debounce(this.args["--file-push-delay"], ()=>{
365
- const localFiles = new Map(localFilesBuffer.entries());
366
- localFilesBuffer.clear();
367
- this._enqueue(async ()=>{
368
- const changed = [];
369
- const deleted = [];
370
- await pMap(localFiles, async ([normalizedPath, file])=>{
371
- if ("isDeleted" in file) {
372
- deleted.push({
373
- path: normalizedPath
374
- });
375
- return;
376
- }
377
- try {
378
- changed.push({
379
- path: normalizedPath,
380
- oldPath: "oldPath" in file ? file.oldPath : undefined,
381
- mode: file.mode,
382
- content: file.isDirectory ? "" : await fs.readFile(this.filesync.absolute(normalizedPath), FileSyncEncoding.Base64),
383
- encoding: FileSyncEncoding.Base64
384
- });
385
- } catch (error) {
386
- // A file could have been changed and then deleted before we process the change event, so the readFile
387
- // above will raise an ENOENT. This is normal operation, so just ignore this event.
388
- swallowEnoent(error);
389
- }
390
- });
391
- if (changed.length === 0 && deleted.length === 0) {
392
- return;
266
+ break;
393
267
  }
394
- const { publishFileSyncEvents } = await this.graphql.query({
395
- query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
396
- variables: {
397
- input: {
398
- expectedRemoteFilesVersion: String(this.filesync.filesVersion),
399
- changed,
400
- deleted
401
- }
402
- }
403
- });
404
- await this.filesync.write(publishFileSyncEvents.remoteFilesVersion, [], []);
405
- println`Sent {gray ${dayjs().format("hh:mm:ss A")}}`;
406
- printPaths("→", changed.map((x)=>x.path), deleted.map((x)=>x.path));
407
- });
408
- });
409
- this.watcher = new FSWatcher(this.filesync.dir, {
410
- // don't emit an event for every watched file on boot
411
- ignoreInitial: true,
412
- ignore: (path)=>this.filesync.ignores(path),
413
- renameDetection: true,
414
- recursive: true,
415
- debounce: this.args["--file-watch-debounce"],
416
- pollingInterval: this.args["--file-watch-poll-interval"],
417
- pollingTimeout: this.args["--file-watch-poll-timeout"],
418
- renameTimeout: this.args["--file-watch-rename-timeout"]
419
- });
420
- this.watcher.once("error", (error)=>void this.stop(error));
421
- this.watcher.on("all", (event, absolutePath, renamedPath)=>{
422
- const filepath = event === "rename" || event === "renameDir" ? renamedPath : absolutePath;
423
- const isDirectory = event === "renameDir" || event === "addDir" || event === "unlinkDir";
424
- const normalizedPath = this.filesync.normalize(filepath, isDirectory);
425
- this.log.debug("file event", {
426
- event,
427
- path: normalizedPath,
428
- isDirectory,
429
- recentRemoteChanges: Array.from(this.recentRemoteChanges.keys())
430
- });
431
- if (filepath === this.filesync.absolute(".ignore")) {
432
- this.filesync.reloadIgnorePaths();
433
- } else if (this.filesync.ignores(filepath)) {
434
- return;
435
- }
436
- if (this.recentRemoteChanges.delete(normalizedPath)) {
437
- return;
438
- }
439
- switch(event){
440
- case "add":
441
- case "addDir":
442
- case "change":
443
- {
444
- const stats = fs.statSync(filepath);
445
- localFilesBuffer.set(normalizedPath, {
446
- mode: stats.mode,
447
- isDirectory
448
- });
449
- break;
450
- }
451
- case "unlink":
452
- case "unlinkDir":
453
- {
454
- localFilesBuffer.set(normalizedPath, {
455
- isDeleted: true,
456
- isDirectory
457
- });
458
- break;
459
- }
460
- case "rename":
461
- case "renameDir":
462
- {
463
- const stats = fs.statSync(filepath);
464
- localFilesBuffer.set(normalizedPath, {
465
- oldPath: this.filesync.normalize(absolutePath, isDirectory),
466
- newPath: normalizedPath,
467
- isDirectory,
468
- mode: stats.mode
469
- });
470
- break;
471
- }
472
- }
473
- this.publish();
474
- });
475
- this.status = 1;
476
- println();
477
- println`
478
- {bold ggt v${config.version}}
268
+ }
269
+ sendChangesToGadget();
270
+ }).once("error", (error)=>ctx.abort(error));
271
+ ctx.log.printlns`
272
+ ggt v${config.version}
479
273
 
480
- App ${this.filesync.app.slug}
481
- Editor https://${this.filesync.app.slug}.gadget.app/edit
482
- Playground https://${this.filesync.app.slug}.gadget.app/api/graphql/playground
483
- Docs https://docs.gadget.dev/api/${this.filesync.app.slug}
274
+ App ${filesync.app.slug}
275
+ Editor https://${filesync.app.slug}.gadget.app/edit
276
+ Playground https://${filesync.app.slug}.gadget.app/api/graphql/playground
277
+ Docs https://docs.gadget.dev/api/${filesync.app.slug}
484
278
 
485
- {underline Endpoints} ${this.filesync.app.hasSplitEnvironments ? `
486
- • https://${this.filesync.app.primaryDomain}
487
- • https://${this.filesync.app.slug}--development.gadget.app` : `
488
- • https://${this.filesync.app.primaryDomain}`}
279
+ Endpoints ${filesync.app.hasSplitEnvironments ? `
280
+ • https://${filesync.app.primaryDomain}
281
+ • https://${filesync.app.slug}--development.gadget.app` : `
282
+ • https://${filesync.app.primaryDomain}`}
489
283
 
490
- Watching for file changes... {gray Press Ctrl+C to stop}
491
- `;
492
- println();
493
- await stopped;
494
- if (error) {
495
- notify({
496
- subtitle: "Uh oh!",
497
- message: "An error occurred while syncing files"
284
+ Watching for file changes... {gray Press Ctrl+C to stop}
285
+ `;
286
+ ctx.onAbort(async (reason)=>{
287
+ ctx.log.info("stopping", {
288
+ reason
289
+ });
290
+ unsubscribeFromGadgetChanges();
291
+ fileWatcher.close();
292
+ clearInterval(clearRecentWritesInterval);
293
+ sendChangesToGadget.flush();
294
+ try {
295
+ await filesync.idle();
296
+ } catch (error) {
297
+ ctx.log.error("error while waiting for idle", {
298
+ error
498
299
  });
499
- throw error;
500
- } else {
501
- println("Goodbye!");
502
300
  }
503
- }
504
- /**
505
- * Enqueues a function that handles file-sync events onto the {@linkcode queue}.
506
- *
507
- * @param fn The function to enqueue.
508
- */ _enqueue(fn) {
509
- void this.queue.add(fn).catch(this.stop);
510
- }
511
- constructor(){
512
- _define_property(this, "args", void 0);
513
- /**
514
- * The current status of the sync process.
515
- */ _define_property(this, "status", 0);
516
- /**
517
- * A list of filepaths that have changed because of a remote file-sync
518
- * event. This is used to avoid sending files that we recently
519
- * received from a remote file-sync event.
520
- */ _define_property(this, "recentRemoteChanges", new Map());
521
- /**
522
- * A FIFO async callback queue that ensures we process file-sync events in the order they occurred.
523
- */ _define_property(this, "queue", new PQueue({
524
- concurrency: 1
525
- }));
526
- /**
527
- * A GraphQL client connected to the app's /edit/api/graphql-ws endpoint
528
- */ _define_property(this, "graphql", void 0);
529
- /**
530
- * Watches the local filesystem for changes.
531
- */ _define_property(this, "watcher", void 0);
532
- /**
533
- * Handles writing files to the local filesystem.
534
- */ _define_property(this, "filesync", void 0);
535
- /**
536
- * A debounced function that enqueue's local file changes to be sent to Gadget.
537
- */ _define_property(this, "publish", void 0);
538
- /**
539
- * Gracefully stops the sync.
540
- */ _define_property(this, "stop", void 0);
541
- /**
542
- * A logger for the sync command.
543
- */ _define_property(this, "log", createLogger("sync", ()=>{
544
- return {
545
- app: this.filesync.app.slug,
546
- filesVersion: String(this.filesync.filesVersion),
547
- mtime: this.filesync.mtime
548
- };
549
- }));
550
- }
551
- }
552
- const sync = new Sync();
553
- export const init = sync.init.bind(sync);
554
- export const run = sync.run.bind(sync);
301
+ if (isAbortError(reason)) {
302
+ ctx.log.printlns("Goodbye!");
303
+ return;
304
+ }
305
+ notify({
306
+ subtitle: "Uh oh!",
307
+ message: "An error occurred while syncing files"
308
+ });
309
+ await reportErrorAndExit(reason);
310
+ });
311
+ };
555
312
 
556
313
  //# sourceMappingURL=sync.js.map