@swell/cli 2.0.1-alpha.9 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-command.d.ts +1 -1
- package/dist/app-command.js +9 -9
- package/dist/commands/app/dev.d.ts +16 -0
- package/dist/commands/app/dev.js +28 -0
- package/dist/commands/app/frontend/deploy.d.ts +15 -0
- package/dist/commands/app/frontend/deploy.js +41 -0
- package/dist/commands/app/frontend/dev.d.ts +19 -0
- package/dist/commands/app/frontend/dev.js +108 -0
- package/dist/commands/app/info.js +14 -9
- package/dist/commands/app/init.d.ts +12 -0
- package/dist/commands/app/init.js +34 -0
- package/dist/commands/app/install.d.ts +4 -4
- package/dist/commands/app/install.js +36 -42
- package/dist/commands/app/pull.d.ts +15 -0
- package/dist/commands/app/pull.js +50 -0
- package/dist/commands/app/push.d.ts +11 -14
- package/dist/commands/app/push.js +64 -138
- package/dist/commands/app/release.d.ts +13 -0
- package/dist/commands/app/release.js +148 -0
- package/dist/commands/app/version.d.ts +5 -4
- package/dist/commands/app/version.js +63 -42
- package/dist/commands/app/watch.d.ts +2 -4
- package/dist/commands/app/watch.js +3 -83
- package/dist/commands/create/app.d.ts +35 -11
- package/dist/commands/create/app.js +143 -137
- package/dist/commands/create/content.d.ts +2 -2
- package/dist/commands/create/content.js +10 -10
- package/dist/commands/create/frontend.d.ts +12 -0
- package/dist/commands/create/frontend.js +38 -0
- package/dist/commands/create/function.d.ts +3 -3
- package/dist/commands/create/function.js +22 -22
- package/dist/commands/create/model.d.ts +2 -2
- package/dist/commands/create/model.js +11 -11
- package/dist/commands/create/notification.d.ts +4 -4
- package/dist/commands/create/notification.js +38 -40
- package/dist/commands/env.d.ts +12 -0
- package/dist/commands/env.js +31 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +18 -24
- package/dist/commands/logs.js +0 -1
- package/dist/commands/theme/dev.d.ts +21 -0
- package/dist/commands/theme/dev.js +164 -0
- package/dist/commands/theme/init.d.ts +9 -0
- package/dist/commands/theme/init.js +15 -0
- package/dist/commands/theme/pull.d.ts +20 -0
- package/dist/commands/theme/pull.js +65 -0
- package/dist/commands/theme/push.d.ts +19 -0
- package/dist/commands/theme/push.js +89 -0
- package/dist/create-app-command.d.ts +20 -0
- package/dist/create-app-command.js +253 -0
- package/dist/create-collection-command.d.ts +9 -9
- package/dist/create-collection-command.js +23 -23
- package/dist/{create-command.d.ts → create-config-command.d.ts} +1 -1
- package/dist/{create-command.js → create-config-command.js} +7 -7
- package/dist/env/local.d.ts +8 -0
- package/dist/env/local.js +7 -0
- package/dist/env/production.d.ts +8 -0
- package/dist/env/production.js +7 -0
- package/dist/env/review.d.ts +8 -0
- package/dist/env/review.js +7 -0
- package/dist/env/staging.d.ts +8 -0
- package/dist/env/staging.js +7 -0
- package/dist/lib/api.d.ts +4 -2
- package/dist/lib/api.js +50 -21
- package/dist/lib/app-config.d.ts +13 -5
- package/dist/lib/app-config.js +62 -24
- package/dist/lib/apps/app-config.d.ts +17 -3
- package/dist/lib/apps/app-config.js +194 -66
- package/dist/lib/apps/index.d.ts +93 -12
- package/dist/lib/apps/index.js +155 -50
- package/dist/lib/apps/paths.d.ts +14 -7
- package/dist/lib/apps/paths.js +73 -28
- package/dist/lib/bundle.js +15 -3
- package/dist/lib/config.d.ts +9 -2
- package/dist/lib/config.js +49 -1
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +24 -6
- package/dist/lib/create/content.d.ts +2 -2
- package/dist/lib/create/content.js +1 -1
- package/dist/lib/create/function.d.ts +2 -2
- package/dist/lib/create/function.js +2 -2
- package/dist/lib/create/index.d.ts +1 -1
- package/dist/lib/create/index.js +11 -7
- package/dist/lib/create/model.d.ts +2 -2
- package/dist/lib/create/notification.d.ts +2 -2
- package/dist/lib/info.js +2 -2
- package/dist/lib/logs/index.js +1 -1
- package/dist/lib/sessions.js +2 -2
- package/dist/lib/stores.d.ts +9 -4
- package/dist/lib/stores.js +13 -8
- package/dist/lib/style.d.ts +2 -0
- package/dist/lib/style.js +2 -0
- package/dist/lib/swell-function-wrapper.d.ts +40 -1
- package/dist/lib/swell-function-wrapper.js +107 -22
- package/dist/push-app-command.d.ts +73 -0
- package/dist/push-app-command.js +743 -0
- package/dist/remote-app-command.d.ts +37 -11
- package/dist/remote-app-command.js +432 -169
- package/oclif.manifest.json +872 -123
- package/package.json +14 -1
- package/dist/commands/app/_pull.d.ts +0 -8
- package/dist/commands/app/_pull.js +0 -45
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
2
|
+
import { $ } from 'execa';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import Stream from 'node:stream';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { default as swellConfig } from './lib/app-config.js';
|
|
8
|
+
import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
|
|
9
|
+
import { default as localConfig } from './lib/config.js';
|
|
10
|
+
import { toAppId } from './lib/create/index.js';
|
|
11
|
+
import style from './lib/style.js';
|
|
12
|
+
import { RemoteAppCommand } from './remote-app-command.js';
|
|
13
|
+
const PUSH_CONCURRENCY = 3;
|
|
14
|
+
export class PushAppCommand extends RemoteAppCommand {
|
|
15
|
+
frontendPath = '';
|
|
16
|
+
logWatchChanges = true;
|
|
17
|
+
onWatchChange;
|
|
18
|
+
watchingChangeQueue = [];
|
|
19
|
+
watchingChangeQueueRunning = false;
|
|
20
|
+
watchingFiles = [];
|
|
21
|
+
watchListener = async (eventType, filename) => {
|
|
22
|
+
const configFile = filename;
|
|
23
|
+
const pathParsed = path.parse(configFile);
|
|
24
|
+
const fileDirs = pathParsed.dir.split(path.sep);
|
|
25
|
+
// only the first directory is relevant for us
|
|
26
|
+
const configDir = fileDirs[0];
|
|
27
|
+
// we only want to watch files in the root of the app directory
|
|
28
|
+
// and ignore build files etc
|
|
29
|
+
this.setWatchingFiles();
|
|
30
|
+
const isWatching = this.watchingFiles.includes(configFile);
|
|
31
|
+
// TODO: renaming a folder doesn't seem to work
|
|
32
|
+
// because isWatching = false for the new files
|
|
33
|
+
if (!configDir) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const configType = getConfigTypeFromPath(configDir);
|
|
37
|
+
if (!configType) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let watchFileEvent;
|
|
41
|
+
// per node docs:
|
|
42
|
+
//
|
|
43
|
+
// On most platforms, 'rename' is emitted whenever a filename appears
|
|
44
|
+
// or disappears in the directory.
|
|
45
|
+
//
|
|
46
|
+
// we want however to identify delete file events so we can send the
|
|
47
|
+
// correct API request
|
|
48
|
+
if (eventType === 'rename') {
|
|
49
|
+
watchFileEvent = 'new';
|
|
50
|
+
if (!filePathExists(path.join(this.appPath, configFile))) {
|
|
51
|
+
watchFileEvent = 'deleted';
|
|
52
|
+
}
|
|
53
|
+
else if (!isWatching) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (isWatching) {
|
|
58
|
+
watchFileEvent = 'change';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await this.handleWatchFileChange(configFile, configType, watchFileEvent);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// we don't want to break the watcher if an error is thrown while
|
|
68
|
+
// handling a file change
|
|
69
|
+
// for tests though, we want to break the watcher
|
|
70
|
+
if (process.env.NODE_ENV === 'test') {
|
|
71
|
+
// throwing does not break the test so we need to hard exit and log
|
|
72
|
+
// the error
|
|
73
|
+
console.error(style.error(error));
|
|
74
|
+
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
async buildFrontend(projectType) {
|
|
80
|
+
this.log(`Building ${projectType.name} frontend...\n`);
|
|
81
|
+
// TODO: check package.json for a "build" command and run that if it exists
|
|
82
|
+
await this.exec(projectType.buildCommand);
|
|
83
|
+
}
|
|
84
|
+
async chooseAppToPull(query) {
|
|
85
|
+
const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
|
|
86
|
+
const typeLabelPrefixed = this.appType === 'theme' ? 'a theme' : 'an app';
|
|
87
|
+
const typeQuery = this.appType === 'theme' ? { type: 'theme' } : { type: 'storefront' };
|
|
88
|
+
const apps = await this.api.get({
|
|
89
|
+
adminPath: `/apps`,
|
|
90
|
+
}, { query: { limit: null, ...typeQuery, ...query } });
|
|
91
|
+
if (!apps.count) {
|
|
92
|
+
const currentStore = localConfig.getDefaultStore();
|
|
93
|
+
this.error(`There are no ${typeLabelPlural} associated with your store ${style.storeId(currentStore)}`);
|
|
94
|
+
}
|
|
95
|
+
const appSlugId = (await select({
|
|
96
|
+
choices: apps.results.map((app) => ({
|
|
97
|
+
name: `${style.appConfigValue(app.name)} (${getAppSlugId(app)})`,
|
|
98
|
+
value: app.private_id,
|
|
99
|
+
})),
|
|
100
|
+
message: `Choose ${typeLabelPrefixed} to pull`,
|
|
101
|
+
}));
|
|
102
|
+
const appId = apps.results.find((app) => app.private_id === appSlugId).id;
|
|
103
|
+
const app = await this.getApp(appId);
|
|
104
|
+
this.log();
|
|
105
|
+
return app;
|
|
106
|
+
}
|
|
107
|
+
async createAppStorefront(hasOtherStorefronts = true) {
|
|
108
|
+
let appId;
|
|
109
|
+
let themeAppId;
|
|
110
|
+
if (this.app.type === 'theme') {
|
|
111
|
+
themeAppId = this.app.id;
|
|
112
|
+
const storefrontApps = await this.handleRequestErrors(async () => this.api.get({
|
|
113
|
+
adminPath: `/apps?type=storefront&kind=shop`,
|
|
114
|
+
}));
|
|
115
|
+
const compatibleStorefrontApps = storefrontApps.results.filter((app) => app.installed &&
|
|
116
|
+
app.storefront?.theme?.provider === 'app' &&
|
|
117
|
+
this.app.theme?.storefront?.app === toAppId(app.private_id));
|
|
118
|
+
if (compatibleStorefrontApps.length === 0) {
|
|
119
|
+
this.error('No compatible storefront apps found for this theme. Install a new storefront app first.');
|
|
120
|
+
}
|
|
121
|
+
const targetApp = compatibleStorefrontApps.length === 1
|
|
122
|
+
? compatibleStorefrontApps[0]
|
|
123
|
+
: await select({
|
|
124
|
+
choices: compatibleStorefrontApps.map((app) => ({
|
|
125
|
+
name: `${style.appConfigValue(app.name)} v${app.version}`,
|
|
126
|
+
value: app,
|
|
127
|
+
})),
|
|
128
|
+
message: 'Choose a storefront app',
|
|
129
|
+
});
|
|
130
|
+
appId = targetApp.id;
|
|
131
|
+
this.log(`\n${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
|
|
132
|
+
}
|
|
133
|
+
else if (this.app.type === 'storefront') {
|
|
134
|
+
appId = this.app.id;
|
|
135
|
+
const themeApps = await this.handleRequestErrors(async () => this.api.get({
|
|
136
|
+
adminPath: `/apps?type=theme`,
|
|
137
|
+
}));
|
|
138
|
+
const compatibleThemeApps = themeApps.results.filter((app) => app.installed &&
|
|
139
|
+
Object.keys(app.compatibilities || {}).includes(this.app.private_id));
|
|
140
|
+
if (compatibleThemeApps.length === 0) {
|
|
141
|
+
this.error('No compatible theme apps found for this storefront. Install a new theme app first.');
|
|
142
|
+
}
|
|
143
|
+
let targetApp;
|
|
144
|
+
if (compatibleThemeApps.length === 1) {
|
|
145
|
+
targetApp = compatibleThemeApps[0];
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
targetApp = await select({
|
|
149
|
+
choices: compatibleThemeApps.map((app) => ({
|
|
150
|
+
name: `${style.appConfigValue(app.name)} v${app.version}`,
|
|
151
|
+
value: app,
|
|
152
|
+
})),
|
|
153
|
+
message: 'Choose a theme app',
|
|
154
|
+
});
|
|
155
|
+
this.log();
|
|
156
|
+
}
|
|
157
|
+
themeAppId = targetApp.id;
|
|
158
|
+
this.log(`${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.error('App type not supported for storefront creation');
|
|
162
|
+
}
|
|
163
|
+
let name;
|
|
164
|
+
if (hasOtherStorefronts) {
|
|
165
|
+
name = await input({
|
|
166
|
+
default: this.app.name,
|
|
167
|
+
message: 'Name your storefront',
|
|
168
|
+
});
|
|
169
|
+
if (name === this.app.name) {
|
|
170
|
+
name = undefined; // undefined allows the api to auto increment name
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return this.handleRequestErrors(async () => this.api.post({
|
|
174
|
+
adminPath: `/storefronts`,
|
|
175
|
+
}, {
|
|
176
|
+
body: {
|
|
177
|
+
app_id: appId,
|
|
178
|
+
name,
|
|
179
|
+
theme_app_id: themeAppId,
|
|
180
|
+
},
|
|
181
|
+
onAsyncGetPath: (response) => ({
|
|
182
|
+
adminPath: `/storefronts/${response.id}`,
|
|
183
|
+
}),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
async deployAppFrontend(force, log) {
|
|
187
|
+
const currentStore = localConfig.getDefaultStore();
|
|
188
|
+
const projectType = this.getFrontendProjectType(false);
|
|
189
|
+
if (!projectType) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const deploymentHash = this.getFrontendDeploymentHash();
|
|
193
|
+
// Build and deploy only if there are files to push
|
|
194
|
+
if (deploymentHash &&
|
|
195
|
+
(force || deploymentHash !== this.app.frontend?.deployment_hash)) {
|
|
196
|
+
await this.buildFrontend(projectType);
|
|
197
|
+
const deploymentUrl = await this.deployFrontend(projectType);
|
|
198
|
+
await this.updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash);
|
|
199
|
+
}
|
|
200
|
+
if (log !== false) {
|
|
201
|
+
if (this.app.type === 'storefront' && this.storefront) {
|
|
202
|
+
this.logStorefrontFrontendUrl(this.storefront);
|
|
203
|
+
}
|
|
204
|
+
else if (this.app.type !== 'storefront') {
|
|
205
|
+
// Get updated app with `installed` property
|
|
206
|
+
this.app = await this.getApp(this.app.id);
|
|
207
|
+
this.logAppFrontendUrl();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async deployFrontend(projectType) {
|
|
212
|
+
// TODO: check package.json for a "deploy" command and run that if it exists
|
|
213
|
+
// Note it must return a URL somehow
|
|
214
|
+
try {
|
|
215
|
+
return this.wranglerDeployFrontend(projectType);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.error(`Error deploying frontend: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async ensureAppExists(file, shouldCreate = true) {
|
|
222
|
+
let currentStore = localConfig.getDefaultStore();
|
|
223
|
+
this.api.setStoreEnv(currentStore, 'test');
|
|
224
|
+
const loggedIn = await this.ensureLoggedIn();
|
|
225
|
+
if (loggedIn) {
|
|
226
|
+
currentStore = localConfig.getDefaultStore();
|
|
227
|
+
this.api.setStoreEnv(currentStore, 'test');
|
|
228
|
+
}
|
|
229
|
+
const app = await this.getAppWithConfig('').catch((_error) =>
|
|
230
|
+
// Ignore so we can continue to create it
|
|
231
|
+
null);
|
|
232
|
+
const isTheme = app?.type === 'theme';
|
|
233
|
+
if (app && !isTheme && this.appType === 'theme') {
|
|
234
|
+
this.error(`App ${style.appConfigValue(app?.name)} is not a theme.`);
|
|
235
|
+
}
|
|
236
|
+
const confirmExistingApp = await this.confirmRemoveInstalledApp(app, currentStore);
|
|
237
|
+
if (confirmExistingApp === false) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
// Do not create or update if app already exists and not forcing it
|
|
241
|
+
if (!shouldCreate && app) {
|
|
242
|
+
this.app = app;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
// If app exists but owned by another client, create a new instance
|
|
246
|
+
if (app?.client_id && app.client_id !== currentStore) {
|
|
247
|
+
// Create a new app
|
|
248
|
+
this.app = await this.getCreateUpdateApp();
|
|
249
|
+
if (!this.app) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (!file) {
|
|
254
|
+
// Update existing app if not targeting a file
|
|
255
|
+
this.app = await this.getCreateUpdateApp(app);
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
async ensureLoggedIn() {
|
|
260
|
+
try {
|
|
261
|
+
const user = await this.api.get({ adminPath: 'user' });
|
|
262
|
+
if (!user) {
|
|
263
|
+
throw new Error('Failed to log in');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
this.log('Logging in...\n');
|
|
268
|
+
await this.config.runCommand('login');
|
|
269
|
+
this.log();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async exec(command, onOutput) {
|
|
274
|
+
const $$ = $({
|
|
275
|
+
cwd: this.frontendPath,
|
|
276
|
+
shell: true,
|
|
277
|
+
stderr: onOutput ? 'pipe' : 'inherit',
|
|
278
|
+
stdin: 'inherit',
|
|
279
|
+
stdout: onOutput ? 'pipe' : 'inherit',
|
|
280
|
+
});
|
|
281
|
+
if (onOutput) {
|
|
282
|
+
const outStream = new Stream.Writable();
|
|
283
|
+
outStream._write = (chunk, _encoding, next) => {
|
|
284
|
+
const string = chunk.toString();
|
|
285
|
+
const out = onOutput(string);
|
|
286
|
+
if (out !== false) {
|
|
287
|
+
console.log(string);
|
|
288
|
+
}
|
|
289
|
+
next();
|
|
290
|
+
};
|
|
291
|
+
await $$ `${command}`.pipeStdout?.(outStream).pipeStderr?.(outStream);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
await $$ `${command}`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async getAllAppStorefronts(params) {
|
|
298
|
+
const query = Object.entries(params || {})
|
|
299
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
300
|
+
.join('&');
|
|
301
|
+
return this.handleRequestErrors(async () => this.api.get({
|
|
302
|
+
adminPath: `/apps/${this.app.id}/storefronts?${query}`,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
async getAllStorefronts(params) {
|
|
306
|
+
const query = Object.entries(params || {})
|
|
307
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
308
|
+
.join('&');
|
|
309
|
+
return this.handleRequestErrors(async () => this.api.get({
|
|
310
|
+
adminPath: `/storefronts?${query}`,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
async getAppStorefront(params) {
|
|
314
|
+
const query = Object.entries(params || {})
|
|
315
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
316
|
+
.join('&');
|
|
317
|
+
try {
|
|
318
|
+
return this.handleRequestErrors(async () => this.api.get({
|
|
319
|
+
adminPath: `/apps/${this.app.id}/storefront?${query}`,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// noop
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async getAppToPull(args, shouldChoose = true) {
|
|
327
|
+
const { appId, targetPath } = args;
|
|
328
|
+
const currentStore = localConfig.getDefaultStore();
|
|
329
|
+
this.api.setStoreEnv(currentStore, 'test');
|
|
330
|
+
this.resolveAppPath(appId, targetPath);
|
|
331
|
+
if (isPathDirectory(this.appPath)) {
|
|
332
|
+
this.swellConfig = swellConfig(this.appPath);
|
|
333
|
+
const localAppId = this.swellConfig.get('id');
|
|
334
|
+
const localAppType = this.swellConfig.get('type');
|
|
335
|
+
if (localAppId) {
|
|
336
|
+
if (appId && appId !== localAppId) {
|
|
337
|
+
this.error(`${localAppType === 'theme' ? 'A theme' : 'An app'} already exists in this path with a different ID: ${style.appConfigValue(localAppId)}`);
|
|
338
|
+
}
|
|
339
|
+
return this.getExistingApp(localAppId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const app = appId
|
|
343
|
+
? await this.getExistingApp(appId)
|
|
344
|
+
: shouldChoose
|
|
345
|
+
? await this.chooseAppToPull()
|
|
346
|
+
: undefined;
|
|
347
|
+
if (shouldChoose) {
|
|
348
|
+
this.swellConfig = swellConfig(this.appPath);
|
|
349
|
+
}
|
|
350
|
+
return app;
|
|
351
|
+
}
|
|
352
|
+
async getExistingApp(appId) {
|
|
353
|
+
try {
|
|
354
|
+
return this.getApp(appId);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error.status === 404) {
|
|
358
|
+
const labelPrefix = this.appType === 'theme' ? 'Theme' : 'App';
|
|
359
|
+
this.error(`${labelPrefix} '${appId}' not found.`);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
getFrontendDeploymentHash() {
|
|
367
|
+
const localConfigs = this.getLocalAppConfigs();
|
|
368
|
+
let frontendHashes = localConfigs
|
|
369
|
+
.filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
|
|
370
|
+
.map((config) => config.hash)
|
|
371
|
+
.sort()
|
|
372
|
+
.join('|');
|
|
373
|
+
if (frontendHashes.length > 0) {
|
|
374
|
+
const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
|
|
375
|
+
frontendHashes += `|${packageHash}`;
|
|
376
|
+
}
|
|
377
|
+
return frontendHashes?.length > 0 ? hashString(frontendHashes) : undefined;
|
|
378
|
+
}
|
|
379
|
+
getFrontendProjectType(required = true) {
|
|
380
|
+
this.frontendPath = path.join(this.appPath, 'frontend');
|
|
381
|
+
if (!required) {
|
|
382
|
+
const frontendExists = filePathExists(this.frontendPath);
|
|
383
|
+
if (!frontendExists) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const projectType = getFrontendProjectType(this.appPath);
|
|
388
|
+
if (!projectType) {
|
|
389
|
+
this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
|
|
390
|
+
}
|
|
391
|
+
return projectType;
|
|
392
|
+
}
|
|
393
|
+
async getStorefrontToPull(args = {}, flags = {}) {
|
|
394
|
+
const { appId, targetPath } = args;
|
|
395
|
+
const targetApp = await this.getAppToPull(args, false);
|
|
396
|
+
const targetStorefront = flags['storefront-id'];
|
|
397
|
+
this.resolveAppPath(appId, targetPath);
|
|
398
|
+
// Only use default if path exists
|
|
399
|
+
let currentStorefront;
|
|
400
|
+
if (isPathDirectory(this.appPath)) {
|
|
401
|
+
currentStorefront = localConfig.getDefaultStorefront(this.appPath);
|
|
402
|
+
}
|
|
403
|
+
let storefrontId = targetStorefront || targetApp ? currentStorefront : undefined;
|
|
404
|
+
if (!storefrontId || flags['storefront-select']) {
|
|
405
|
+
const storefronts = await this.getAllStorefronts(targetApp?.id
|
|
406
|
+
? this.appType === 'theme'
|
|
407
|
+
? { theme_app_id: targetApp?.id }
|
|
408
|
+
: { app_id: targetApp?.id }
|
|
409
|
+
: { type: this.appType });
|
|
410
|
+
if (storefronts.count > 0) {
|
|
411
|
+
storefrontId = await select({
|
|
412
|
+
choices: storefronts.results.map((storefront) => ({
|
|
413
|
+
name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
|
|
414
|
+
value: storefront.id,
|
|
415
|
+
})),
|
|
416
|
+
message: 'Choose a storefront',
|
|
417
|
+
});
|
|
418
|
+
this.storefront = storefronts.results.find((storefront) => storefront.id === storefrontId);
|
|
419
|
+
const appId = this.appType === 'theme'
|
|
420
|
+
? this.storefront.theme_app_id
|
|
421
|
+
: this.storefront.app_id;
|
|
422
|
+
this.resolveAppPath(this.storefront.name, targetPath);
|
|
423
|
+
this.app = await this.getApp(appId);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
// Theme apps should always have a storefront, so this shouldn't be necessary
|
|
427
|
+
// However it's a good fallback in case the storefront was deleted
|
|
428
|
+
// Choosing a theme app will proceed to pull app files
|
|
429
|
+
this.app = await this.chooseAppToPull();
|
|
430
|
+
this.resolveAppPath(this.app.name, targetPath);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
this.app = targetApp;
|
|
435
|
+
}
|
|
436
|
+
if (storefrontId && !this.storefront) {
|
|
437
|
+
this.storefront = await this.getAppStorefront({
|
|
438
|
+
storefront_id: storefrontId,
|
|
439
|
+
type: 'theme',
|
|
440
|
+
});
|
|
441
|
+
if (!this.storefront) {
|
|
442
|
+
this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
|
|
443
|
+
}
|
|
444
|
+
const appId = this.appType === 'theme'
|
|
445
|
+
? this.storefront.theme_app_id
|
|
446
|
+
: this.storefront.app_id;
|
|
447
|
+
this.app = await this.getApp(appId);
|
|
448
|
+
}
|
|
449
|
+
this.swellConfig = swellConfig(this.appPath);
|
|
450
|
+
return this.storefront;
|
|
451
|
+
}
|
|
452
|
+
async getStorefrontToPush(flags = {}) {
|
|
453
|
+
const targetStorefront = flags['storefront-id'];
|
|
454
|
+
const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
|
|
455
|
+
let storefrontId = targetStorefront || currentStorefront;
|
|
456
|
+
if (!storefrontId || flags['storefront-select']) {
|
|
457
|
+
const storefronts = await this.getAllAppStorefronts({
|
|
458
|
+
type: this.appType,
|
|
459
|
+
});
|
|
460
|
+
if (storefronts.count > 0) {
|
|
461
|
+
storefrontId = await select({
|
|
462
|
+
choices: [
|
|
463
|
+
...storefronts.results.map((storefront) => ({
|
|
464
|
+
name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
|
|
465
|
+
value: storefront.id,
|
|
466
|
+
})),
|
|
467
|
+
{
|
|
468
|
+
name: 'Create new storefront',
|
|
469
|
+
value: null,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
message: 'Choose a storefront',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (!storefrontId) {
|
|
476
|
+
this.storefront = await this.createAppStorefront(storefronts.count > 0);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (storefrontId) {
|
|
480
|
+
this.storefront = await this.getAppStorefront({
|
|
481
|
+
storefront_id: storefrontId,
|
|
482
|
+
type: this.appType,
|
|
483
|
+
});
|
|
484
|
+
if (!this.storefront) {
|
|
485
|
+
if (targetStorefront) {
|
|
486
|
+
this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
|
|
487
|
+
}
|
|
488
|
+
this.log(`Storefront ${style.appConfigValue(storefrontId)} not found.\n`);
|
|
489
|
+
localConfig.setDefaultStorefront(this.appPath, '');
|
|
490
|
+
return this.getStorefrontToPush();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
this.app = await this.getApp(this.app.id);
|
|
494
|
+
return this.storefront;
|
|
495
|
+
}
|
|
496
|
+
async handleWatchFileChange(configFile, configType, event) {
|
|
497
|
+
let config;
|
|
498
|
+
if (event === 'deleted') {
|
|
499
|
+
config = findAppConfig(this.app, configFile, configType);
|
|
500
|
+
if (!config) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.watchingChangeQueue.push({
|
|
504
|
+
action: 'remove',
|
|
505
|
+
appConfig: config,
|
|
506
|
+
configFile,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
config = appConfigFromFile(configFile, configType, this.appPath);
|
|
511
|
+
this.watchingChangeQueue.push({
|
|
512
|
+
action: 'push',
|
|
513
|
+
appConfig: config,
|
|
514
|
+
configFile,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
await this.runWatchChangeQueue();
|
|
518
|
+
}
|
|
519
|
+
logAppFrontendUrl() {
|
|
520
|
+
const currentStore = localConfig.getDefaultStore();
|
|
521
|
+
// Shouldn't happen, but just in case
|
|
522
|
+
if (!this.app.installed?.id) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.log(`View your app at ${style.link(this.appFrontendUrl(currentStore, this.app.installed.id))}.`);
|
|
526
|
+
}
|
|
527
|
+
logDashboardUrl(flags) {
|
|
528
|
+
const currentStore = localConfig.getDefaultStore();
|
|
529
|
+
this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, currentStore))}.`);
|
|
530
|
+
}
|
|
531
|
+
logStorefrontConnected() {
|
|
532
|
+
if (!this.storefront) {
|
|
533
|
+
this.error('Storefront not connected');
|
|
534
|
+
}
|
|
535
|
+
const { name, theme_app } = this.storefront;
|
|
536
|
+
const themeLog = theme_app && theme_app.name !== name
|
|
537
|
+
? ` [theme] ${style.appName(theme_app.name)} v${theme_app.version}`
|
|
538
|
+
: '';
|
|
539
|
+
this.log(`[storefront] ${style.appName(name)}${themeLog}\n`);
|
|
540
|
+
}
|
|
541
|
+
logStorefrontFrontendUrl(storefront, branchId) {
|
|
542
|
+
const currentStore = localConfig.getDefaultStore();
|
|
543
|
+
this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
|
|
544
|
+
}
|
|
545
|
+
async pushFile(relativePath) {
|
|
546
|
+
const basePath = relativePath.split(path.sep)[0];
|
|
547
|
+
const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
|
|
548
|
+
const config = appConfigFromFile(relativePath, configType, this.appPath);
|
|
549
|
+
await this.pushRemoteFile(config);
|
|
550
|
+
}
|
|
551
|
+
async pushFilePath(relativePath, force) {
|
|
552
|
+
const topDir = relativePath.split(path.sep)[0];
|
|
553
|
+
const configType = (getConfigTypeFromPath(topDir) || 'file');
|
|
554
|
+
const configTypeKey = getConfigTypeKeyFromValue(configType) || 'file';
|
|
555
|
+
const exAppConfigs = (this.app?.configs || []).filter((config) => config.filePath?.startsWith(`${relativePath}/`));
|
|
556
|
+
const allFiles = [];
|
|
557
|
+
const pushFiles = [];
|
|
558
|
+
for (const { configFile } of allConfigFilesInDir(this.appPath, relativePath, configTypeKey)) {
|
|
559
|
+
const config = appConfigFromFile(configFile, configType, this.appPath);
|
|
560
|
+
allFiles.push(config);
|
|
561
|
+
const exConfig = exAppConfigs.find((exConfig) => config.filePath === exConfig.filePath);
|
|
562
|
+
if (!force && exConfig && exConfig.hash === config.hash) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
pushFiles.push(config);
|
|
566
|
+
}
|
|
567
|
+
const filesToRemove = exAppConfigs.filter((config) => !allFiles.some((appConfig) => appConfig.filePath === config.filePath));
|
|
568
|
+
const hasChanges = pushFiles.length > 0 || filesToRemove.length > 0;
|
|
569
|
+
if (pushFiles.length > 0) {
|
|
570
|
+
const pushConcurrency = configTypeKey === 'ASSET' ? 1 : PUSH_CONCURRENCY;
|
|
571
|
+
while (pushFiles.length > 0) {
|
|
572
|
+
// eslint-disable-next-line no-await-in-loop
|
|
573
|
+
await Promise.all(pushFiles
|
|
574
|
+
.splice(0, pushConcurrency)
|
|
575
|
+
.map(async (appConfig) => {
|
|
576
|
+
await this.pushRemoteFile(appConfig);
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Remove any deleted files
|
|
581
|
+
if (filesToRemove.length > 0) {
|
|
582
|
+
while (filesToRemove.length > 0) {
|
|
583
|
+
// eslint-disable-next-line no-await-in-loop
|
|
584
|
+
await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (appConfig) => {
|
|
585
|
+
await this.removeRemoteFile(appConfig);
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!hasChanges && !force) {
|
|
590
|
+
this.log(`${style.success('✔')} All ${style.path(relativePath)} files are up to date.`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
this.log();
|
|
594
|
+
this.log(`${style.success('✔')} Pushed ${style.path(relativePath)} to ${style.appConfigValue(this.app.name)}.`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
resolveAppPath(appId, targetPath) {
|
|
598
|
+
const resolvedAppId = appId || '';
|
|
599
|
+
let resolvedPath = targetPath || resolvedAppId;
|
|
600
|
+
// If app_id matches directory, use it as target path
|
|
601
|
+
if (!targetPath &&
|
|
602
|
+
isPathDirectory(path.resolve(process.cwd(), resolvedAppId))) {
|
|
603
|
+
resolvedPath = resolvedAppId;
|
|
604
|
+
}
|
|
605
|
+
// Reset app config and path if appId is provided
|
|
606
|
+
if (resolvedPath && this.appPath === '.') {
|
|
607
|
+
this.appPath = path.resolve(process.cwd(), resolvedPath);
|
|
608
|
+
}
|
|
609
|
+
return this.appPath;
|
|
610
|
+
}
|
|
611
|
+
async runWatchChangeQueue() {
|
|
612
|
+
if (this.watchingChangeQueueRunning) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
while (this.watchingChangeQueue.length > 0) {
|
|
616
|
+
this.watchingChangeQueueRunning = true;
|
|
617
|
+
// eslint-disable-next-line no-await-in-loop
|
|
618
|
+
await Promise.all(this.watchingChangeQueue
|
|
619
|
+
.splice(0, PUSH_CONCURRENCY)
|
|
620
|
+
.map(async ({ action, appConfig }) => {
|
|
621
|
+
const result = await (action === 'remove'
|
|
622
|
+
? this.removeRemoteFile(appConfig, this.logWatchChanges)
|
|
623
|
+
: this.pushRemoteFile(appConfig, this.logWatchChanges));
|
|
624
|
+
this.onWatchChange?.(appConfig, result);
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
this.watchingChangeQueueRunning = false;
|
|
628
|
+
// refetch app to get updated configs
|
|
629
|
+
this.app = await this.getAppWithConfig(this.app.id);
|
|
630
|
+
}
|
|
631
|
+
saveCurrentStorefront() {
|
|
632
|
+
const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
|
|
633
|
+
if (this.storefront && this.storefront.id !== currentStorefront) {
|
|
634
|
+
localConfig.setDefaultStorefront(this.appPath, this.storefront.id);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
setWatchingFiles() {
|
|
638
|
+
this.watchingFiles = globAllFilesByPath(this.appPath);
|
|
639
|
+
}
|
|
640
|
+
async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
|
|
641
|
+
const spinner = ora();
|
|
642
|
+
spinner.start(`Updating frontend deployment...`);
|
|
643
|
+
await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}` }, {
|
|
644
|
+
body: {
|
|
645
|
+
$client_id: currentStore,
|
|
646
|
+
$environment_id: 'test',
|
|
647
|
+
frontend: {
|
|
648
|
+
deployment_hash: deploymentHash,
|
|
649
|
+
framework: projectType.slug,
|
|
650
|
+
service: 'cloudflare',
|
|
651
|
+
url: deploymentUrl,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
}), () => spinner.fail('Error updating app deployment'));
|
|
655
|
+
spinner.succeed('Updated frontend deployment.\n');
|
|
656
|
+
}
|
|
657
|
+
async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
|
|
658
|
+
this.logWatchChanges = logChanges ?? true;
|
|
659
|
+
this.onWatchChange = onChange;
|
|
660
|
+
this.setWatchingFiles();
|
|
661
|
+
if (syncAll) {
|
|
662
|
+
await this.pushAppConfigs();
|
|
663
|
+
fs.watch(this.appPath, { recursive: true }, async (_eventType, _filename) => {
|
|
664
|
+
await this.pushAppConfigs();
|
|
665
|
+
this.setWatchingFiles();
|
|
666
|
+
});
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
fs.watch(this.appPath, { recursive: true }, this.watchListener);
|
|
670
|
+
}
|
|
671
|
+
async wranglerDeployFrontend(projectType) {
|
|
672
|
+
let deploymentUrl;
|
|
673
|
+
let interactiveError = false;
|
|
674
|
+
this.log(`\nDeploying to Cloudflare...\n`);
|
|
675
|
+
try {
|
|
676
|
+
await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
|
|
677
|
+
// Dependent on wrangler output
|
|
678
|
+
// Parse the deployment URL from the wrangler output.
|
|
679
|
+
const match = string.match(/Take a peek over at (http\S+)/);
|
|
680
|
+
if (match && match.length > 0) {
|
|
681
|
+
deploymentUrl = match[1];
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
if (interactiveError ||
|
|
685
|
+
string.includes('non-interactive mode') ||
|
|
686
|
+
string.includes('non-interactive environment')) {
|
|
687
|
+
interactiveError = true;
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
// noop
|
|
694
|
+
if (interactiveError) {
|
|
695
|
+
this.log(style.funcWarn(`Your Cloudflare environment must be initialized by logging in with \`wrangler login\`, and exporting the \`CLOUDFLARE_ACCOUNT_ID\` environment variable. Refer to https://developers.cloudflare.com/workers/wrangler/configuration/ for details, and re-run this command to connect the deployment with your app.`));
|
|
696
|
+
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!deploymentUrl) {
|
|
704
|
+
this.error('Unable to retrieve deployment URL.');
|
|
705
|
+
}
|
|
706
|
+
return deploymentUrl;
|
|
707
|
+
}
|
|
708
|
+
async confirmRemoveInstalledApp(app, currentStore) {
|
|
709
|
+
if (!app?.private_id) {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
const existingInstalledApp = await this.api.get({
|
|
713
|
+
adminPath: `/client/apps/${app.private_id}`,
|
|
714
|
+
}, { query: { versioned: true } });
|
|
715
|
+
if (existingInstalledApp) {
|
|
716
|
+
const continuePush = await confirm({
|
|
717
|
+
default: false,
|
|
718
|
+
message: `${style.storeId(currentStore)} has an installed version of ${app.name ? style.appConfigValue(app.name) : 'this app'} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
|
|
719
|
+
});
|
|
720
|
+
if (!continuePush) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
// uninstall the existing app
|
|
724
|
+
const spinner = ora();
|
|
725
|
+
spinner.start('Removing installed app...');
|
|
726
|
+
try {
|
|
727
|
+
await this.api.put({
|
|
728
|
+
adminPath: `/client/apps/${app.id}`,
|
|
729
|
+
}, {
|
|
730
|
+
body: { uninstalled: true },
|
|
731
|
+
});
|
|
732
|
+
spinner.succeed('Installed app removed.');
|
|
733
|
+
this.log();
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
|
|
737
|
+
this.logError(error.message);
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return existingInstalledApp;
|
|
742
|
+
}
|
|
743
|
+
}
|