@swell/cli 2.9.26 → 2.9.27
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.d.ts +1 -0
- package/dist/commands/app/dev.js +26 -2
- package/dist/commands/create/component.d.ts +19 -0
- package/dist/commands/create/component.js +84 -0
- package/dist/commands/create/index.js +5 -1
- package/dist/lib/apps/app-config.d.ts +13 -0
- package/dist/lib/apps/app-config.js +66 -29
- package/dist/lib/apps/components.d.ts +32 -0
- package/dist/lib/apps/components.js +81 -0
- package/dist/lib/apps/index.d.ts +1 -0
- package/dist/lib/apps/index.js +5 -3
- package/dist/lib/bundle.d.ts +21 -1
- package/dist/lib/bundle.js +88 -5
- package/dist/lib/create/component.d.ts +18 -0
- package/dist/lib/create/component.js +186 -0
- package/dist/lib/create/content.d.ts +1 -1
- package/dist/lib/create/setting.d.ts +1 -1
- package/dist/lib/function-source-analysis.d.ts +11 -0
- package/dist/lib/function-source-analysis.js +33 -0
- package/dist/lib/swell-function-wrapper.js +31 -0
- package/dist/push-app-command.js +11 -0
- package/oclif.manifest.json +66 -2
- package/package.json +3 -1
package/dist/commands/app/dev.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as http from 'node:http';
|
|
|
7
7
|
import * as os from 'node:os';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import ora from 'ora';
|
|
10
|
+
import { COMPONENT_DEV_PATH, listRootComponentFiles, serveComponentModule, } from '../../lib/apps/components.js';
|
|
10
11
|
import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
|
|
11
12
|
import { bundleFunction } from '../../lib/bundle.js';
|
|
12
13
|
import { FUNCTION_COMPATIBILITY_DATE, getLocalFrontendHost, } from '../../lib/constants.js';
|
|
@@ -194,6 +195,10 @@ export default class AppDev extends PushAppCommand {
|
|
|
194
195
|
res.writeHead(status);
|
|
195
196
|
res.end(message);
|
|
196
197
|
};
|
|
198
|
+
if (url.pathname.startsWith(COMPONENT_DEV_PATH)) {
|
|
199
|
+
serveComponentModule(this.appPath, url.pathname, res).catch((error) => fail(500, error.message));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
197
202
|
if (this.functionPorts.has(functionName)) {
|
|
198
203
|
const targetPort = this.functionPorts.get(functionName);
|
|
199
204
|
// Log when response is finished, method, status, and time to execute by proxy
|
|
@@ -443,15 +448,26 @@ ENVIRONMENT = "development"
|
|
|
443
448
|
async startAppFunctionServer(spinner, serverPort) {
|
|
444
449
|
// Get all functions in this app
|
|
445
450
|
const functions = await this.getAppFunctions();
|
|
451
|
+
const components = await listRootComponentFiles(this.appPath);
|
|
446
452
|
const projectType = await this.getFrontendProjectType(false);
|
|
447
453
|
const shouldLogPreviewUrl = this.app.type === 'storefront' && !projectType;
|
|
448
454
|
if (functions.length === 0) {
|
|
449
|
-
if (!projectType &&
|
|
455
|
+
if (!projectType &&
|
|
456
|
+
this.app.type !== 'storefront' &&
|
|
457
|
+
components.length === 0) {
|
|
450
458
|
spinner.stop();
|
|
451
|
-
this.log(`No functions or frontend detected. Exiting.\n`);
|
|
459
|
+
this.log(`No functions, components or frontend detected. Exiting.\n`);
|
|
452
460
|
await this.cleanupOnExit();
|
|
453
461
|
return;
|
|
454
462
|
}
|
|
463
|
+
// Components are pushed again when they change; the tmp directory lets
|
|
464
|
+
// a function added while dev runs start like it does in the full mode
|
|
465
|
+
if (components.length > 0) {
|
|
466
|
+
await this.createTmpDirectory();
|
|
467
|
+
this.watchForChanges({
|
|
468
|
+
onChange: this.onChangeFunctionWatcher.bind(this),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
455
471
|
// Create routing server to proxy requests to frontend even without functions
|
|
456
472
|
await this.createFunctionRouter(serverPort);
|
|
457
473
|
if (shouldLogPreviewUrl) {
|
|
@@ -461,6 +477,7 @@ ENVIRONMENT = "development"
|
|
|
461
477
|
else {
|
|
462
478
|
spinner.stop();
|
|
463
479
|
}
|
|
480
|
+
this.logComponentModules(components, serverPort);
|
|
464
481
|
return;
|
|
465
482
|
}
|
|
466
483
|
// Create TMP directory for wrangler configs and bundled functions
|
|
@@ -480,6 +497,13 @@ ENVIRONMENT = "development"
|
|
|
480
497
|
if (shouldLogPreviewUrl) {
|
|
481
498
|
this.logLocalPreviewUrl();
|
|
482
499
|
}
|
|
500
|
+
this.logComponentModules(components, serverPort);
|
|
501
|
+
}
|
|
502
|
+
logComponentModules(components, serverPort) {
|
|
503
|
+
if (components.length === 0) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
this.log(`${style.success('→')} Component modules at: http://localhost:${serverPort}${COMPONENT_DEV_PATH}<Name>.js\n`);
|
|
483
507
|
}
|
|
484
508
|
async startFunctionServers(functions) {
|
|
485
509
|
const functionStatus = new Map();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CreateConfigCommand } from '../../create-config-command.js';
|
|
2
|
+
import { HelpMeta } from '../../help/types.js';
|
|
3
|
+
import { ConfigType } from '../../lib/apps/index.js';
|
|
4
|
+
export default class CreateComponent extends CreateConfigCommand {
|
|
5
|
+
static args: {
|
|
6
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static description: string;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static helpMeta: HelpMeta;
|
|
11
|
+
static flags: {
|
|
12
|
+
description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
static summary: string;
|
|
17
|
+
createType: ConfigType;
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { CreateConfigCommand } from '../../create-config-command.js';
|
|
4
|
+
import { ConfigType } from '../../lib/apps/index.js';
|
|
5
|
+
import { getComponentTemplate, setupComponentSupport, toComponentName, } from '../../lib/create/component.js';
|
|
6
|
+
export default class CreateComponent extends CreateConfigCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
name: Args.string({
|
|
9
|
+
default: '',
|
|
10
|
+
description: 'Component name (e.g., ColorPicker)',
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
static description = 'Create an app component in the components folder.';
|
|
14
|
+
static examples = [
|
|
15
|
+
'$ swell create component',
|
|
16
|
+
'$ swell create component ColorPicker -d "Brand color picker" -y',
|
|
17
|
+
];
|
|
18
|
+
static helpMeta = {
|
|
19
|
+
usageDirect: '<name> [-d <description>] -y',
|
|
20
|
+
};
|
|
21
|
+
static flags = {
|
|
22
|
+
description: Flags.string({
|
|
23
|
+
char: 'd',
|
|
24
|
+
default: '',
|
|
25
|
+
description: 'Description',
|
|
26
|
+
}),
|
|
27
|
+
overwrite: Flags.boolean({
|
|
28
|
+
default: false,
|
|
29
|
+
description: 'Overwrite existing file',
|
|
30
|
+
}),
|
|
31
|
+
yes: Flags.boolean({
|
|
32
|
+
char: 'y',
|
|
33
|
+
description: 'Skip prompts, require all arguments',
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
static summary = 'Create an app component in the components folder.';
|
|
37
|
+
createType = ConfigType.COMPONENT;
|
|
38
|
+
async run() {
|
|
39
|
+
const { args, flags } = await this.parse(CreateComponent);
|
|
40
|
+
const { description: descriptionFlag, overwrite, yes } = flags;
|
|
41
|
+
let name = args.name;
|
|
42
|
+
let description = descriptionFlag;
|
|
43
|
+
if (yes) {
|
|
44
|
+
if (!name) {
|
|
45
|
+
this.error('Missing required argument for non-interactive mode: NAME\n\nExample: swell create component ColorPicker -y', { exit: 1 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (!name) {
|
|
50
|
+
name = await input({
|
|
51
|
+
default: 'MyComponent',
|
|
52
|
+
message: 'Component name',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!description) {
|
|
56
|
+
description = await input({
|
|
57
|
+
message: 'Describe what the component does',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const componentName = toComponentName(name);
|
|
62
|
+
if (!componentName) {
|
|
63
|
+
this.error(`Invalid component name: ${name}`, { exit: 1 });
|
|
64
|
+
}
|
|
65
|
+
const created = await this.createFile({
|
|
66
|
+
extension: 'tsx',
|
|
67
|
+
fileBody: getComponentTemplate(componentName, description),
|
|
68
|
+
fileName: componentName,
|
|
69
|
+
}, overwrite, !yes);
|
|
70
|
+
if (!created) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const { changed, dependenciesAdded, warnings } = await setupComponentSupport(this.appPath);
|
|
74
|
+
if (changed.length > 0) {
|
|
75
|
+
const install = dependenciesAdded
|
|
76
|
+
? ' Run `npm install` to install preact and @swell/apps-sdk.'
|
|
77
|
+
: '';
|
|
78
|
+
this.log(`Updated ${changed.join(', ')}.${install}`);
|
|
79
|
+
}
|
|
80
|
+
for (const warning of warnings) {
|
|
81
|
+
this.warn(warning);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
export default class Create extends Command {
|
|
4
|
-
static description = 'Scaffold apps, models, functions, and notifications.';
|
|
4
|
+
static description = 'Scaffold apps, models, functions, components, and notifications.';
|
|
5
5
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
6
6
|
async run() {
|
|
7
7
|
const createTarget = await select({
|
|
@@ -18,6 +18,10 @@ export default class Create extends Command {
|
|
|
18
18
|
name: 'Content',
|
|
19
19
|
value: 'content',
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Component',
|
|
23
|
+
value: 'component',
|
|
24
|
+
},
|
|
21
25
|
{
|
|
22
26
|
name: 'Function',
|
|
23
27
|
value: 'function',
|
|
@@ -50,6 +50,12 @@ export declare abstract class AppConfig {
|
|
|
50
50
|
* @returns {Promise<string | undefined>} the hash
|
|
51
51
|
*/
|
|
52
52
|
protected contentHash(): Promise<string | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* File hash combined with the app files this file imports, for configs that
|
|
55
|
+
* deploy as a bundle: a change in shared code redeploys them
|
|
56
|
+
* @returns {Promise<string | undefined>} the hash, or undefined until the file compiles
|
|
57
|
+
*/
|
|
58
|
+
protected bundleHash(): Promise<string | undefined>;
|
|
53
59
|
/**
|
|
54
60
|
* Full path of the app directory this config belongs to
|
|
55
61
|
* @returns {string} the path
|
|
@@ -60,6 +66,11 @@ export declare abstract class AppConfig {
|
|
|
60
66
|
postData(): Promise<any>;
|
|
61
67
|
prepareFileData(): any;
|
|
62
68
|
abstract hasValues: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether .json files are parsed into values; otherwise they are pushed as raw files
|
|
71
|
+
* @returns {boolean} true to parse
|
|
72
|
+
*/
|
|
73
|
+
protected parsesJsonValues(): boolean;
|
|
63
74
|
/** build the JSON used when making requests (usually push) to the API */
|
|
64
75
|
protected abstract preparePostData(postData: any): Promise<any>;
|
|
65
76
|
abstract type: ConfigType;
|
|
@@ -104,7 +115,9 @@ export declare class AppConfigFunction extends AppConfig {
|
|
|
104
115
|
export declare class AppConfigComponent extends AppConfig {
|
|
105
116
|
hasValues: boolean;
|
|
106
117
|
type: ConfigType;
|
|
118
|
+
protected parsesJsonValues(): boolean;
|
|
107
119
|
isRootComponent(): boolean;
|
|
120
|
+
protected contentHash(): Promise<string | undefined>;
|
|
108
121
|
preparePostData(postData: any): Promise<any>;
|
|
109
122
|
}
|
|
110
123
|
export declare class AppConfigAsset extends AppConfigDefault {
|
|
@@ -3,8 +3,8 @@ import isEmpty from 'lodash/isEmpty.js';
|
|
|
3
3
|
import { detectFilenameMime } from 'mime-detect';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
-
import { bundleFunction, bundleComponent, bundleWorkflow, getLocalInputs, } from '../bundle.js';
|
|
7
|
-
import { analyzeFunctionSource, hasKindDiagnostic, workflowStaticConfigError, } from '../function-source-analysis.js';
|
|
6
|
+
import { bundleFunction, bundleComponent, bundleComponentModule, bundleWorkflow, getLocalInputs, } from '../bundle.js';
|
|
7
|
+
import { analyzeFunctionSource, hasKindDiagnostic, readStaticConfig, workflowStaticConfigError, } from '../function-source-analysis.js';
|
|
8
8
|
import { AllConfigPaths, ConfigType, filePathExistsAsync, hashFile, hashString, } from './index.js';
|
|
9
9
|
export class IgnoringFileError extends Error {
|
|
10
10
|
constructor(message) {
|
|
@@ -95,6 +95,30 @@ export class AppConfig {
|
|
|
95
95
|
async contentHash() {
|
|
96
96
|
return this.fileHash;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* File hash combined with the app files this file imports, for configs that
|
|
100
|
+
* deploy as a bundle: a change in shared code redeploys them
|
|
101
|
+
* @returns {Promise<string | undefined>} the hash, or undefined until the file compiles
|
|
102
|
+
*/
|
|
103
|
+
async bundleHash() {
|
|
104
|
+
if (!this.fileHash) {
|
|
105
|
+
return this.fileHash;
|
|
106
|
+
}
|
|
107
|
+
const appRoot = this.appRoot();
|
|
108
|
+
let inputs;
|
|
109
|
+
try {
|
|
110
|
+
inputs = await getLocalInputs(this.appPath, appRoot);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Unknown until it compiles: push tries it and reports the error
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const imports = inputs
|
|
117
|
+
.filter((input) => input !== this.filePath)
|
|
118
|
+
.sort()
|
|
119
|
+
.map((input) => hashFile(path.join(appRoot, input), undefined, input));
|
|
120
|
+
return hashString(this.fileHash, ...imports);
|
|
121
|
+
}
|
|
98
122
|
/**
|
|
99
123
|
* Full path of the app directory this config belongs to
|
|
100
124
|
* @returns {string} the path
|
|
@@ -166,7 +190,7 @@ export class AppConfig {
|
|
|
166
190
|
if (this.fileData === undefined) {
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
169
|
-
if (this.
|
|
193
|
+
if (this.parsesJsonValues() && this.filePath.endsWith('.json')) {
|
|
170
194
|
try {
|
|
171
195
|
this.values = JSON.parse(this.fileData.toString('utf8'));
|
|
172
196
|
}
|
|
@@ -205,6 +229,13 @@ export class AppConfig {
|
|
|
205
229
|
? { $base64: this.fileData.toString('base64') }
|
|
206
230
|
: this.fileData?.toString('utf8');
|
|
207
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Whether .json files are parsed into values; otherwise they are pushed as raw files
|
|
234
|
+
* @returns {boolean} true to parse
|
|
235
|
+
*/
|
|
236
|
+
parsesJsonValues() {
|
|
237
|
+
return this.hasValues;
|
|
238
|
+
}
|
|
208
239
|
}
|
|
209
240
|
export class AppConfigDefault extends AppConfig {
|
|
210
241
|
hasValues = false;
|
|
@@ -280,23 +311,7 @@ export class AppConfigFunction extends AppConfig {
|
|
|
280
311
|
// A function deploys as a bundle, so the hash includes the local files it
|
|
281
312
|
// imports: a change in shared code under functions/lib redeploys it
|
|
282
313
|
async contentHash() {
|
|
283
|
-
|
|
284
|
-
return this.fileHash;
|
|
285
|
-
}
|
|
286
|
-
const appRoot = this.appRoot();
|
|
287
|
-
let inputs;
|
|
288
|
-
try {
|
|
289
|
-
inputs = await getLocalInputs(this.appPath, appRoot);
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
// Unknown until it compiles: push tries it and reports the error
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
const imports = inputs
|
|
296
|
-
.filter((input) => input !== this.filePath)
|
|
297
|
-
.sort()
|
|
298
|
-
.map((input) => hashFile(path.join(appRoot, input), undefined, input));
|
|
299
|
-
return hashString(this.fileHash, ...imports);
|
|
314
|
+
return this.isRootFunction() ? this.bundleHash() : this.fileHash;
|
|
300
315
|
}
|
|
301
316
|
async preparePostData(postData) {
|
|
302
317
|
if (this.isRootFunction()) {
|
|
@@ -352,10 +367,19 @@ export class AppConfigFunction extends AppConfig {
|
|
|
352
367
|
export class AppConfigComponent extends AppConfig {
|
|
353
368
|
hasValues = true;
|
|
354
369
|
type = ConfigType.COMPONENT;
|
|
370
|
+
// Only .tsx/.jsx files are components, any .json under components/ is shared source
|
|
371
|
+
parsesJsonValues() {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
355
374
|
isRootComponent() {
|
|
356
375
|
return (this.isRootConfig('components') &&
|
|
357
376
|
(this.filePath.endsWith('.jsx') || this.filePath.endsWith('.tsx')));
|
|
358
377
|
}
|
|
378
|
+
// A component deploys as a bundle, so the hash includes the local files it
|
|
379
|
+
// imports: a change in shared code under components/lib redeploys it
|
|
380
|
+
async contentHash() {
|
|
381
|
+
return this.isRootComponent() ? this.bundleHash() : this.fileHash;
|
|
382
|
+
}
|
|
359
383
|
async preparePostData(postData) {
|
|
360
384
|
if (!this.isRootComponent()) {
|
|
361
385
|
return postData;
|
|
@@ -366,19 +390,32 @@ export class AppConfigComponent extends AppConfig {
|
|
|
366
390
|
if (!fileData) {
|
|
367
391
|
return;
|
|
368
392
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
const source = this.fileData?.toString('utf8') ?? '';
|
|
394
|
+
const { config, errors } = readStaticConfig(source, this.filePath);
|
|
395
|
+
if (errors.length > 0) {
|
|
396
|
+
throw new Error(`config must be static metadata: ${errors.join('; ')}`);
|
|
397
|
+
}
|
|
398
|
+
// Checkout payment components still load through swell-js, which evaluates the legacy IIFE
|
|
399
|
+
if (config?.extension) {
|
|
400
|
+
postData.build_file = {
|
|
401
|
+
content_type: 'application/javascript',
|
|
402
|
+
data: await bundleComponent(this.appPath),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const { code } = await bundleComponentModule(this.filePath, this.appRoot());
|
|
407
|
+
// The CDN keeps a file's type only through an extension it maps, and
|
|
408
|
+
// it maps application/javascript but not text/javascript. Frames
|
|
409
|
+
// import the bundle as a module, which browsers refuse as text/plain
|
|
410
|
+
postData.build_file = {
|
|
411
|
+
content_type: 'application/javascript',
|
|
412
|
+
data: code,
|
|
413
|
+
};
|
|
372
414
|
}
|
|
373
|
-
// Save the original file and the bundled version
|
|
374
415
|
postData.file = {
|
|
375
416
|
data: fileData,
|
|
376
417
|
};
|
|
377
|
-
postData.
|
|
378
|
-
content_type: 'application/javascript',
|
|
379
|
-
data: code,
|
|
380
|
-
};
|
|
381
|
-
postData.values = config;
|
|
418
|
+
postData.values = config ?? {};
|
|
382
419
|
}
|
|
383
420
|
catch (error) {
|
|
384
421
|
throw new FunctionProcessingError(`Unable to compile component ${this.name}`, error);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type * as http from 'node:http';
|
|
3
|
+
/** Path of component modules on the `swell app dev` proxy */
|
|
4
|
+
export declare const COMPONENT_DEV_PATH = "/.swell/components/";
|
|
5
|
+
/**
|
|
6
|
+
* Whether a file is a component entry: a .tsx or .jsx file directly in components/
|
|
7
|
+
* @param {string} file path relative to the app
|
|
8
|
+
* @returns {boolean} true for a root component
|
|
9
|
+
*/
|
|
10
|
+
export declare function isRootComponentFile(file: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Component entries of an app
|
|
13
|
+
* @param {string} appPath app directory
|
|
14
|
+
* @returns {Promise<string[]>} paths relative to the app, sorted
|
|
15
|
+
*/
|
|
16
|
+
export declare function listRootComponentFiles(appPath: string): Promise<string[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Components to rebuild after a change: root components bundle their shared
|
|
19
|
+
* code, so a change under components/ outside the root rebuilds all of them
|
|
20
|
+
* @param {string} appPath app directory
|
|
21
|
+
* @param {string} changedFile changed file, relative to the app
|
|
22
|
+
* @returns {Promise<string[]>} root components to push again
|
|
23
|
+
*/
|
|
24
|
+
export declare function componentsToRebuild(appPath: string, changedFile: string): Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Builds and serves `/.swell/components/<Name>.js` for `swell app dev`
|
|
27
|
+
* @param {string} appPath app directory
|
|
28
|
+
* @param {string} pathname request path
|
|
29
|
+
* @param {http.ServerResponse} res response to write
|
|
30
|
+
* @returns {Promise<void>} resolves when the response is written
|
|
31
|
+
*/
|
|
32
|
+
export declare function serveComponentModule(appPath: string, pathname: string, res: http.ServerResponse): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { bundleComponentModule } from '../bundle.js';
|
|
4
|
+
import { getNameFromFilePath } from './index.js';
|
|
5
|
+
/** Path of component modules on the `swell app dev` proxy */
|
|
6
|
+
export const COMPONENT_DEV_PATH = '/.swell/components/';
|
|
7
|
+
const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx']);
|
|
8
|
+
/**
|
|
9
|
+
* Whether a file is a component entry: a .tsx or .jsx file directly in components/
|
|
10
|
+
* @param {string} file path relative to the app
|
|
11
|
+
* @returns {boolean} true for a root component
|
|
12
|
+
*/
|
|
13
|
+
export function isRootComponentFile(file) {
|
|
14
|
+
const parts = file.split('/');
|
|
15
|
+
return (parts.length === 2 &&
|
|
16
|
+
parts[0] === 'components' &&
|
|
17
|
+
COMPONENT_EXTENSIONS.has(path.extname(parts[1])));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Component entries of an app
|
|
21
|
+
* @param {string} appPath app directory
|
|
22
|
+
* @returns {Promise<string[]>} paths relative to the app, sorted
|
|
23
|
+
*/
|
|
24
|
+
export async function listRootComponentFiles(appPath) {
|
|
25
|
+
let names;
|
|
26
|
+
try {
|
|
27
|
+
names = await fs.readdir(path.join(appPath, 'components'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return names
|
|
33
|
+
.map((name) => `components/${name}`)
|
|
34
|
+
.filter((file) => isRootComponentFile(file))
|
|
35
|
+
.sort();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Components to rebuild after a change: root components bundle their shared
|
|
39
|
+
* code, so a change under components/ outside the root rebuilds all of them
|
|
40
|
+
* @param {string} appPath app directory
|
|
41
|
+
* @param {string} changedFile changed file, relative to the app
|
|
42
|
+
* @returns {Promise<string[]>} root components to push again
|
|
43
|
+
*/
|
|
44
|
+
export async function componentsToRebuild(appPath, changedFile) {
|
|
45
|
+
if (!changedFile.startsWith('components/') ||
|
|
46
|
+
isRootComponentFile(changedFile)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return listRootComponentFiles(appPath);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds and serves `/.swell/components/<Name>.js` for `swell app dev`
|
|
53
|
+
* @param {string} appPath app directory
|
|
54
|
+
* @param {string} pathname request path
|
|
55
|
+
* @param {http.ServerResponse} res response to write
|
|
56
|
+
* @returns {Promise<void>} resolves when the response is written
|
|
57
|
+
*/
|
|
58
|
+
export async function serveComponentModule(appPath, pathname, res) {
|
|
59
|
+
const headers = {
|
|
60
|
+
'Access-Control-Allow-Origin': '*',
|
|
61
|
+
'Cache-Control': 'no-store',
|
|
62
|
+
};
|
|
63
|
+
const name = /^\/\.swell\/components\/([\w-]+)\.js$/.exec(pathname)?.[1];
|
|
64
|
+
const files = name ? await listRootComponentFiles(appPath) : [];
|
|
65
|
+
const file = files.find((entry) => getNameFromFilePath(entry) === name);
|
|
66
|
+
if (!file) {
|
|
67
|
+
res.writeHead(404, headers).end('Component not found');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const { code } = await bundleComponentModule(file, appPath);
|
|
72
|
+
res
|
|
73
|
+
.writeHead(200, { ...headers, 'Content-Type': 'text/javascript' })
|
|
74
|
+
.end(code);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
res
|
|
78
|
+
.writeHead(500, { ...headers, 'Content-Type': 'text/plain' })
|
|
79
|
+
.end(error.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -205,6 +205,7 @@ export declare function writeJsonFile(file: string, data?: any): Promise<void>;
|
|
|
205
205
|
export declare function hashFile(filePath: string, fileContents?: Buffer, relativePath?: string): string;
|
|
206
206
|
export declare function hashString(...contents: (Buffer | string)[]): string;
|
|
207
207
|
export declare function appAssetImage(appPath: string, fileName: string): Promise<string | undefined>;
|
|
208
|
+
export declare function getNameFromFilePath(filePath: string): string;
|
|
208
209
|
export declare function appConfigFromFile(filePath: string, configType: ConfigType, appPath: string): Promise<AppConfig>;
|
|
209
210
|
export declare function findAppConfig(app: App, filePath: string, configType: ConfigType): AppConfig | undefined;
|
|
210
211
|
interface BatchAppFiles {
|
package/dist/lib/apps/index.js
CHANGED
|
@@ -52,9 +52,11 @@ export var ConfigType;
|
|
|
52
52
|
ConfigType["THEME"] = "theme";
|
|
53
53
|
ConfigType["WEBHOOK"] = "webhook";
|
|
54
54
|
})(ConfigType || (ConfigType = {}));
|
|
55
|
-
// Models
|
|
55
|
+
// Models first; then components, which content and settings fields reference
|
|
56
|
+
// by name; then content
|
|
56
57
|
const ConfigTypeBatchOrder = [
|
|
57
58
|
ConfigType.MODEL,
|
|
59
|
+
ConfigType.COMPONENT,
|
|
58
60
|
ConfigType.CONTENT,
|
|
59
61
|
ConfigType.ASSET,
|
|
60
62
|
ConfigType.FUNCTION,
|
|
@@ -63,7 +65,6 @@ const ConfigTypeBatchOrder = [
|
|
|
63
65
|
ConfigType.WEBHOOK,
|
|
64
66
|
ConfigType.THEME,
|
|
65
67
|
ConfigType.FRONTEND,
|
|
66
|
-
ConfigType.COMPONENT,
|
|
67
68
|
ConfigType.FILE,
|
|
68
69
|
];
|
|
69
70
|
// All available configs
|
|
@@ -89,6 +90,7 @@ const StandardConfigTypes = [
|
|
|
89
90
|
'NOTIFICATION',
|
|
90
91
|
'SETTING',
|
|
91
92
|
'WEBHOOK',
|
|
93
|
+
'COMPONENT',
|
|
92
94
|
];
|
|
93
95
|
// Configs available for storefront app types
|
|
94
96
|
export const StorefrontConfigTypes = [
|
|
@@ -359,7 +361,7 @@ export async function appAssetImage(appPath, fileName) {
|
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
|
-
function getNameFromFilePath(filePath) {
|
|
364
|
+
export function getNameFromFilePath(filePath) {
|
|
363
365
|
return path.parse(filePath).name.replaceAll('_', '-');
|
|
364
366
|
}
|
|
365
367
|
export async function appConfigFromFile(filePath, configType, appPath) {
|
package/dist/lib/bundle.d.ts
CHANGED
|
@@ -9,4 +9,24 @@ import type { FunctionSourceAnalysis } from './function-source-analysis.js';
|
|
|
9
9
|
export declare function getLocalInputs(entryPoint: string, rootDir: string): Promise<string[]>;
|
|
10
10
|
export declare function bundleFunction(filePath: string): Promise<any>;
|
|
11
11
|
export declare function bundleWorkflow(filePath: string, analysis: FunctionSourceAnalysis): Promise<any>;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Legacy build for checkout payment components, which swell-js evaluates as an
|
|
14
|
+
* IIFE exposing `Component.default`, `Component.config` and `Component.preact`.
|
|
15
|
+
* @param {string} filePath absolute path of the component file
|
|
16
|
+
* @returns {Promise<string>} the bundle code
|
|
17
|
+
*/
|
|
18
|
+
export declare function bundleComponent(filePath: string): Promise<string>;
|
|
19
|
+
export interface ComponentModuleBuild {
|
|
20
|
+
/** ES module exporting mount, update and unmount */
|
|
21
|
+
code: string;
|
|
22
|
+
/** App files the component imports, relative to the app root */
|
|
23
|
+
inputs: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds a component into the module the component frame loads: generated
|
|
27
|
+
* mount, update and unmount functions render its default export with Preact.
|
|
28
|
+
* @param {string} filePath component file, relative to appRoot
|
|
29
|
+
* @param {string} appRoot app directory
|
|
30
|
+
* @returns {Promise<ComponentModuleBuild>} the module code and the app files it includes
|
|
31
|
+
*/
|
|
32
|
+
export declare function bundleComponentModule(filePath: string, appRoot: string): Promise<ComponentModuleBuild>;
|
package/dist/lib/bundle.js
CHANGED
|
@@ -84,6 +84,12 @@ export async function bundleWorkflow(filePath, analysis) {
|
|
|
84
84
|
throw new Error(`Unable to compile workflow "${filePath}": ${error.message}`, { cause: error });
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Legacy build for checkout payment components, which swell-js evaluates as an
|
|
89
|
+
* IIFE exposing `Component.default`, `Component.config` and `Component.preact`.
|
|
90
|
+
* @param {string} filePath absolute path of the component file
|
|
91
|
+
* @returns {Promise<string>} the bundle code
|
|
92
|
+
*/
|
|
87
93
|
export async function bundleComponent(filePath) {
|
|
88
94
|
try {
|
|
89
95
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -113,11 +119,7 @@ export async function bundleComponent(filePath) {
|
|
|
113
119
|
write: false,
|
|
114
120
|
logLevel: 'silent',
|
|
115
121
|
});
|
|
116
|
-
|
|
117
|
-
// eslint-disable-next-line no-new-func
|
|
118
|
-
const evalFn = new Function(`${code}\nreturn { ...Component }`);
|
|
119
|
-
const { config } = evalFn();
|
|
120
|
-
return { code, config };
|
|
122
|
+
return buildResult.outputFiles[0].text;
|
|
121
123
|
}
|
|
122
124
|
catch (error) {
|
|
123
125
|
throw new Error(`Unable to compile component "${filePath}": ${error.message}`, { cause: error });
|
|
@@ -435,3 +437,84 @@ function replaceModuleExports(code) {
|
|
|
435
437
|
.replaceAll(/^module\.exports\b/gm, 'var moduleExports')
|
|
436
438
|
.replaceAll(/\bmodule\.exports\b/gm, 'moduleExports');
|
|
437
439
|
}
|
|
440
|
+
const COMPONENT_ENTRY = 'swell-component-entry.js';
|
|
441
|
+
// Components render with Preact; React imports in third-party libraries resolve to its compat layer
|
|
442
|
+
const COMPONENT_ALIASES = {
|
|
443
|
+
react: 'preact/compat',
|
|
444
|
+
'react-dom': 'preact/compat',
|
|
445
|
+
};
|
|
446
|
+
// The nearest tsconfig.json overrides esbuild's jsx options per file, and
|
|
447
|
+
// apps may still say "jsx": "react". A pragma wins over both, so the app's
|
|
448
|
+
// own .jsx/.tsx files always compile with Preact's automatic runtime, while
|
|
449
|
+
// the rest of its tsconfig (paths, baseUrl) still applies.
|
|
450
|
+
const preactJsxPlugin = {
|
|
451
|
+
name: 'swell-preact-jsx',
|
|
452
|
+
setup(build) {
|
|
453
|
+
build.onLoad({ filter: /\.[jt]sx$/ }, async (args) => {
|
|
454
|
+
if (args.path.split(path.sep).includes('node_modules')) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const source = await fs.readFile(args.path, 'utf8');
|
|
458
|
+
return {
|
|
459
|
+
contents: `/** @jsxRuntime automatic @jsxImportSource preact */ ${source}`,
|
|
460
|
+
loader: args.path.endsWith('.tsx') ? 'tsx' : 'jsx',
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Builds a component into the module the component frame loads: generated
|
|
467
|
+
* mount, update and unmount functions render its default export with Preact.
|
|
468
|
+
* @param {string} filePath component file, relative to appRoot
|
|
469
|
+
* @param {string} appRoot app directory
|
|
470
|
+
* @returns {Promise<ComponentModuleBuild>} the module code and the app files it includes
|
|
471
|
+
*/
|
|
472
|
+
export async function bundleComponentModule(filePath, appRoot) {
|
|
473
|
+
const entry = `./${filePath.split(path.sep).join('/')}`;
|
|
474
|
+
try {
|
|
475
|
+
const { metafile, outputFiles } = await esbuild.build({
|
|
476
|
+
absWorkingDir: appRoot,
|
|
477
|
+
alias: COMPONENT_ALIASES,
|
|
478
|
+
bundle: true,
|
|
479
|
+
format: 'esm',
|
|
480
|
+
jsx: 'automatic',
|
|
481
|
+
jsxImportSource: 'preact',
|
|
482
|
+
logLevel: 'silent',
|
|
483
|
+
metafile: true,
|
|
484
|
+
minify: true,
|
|
485
|
+
platform: 'browser',
|
|
486
|
+
plugins: [preactJsxPlugin],
|
|
487
|
+
stdin: {
|
|
488
|
+
contents: [
|
|
489
|
+
"import { h, render } from 'preact';",
|
|
490
|
+
`import Component from ${JSON.stringify(entry)};`,
|
|
491
|
+
'export function mount(root, props) { render(h(Component, props), root); }',
|
|
492
|
+
'export function update(root, props) { render(h(Component, props), root); }',
|
|
493
|
+
'export function unmount(root) { render(null, root); }',
|
|
494
|
+
].join('\n'),
|
|
495
|
+
loader: 'js',
|
|
496
|
+
resolveDir: appRoot,
|
|
497
|
+
sourcefile: COMPONENT_ENTRY,
|
|
498
|
+
},
|
|
499
|
+
target: 'es2020',
|
|
500
|
+
write: false,
|
|
501
|
+
});
|
|
502
|
+
const inputs = Object.keys(metafile.inputs).filter((input) => !input.includes('node_modules/') &&
|
|
503
|
+
!input.startsWith('<') &&
|
|
504
|
+
input !== COMPONENT_ENTRY);
|
|
505
|
+
return { code: outputFiles[0].text, inputs };
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
const nonJs = /Cannot import "([^"]+)" into a JavaScript file|No loader is configured for "[^"]+" files: (\S+)/.exec(error.message);
|
|
509
|
+
let reason = error.message;
|
|
510
|
+
if (/Could not resolve "preact(?:\/[^"]*)?"/.test(error.message)) {
|
|
511
|
+
reason = 'install preact in the app (npm install preact)';
|
|
512
|
+
}
|
|
513
|
+
else if (nonJs) {
|
|
514
|
+
reason = `components cannot import CSS or other non-JavaScript files yet (${nonJs[1] ?? nonJs[2]}); use inline styles or a <style> element`;
|
|
515
|
+
}
|
|
516
|
+
throw new Error(`Unable to compile component "${filePath}": ${reason}`, {
|
|
517
|
+
cause: error,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const PREACT_VERSION = "^10.28.0";
|
|
2
|
+
export declare const APPS_SDK_VERSION = "^2.0.0-alpha.6";
|
|
3
|
+
export declare const toComponentName: (value: string) => string;
|
|
4
|
+
export declare function getComponentTemplate(name: string, description: string): string;
|
|
5
|
+
export interface ComponentSetupResult {
|
|
6
|
+
changed: string[];
|
|
7
|
+
dependenciesAdded: boolean;
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Adds preact and @swell/apps-sdk to package.json, and gives components their
|
|
12
|
+
* own tsconfig, excluded from the root one and checked by the typecheck script.
|
|
13
|
+
* Never overwrites values the app already has, except a 2.x SDK from before
|
|
14
|
+
* components.
|
|
15
|
+
* @param {string} appPath app directory
|
|
16
|
+
* @returns {Promise<ComponentSetupResult>} files changed and warnings to show
|
|
17
|
+
*/
|
|
18
|
+
export declare function setupComponentSupport(appPath: string): Promise<ComponentSetupResult>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as semver from 'semver';
|
|
4
|
+
export const PREACT_VERSION = '^10.28.0';
|
|
5
|
+
export const APPS_SDK_VERSION = '^2.0.0-alpha.6';
|
|
6
|
+
// The first release with the ./components entry
|
|
7
|
+
const COMPONENTS_SDK = semver.minVersion(APPS_SDK_VERSION);
|
|
8
|
+
// Example: 'color picker' => 'ColorPicker'
|
|
9
|
+
export const toComponentName = (value) => {
|
|
10
|
+
const name = value
|
|
11
|
+
.split('.')[0]
|
|
12
|
+
.split(/[^\dA-Za-z]+/)
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
15
|
+
.join('');
|
|
16
|
+
// A component name is also its function name, so it must start with a letter
|
|
17
|
+
return /^[A-Za-z]/.test(name) ? name : '';
|
|
18
|
+
};
|
|
19
|
+
export function getComponentTemplate(name, description) {
|
|
20
|
+
return `import type { ComponentProps } from '@swell/apps-sdk/components';
|
|
21
|
+
|
|
22
|
+
export const config = {
|
|
23
|
+
description: ${JSON.stringify(description)},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function ${name}({ value, setValue, readonly }: ComponentProps<string>) {
|
|
27
|
+
return (
|
|
28
|
+
<input
|
|
29
|
+
value={value ?? ''}
|
|
30
|
+
disabled={readonly}
|
|
31
|
+
onInput={(event) => setValue(event.currentTarget.value)}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
// undefined when the file is missing, null when it is not valid JSON
|
|
38
|
+
async function readJson(file) {
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = await fs.readFile(file, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const writeJson = (file, value) => fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
54
|
+
// Appended to the app's typecheck script, as the scaffold does for test/ and frontend/
|
|
55
|
+
const TYPECHECK_COMPONENTS = ' && ([ ! -f components/tsconfig.json ] || tsc --noEmit -p components)';
|
|
56
|
+
/**
|
|
57
|
+
* Components render in the browser with Preact, while the root tsconfig
|
|
58
|
+
* checks functions with the worker libs, which conflict with the DOM ones.
|
|
59
|
+
* Components get their own config instead.
|
|
60
|
+
* @param {boolean} extendsRoot whether the app has a root tsconfig to extend
|
|
61
|
+
* @returns {object} the contents of components/tsconfig.json
|
|
62
|
+
*/
|
|
63
|
+
function getComponentsTsconfig(extendsRoot) {
|
|
64
|
+
const compilerOptions = {
|
|
65
|
+
jsx: 'react-jsx',
|
|
66
|
+
jsxImportSource: 'preact',
|
|
67
|
+
lib: ['esnext', 'dom', 'dom.iterable'],
|
|
68
|
+
types: [],
|
|
69
|
+
// Relative to this file when no baseUrl is set
|
|
70
|
+
paths: {
|
|
71
|
+
react: ['../node_modules/preact/compat'],
|
|
72
|
+
'react-dom': ['../node_modules/preact/compat'],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
if (extendsRoot) {
|
|
76
|
+
return {
|
|
77
|
+
extends: '../tsconfig.json',
|
|
78
|
+
compilerOptions,
|
|
79
|
+
include: ['**/*'],
|
|
80
|
+
// The root config excludes components, which would exclude every file here
|
|
81
|
+
exclude: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
compilerOptions: {
|
|
86
|
+
...compilerOptions,
|
|
87
|
+
module: 'esnext',
|
|
88
|
+
moduleResolution: 'bundler',
|
|
89
|
+
target: 'esnext',
|
|
90
|
+
noEmit: true,
|
|
91
|
+
},
|
|
92
|
+
include: ['**/*'],
|
|
93
|
+
exclude: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Adds preact and @swell/apps-sdk to package.json, and gives components their
|
|
98
|
+
* own tsconfig, excluded from the root one and checked by the typecheck script.
|
|
99
|
+
* Never overwrites values the app already has, except a 2.x SDK from before
|
|
100
|
+
* components.
|
|
101
|
+
* @param {string} appPath app directory
|
|
102
|
+
* @returns {Promise<ComponentSetupResult>} files changed and warnings to show
|
|
103
|
+
*/
|
|
104
|
+
export async function setupComponentSupport(appPath) {
|
|
105
|
+
const result = {
|
|
106
|
+
changed: [],
|
|
107
|
+
dependenciesAdded: false,
|
|
108
|
+
warnings: [],
|
|
109
|
+
};
|
|
110
|
+
const packagePath = path.join(appPath, 'package.json');
|
|
111
|
+
const pkg = await readJson(packagePath);
|
|
112
|
+
if (pkg) {
|
|
113
|
+
if (!pkg.dependencies?.preact) {
|
|
114
|
+
pkg.dependencies = { ...pkg.dependencies, preact: PREACT_VERSION };
|
|
115
|
+
result.dependenciesAdded = true;
|
|
116
|
+
}
|
|
117
|
+
const sdkSection = ['dependencies', 'devDependencies'].find((section) => pkg[section]?.['@swell/apps-sdk']);
|
|
118
|
+
if (sdkSection) {
|
|
119
|
+
const range = pkg[sdkSection]['@swell/apps-sdk'];
|
|
120
|
+
const min = semver.validRange(range) ? semver.minVersion(range) : null;
|
|
121
|
+
if (min && semver.lt(min, COMPONENTS_SDK)) {
|
|
122
|
+
// `swell create tests` pins an earlier 2.x; 1.x is another SDK the app may use
|
|
123
|
+
if (semver.major(min) === 2) {
|
|
124
|
+
pkg[sdkSection]['@swell/apps-sdk'] = APPS_SDK_VERSION;
|
|
125
|
+
result.dependenciesAdded = true;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
result.warnings.push(`@swell/apps-sdk ${range} has no components: install @swell/apps-sdk@${APPS_SDK_VERSION} to type them.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
pkg.devDependencies = {
|
|
134
|
+
...pkg.devDependencies,
|
|
135
|
+
'@swell/apps-sdk': APPS_SDK_VERSION,
|
|
136
|
+
};
|
|
137
|
+
result.dependenciesAdded = true;
|
|
138
|
+
}
|
|
139
|
+
const typecheck = pkg.scripts?.typecheck;
|
|
140
|
+
const typecheckUpdated = typeof typecheck === 'string' && !typecheck.includes('components');
|
|
141
|
+
if (typecheckUpdated) {
|
|
142
|
+
pkg.scripts.typecheck = typecheck + TYPECHECK_COMPONENTS;
|
|
143
|
+
}
|
|
144
|
+
if (result.dependenciesAdded || typecheckUpdated) {
|
|
145
|
+
await writeJson(packagePath, pkg);
|
|
146
|
+
result.changed.push('package.json');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
result.warnings.push(pkg === undefined
|
|
151
|
+
? 'package.json not found: run `npm init`, then `npm install preact` and `npm install -D @swell/apps-sdk`.'
|
|
152
|
+
: 'package.json could not be parsed: add preact to dependencies and @swell/apps-sdk to devDependencies.');
|
|
153
|
+
}
|
|
154
|
+
const tsconfigPath = path.join(appPath, 'tsconfig.json');
|
|
155
|
+
const tsconfig = await readJson(tsconfigPath);
|
|
156
|
+
if (tsconfig) {
|
|
157
|
+
const { exclude } = tsconfig;
|
|
158
|
+
if (!Array.isArray(exclude) || !exclude.includes('components')) {
|
|
159
|
+
// An explicit exclude replaces TypeScript's default one, so keep node_modules
|
|
160
|
+
tsconfig.exclude = [
|
|
161
|
+
...(Array.isArray(exclude) ? exclude : ['node_modules']),
|
|
162
|
+
'components',
|
|
163
|
+
];
|
|
164
|
+
await writeJson(tsconfigPath, tsconfig);
|
|
165
|
+
result.changed.push('tsconfig.json');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const componentsTsconfigPath = path.join(appPath, 'components', 'tsconfig.json');
|
|
169
|
+
await fs.mkdir(path.dirname(componentsTsconfigPath), { recursive: true });
|
|
170
|
+
const created = await fs
|
|
171
|
+
.writeFile(componentsTsconfigPath, `${JSON.stringify(getComponentsTsconfig(Boolean(tsconfig)), null, 2)}\n`, { flag: 'wx' })
|
|
172
|
+
.then(() => true, (error) => {
|
|
173
|
+
if (error.code !== 'EEXIST')
|
|
174
|
+
throw error;
|
|
175
|
+
return false;
|
|
176
|
+
});
|
|
177
|
+
if (created) {
|
|
178
|
+
result.changed.push('components/tsconfig.json');
|
|
179
|
+
}
|
|
180
|
+
if (tsconfig === null) {
|
|
181
|
+
result.warnings.push(created
|
|
182
|
+
? 'tsconfig.json could not be parsed: add "components" to its "exclude". components/tsconfig.json was created without "extends".'
|
|
183
|
+
: 'tsconfig.json could not be parsed: add "components" to its "exclude".');
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
@@ -13,7 +13,7 @@ interface ContentViewProps {
|
|
|
13
13
|
tabs: [];
|
|
14
14
|
}
|
|
15
15
|
type ContentView = 'edit' | 'list' | 'new';
|
|
16
|
-
type ContentType = 'asset' | 'basic_html' | 'boolean' | 'category_lookup' | 'checkbox' | 'checkboxes' | 'collection' | 'color' | 'color_scheme' | 'color_scheme_group' | 'currency' | 'customer_lookup' | 'date' | 'datetime' | 'document' | 'dropdown' | 'email' | 'field_group' | 'font_family' | 'generic_lookup' | 'html' | 'icon' | 'image' | 'long_text' | 'lookup' | 'markdown' | 'number' | 'percent' | 'phone' | 'product_lookup' | 'radio' | 'rich_html' | 'rich_text' | 'select' | 'short_text' | 'slider' | 'slug' | 'tags' | 'text' | 'textarea' | 'time' | 'toggle' | 'url' | 'variant_lookup' | 'video';
|
|
16
|
+
type ContentType = 'asset' | 'basic_html' | 'boolean' | 'category_lookup' | 'checkbox' | 'checkboxes' | 'collection' | 'color' | 'color_scheme' | 'color_scheme_group' | 'component' | 'currency' | 'customer_lookup' | 'date' | 'datetime' | 'document' | 'dropdown' | 'email' | 'field_group' | 'font_family' | 'generic_lookup' | 'html' | 'icon' | 'image' | 'long_text' | 'lookup' | 'markdown' | 'number' | 'percent' | 'phone' | 'product_lookup' | 'radio' | 'rich_html' | 'rich_text' | 'select' | 'short_text' | 'slider' | 'slug' | 'tags' | 'text' | 'textarea' | 'time' | 'toggle' | 'url' | 'variant_lookup' | 'video';
|
|
17
17
|
interface ContentFieldProps {
|
|
18
18
|
id: string;
|
|
19
19
|
label: string;
|
|
@@ -9,7 +9,7 @@ interface SettingFieldProps {
|
|
|
9
9
|
label: string;
|
|
10
10
|
type?: SettingFieldType;
|
|
11
11
|
}
|
|
12
|
-
type SettingFieldType = 'asset' | 'basic_html' | 'boolean' | 'category_lookup' | 'checkbox' | 'checkboxes' | 'collection' | 'color' | 'color_scheme' | 'color_scheme_group' | 'currency' | 'customer_lookup' | 'date' | 'datetime' | 'document' | 'dropdown' | 'email' | 'field_group' | 'font_family' | 'generic_lookup' | 'html' | 'icon' | 'image' | 'long_text' | 'lookup' | 'markdown' | 'number' | 'percent' | 'phone' | 'product_lookup' | 'radio' | 'rich_html' | 'rich_text' | 'select' | 'short_text' | 'slider' | 'slug' | 'tags' | 'text' | 'textarea' | 'time' | 'toggle' | 'url' | 'variant_lookup' | 'video';
|
|
12
|
+
type SettingFieldType = 'asset' | 'basic_html' | 'boolean' | 'category_lookup' | 'checkbox' | 'checkboxes' | 'collection' | 'color' | 'color_scheme' | 'color_scheme_group' | 'component' | 'currency' | 'customer_lookup' | 'date' | 'datetime' | 'document' | 'dropdown' | 'email' | 'field_group' | 'font_family' | 'generic_lookup' | 'html' | 'icon' | 'image' | 'long_text' | 'lookup' | 'markdown' | 'number' | 'percent' | 'phone' | 'product_lookup' | 'radio' | 'rich_html' | 'rich_text' | 'select' | 'short_text' | 'slider' | 'slug' | 'tags' | 'text' | 'textarea' | 'time' | 'toggle' | 'url' | 'variant_lookup' | 'video';
|
|
13
13
|
export declare function parseFields(fields: string[]): SettingFieldProps[];
|
|
14
14
|
export declare const toSettingLabel: (value: string) => string;
|
|
15
15
|
export {};
|
|
@@ -14,3 +14,14 @@ export declare function analyzeFunctionSourceText(sourceText: string, filePath?:
|
|
|
14
14
|
export declare function getFunctionTriggers(config: Record<string, unknown>): string[];
|
|
15
15
|
export declare function hasKindDiagnostic(analysis: FunctionSourceAnalysis): boolean;
|
|
16
16
|
export declare function workflowStaticConfigError(): Error;
|
|
17
|
+
export interface StaticConfig {
|
|
18
|
+
config?: Record<string, unknown>;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads `export const config = {...}` as static JSON-like data without running the module.
|
|
23
|
+
* @param {string} sourceText source code of the file
|
|
24
|
+
* @param {string} filePath file name, used to pick the parser (TS, TSX, JS or JSX)
|
|
25
|
+
* @returns {StaticConfig} the config when present and static, otherwise the errors
|
|
26
|
+
*/
|
|
27
|
+
export declare function readStaticConfig(sourceText: string, filePath: string): StaticConfig;
|
|
@@ -76,6 +76,39 @@ export function hasKindDiagnostic(analysis) {
|
|
|
76
76
|
export function workflowStaticConfigError() {
|
|
77
77
|
return new Error('Workflow configs must export a static object literal so the CLI can choose the workflow bundling path before compiling.');
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads `export const config = {...}` as static JSON-like data without running the module.
|
|
81
|
+
* @param {string} sourceText source code of the file
|
|
82
|
+
* @param {string} filePath file name, used to pick the parser (TS, TSX, JS or JSX)
|
|
83
|
+
* @returns {StaticConfig} the config when present and static, otherwise the errors
|
|
84
|
+
*/
|
|
85
|
+
export function readStaticConfig(sourceText, filePath) {
|
|
86
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind(filePath));
|
|
87
|
+
const configExport = findConfigExport(sourceFile);
|
|
88
|
+
if (!configExport) {
|
|
89
|
+
return { errors: [] };
|
|
90
|
+
}
|
|
91
|
+
if (!configExport.initializer) {
|
|
92
|
+
return { errors: ['config must be initialized with an object literal.'] };
|
|
93
|
+
}
|
|
94
|
+
const result = evaluateStatic(configExport.initializer);
|
|
95
|
+
if (result.errors.length > 0) {
|
|
96
|
+
return { errors: result.errors };
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(result.value)) {
|
|
99
|
+
return { errors: ['config must be an object literal.'] };
|
|
100
|
+
}
|
|
101
|
+
return { config: result.value, errors: [] };
|
|
102
|
+
}
|
|
103
|
+
function scriptKind(filePath) {
|
|
104
|
+
if (filePath.endsWith('.tsx'))
|
|
105
|
+
return ts.ScriptKind.TSX;
|
|
106
|
+
if (filePath.endsWith('.jsx'))
|
|
107
|
+
return ts.ScriptKind.JSX;
|
|
108
|
+
if (filePath.endsWith('.ts'))
|
|
109
|
+
return ts.ScriptKind.TS;
|
|
110
|
+
return ts.ScriptKind.JS;
|
|
111
|
+
}
|
|
79
112
|
function findConfigExport(sourceFile) {
|
|
80
113
|
const localVariables = new Map();
|
|
81
114
|
let exportedConfigLocalName;
|
|
@@ -107,6 +107,10 @@ class SwellRequest {
|
|
|
107
107
|
this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id;
|
|
108
108
|
// Check if the request is from a local development environment
|
|
109
109
|
this.isLocalDev = req.headers.get('Swell-Local-Dev') === 'true';
|
|
110
|
+
// Context of a call from an app component (Swell-Context). Only the platform
|
|
111
|
+
// sets this header; verify it with verifySwellContext from @swell/apps-sdk
|
|
112
|
+
// when a function needs cryptographic proof
|
|
113
|
+
this.swellContext = this.parseSwellContext(req.headers.get('Swell-Context'));
|
|
110
114
|
// Swell client
|
|
111
115
|
this.swell = new SwellAPI(this, context);
|
|
112
116
|
// URL of the original request
|
|
@@ -160,6 +164,33 @@ class SwellRequest {
|
|
|
160
164
|
return {};
|
|
161
165
|
}
|
|
162
166
|
}
|
|
167
|
+
parseSwellContext(token) {
|
|
168
|
+
const payload = token?.split('.')[1];
|
|
169
|
+
if (!payload) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
let claims;
|
|
173
|
+
try {
|
|
174
|
+
const binary = atob(payload.replaceAll('-', '+').replaceAll('_', '/'));
|
|
175
|
+
claims = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0))));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (!claims || typeof claims !== 'object') {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
appId: claims.app_id,
|
|
185
|
+
installationId: claims.installation_id,
|
|
186
|
+
storeId: claims.store_id,
|
|
187
|
+
storeUser: claims.admin?.user_id
|
|
188
|
+
? { storeId: claims.store_id, userId: claims.admin.user_id }
|
|
189
|
+
: null,
|
|
190
|
+
storefrontId: claims.storefront_id ?? undefined,
|
|
191
|
+
surface: claims.surface ?? undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
163
194
|
log(level, ...line) {
|
|
164
195
|
originalConsole[level]?.(...line);
|
|
165
196
|
this._logs.push({
|
package/dist/push-app-command.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import Stream from 'node:stream';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { default as swellConfig } from './lib/app-config.js';
|
|
9
|
+
import { componentsToRebuild } from './lib/apps/components.js';
|
|
9
10
|
import { buildFrontendPackage, getManagedFrontendFramework, } from './lib/apps/frontend-package.js';
|
|
10
11
|
import { warnFrontendRouteCollisions } from './lib/apps/frontend-routes.js';
|
|
11
12
|
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashFile, hashString, isPathDirectory, toPosixPath, } from './lib/apps/index.js';
|
|
@@ -714,6 +715,16 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
714
715
|
appConfig: config,
|
|
715
716
|
configFile,
|
|
716
717
|
});
|
|
718
|
+
// Root components bundle their shared code, so push them again too
|
|
719
|
+
const componentFiles = await componentsToRebuild(this.appPath, configFile);
|
|
720
|
+
const componentConfigs = await Promise.all(componentFiles.map((file) => appConfigFromFile(file, ConfigType.COMPONENT, this.appPath)));
|
|
721
|
+
for (const appConfig of componentConfigs) {
|
|
722
|
+
this.watchingChangeQueue.set(appConfig.filePath, {
|
|
723
|
+
action: 'push',
|
|
724
|
+
appConfig,
|
|
725
|
+
configFile: appConfig.filePath,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
717
728
|
break;
|
|
718
729
|
}
|
|
719
730
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1652,6 +1652,70 @@
|
|
|
1652
1652
|
"app.js"
|
|
1653
1653
|
]
|
|
1654
1654
|
},
|
|
1655
|
+
"create:component": {
|
|
1656
|
+
"aliases": [],
|
|
1657
|
+
"args": {
|
|
1658
|
+
"name": {
|
|
1659
|
+
"default": "",
|
|
1660
|
+
"description": "Component name (e.g., ColorPicker)",
|
|
1661
|
+
"name": "name"
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
"description": "Create an app component in the components folder.",
|
|
1665
|
+
"examples": [
|
|
1666
|
+
"$ swell create component",
|
|
1667
|
+
"$ swell create component ColorPicker -d \"Brand color picker\" -y"
|
|
1668
|
+
],
|
|
1669
|
+
"flags": {
|
|
1670
|
+
"app-path": {
|
|
1671
|
+
"description": "Path to your app directory",
|
|
1672
|
+
"name": "app-path",
|
|
1673
|
+
"hasDynamicHelp": false,
|
|
1674
|
+
"multiple": false,
|
|
1675
|
+
"type": "option"
|
|
1676
|
+
},
|
|
1677
|
+
"description": {
|
|
1678
|
+
"char": "d",
|
|
1679
|
+
"description": "Description",
|
|
1680
|
+
"name": "description",
|
|
1681
|
+
"default": "",
|
|
1682
|
+
"hasDynamicHelp": false,
|
|
1683
|
+
"multiple": false,
|
|
1684
|
+
"type": "option"
|
|
1685
|
+
},
|
|
1686
|
+
"overwrite": {
|
|
1687
|
+
"description": "Overwrite existing file",
|
|
1688
|
+
"name": "overwrite",
|
|
1689
|
+
"allowNo": false,
|
|
1690
|
+
"type": "boolean"
|
|
1691
|
+
},
|
|
1692
|
+
"yes": {
|
|
1693
|
+
"char": "y",
|
|
1694
|
+
"description": "Skip prompts, require all arguments",
|
|
1695
|
+
"name": "yes",
|
|
1696
|
+
"allowNo": false,
|
|
1697
|
+
"type": "boolean"
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
"hasDynamicHelp": false,
|
|
1701
|
+
"hiddenAliases": [],
|
|
1702
|
+
"id": "create:component",
|
|
1703
|
+
"pluginAlias": "@swell/cli",
|
|
1704
|
+
"pluginName": "@swell/cli",
|
|
1705
|
+
"pluginType": "core",
|
|
1706
|
+
"strict": true,
|
|
1707
|
+
"summary": "Create an app component in the components folder.",
|
|
1708
|
+
"helpMeta": {
|
|
1709
|
+
"usageDirect": "<name> [-d <description>] -y"
|
|
1710
|
+
},
|
|
1711
|
+
"isESM": true,
|
|
1712
|
+
"relativePath": [
|
|
1713
|
+
"dist",
|
|
1714
|
+
"commands",
|
|
1715
|
+
"create",
|
|
1716
|
+
"component.js"
|
|
1717
|
+
]
|
|
1718
|
+
},
|
|
1655
1719
|
"create:content": {
|
|
1656
1720
|
"aliases": [],
|
|
1657
1721
|
"args": {
|
|
@@ -1978,7 +2042,7 @@
|
|
|
1978
2042
|
"create": {
|
|
1979
2043
|
"aliases": [],
|
|
1980
2044
|
"args": {},
|
|
1981
|
-
"description": "Scaffold apps, models, functions, and notifications.",
|
|
2045
|
+
"description": "Scaffold apps, models, functions, components, and notifications.",
|
|
1982
2046
|
"examples": [
|
|
1983
2047
|
"<%= config.bin %> <%= command.id %>"
|
|
1984
2048
|
],
|
|
@@ -3606,5 +3670,5 @@
|
|
|
3606
3670
|
]
|
|
3607
3671
|
}
|
|
3608
3672
|
},
|
|
3609
|
-
"version": "2.9.
|
|
3673
|
+
"version": "2.9.27"
|
|
3610
3674
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swell/cli",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swell's command line interface/utility",
|
|
6
6
|
"keywords": [
|
|
@@ -80,9 +80,11 @@
|
|
|
80
80
|
"eslint-config-prettier": "9.0.0",
|
|
81
81
|
"eslint-plugin-prettier": "5.0.1",
|
|
82
82
|
"eslint-plugin-unicorn": "48.0.1",
|
|
83
|
+
"happy-dom": "20.14.5",
|
|
83
84
|
"mocha": "11.7.6",
|
|
84
85
|
"nock": "13.3.6",
|
|
85
86
|
"oclif": "4.0.3",
|
|
87
|
+
"preact": "10.28.0",
|
|
86
88
|
"shx": "0.3.4",
|
|
87
89
|
"testdouble": "3.20.0",
|
|
88
90
|
"ts-node": "10.9.1"
|