@swell/cli 2.9.32 → 2.9.33
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/commands/app/dev.js +2 -1
- package/dist/commands/app/frontend/dev.js +4 -2
- package/dist/commands/app/push.d.ts +1 -0
- package/dist/commands/app/push.js +38 -3
- package/dist/commands/app/version.js +3 -0
- package/dist/commands/create/function.js +14 -4
- package/dist/commands/create/model.js +9 -24
- package/dist/commands/inspect/components.d.ts +31 -0
- package/dist/commands/inspect/components.js +110 -0
- package/dist/commands/inspect/extensions.js +18 -9
- package/dist/commands/inspect/frontend.d.ts +37 -0
- package/dist/commands/inspect/frontend.js +75 -0
- package/dist/commands/inspect/functions.d.ts +11 -1
- package/dist/commands/inspect/functions.js +89 -6
- package/dist/commands/inspect/index.js +2 -0
- package/dist/inspect-resource-command.d.ts +1 -1
- package/dist/inspect-resource-command.js +8 -2
- package/dist/lib/api-errors.d.ts +1 -0
- package/dist/lib/api-errors.js +28 -0
- package/dist/lib/apps/components.d.ts +7 -0
- package/dist/lib/apps/components.js +10 -0
- package/dist/lib/apps/extension-manifest.d.ts +1 -0
- package/dist/lib/apps/extension-manifest.js +43 -0
- package/dist/lib/apps/index.d.ts +1 -0
- package/dist/lib/apps/index.js +30 -0
- package/dist/lib/apps/paths.d.ts +8 -0
- package/dist/lib/apps/paths.js +34 -4
- package/dist/lib/bundle.js +15 -2
- package/dist/lib/create/function.d.ts +1 -1
- package/dist/lib/create/function.js +9 -5
- package/dist/lib/create/index.js +2 -1
- package/dist/lib/create/model.d.ts +7 -3
- package/dist/lib/create/model.js +67 -9
- package/dist/lib/inspect/extensions.d.ts +4 -3
- package/dist/lib/inspect/extensions.js +36 -5
- package/dist/lib/swell-function-wrapper.js +5 -4
- package/dist/push-app-command.js +32 -19
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +78 -47
- package/oclif.manifest.json +139 -6
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
|
|
2
|
+
import { slugFromInstalledApp } from '../../lib/apps/slug.js';
|
|
2
3
|
function deriveTrigger(r) {
|
|
3
4
|
if (r.cron?.schedule) {
|
|
4
5
|
return `cron (${r.cron.schedule})`;
|
|
@@ -25,7 +26,7 @@ export default class Functions extends InspectResourceCommand {
|
|
|
25
26
|
static summary = 'Functions with scheduling and trigger state.';
|
|
26
27
|
static description = `Lists functions across all apps, grouped by app. Pass --app=<slug> or --app=. (current swell.json) to scope.
|
|
27
28
|
|
|
28
|
-
List output: paste-back key + status (disabled, trigger, next cron run, last
|
|
29
|
+
List output: paste-back key + status (disabled or auto-disabled, failing since and failure count, the date it gets disabled, trigger, next cron run) and whether its app is inactive, which stops all of its functions. The detail view also shows the last failed delivery.
|
|
29
30
|
|
|
30
31
|
Pass an identifier to view a single record as JSON.
|
|
31
32
|
|
|
@@ -47,15 +48,37 @@ Identifier forms:
|
|
|
47
48
|
resourceLabelSingular = 'function';
|
|
48
49
|
adminPath = '/data/:functions';
|
|
49
50
|
commandName = 'functions';
|
|
51
|
+
inactiveApps = [];
|
|
50
52
|
async run() {
|
|
51
53
|
const { args, flags } = await this.parse(Functions);
|
|
52
54
|
await this.runInspect({ args, flags });
|
|
53
55
|
}
|
|
56
|
+
async getListResults(scope, flags) {
|
|
57
|
+
const [results, inactiveApps] = await Promise.all([
|
|
58
|
+
super.getListResults(scope, flags),
|
|
59
|
+
this.getInactiveApps(),
|
|
60
|
+
]);
|
|
61
|
+
this.inactiveApps = inactiveApps;
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
54
64
|
metaFor(r) {
|
|
55
65
|
const parts = [];
|
|
56
|
-
if (
|
|
66
|
+
if (this.inactiveApps.some((app) => app.app_id === r.app_id)) {
|
|
67
|
+
parts.push('app inactive');
|
|
68
|
+
}
|
|
69
|
+
if (r.auto_disabled) {
|
|
70
|
+
parts.push('auto-disabled');
|
|
71
|
+
}
|
|
72
|
+
else if (!r.enabled) {
|
|
57
73
|
parts.push('disabled');
|
|
58
74
|
}
|
|
75
|
+
const failed = r.attempts_failed ?? 0;
|
|
76
|
+
if (failed > 0) {
|
|
77
|
+
const since = r.date_first_failed
|
|
78
|
+
? `failing since ${r.date_first_failed} · `
|
|
79
|
+
: '';
|
|
80
|
+
parts.push(`${since}${failed} fail${failed === 1 ? '' : 's'}`);
|
|
81
|
+
}
|
|
59
82
|
const trigger = deriveTrigger(r);
|
|
60
83
|
if (trigger) {
|
|
61
84
|
parts.push(trigger);
|
|
@@ -63,21 +86,60 @@ Identifier forms:
|
|
|
63
86
|
if (r.date_cron_scheduled) {
|
|
64
87
|
parts.push(`next ${r.date_cron_scheduled}`);
|
|
65
88
|
}
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
const disableDate = failed > 0 && !r.auto_disabled && getDisableDate(r);
|
|
90
|
+
if (disableDate) {
|
|
91
|
+
parts.push(`may disable from ${disableDate}`);
|
|
68
92
|
}
|
|
69
93
|
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
70
94
|
}
|
|
71
95
|
filterListResults(results) {
|
|
72
96
|
return results.filter((record) => record.kind !== 'workflow');
|
|
73
97
|
}
|
|
74
|
-
emitDetail(record, flags) {
|
|
98
|
+
async emitDetail(record, flags) {
|
|
75
99
|
if (record.kind === 'workflow') {
|
|
76
100
|
const workflowName = record.name || record.id;
|
|
77
101
|
const workflowRef = record.id || record.name;
|
|
78
102
|
throw new Error(`Function '${workflowName}' is a workflow. Use "swell inspect workflows ${workflowRef}" instead.`);
|
|
79
103
|
}
|
|
80
|
-
super.emitDetail(record, flags);
|
|
104
|
+
super.emitDetail(record, flags, flags.json ? [] : await this.detailNotes(record));
|
|
105
|
+
}
|
|
106
|
+
// Notes are best effort: a function shows without those it cannot read
|
|
107
|
+
async detailNotes(record) {
|
|
108
|
+
const notes = [];
|
|
109
|
+
const inactiveApps = await this.getInactiveApps().catch(() => []);
|
|
110
|
+
const inactiveApp = inactiveApps.find((app) => app.app_id === record.app_id);
|
|
111
|
+
if (inactiveApp) {
|
|
112
|
+
notes.push(`App ${slugFromInstalledApp(inactiveApp) ?? inactiveApp.app_id} is inactive: none of its functions run.`);
|
|
113
|
+
}
|
|
114
|
+
if (record.id &&
|
|
115
|
+
(record.auto_disabled || (record.attempts_failed ?? 0) > 0)) {
|
|
116
|
+
const failure = await this.lastFailedDelivery(record.id).catch(() => { });
|
|
117
|
+
if (failure) {
|
|
118
|
+
notes.push(`Last failed delivery: ${failure}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return notes;
|
|
122
|
+
}
|
|
123
|
+
async lastFailedDelivery(functionId) {
|
|
124
|
+
const response = await this.api.get({ adminPath: '/data/events:webhooks' }, { query: { function_id: functionId, sort: 'date_sent desc', limit: 1 } });
|
|
125
|
+
const delivery = response?.results?.[0];
|
|
126
|
+
if (!delivery) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const { date_sent: dateSent, response: answer, status } = delivery;
|
|
130
|
+
if (status >= 200 && status < 300) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const message = answer?.error?.message ??
|
|
134
|
+
answer?.message ??
|
|
135
|
+
(typeof answer === 'string' ? answer : undefined);
|
|
136
|
+
return [dateSent, status ?? 'no response', message]
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join(' · ');
|
|
139
|
+
}
|
|
140
|
+
async getInactiveApps() {
|
|
141
|
+
const response = await this.api.get({ adminPath: '/client/apps' });
|
|
142
|
+
return (response?.results ?? []).filter((app) => app.active === false);
|
|
81
143
|
}
|
|
82
144
|
/**
|
|
83
145
|
* Model-triggered functions emit `/events:webhooks` rows on each invocation.
|
|
@@ -96,3 +158,24 @@ Identifier forms:
|
|
|
96
158
|
return lines;
|
|
97
159
|
}
|
|
98
160
|
}
|
|
161
|
+
// A function with a timeout over this is switched off sooner
|
|
162
|
+
const LONG_TIMEOUT_MS = 10000;
|
|
163
|
+
const LONG_TIMEOUT_DISABLE_DAYS = 2;
|
|
164
|
+
/**
|
|
165
|
+
* When a failing function is switched off if it keeps failing: at its final
|
|
166
|
+
* attempt, or two days after its first failure when its timeout is long. A
|
|
167
|
+
* 404 switches it off at once.
|
|
168
|
+
* @param r The function
|
|
169
|
+
* @returns The date, as the API gives dates
|
|
170
|
+
*/
|
|
171
|
+
function getDisableDate(r) {
|
|
172
|
+
if ((r.timeout ?? 0) > LONG_TIMEOUT_MS) {
|
|
173
|
+
if (!r.date_first_failed) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const date = new Date(r.date_first_failed);
|
|
177
|
+
date.setUTCDate(date.getUTCDate() + LONG_TIMEOUT_DISABLE_DAYS);
|
|
178
|
+
return date.toISOString();
|
|
179
|
+
}
|
|
180
|
+
return r.date_final_attempt;
|
|
181
|
+
}
|
|
@@ -11,6 +11,8 @@ export default class Inspect extends Command {
|
|
|
11
11
|
'swell inspect workflow-runs --workflow app.my-app.run-import --status active',
|
|
12
12
|
'swell inspect notifications com.orders.receipt.v2',
|
|
13
13
|
'swell inspect settings app.my-app',
|
|
14
|
+
'swell inspect components --app=.',
|
|
15
|
+
'swell inspect frontend app.my-app',
|
|
14
16
|
];
|
|
15
17
|
async run() {
|
|
16
18
|
this.log('Run "swell inspect --help" to see available subcommands.');
|
|
@@ -87,7 +87,7 @@ export declare abstract class InspectResourceCommand extends SwellCommand {
|
|
|
87
87
|
* `--json` is the machine-readable contract — hints belong in the same
|
|
88
88
|
* stream as the human-facing output and are simply omitted there.
|
|
89
89
|
*/
|
|
90
|
-
protected emitDetail(record: any, flags: InspectResourceCommandParsed['flags']): void;
|
|
90
|
+
protected emitDetail(record: any, flags: InspectResourceCommandParsed['flags'], notes?: string[]): Promise<void> | void;
|
|
91
91
|
private lookupByAppAndName;
|
|
92
92
|
private printPreamble;
|
|
93
93
|
protected throwNotFound(identifier: string): never;
|
|
@@ -189,7 +189,7 @@ export class InspectResourceCommand extends SwellCommand {
|
|
|
189
189
|
if (!record) {
|
|
190
190
|
this.throwNotFound(identifier);
|
|
191
191
|
}
|
|
192
|
-
this.emitDetail(record, flags);
|
|
192
|
+
await this.emitDetail(record, flags);
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
195
|
* Per-subclass detail-mode hints. Return runnable commands pointing to
|
|
@@ -208,11 +208,17 @@ export class InspectResourceCommand extends SwellCommand {
|
|
|
208
208
|
* `--json` is the machine-readable contract — hints belong in the same
|
|
209
209
|
* stream as the human-facing output and are simply omitted there.
|
|
210
210
|
*/
|
|
211
|
-
emitDetail(record, flags) {
|
|
211
|
+
emitDetail(record, flags, notes = []) {
|
|
212
212
|
this.log(JSON.stringify(record, null, 2));
|
|
213
213
|
if (flags.json) {
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
|
+
if (notes.length > 0) {
|
|
217
|
+
this.log();
|
|
218
|
+
for (const note of notes) {
|
|
219
|
+
this.log(note);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
const lines = this.hints(record);
|
|
217
223
|
if (lines.length === 0) {
|
|
218
224
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function errorReasons(errors: unknown, path?: string[]): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Every reason an API error answer gives, each with the path of the field it
|
|
2
|
+
// is about: errors come as { field: { message } }, nested, as strings or as
|
|
3
|
+
// { error } without a field
|
|
4
|
+
export function errorReasons(errors, path = []) {
|
|
5
|
+
if (typeof errors === 'string') {
|
|
6
|
+
return [formatReason(path, errors)];
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(errors)) {
|
|
9
|
+
return errors.flatMap((item) => errorReasons(item, path));
|
|
10
|
+
}
|
|
11
|
+
if (!errors || typeof errors !== 'object') {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const { message, ...fields } = errors;
|
|
15
|
+
if (typeof message === 'string') {
|
|
16
|
+
return [formatReason(path, message)];
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(fields).flatMap(([key, value]) =>
|
|
19
|
+
// An error's code names its kind, not a reason
|
|
20
|
+
key === 'code' && typeof value === 'string'
|
|
21
|
+
? []
|
|
22
|
+
: // Developers know an app's private_id as the id in swell.json
|
|
23
|
+
errorReasons(value, [...path, key === 'private_id' ? 'id' : key]));
|
|
24
|
+
}
|
|
25
|
+
function formatReason(path, message) {
|
|
26
|
+
const fieldPath = path.filter((key) => key !== 'error').join('.');
|
|
27
|
+
return fieldPath ? `${fieldPath}: ${message}` : message;
|
|
28
|
+
}
|
|
@@ -8,6 +8,13 @@ export declare const COMPONENT_DEV_PATH = "/.swell/components/";
|
|
|
8
8
|
* @returns {boolean} true for a root component
|
|
9
9
|
*/
|
|
10
10
|
export declare function isRootComponentFile(file: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a component file has a name the platform cannot serve: component
|
|
13
|
+
* frames load by a name of letters, digits and dashes
|
|
14
|
+
* @param {string} file path relative to the app
|
|
15
|
+
* @returns {boolean} true for a root component that can never load
|
|
16
|
+
*/
|
|
17
|
+
export declare function isUnloadableComponentFile(file: string): boolean;
|
|
11
18
|
/**
|
|
12
19
|
* Component entries of an app
|
|
13
20
|
* @param {string} appPath app directory
|
|
@@ -16,6 +16,16 @@ export function isRootComponentFile(file) {
|
|
|
16
16
|
parts[0] === 'components' &&
|
|
17
17
|
COMPONENT_EXTENSIONS.has(path.extname(parts[1])));
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a component file has a name the platform cannot serve: component
|
|
21
|
+
* frames load by a name of letters, digits and dashes
|
|
22
|
+
* @param {string} file path relative to the app
|
|
23
|
+
* @returns {boolean} true for a root component that can never load
|
|
24
|
+
*/
|
|
25
|
+
export function isUnloadableComponentFile(file) {
|
|
26
|
+
return (isRootComponentFile(file) &&
|
|
27
|
+
!/^[\dA-Za-z-]+$/.test(getNameFromFilePath(file)));
|
|
28
|
+
}
|
|
19
29
|
/**
|
|
20
30
|
* Component entries of an app
|
|
21
31
|
* @param {string} appPath app directory
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function extensionManifestWarnings(extensions: unknown): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The fields of an extension in swell.json that the platform reads: the
|
|
2
|
+
// dashboard shows name, description and setting, and reads method (card or
|
|
3
|
+
// another method) and subscriptions of a payment; it shows the icons, and the
|
|
4
|
+
// logo of a card gateway. Checkout itself reads only the settings the
|
|
5
|
+
// merchant activates.
|
|
6
|
+
const READ_FIELDS = {
|
|
7
|
+
payment: [
|
|
8
|
+
'method',
|
|
9
|
+
'subscriptions',
|
|
10
|
+
'method_icon_src',
|
|
11
|
+
'gateway_icon_src',
|
|
12
|
+
'gateway_logo_src',
|
|
13
|
+
],
|
|
14
|
+
shipping: ['carrier_icon_src'],
|
|
15
|
+
tax: [],
|
|
16
|
+
};
|
|
17
|
+
const COMMON_FIELDS = ['id', 'type', 'name', 'description', 'setting'];
|
|
18
|
+
// Declared on the app model, but nothing reads them
|
|
19
|
+
const UNREAD_FIELDS = new Set([
|
|
20
|
+
'gateway',
|
|
21
|
+
'carrier',
|
|
22
|
+
'method_logo_src',
|
|
23
|
+
'carrier_logo_src',
|
|
24
|
+
]);
|
|
25
|
+
export function extensionManifestWarnings(extensions) {
|
|
26
|
+
if (!Array.isArray(extensions)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return extensions.flatMap((extension) => {
|
|
30
|
+
if (!extension || typeof extension !== 'object') {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const read = new Set([
|
|
34
|
+
...COMMON_FIELDS,
|
|
35
|
+
...(READ_FIELDS[extension.type] ?? []),
|
|
36
|
+
]);
|
|
37
|
+
return Object.keys(extension)
|
|
38
|
+
.filter((field) => !read.has(field))
|
|
39
|
+
.map((field) => UNREAD_FIELDS.has(field)
|
|
40
|
+
? `swell.json extension "${extension.id}": ${field} has no effect`
|
|
41
|
+
: `swell.json extension "${extension.id}": unknown field ${field}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -206,6 +206,7 @@ export declare function hashFile(filePath: string, fileContents?: Buffer, relati
|
|
|
206
206
|
export declare function hashString(...contents: (Buffer | string)[]): string;
|
|
207
207
|
export declare function appAssetImage(appPath: string, fileName: string): Promise<string | undefined>;
|
|
208
208
|
export declare function getNameFromFilePath(filePath: string): string;
|
|
209
|
+
export declare function kebabCaseFilePath(filePath: string): string | undefined;
|
|
209
210
|
export declare function appConfigFromFile(filePath: string, configType: ConfigType, appPath: string): Promise<AppConfig>;
|
|
210
211
|
export declare function findAppConfig(app: App, filePath: string, configType: ConfigType): AppConfig | undefined;
|
|
211
212
|
interface BatchAppFiles {
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -364,6 +364,36 @@ export async function appAssetImage(appPath, fileName) {
|
|
|
364
364
|
export function getNameFromFilePath(filePath) {
|
|
365
365
|
return path.parse(filePath).name.replaceAll('_', '-');
|
|
366
366
|
}
|
|
367
|
+
// Configs in the root of these directories are named after their file
|
|
368
|
+
const FILE_NAMED_CONFIG_DIRS = new Set([
|
|
369
|
+
AllConfigPaths.COMPONENT,
|
|
370
|
+
AllConfigPaths.CONTENT,
|
|
371
|
+
AllConfigPaths.FUNCTION,
|
|
372
|
+
AllConfigPaths.MODEL,
|
|
373
|
+
AllConfigPaths.NOTIFICATION,
|
|
374
|
+
AllConfigPaths.SETTING,
|
|
375
|
+
AllConfigPaths.WEBHOOK,
|
|
376
|
+
]);
|
|
377
|
+
// Among components, only .jsx and .tsx files are components; others are
|
|
378
|
+
// modules the components import
|
|
379
|
+
const COMPONENT_FILE_EXTENSIONS = new Set(['.jsx', '.tsx']);
|
|
380
|
+
// The name of a config turns an underscore in its file name into a dash, so
|
|
381
|
+
// the file goes by another name than what it deploys as unless renamed to this
|
|
382
|
+
export function kebabCaseFilePath(filePath) {
|
|
383
|
+
const [dir, file, ...nested] = filePath.split('/');
|
|
384
|
+
if (nested.length > 0 ||
|
|
385
|
+
!FILE_NAMED_CONFIG_DIRS.has(dir) ||
|
|
386
|
+
!file?.includes('_') ||
|
|
387
|
+
(dir === AllConfigPaths.COMPONENT &&
|
|
388
|
+
!COMPONENT_FILE_EXTENSIONS.has(path.extname(file)))) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const name = getNameFromFilePath(file);
|
|
392
|
+
if (name.startsWith('-')) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return `${dir}/${name}${path.extname(file)}`;
|
|
396
|
+
}
|
|
367
397
|
export async function appConfigFromFile(filePath, configType, appPath) {
|
|
368
398
|
const name = getNameFromFilePath(filePath);
|
|
369
399
|
// Notification basenames must not contain dots: the inspect identifier
|
package/dist/lib/apps/paths.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type GlobbyFilterFunction } from 'globby';
|
|
2
2
|
import { ConfigType } from './index.js';
|
|
3
|
+
export declare function isCredentialFile(relativePath: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The .env files of the frontend that the push would have pushed, which its
|
|
6
|
+
* build reads: their change deploys the frontend again, though they stay local
|
|
7
|
+
* @param {string} appPath the app directory
|
|
8
|
+
* @returns {Promise<string[]>} paths relative to the app, sorted
|
|
9
|
+
*/
|
|
10
|
+
export declare function globFrontendEnvFiles(appPath: string): Promise<string[]>;
|
|
3
11
|
export declare function toPosixPath(inputPath: string): string;
|
|
4
12
|
export declare function globAllFilesByPath(appPath: string, dirPath?: string): Promise<string[]>;
|
|
5
13
|
export declare function getGlobIgnorePathsChecker(appPath: string): Promise<GlobbyFilterFunction>;
|
package/dist/lib/apps/paths.js
CHANGED
|
@@ -2,6 +2,39 @@ import { globby, globbySync, isGitIgnored, } from 'globby';
|
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { AllConfigPaths, ConfigType, StandardConfigPaths, StorefrontConfigPaths, ThemeConfigPaths, filePathExists, } from './index.js';
|
|
5
|
+
// .env and .dev.vars files can hold real credentials: never pushed, whatever
|
|
6
|
+
// the user's .gitignore says
|
|
7
|
+
const CREDENTIAL_FILE_GLOBS = [
|
|
8
|
+
'**/.dev.vars',
|
|
9
|
+
'**/.dev.vars.*',
|
|
10
|
+
'**/.env',
|
|
11
|
+
'**/.env.*',
|
|
12
|
+
];
|
|
13
|
+
export function isCredentialFile(relativePath) {
|
|
14
|
+
const name = path.posix.basename(toPosixPath(relativePath));
|
|
15
|
+
return (name === '.env' ||
|
|
16
|
+
name.startsWith('.env.') ||
|
|
17
|
+
name === '.dev.vars' ||
|
|
18
|
+
name.startsWith('.dev.vars.'));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The .env files of the frontend that the push would have pushed, which its
|
|
22
|
+
* build reads: their change deploys the frontend again, though they stay local
|
|
23
|
+
* @param {string} appPath the app directory
|
|
24
|
+
* @returns {Promise<string[]>} paths relative to the app, sorted
|
|
25
|
+
*/
|
|
26
|
+
export async function globFrontendEnvFiles(appPath) {
|
|
27
|
+
const files = await globby([
|
|
28
|
+
`${AllConfigPaths.FRONTEND}/**/.env`,
|
|
29
|
+
`${AllConfigPaths.FRONTEND}/**/.env.*`,
|
|
30
|
+
], {
|
|
31
|
+
cwd: appPath,
|
|
32
|
+
dot: true,
|
|
33
|
+
gitignore: true,
|
|
34
|
+
ignore: ['**/node_modules/**', '**/.wrangler/**'],
|
|
35
|
+
});
|
|
36
|
+
return files.sort();
|
|
37
|
+
}
|
|
5
38
|
function globOptions(appPath, options) {
|
|
6
39
|
return {
|
|
7
40
|
cwd: appPath,
|
|
@@ -9,8 +42,6 @@ function globOptions(appPath, options) {
|
|
|
9
42
|
gitignore: true,
|
|
10
43
|
...options,
|
|
11
44
|
expandDirectories: true,
|
|
12
|
-
// .dev.vars can hold real credentials — ignored here in case the
|
|
13
|
-
// user's .gitignore doesn't cover it.
|
|
14
45
|
ignore: [
|
|
15
46
|
'**/.swellrc',
|
|
16
47
|
'**/.git/**',
|
|
@@ -21,8 +52,7 @@ function globOptions(appPath, options) {
|
|
|
21
52
|
'**/bun.lockb',
|
|
22
53
|
'**/bun.lock',
|
|
23
54
|
'**/.wrangler/**',
|
|
24
|
-
|
|
25
|
-
'**/.dev.vars.*',
|
|
55
|
+
...CREDENTIAL_FILE_GLOBS,
|
|
26
56
|
...(options?.ignore || []),
|
|
27
57
|
],
|
|
28
58
|
};
|
package/dist/lib/bundle.js
CHANGED
|
@@ -266,13 +266,26 @@ class SwellWorkflowRuntimeProxy {
|
|
|
266
266
|
this.apiHost = getRuntimeApiHost(runtime);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
api(method, path, data) {
|
|
270
|
-
|
|
269
|
+
async api(method, path, data) {
|
|
270
|
+
const result = await this.request({
|
|
271
271
|
type: 'api',
|
|
272
272
|
method,
|
|
273
273
|
path,
|
|
274
274
|
data,
|
|
275
275
|
});
|
|
276
|
+
|
|
277
|
+
// A write the API refuses answers with its errors. It fails as it does in
|
|
278
|
+
// functions, and is not retried, since the same write would fail again
|
|
279
|
+
if (method !== 'get' && result && result.errors) {
|
|
280
|
+
const error = new NonRetryableError(
|
|
281
|
+
method.toUpperCase() + ' ' + path + '\\n' + JSON.stringify(result.errors, null, 2),
|
|
282
|
+
);
|
|
283
|
+
error.status = 400;
|
|
284
|
+
error.body = result.errors;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result;
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
settings() {
|
|
@@ -11,7 +11,7 @@ export function getFunctionTemplate({ description, events, extension, functionNa
|
|
|
11
11
|
const eventsList = events
|
|
12
12
|
.split(/\s*,\s*/g)
|
|
13
13
|
.filter(Boolean)
|
|
14
|
-
.map((eventName) =>
|
|
14
|
+
.map((eventName) => JSON.stringify(eventName))
|
|
15
15
|
.join(', ');
|
|
16
16
|
typeAppend = `,
|
|
17
17
|
model: {
|
|
@@ -28,15 +28,19 @@ export function getFunctionTemplate({ description, events, extension, functionNa
|
|
|
28
28
|
if (trigger === 'cron') {
|
|
29
29
|
typeAppend = `,
|
|
30
30
|
cron: {
|
|
31
|
-
schedule:
|
|
31
|
+
schedule: ${JSON.stringify(schedule)},
|
|
32
32
|
},`;
|
|
33
|
+
}
|
|
34
|
+
if (trigger === 'action') {
|
|
35
|
+
typeAppend = `,
|
|
36
|
+
action: true,`;
|
|
33
37
|
}
|
|
34
38
|
let extensionAppend = '';
|
|
35
39
|
if (extension) {
|
|
36
|
-
extensionAppend = `extension:
|
|
40
|
+
extensionAppend = `extension: ${JSON.stringify(extension)},\n `;
|
|
37
41
|
}
|
|
38
42
|
return `export const config${language === 'ts' ? ': SwellConfig' : ''} = {
|
|
39
|
-
${extensionAppend}description:
|
|
43
|
+
${extensionAppend}description: ${JSON.stringify(description)}${typeAppend}
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
export default async function (req${language === 'ts' ? ': SwellRequest' : ''}) {
|
|
@@ -50,7 +54,7 @@ function getWorkflowTemplate({ description, functionName, language, }) {
|
|
|
50
54
|
const stepParam = language === 'ts' ? 'step: SwellWorkflowStep' : 'step';
|
|
51
55
|
return `export const config = {
|
|
52
56
|
kind: 'workflow',
|
|
53
|
-
description:
|
|
57
|
+
description: ${JSON.stringify(description)}
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
export default class ${className} {
|
package/dist/lib/create/index.js
CHANGED
|
@@ -15,8 +15,9 @@ export const toAppId = (value) => String(value || '')
|
|
|
15
15
|
.replaceAll(/([a-z])([A-Z])/g, '$1_$2')
|
|
16
16
|
.toLowerCase();
|
|
17
17
|
// Example: 'My Thing' => 'my-thing'
|
|
18
|
+
// And: 'sync_order' => 'sync-order', the name a function deploys under
|
|
18
19
|
// And: 'myThing' => 'myThing'
|
|
19
|
-
export const toFunctionFileName = (value) => value.split('.')[0].
|
|
20
|
+
export const toFunctionFileName = (value) => value.split('.')[0].replaceAll(/[\s_]+/g, '-');
|
|
20
21
|
// Example: 'My Thing' => 'my-things'
|
|
21
22
|
export const toCollectionName = (value) => pluralize(value.replaceAll(/[^A-Za-z]/g, '-').replace(' ', '')).toLowerCase();
|
|
22
23
|
// Example: 'my-things' => 'My Things'
|
|
@@ -13,11 +13,15 @@ interface ModelEvent {
|
|
|
13
13
|
id: ModelEventID;
|
|
14
14
|
}
|
|
15
15
|
type ModelEventID = 'created' | 'deleted' | 'updated';
|
|
16
|
-
type ModelFieldResponse = Record<string,
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
type ModelFieldResponse = Record<string, ModelField>;
|
|
17
|
+
export declare const FIELD_TYPES: readonly ["string", "int", "float", "bool", "date", "currency", "objectid", "array", "object", "collection", "link", "file", "filedata"];
|
|
18
|
+
type FieldType = (typeof FIELD_TYPES)[number];
|
|
19
|
+
interface ModelField {
|
|
20
|
+
model?: string;
|
|
19
21
|
type: FieldType;
|
|
22
|
+
value_type?: string;
|
|
20
23
|
}
|
|
24
|
+
export declare function fieldPairProblem(pair: string): string | undefined;
|
|
21
25
|
export declare function parseFields(fields: string[]): ModelFieldResponse;
|
|
22
26
|
export declare function parseEvents(events: string[]): ModelEventObject;
|
|
23
27
|
export {};
|
package/dist/lib/create/model.js
CHANGED
|
@@ -1,14 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// The field types of the model schema, https://json.swell.store/model.json
|
|
2
|
+
export const FIELD_TYPES = [
|
|
3
|
+
'string',
|
|
4
|
+
'int',
|
|
5
|
+
'float',
|
|
6
|
+
'bool',
|
|
7
|
+
'date',
|
|
8
|
+
'currency',
|
|
9
|
+
'objectid',
|
|
10
|
+
'array',
|
|
11
|
+
'object',
|
|
12
|
+
'collection',
|
|
13
|
+
'link',
|
|
14
|
+
'file',
|
|
15
|
+
'filedata',
|
|
16
|
+
];
|
|
17
|
+
// Items of an array given as a flag; object and link items need more than a type
|
|
18
|
+
const ARRAY_VALUE_TYPES = [
|
|
19
|
+
'string',
|
|
20
|
+
'int',
|
|
21
|
+
'float',
|
|
22
|
+
'bool',
|
|
23
|
+
'date',
|
|
24
|
+
'currency',
|
|
25
|
+
'objectid',
|
|
26
|
+
'file',
|
|
27
|
+
'filedata',
|
|
28
|
+
];
|
|
29
|
+
function splitFieldPair(pair) {
|
|
30
|
+
return pair.split(':').map((part) => part.trim());
|
|
31
|
+
}
|
|
32
|
+
// Why a field given as id:type, id:link:<model> or id:array:<type> cannot be
|
|
33
|
+
// written, if it cannot
|
|
34
|
+
export function fieldPairProblem(pair) {
|
|
35
|
+
const [id, type, target] = splitFieldPair(pair);
|
|
36
|
+
if (!id || !type) {
|
|
37
|
+
return `${pair}: expected id:type`;
|
|
38
|
+
}
|
|
39
|
+
if (type === 'image') {
|
|
40
|
+
return `${pair}: Invalid field type 'image'; use file`;
|
|
41
|
+
}
|
|
42
|
+
if (type === 'collection') {
|
|
43
|
+
return `${pair}: A collection needs its fields; add it with them to the model file`;
|
|
44
|
+
}
|
|
45
|
+
if (!FIELD_TYPES.includes(type)) {
|
|
46
|
+
return `${pair}: unknown type '${type}'`;
|
|
47
|
+
}
|
|
48
|
+
if (type === 'link' && !target) {
|
|
49
|
+
return `${pair}: a link needs the model it links to, e.g. ${id}:link:accounts`;
|
|
4
50
|
}
|
|
51
|
+
if (type === 'array' && target && !ARRAY_VALUE_TYPES.includes(target)) {
|
|
52
|
+
return `${pair}: an array given here holds ${ARRAY_VALUE_TYPES.join(', ')}`;
|
|
53
|
+
}
|
|
54
|
+
if (target && type !== 'link' && type !== 'array') {
|
|
55
|
+
return `${pair}: only link and array take a third part`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function parseFields(fields) {
|
|
5
59
|
const fieldResponse = {};
|
|
6
|
-
for (const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
fieldResponse[id] = {
|
|
10
|
-
|
|
11
|
-
|
|
60
|
+
for (const pair of fields) {
|
|
61
|
+
const [id, type, target] = splitFieldPair(pair);
|
|
62
|
+
if (type === 'link') {
|
|
63
|
+
fieldResponse[id] = { type, model: target };
|
|
64
|
+
}
|
|
65
|
+
else if (type === 'array') {
|
|
66
|
+
fieldResponse[id] = { type, value_type: target || 'string' };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
fieldResponse[id] = { type };
|
|
12
70
|
}
|
|
13
71
|
}
|
|
14
72
|
return fieldResponse;
|
|
@@ -24,7 +24,7 @@ export interface ManifestEntry {
|
|
|
24
24
|
}
|
|
25
25
|
/** CLI-resolved type. Diverges from raw manifest `type` for payment (card vs alt). */
|
|
26
26
|
export type ExtensionType = 'card' | 'alt' | 'shipping' | 'tax';
|
|
27
|
-
export type ExtensionStatus = 'not deployed' | 'not activated' | 'app id mismatch' | 'id mismatch' | 'not selected' | 'gateway missing' | 'not enabled' | 'no handler' | 'handler mismatch' | 'activated';
|
|
27
|
+
export type ExtensionStatus = 'not deployed' | 'settings unavailable' | 'not activated' | 'app id mismatch' | 'id mismatch' | 'not selected' | 'gateway missing' | 'not enabled' | 'no handler' | 'handler mismatch' | 'activated';
|
|
28
28
|
export type ActionOwner = 'dev' | 'merchant' | null;
|
|
29
29
|
export interface FunctionRecord {
|
|
30
30
|
id?: string;
|
|
@@ -163,8 +163,6 @@ export declare function formatExtensionKey(slug: string, extId: string): string;
|
|
|
163
163
|
* resolves to `card` even with no explicit `method`.
|
|
164
164
|
*/
|
|
165
165
|
export declare function resolveExtensionType(entry: Pick<ManifestEntry, 'type' | 'method' | 'id'>): ExtensionType;
|
|
166
|
-
/** Apply the platform's `if(method, method, id)` formula. */
|
|
167
|
-
export declare function paymentMethodIdFor(entry: Pick<ManifestEntry, 'method' | 'id'>): string;
|
|
168
166
|
/** Shipping records use `app_<appId>_<extId>` as both the row id and the carrier id. */
|
|
169
167
|
export declare function shippingBindId(appId: string, extId: string): string;
|
|
170
168
|
/** Card-gateway records use the same id form as shipping rows. */
|
|
@@ -229,12 +227,15 @@ export interface BuildDetailInput {
|
|
|
229
227
|
payments?: PaymentSettings | null;
|
|
230
228
|
shipments?: ShippingSettings | null;
|
|
231
229
|
taxes?: TaxSettingsRecord | null;
|
|
230
|
+
/** Why each settings record could not be read, if it could not. */
|
|
231
|
+
unavailableSettings?: UnavailableSettings;
|
|
232
232
|
/** Functions in the same app whose top-level `extension` matches this extId. */
|
|
233
233
|
functions: FunctionRecord[];
|
|
234
234
|
/** Components in the same app whose `values.extension` matches this extId. */
|
|
235
235
|
components: ComponentRecord[];
|
|
236
236
|
localDiff?: LocalDiff | null;
|
|
237
237
|
}
|
|
238
|
+
export type UnavailableSettings = Partial<Record<'payments' | 'shipments' | 'taxes', string>>;
|
|
238
239
|
export interface BuildOrphanInput {
|
|
239
240
|
appId: string;
|
|
240
241
|
appSlug: string;
|