@swell/cli 2.9.31 → 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/action.d.ts +25 -0
- package/dist/commands/app/action.js +233 -0
- package/dist/commands/app/dev.js +2 -1
- package/dist/commands/app/frontend/deploy.d.ts +1 -0
- package/dist/commands/app/frontend/deploy.js +4 -0
- package/dist/commands/app/frontend/dev.js +4 -2
- package/dist/commands/app/push.d.ts +3 -0
- package/dist/commands/app/push.js +100 -4
- 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/create/webhook.js +7 -13
- 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/commands/schema.js +5 -0
- package/dist/help/custom-help.js +5 -1
- 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/actions.d.ts +45 -0
- package/dist/lib/apps/actions.js +114 -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/listing.d.ts +1 -0
- package/dist/lib/apps/listing.js +41 -0
- package/dist/lib/apps/paths.d.ts +8 -0
- package/dist/lib/apps/paths.js +34 -4
- package/dist/lib/bundle.js +26 -41
- 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/function-declarations.d.ts +8 -0
- package/dist/lib/function-declarations.js +27 -0
- package/dist/lib/inspect/extensions.d.ts +4 -3
- package/dist/lib/inspect/extensions.js +36 -5
- package/dist/lib/swell-function-wrapper.js +13 -5
- package/dist/lib/swell-workflow-request.d.ts +34 -0
- package/dist/lib/swell-workflow-request.js +58 -0
- package/dist/push-app-command.d.ts +2 -1
- package/dist/push-app-command.js +43 -22
- package/dist/remote-app-command.d.ts +2 -2
- package/dist/remote-app-command.js +79 -51
- package/oclif.manifest.json +263 -10
- package/package.json +2 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function isFunctionAction(item) {
|
|
2
|
+
return (typeof item?.id === 'string' &&
|
|
3
|
+
typeof item?.function === 'string');
|
|
4
|
+
}
|
|
5
|
+
// The dashboard renders the fields of rows and groups in place
|
|
6
|
+
function flattenFields(fields) {
|
|
7
|
+
if (!Array.isArray(fields)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return fields.flatMap((field) => field?.type === 'field_row' || field?.type === 'field_group'
|
|
11
|
+
? flattenFields(field.fields)
|
|
12
|
+
: [field]);
|
|
13
|
+
}
|
|
14
|
+
function getActionFields(fields) {
|
|
15
|
+
return flattenFields(fields).filter((field) => field?.type === 'action');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The function actions a content config declares.
|
|
19
|
+
*
|
|
20
|
+
* @param values the values of the content config
|
|
21
|
+
* @returns the actions
|
|
22
|
+
*/
|
|
23
|
+
export function getContentActions(values) {
|
|
24
|
+
const views = Array.isArray(values?.views) ? values.views : [];
|
|
25
|
+
const actions = [];
|
|
26
|
+
for (const view of views) {
|
|
27
|
+
const source = view.type || (view.id === 'list' ? 'list' : 'record');
|
|
28
|
+
for (const action of [
|
|
29
|
+
...(view.actions || []),
|
|
30
|
+
...(view.extra_actions || []),
|
|
31
|
+
]) {
|
|
32
|
+
if (isFunctionAction(action)) {
|
|
33
|
+
actions.push({ ...pickAction(action), source, view: view.id });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const action of view.bulk_actions || []) {
|
|
37
|
+
if (isFunctionAction(action)) {
|
|
38
|
+
actions.push({
|
|
39
|
+
...pickAction(action),
|
|
40
|
+
...(view.query ? { query: view.query } : undefined),
|
|
41
|
+
source: 'bulk',
|
|
42
|
+
view: view.id,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const fields = [
|
|
48
|
+
...(values?.fields || []),
|
|
49
|
+
...views.flatMap((view) => [
|
|
50
|
+
...(view.fields || []),
|
|
51
|
+
...(view.tabs || []).flatMap((tab) => tab?.fields || []),
|
|
52
|
+
]),
|
|
53
|
+
];
|
|
54
|
+
for (const field of getActionFields(fields)) {
|
|
55
|
+
if (isFunctionAction(field)) {
|
|
56
|
+
actions.push({ ...pickAction(field), source: 'field' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return actions;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The function actions a settings config declares.
|
|
63
|
+
*
|
|
64
|
+
* @param values the values of the settings config
|
|
65
|
+
* @param settings the name of the settings config
|
|
66
|
+
* @returns the actions
|
|
67
|
+
*/
|
|
68
|
+
export function getSettingsActions(values, settings) {
|
|
69
|
+
const actions = [];
|
|
70
|
+
for (const action of values?.actions || []) {
|
|
71
|
+
if (isFunctionAction(action)) {
|
|
72
|
+
actions.push({ ...pickAction(action), settings, source: 'settings' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const field of getActionFields(values?.fields)) {
|
|
76
|
+
if (isFunctionAction(field)) {
|
|
77
|
+
actions.push({ ...pickAction(field), settings, source: 'field' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return actions;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The ids of the fields of an action's modal, as the platform finds them to
|
|
84
|
+
* pass on only their values.
|
|
85
|
+
*
|
|
86
|
+
* @param modal the modal of the action
|
|
87
|
+
* @returns the field ids
|
|
88
|
+
*/
|
|
89
|
+
export function getModalFieldIds(modal) {
|
|
90
|
+
return flattenFields(modal.fields)
|
|
91
|
+
.map((field) => field?.id)
|
|
92
|
+
.filter((id) => typeof id === 'string');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A function name as the platform compares an action's `function` with it.
|
|
96
|
+
*
|
|
97
|
+
* @param name a function name
|
|
98
|
+
* @returns the name in the form the platform compares
|
|
99
|
+
*/
|
|
100
|
+
export function functionSlug(name) {
|
|
101
|
+
return name
|
|
102
|
+
.replaceAll(/[^\s\w./:-]/g, '')
|
|
103
|
+
.replaceAll(/[\s./:_-]+/g, '-')
|
|
104
|
+
.replaceAll(/^-+|-+$/g, '')
|
|
105
|
+
.replaceAll(/([a-z])([A-Z])/g, '$1-$2')
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
function pickAction(action) {
|
|
109
|
+
return {
|
|
110
|
+
function: action.function,
|
|
111
|
+
id: action.id,
|
|
112
|
+
...(action.modal ? { modal: action.modal } : undefined),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function warnListingSources(appPath: string, config: Record<string, any>, warn: (message: string) => void): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const LISTING_SOURCE_FIELDS = ['logo_src', 'preview_src', 'preview_mobile_src'];
|
|
4
|
+
const LISTING_SOURCE_LISTS = ['images', 'highlights'];
|
|
5
|
+
// The listing takes its full description from DESCRIPTION.md and each image
|
|
6
|
+
// from the asset whose path from the app root is the swell.json value as
|
|
7
|
+
// written, so values that point elsewhere do nothing. The platform takes an
|
|
8
|
+
// empty value as unset.
|
|
9
|
+
export function warnListingSources(appPath, config, warn) {
|
|
10
|
+
if (config.full_description !== undefined) {
|
|
11
|
+
warn('swell.json sets full_description, which push does not apply: the full description is the content of DESCRIPTION.md at the app root.');
|
|
12
|
+
}
|
|
13
|
+
const sources = [
|
|
14
|
+
...LISTING_SOURCE_FIELDS.map((field) => [
|
|
15
|
+
field,
|
|
16
|
+
config[field],
|
|
17
|
+
]),
|
|
18
|
+
...LISTING_SOURCE_LISTS.flatMap((list) => (Array.isArray(config[list]) ? config[list] : []).map((item, index) => [
|
|
19
|
+
`${list}[${index}].image_src`,
|
|
20
|
+
item?.image_src,
|
|
21
|
+
])),
|
|
22
|
+
];
|
|
23
|
+
for (const [field, source] of sources) {
|
|
24
|
+
if (typeof source !== 'string' || source === '') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const filePath = path.posix.normalize(source.replaceAll('\\', '/'));
|
|
28
|
+
const stats = statSync(path.join(appPath, filePath), {
|
|
29
|
+
throwIfNoEntry: false,
|
|
30
|
+
});
|
|
31
|
+
if (!stats?.isFile()) {
|
|
32
|
+
warn(`swell.json ${field} names ${source}, which the app does not have: the listing gets no image from it.`);
|
|
33
|
+
}
|
|
34
|
+
else if (!filePath.startsWith('assets/')) {
|
|
35
|
+
warn(`swell.json ${field} names ${source}, which is not in assets/: the listing gets no image from it. Move the file to assets/ and write assets/${path.posix.basename(filePath)}.`);
|
|
36
|
+
}
|
|
37
|
+
else if (filePath !== source) {
|
|
38
|
+
warn(`swell.json ${field} names ${source}, which is not the file's path from the app root: the listing gets no image from it. Write ${filePath}.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
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
|
@@ -60,7 +60,7 @@ export async function bundleFunction(filePath) {
|
|
|
60
60
|
}
|
|
61
61
|
export async function bundleWorkflow(filePath, analysis) {
|
|
62
62
|
try {
|
|
63
|
-
const entrySource = getSwellWorkflowEntrypoint(filePath);
|
|
63
|
+
const entrySource = await getSwellWorkflowEntrypoint(filePath);
|
|
64
64
|
const buildResult = await esbuild.build({
|
|
65
65
|
bundle: true,
|
|
66
66
|
stdin: {
|
|
@@ -130,13 +130,22 @@ async function getSwellFunctionWrapper() {
|
|
|
130
130
|
const __dirname = path.dirname(__filename);
|
|
131
131
|
return fs.readFile(path.join(__dirname, './swell-function-wrapper.js'), 'utf8');
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
// The entry defines these functions itself, so the bundle names no file of
|
|
134
|
+
// the CLI and the workflow module exports only its entrypoint
|
|
135
|
+
async function getSwellWorkflowRequest() {
|
|
136
|
+
const source = await fs.readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), './swell-workflow-request.js'), 'utf8');
|
|
137
|
+
return source.replaceAll(/^export /gm, '');
|
|
138
|
+
}
|
|
139
|
+
async function getSwellWorkflowEntrypoint(filePath) {
|
|
134
140
|
const workflowImport = `./${path.basename(filePath)}`;
|
|
141
|
+
const workflowRequest = await getSwellWorkflowRequest();
|
|
135
142
|
return `
|
|
136
143
|
import { WorkflowEntrypoint } from 'cloudflare:workers';
|
|
137
144
|
import { NonRetryableError } from 'cloudflare:workflows';
|
|
138
145
|
import UserWorkflow from ${JSON.stringify(workflowImport)};
|
|
139
146
|
|
|
147
|
+
${workflowRequest}
|
|
148
|
+
|
|
140
149
|
const RUNTIME_PROXY_PATH = '/:workflows/runtime/request';
|
|
141
150
|
const RUNTIME_TOKEN_PATTERN = /\\bwf_rt_[0-9A-Za-z]+_[0-9A-Za-z]{12,}\\b/g;
|
|
142
151
|
|
|
@@ -210,38 +219,6 @@ function getInvocation(event) {
|
|
|
210
219
|
return payload;
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
function buildWorkflowRequest(invocation, proxy) {
|
|
214
|
-
const runtime = invocation.runtime;
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
id: runtime.request_id,
|
|
218
|
-
appId: runtime.app_slug || runtime.app_id,
|
|
219
|
-
environmentId: runtimeEnvironmentId(runtime),
|
|
220
|
-
store: {
|
|
221
|
-
id: runtime.store_id,
|
|
222
|
-
},
|
|
223
|
-
app: {
|
|
224
|
-
admin_url: runtime.app_admin_url,
|
|
225
|
-
},
|
|
226
|
-
data: invocation.data,
|
|
227
|
-
workflow: {
|
|
228
|
-
workflow_id: runtime.workflow_id,
|
|
229
|
-
workflow_name: runtime.workflow_name,
|
|
230
|
-
workflow_instance_id: runtime.workflow_instance_id,
|
|
231
|
-
trigger: runtime.trigger,
|
|
232
|
-
request_id: runtime.request_id,
|
|
233
|
-
},
|
|
234
|
-
isLocalDev: false,
|
|
235
|
-
swell: {
|
|
236
|
-
get: (path, data) => proxy.api('get', path, data),
|
|
237
|
-
post: (path, data) => proxy.api('post', path, data),
|
|
238
|
-
put: (path, data) => proxy.api('put', path, data),
|
|
239
|
-
delete: (path, data) => proxy.api('delete', path, data),
|
|
240
|
-
settings: () => proxy.settings(),
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
222
|
function buildWorkflowStep(cloudflareStep, proxy) {
|
|
246
223
|
return Object.freeze({
|
|
247
224
|
do(name, optionsOrCallback, maybeCallback) {
|
|
@@ -289,13 +266,26 @@ class SwellWorkflowRuntimeProxy {
|
|
|
289
266
|
this.apiHost = getRuntimeApiHost(runtime);
|
|
290
267
|
}
|
|
291
268
|
|
|
292
|
-
api(method, path, data) {
|
|
293
|
-
|
|
269
|
+
async api(method, path, data) {
|
|
270
|
+
const result = await this.request({
|
|
294
271
|
type: 'api',
|
|
295
272
|
method,
|
|
296
273
|
path,
|
|
297
274
|
data,
|
|
298
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;
|
|
299
289
|
}
|
|
300
290
|
|
|
301
291
|
settings() {
|
|
@@ -361,11 +351,6 @@ function getRuntimeApiHost(runtime) {
|
|
|
361
351
|
return apiHost.replace(/\\/$/, '');
|
|
362
352
|
}
|
|
363
353
|
|
|
364
|
-
// 'test' in the test environment, null in live
|
|
365
|
-
function runtimeEnvironmentId(runtime) {
|
|
366
|
-
return runtime.environment_id || null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
354
|
function runtimeIdentity(runtime) {
|
|
370
355
|
return {
|
|
371
356
|
app_id: runtime.app_id,
|
|
@@ -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;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types an app's functions type-check against: the app's own
|
|
3
|
+
* @swell/app-types when it has the package, otherwise the CLI's.
|
|
4
|
+
*
|
|
5
|
+
* @param directory a directory of the app
|
|
6
|
+
* @returns the declarations, after a line naming the package version
|
|
7
|
+
*/
|
|
8
|
+
export declare function getFunctionDeclarations(directory: string): Promise<string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* The types an app's functions type-check against: the app's own
|
|
6
|
+
* @swell/app-types when it has the package, otherwise the CLI's.
|
|
7
|
+
*
|
|
8
|
+
* @param directory a directory of the app
|
|
9
|
+
* @returns the declarations, after a line naming the package version
|
|
10
|
+
*/
|
|
11
|
+
export async function getFunctionDeclarations(directory) {
|
|
12
|
+
const file = resolveAppTypes(path.join(directory, 'package.json')) ??
|
|
13
|
+
resolveAppTypes(import.meta.url);
|
|
14
|
+
if (!file) {
|
|
15
|
+
throw new Error('@swell/app-types is not installed');
|
|
16
|
+
}
|
|
17
|
+
const { version } = JSON.parse(await fs.readFile(path.join(path.dirname(file), 'package.json'), 'utf8'));
|
|
18
|
+
return `// @swell/app-types ${version}\n${await fs.readFile(file, 'utf8')}`;
|
|
19
|
+
}
|
|
20
|
+
function resolveAppTypes(from) {
|
|
21
|
+
try {
|
|
22
|
+
return createRequire(from).resolve('@swell/app-types/index.d.ts');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|