@swell/cli 2.9.27 → 2.9.29
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/create/component.d.ts +3 -0
- package/dist/commands/create/component.js +48 -3
- package/dist/commands/create/tests.js +11 -2
- package/dist/commands/inspect/extensions.js +3 -3
- package/dist/lib/apps/app-config.d.ts +2 -0
- package/dist/lib/apps/app-config.js +36 -4
- package/dist/lib/apps/component-extension.d.ts +23 -0
- package/dist/lib/apps/component-extension.js +60 -0
- package/dist/lib/apps/frontend-scaffold.js +1 -1
- package/dist/lib/bundle.js +3 -0
- package/dist/lib/create/component.d.ts +6 -2
- package/dist/lib/create/component.js +13 -5
- package/dist/lib/create/tests/templates/index.d.ts +0 -3
- package/dist/lib/create/tests/templates/index.js +0 -3
- package/dist/lib/create/tests/templates/mock-request.js +4 -10
- package/dist/lib/create/tests/templates/tsconfig.js +1 -1
- package/dist/lib/create/tests/templates/vitest-config.d.ts +1 -2
- package/dist/lib/create/tests/templates/vitest-config.js +17 -113
- package/dist/lib/create/tests/types.d.ts +2 -0
- package/dist/lib/create/tests.js +64 -12
- package/dist/lib/inspect/extensions.d.ts +8 -1
- package/dist/lib/inspect/extensions.js +12 -2
- package/dist/lib/swell-function-wrapper.js +1 -0
- package/oclif.manifest.json +19 -4
- package/package.json +1 -1
- package/dist/lib/create/tests/templates/env-dts.d.ts +0 -1
- package/dist/lib/create/tests/templates/env-dts.js +0 -15
- package/dist/lib/create/tests/templates/integration-test.d.ts +0 -1
- package/dist/lib/create/tests/templates/integration-test.js +0 -19
- package/dist/lib/create/tests/templates/swell-client.d.ts +0 -1
- package/dist/lib/create/tests/templates/swell-client.js +0 -128
|
@@ -10,10 +10,13 @@ export default class CreateComponent extends CreateConfigCommand {
|
|
|
10
10
|
static helpMeta: HelpMeta;
|
|
11
11
|
static flags: {
|
|
12
12
|
description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
extension: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
14
|
overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
|
+
target: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
16
|
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
17
|
};
|
|
16
18
|
static summary: string;
|
|
17
19
|
createType: ConfigType;
|
|
20
|
+
private checkConfig;
|
|
18
21
|
run(): Promise<void>;
|
|
19
22
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
3
5
|
import { CreateConfigCommand } from '../../create-config-command.js';
|
|
6
|
+
import { resolveComponentValues, } from '../../lib/apps/component-extension.js';
|
|
7
|
+
import { listRootComponentFiles } from '../../lib/apps/components.js';
|
|
4
8
|
import { ConfigType } from '../../lib/apps/index.js';
|
|
5
9
|
import { getComponentTemplate, setupComponentSupport, toComponentName, } from '../../lib/create/component.js';
|
|
10
|
+
import { readStaticConfig } from '../../lib/function-source-analysis.js';
|
|
6
11
|
export default class CreateComponent extends CreateConfigCommand {
|
|
7
12
|
static args = {
|
|
8
13
|
name: Args.string({
|
|
@@ -14,9 +19,10 @@ export default class CreateComponent extends CreateConfigCommand {
|
|
|
14
19
|
static examples = [
|
|
15
20
|
'$ swell create component',
|
|
16
21
|
'$ swell create component ColorPicker -d "Brand color picker" -y',
|
|
22
|
+
'$ swell create component MyComponent --extension <extension-id> --target <target-id> -y',
|
|
17
23
|
];
|
|
18
24
|
static helpMeta = {
|
|
19
|
-
usageDirect: '<name> [-d <description>] -y',
|
|
25
|
+
usageDirect: '<name> [-d <description>] [--extension <extension-id>] [--target <target-id>...] -y',
|
|
20
26
|
};
|
|
21
27
|
static flags = {
|
|
22
28
|
description: Flags.string({
|
|
@@ -24,10 +30,17 @@ export default class CreateComponent extends CreateConfigCommand {
|
|
|
24
30
|
default: '',
|
|
25
31
|
description: 'Description',
|
|
26
32
|
}),
|
|
33
|
+
extension: Flags.string({
|
|
34
|
+
description: 'Extension id from swell.json: makes the component the UI of that extension',
|
|
35
|
+
}),
|
|
27
36
|
overwrite: Flags.boolean({
|
|
28
37
|
default: false,
|
|
29
38
|
description: 'Overwrite existing file',
|
|
30
39
|
}),
|
|
40
|
+
target: Flags.string({
|
|
41
|
+
description: 'Target id where the component renders; repeat for more targets',
|
|
42
|
+
multiple: true,
|
|
43
|
+
}),
|
|
31
44
|
yes: Flags.boolean({
|
|
32
45
|
char: 'y',
|
|
33
46
|
description: 'Skip prompts, require all arguments',
|
|
@@ -35,9 +48,35 @@ export default class CreateComponent extends CreateConfigCommand {
|
|
|
35
48
|
};
|
|
36
49
|
static summary = 'Create an app component in the components folder.';
|
|
37
50
|
createType = ConfigType.COMPONENT;
|
|
51
|
+
// The same rules as push, so a scaffold that would fail to push is refused here
|
|
52
|
+
async checkConfig(componentName, extension, targets) {
|
|
53
|
+
const configured = this.swellConfig.get('extensions');
|
|
54
|
+
const extensions = Array.isArray(configured)
|
|
55
|
+
? configured
|
|
56
|
+
: [];
|
|
57
|
+
const files = await listRootComponentFiles(this.appPath);
|
|
58
|
+
const siblings = await Promise.all(files.map(async (file) => {
|
|
59
|
+
const source = await fs
|
|
60
|
+
.readFile(path.join(this.appPath, file), 'utf8')
|
|
61
|
+
.catch(() => '');
|
|
62
|
+
return {
|
|
63
|
+
config: readStaticConfig(source, file).config,
|
|
64
|
+
name: path.basename(file).replace(/\.[^.]+$/, ''),
|
|
65
|
+
};
|
|
66
|
+
}));
|
|
67
|
+
try {
|
|
68
|
+
resolveComponentValues(componentName, {
|
|
69
|
+
...(extension && { extension: { id: extension } }),
|
|
70
|
+
...(targets.length > 0 && { targets: targets.map((id) => ({ id })) }),
|
|
71
|
+
}, extensions, siblings);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.error(error.message, { exit: 1 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
38
77
|
async run() {
|
|
39
78
|
const { args, flags } = await this.parse(CreateComponent);
|
|
40
|
-
const { description: descriptionFlag, overwrite, yes } = flags;
|
|
79
|
+
const { description: descriptionFlag, extension, overwrite, target: targets = [], yes, } = flags;
|
|
41
80
|
let name = args.name;
|
|
42
81
|
let description = descriptionFlag;
|
|
43
82
|
if (yes) {
|
|
@@ -62,9 +101,15 @@ export default class CreateComponent extends CreateConfigCommand {
|
|
|
62
101
|
if (!componentName) {
|
|
63
102
|
this.error(`Invalid component name: ${name}`, { exit: 1 });
|
|
64
103
|
}
|
|
104
|
+
if (extension || targets.length > 0) {
|
|
105
|
+
await this.checkConfig(componentName, extension, targets);
|
|
106
|
+
}
|
|
65
107
|
const created = await this.createFile({
|
|
66
108
|
extension: 'tsx',
|
|
67
|
-
fileBody: getComponentTemplate(componentName, description
|
|
109
|
+
fileBody: getComponentTemplate(componentName, description, {
|
|
110
|
+
extension,
|
|
111
|
+
targets,
|
|
112
|
+
}),
|
|
68
113
|
fileName: componentName,
|
|
69
114
|
}, overwrite, !yes);
|
|
70
115
|
if (!created) {
|
|
@@ -4,7 +4,7 @@ import { toAppId } from '../../lib/create/index.js';
|
|
|
4
4
|
import { createTestsScaffold, } from '../../lib/create/tests.js';
|
|
5
5
|
import { detectPackageManager, getPackageManagerCommands, } from '../../lib/package-manager.js';
|
|
6
6
|
export default class CreateTests extends AppCommand {
|
|
7
|
-
static description = 'Initialize a vitest + Cloudflare Workers test setup
|
|
7
|
+
static description = 'Initialize a vitest + Cloudflare Workers test setup for app functions.';
|
|
8
8
|
static examples = [
|
|
9
9
|
'$ swell create tests',
|
|
10
10
|
'$ swell create tests --overwrite',
|
|
@@ -43,7 +43,9 @@ export default class CreateTests extends AppCommand {
|
|
|
43
43
|
this.logScaffoldResult(result, pm);
|
|
44
44
|
}
|
|
45
45
|
logScaffoldResult(result, pm) {
|
|
46
|
-
const hasChanges = result.createdFiles.length > 0 ||
|
|
46
|
+
const hasChanges = result.createdFiles.length > 0 ||
|
|
47
|
+
result.removedFiles.length > 0 ||
|
|
48
|
+
result.packageJsonUpdated;
|
|
47
49
|
if (hasChanges) {
|
|
48
50
|
this.log('');
|
|
49
51
|
this.log('✓ Created test scaffolding');
|
|
@@ -62,6 +64,13 @@ export default class CreateTests extends AppCommand {
|
|
|
62
64
|
this.log(` ${file}`);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
if (result.removedFiles.length > 0) {
|
|
68
|
+
this.log('');
|
|
69
|
+
this.log('Removed (no longer part of the test setup):');
|
|
70
|
+
for (const file of result.removedFiles) {
|
|
71
|
+
this.log(` ${file}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
65
74
|
if (result.packageJsonUpdated) {
|
|
66
75
|
this.log('');
|
|
67
76
|
this.log('Updated package.json with test scripts and devDependencies.');
|
|
@@ -6,7 +6,7 @@ import { resolveInspectScope, ScopeError, } from '../../lib/apps/inspect-scope.j
|
|
|
6
6
|
import { HEX24, isObjectId } from '../../lib/apps/object-id.js';
|
|
7
7
|
import { resolveAppId } from '../../lib/apps/resolve.js';
|
|
8
8
|
import { default as localConfig } from '../../lib/config.js';
|
|
9
|
-
import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
|
|
9
|
+
import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, componentExtensionId, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
|
|
10
10
|
import { renderKeyMetaTable } from '../../lib/inspect/table.js';
|
|
11
11
|
import { SwellCommand } from '../../swell-command.js';
|
|
12
12
|
export default class Extensions extends SwellCommand {
|
|
@@ -144,7 +144,7 @@ Identifier forms:
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
for (const comp of appComponents) {
|
|
147
|
-
const ext = comp.values
|
|
147
|
+
const ext = componentExtensionId(comp.values);
|
|
148
148
|
if (!ext)
|
|
149
149
|
continue;
|
|
150
150
|
if (declaredIds.has(ext))
|
|
@@ -331,7 +331,7 @@ Identifier forms:
|
|
|
331
331
|
// but no manifest entry declares it. Surface as the orphan envelope only
|
|
332
332
|
// when we actually find handlers; otherwise it's a "not found" lookup.
|
|
333
333
|
const orphanFns = allFunctions.filter((f) => f.extension === extId);
|
|
334
|
-
const orphanComps = allComponents.filter((c) => c.values
|
|
334
|
+
const orphanComps = allComponents.filter((c) => componentExtensionId(c.values) === extId);
|
|
335
335
|
if (orphanFns.length === 0 && orphanComps.length === 0) {
|
|
336
336
|
this.error(`No extension '${extId}' declared by app '${appSlug}', and no handlers reference it. ` +
|
|
337
337
|
`List available: swell inspect extensions${scope.appSlug ? ` --app=${scope.appSlug}` : ''}`, { exit: 1 });
|
|
@@ -118,6 +118,8 @@ export declare class AppConfigComponent extends AppConfig {
|
|
|
118
118
|
protected parsesJsonValues(): boolean;
|
|
119
119
|
isRootComponent(): boolean;
|
|
120
120
|
protected contentHash(): Promise<string | undefined>;
|
|
121
|
+
private manifestExtensions;
|
|
122
|
+
private siblingComponents;
|
|
121
123
|
preparePostData(postData: any): Promise<any>;
|
|
122
124
|
}
|
|
123
125
|
export declare class AppConfigAsset extends AppConfigDefault {
|
|
@@ -5,7 +5,9 @@ import * as fs from 'node:fs/promises';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { bundleFunction, bundleComponent, bundleComponentModule, bundleWorkflow, getLocalInputs, } from '../bundle.js';
|
|
7
7
|
import { analyzeFunctionSource, hasKindDiagnostic, readStaticConfig, workflowStaticConfigError, } from '../function-source-analysis.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isLegacyComponent, resolveComponentValues, } from './component-extension.js';
|
|
9
|
+
import { listRootComponentFiles } from './components.js';
|
|
10
|
+
import { AllConfigPaths, ConfigType, filePathExistsAsync, getNameFromFilePath, hashFile, hashString, } from './index.js';
|
|
9
11
|
export class IgnoringFileError extends Error {
|
|
10
12
|
constructor(message) {
|
|
11
13
|
super(message);
|
|
@@ -380,6 +382,31 @@ export class AppConfigComponent extends AppConfig {
|
|
|
380
382
|
async contentHash() {
|
|
381
383
|
return this.isRootComponent() ? this.bundleHash() : this.fileHash;
|
|
382
384
|
}
|
|
385
|
+
// Read only for components that name an extension, so other apps never need a readable swell.json
|
|
386
|
+
async manifestExtensions() {
|
|
387
|
+
try {
|
|
388
|
+
const manifest = JSON.parse(await fs.readFile(path.join(this.appRoot(), 'swell.json'), 'utf8'));
|
|
389
|
+
return Array.isArray(manifest?.extensions) ? manifest.extensions : [];
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
throw new Error(`cannot read swell.json: ${error.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Siblings whose config cannot be read are skipped: their own push reports them
|
|
396
|
+
async siblingComponents() {
|
|
397
|
+
const appRoot = this.appRoot();
|
|
398
|
+
const files = await listRootComponentFiles(appRoot);
|
|
399
|
+
return Promise.all(files
|
|
400
|
+
.filter((file) => file !== this.filePath)
|
|
401
|
+
.map(async (file) => {
|
|
402
|
+
const source = await fs
|
|
403
|
+
.readFile(path.join(appRoot, file), 'utf8')
|
|
404
|
+
// A sibling that vanished or was renamed reads as empty and is skipped
|
|
405
|
+
.catch(() => '');
|
|
406
|
+
const { config } = readStaticConfig(source, file);
|
|
407
|
+
return { name: getNameFromFilePath(file), config };
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
383
410
|
async preparePostData(postData) {
|
|
384
411
|
if (!this.isRootComponent()) {
|
|
385
412
|
return postData;
|
|
@@ -395,8 +422,13 @@ export class AppConfigComponent extends AppConfig {
|
|
|
395
422
|
if (errors.length > 0) {
|
|
396
423
|
throw new Error(`config must be static metadata: ${errors.join('; ')}`);
|
|
397
424
|
}
|
|
398
|
-
|
|
399
|
-
|
|
425
|
+
const name = getNameFromFilePath(this.filePath);
|
|
426
|
+
// Without an object extension only the targets shape is checked, with no manifest read
|
|
427
|
+
const values = config?.extension !== undefined && !isLegacyComponent(config)
|
|
428
|
+
? resolveComponentValues(name, config, await this.manifestExtensions(), await this.siblingComponents())
|
|
429
|
+
: resolveComponentValues(name, config, [], []);
|
|
430
|
+
// Legacy payment components (string extension) still load through swell.app, which evaluates an IIFE
|
|
431
|
+
if (isLegacyComponent(config)) {
|
|
400
432
|
postData.build_file = {
|
|
401
433
|
content_type: 'application/javascript',
|
|
402
434
|
data: await bundleComponent(this.appPath),
|
|
@@ -415,7 +447,7 @@ export class AppConfigComponent extends AppConfig {
|
|
|
415
447
|
postData.file = {
|
|
416
448
|
data: fileData,
|
|
417
449
|
};
|
|
418
|
-
postData.values =
|
|
450
|
+
postData.values = values;
|
|
419
451
|
}
|
|
420
452
|
catch (error) {
|
|
421
453
|
throw new FunctionProcessingError(`Unable to compile component ${this.name}`, error);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ManifestExtension {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SiblingComponent {
|
|
6
|
+
name: string;
|
|
7
|
+
config?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether the config is the legacy swell.app contract (string extension)
|
|
11
|
+
* @param {object} config static component config
|
|
12
|
+
* @returns {boolean} true for a string extension
|
|
13
|
+
*/
|
|
14
|
+
export declare function isLegacyComponent(config: Record<string, unknown> | undefined): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The values to push for a component's static config
|
|
17
|
+
* @param {string} name component name
|
|
18
|
+
* @param {object} config static component config
|
|
19
|
+
* @param {ManifestExtension[]} extensions extensions of swell.json
|
|
20
|
+
* @param {SiblingComponent[]} siblings the app's other root components
|
|
21
|
+
* @returns {object} values with the extension type added
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveComponentValues(name: string, config: Record<string, unknown> | undefined, extensions: ManifestExtension[], siblings: SiblingComponent[]): Record<string, unknown>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
2
|
+
const TARGETS_SHAPE = 'targets must be a list of objects with an id, like [{ id: "<target id>" }]';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the config is the legacy swell.app contract (string extension)
|
|
5
|
+
* @param {object} config static component config
|
|
6
|
+
* @returns {boolean} true for a string extension
|
|
7
|
+
*/
|
|
8
|
+
export function isLegacyComponent(config) {
|
|
9
|
+
return typeof config?.extension === 'string';
|
|
10
|
+
}
|
|
11
|
+
// Target ids and what each target needs are the platform's to check at push
|
|
12
|
+
function checkTargets(config) {
|
|
13
|
+
if (config.targets === undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!Array.isArray(config.targets) ||
|
|
17
|
+
!config.targets.every((item) => isRecord(item) && typeof item.id === 'string' && item.id !== '')) {
|
|
18
|
+
throw new Error(TARGETS_SHAPE);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The values to push for a component's static config
|
|
23
|
+
* @param {string} name component name
|
|
24
|
+
* @param {object} config static component config
|
|
25
|
+
* @param {ManifestExtension[]} extensions extensions of swell.json
|
|
26
|
+
* @param {SiblingComponent[]} siblings the app's other root components
|
|
27
|
+
* @returns {object} values with the extension type added
|
|
28
|
+
*/
|
|
29
|
+
export function resolveComponentValues(name, config, extensions, siblings) {
|
|
30
|
+
const values = { ...config };
|
|
31
|
+
checkTargets(values);
|
|
32
|
+
const { extension } = values;
|
|
33
|
+
// A string extension is the legacy swell.app contract
|
|
34
|
+
if (extension === undefined || typeof extension === 'string') {
|
|
35
|
+
return values;
|
|
36
|
+
}
|
|
37
|
+
if (!isRecord(extension) ||
|
|
38
|
+
typeof extension.id !== 'string' ||
|
|
39
|
+
!extension.id) {
|
|
40
|
+
throw new Error('extension must be an object with an id, like { id: "card" }');
|
|
41
|
+
}
|
|
42
|
+
const matches = extensions.filter((item) => item.id === extension.id);
|
|
43
|
+
if (matches.length > 1) {
|
|
44
|
+
throw new Error(`extension "${extension.id}" is declared more than once in swell.json`);
|
|
45
|
+
}
|
|
46
|
+
const [declared] = matches;
|
|
47
|
+
if (!declared) {
|
|
48
|
+
throw new Error(`extension "${extension.id}" is not declared in swell.json`);
|
|
49
|
+
}
|
|
50
|
+
const other = siblings.find((sibling) => {
|
|
51
|
+
const siblingExtension = sibling.config?.extension;
|
|
52
|
+
return (sibling.name !== name &&
|
|
53
|
+
isRecord(siblingExtension) &&
|
|
54
|
+
siblingExtension.id === extension.id);
|
|
55
|
+
});
|
|
56
|
+
if (other) {
|
|
57
|
+
throw new Error(`extension "${extension.id}" already has the component ${other.name}`);
|
|
58
|
+
}
|
|
59
|
+
return { ...values, extension: { ...extension, type: declared.type } };
|
|
60
|
+
}
|
|
@@ -252,7 +252,7 @@ export async function setupPackage(name, config, pkg) {
|
|
|
252
252
|
moduleResolution: 'bundler',
|
|
253
253
|
types: ['@swell/app-types'],
|
|
254
254
|
},
|
|
255
|
-
exclude: ['node_modules', 'frontend', 'test', 'vitest.config.
|
|
255
|
+
exclude: ['node_modules', 'frontend', 'test', 'vitest.config.mts'],
|
|
256
256
|
};
|
|
257
257
|
await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
|
|
258
258
|
// create frontend can initialize a workspace around an existing manifest.
|
package/dist/lib/bundle.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export declare const PREACT_VERSION = "^10.28.0";
|
|
2
|
-
export declare const APPS_SDK_VERSION = "^2.0.0-alpha.
|
|
2
|
+
export declare const APPS_SDK_VERSION = "^2.0.0-alpha.7";
|
|
3
3
|
export declare const toComponentName: (value: string) => string;
|
|
4
|
-
export
|
|
4
|
+
export interface ComponentTemplateOptions {
|
|
5
|
+
extension?: string;
|
|
6
|
+
targets?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function getComponentTemplate(name: string, description: string, { extension, targets }?: ComponentTemplateOptions): string;
|
|
5
9
|
export interface ComponentSetupResult {
|
|
6
10
|
changed: string[];
|
|
7
11
|
dependenciesAdded: boolean;
|
|
@@ -2,8 +2,8 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as semver from 'semver';
|
|
4
4
|
export const PREACT_VERSION = '^10.28.0';
|
|
5
|
-
export const APPS_SDK_VERSION = '^2.0.0-alpha.
|
|
6
|
-
// The
|
|
5
|
+
export const APPS_SDK_VERSION = '^2.0.0-alpha.7';
|
|
6
|
+
// The minimum SDK the scaffold needs
|
|
7
7
|
const COMPONENTS_SDK = semver.minVersion(APPS_SDK_VERSION);
|
|
8
8
|
// Example: 'color picker' => 'ColorPicker'
|
|
9
9
|
export const toComponentName = (value) => {
|
|
@@ -16,11 +16,19 @@ export const toComponentName = (value) => {
|
|
|
16
16
|
// A component name is also its function name, so it must start with a letter
|
|
17
17
|
return /^[A-Za-z]/.test(name) ? name : '';
|
|
18
18
|
};
|
|
19
|
-
export function getComponentTemplate(name, description) {
|
|
19
|
+
export function getComponentTemplate(name, description, { extension, targets = [] } = {}) {
|
|
20
|
+
const config = [` description: ${JSON.stringify(description)},`];
|
|
21
|
+
if (extension) {
|
|
22
|
+
config.push(` extension: { id: ${JSON.stringify(extension)} },`);
|
|
23
|
+
}
|
|
24
|
+
if (targets.length > 0) {
|
|
25
|
+
const ids = targets.map((id) => `{ id: ${JSON.stringify(id)} }`);
|
|
26
|
+
config.push(` targets: [${ids.join(', ')}],`);
|
|
27
|
+
}
|
|
20
28
|
return `import type { ComponentProps } from '@swell/apps-sdk/components';
|
|
21
29
|
|
|
22
30
|
export const config = {
|
|
23
|
-
|
|
31
|
+
${config.join('\n')}
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
export default function ${name}({ value, setValue, readonly }: ComponentProps<string>) {
|
|
@@ -119,7 +127,7 @@ export async function setupComponentSupport(appPath) {
|
|
|
119
127
|
const range = pkg[sdkSection]['@swell/apps-sdk'];
|
|
120
128
|
const min = semver.validRange(range) ? semver.minVersion(range) : null;
|
|
121
129
|
if (min && semver.lt(min, COMPONENTS_SDK)) {
|
|
122
|
-
// `swell create tests`
|
|
130
|
+
// `swell create tests` pinned an earlier 2.x; 1.x is another SDK the app may use
|
|
123
131
|
if (semver.major(min) === 2) {
|
|
124
132
|
pkg[sdkSection]['@swell/apps-sdk'] = APPS_SDK_VERSION;
|
|
125
133
|
result.dependenciesAdded = true;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export { envDtsTemplate } from './env-dts.js';
|
|
2
|
-
export { integrationTestTemplate } from './integration-test.js';
|
|
3
1
|
export { mockRequestTemplate } from './mock-request.js';
|
|
4
2
|
export { setupGlobalsTemplate } from './setup-globals.js';
|
|
5
|
-
export { swellClientTemplate } from './swell-client.js';
|
|
6
3
|
export { tsconfigTemplate } from './tsconfig.js';
|
|
7
4
|
export { unitTestTemplate } from './unit-test.js';
|
|
8
5
|
export { vitestConfigTemplate } from './vitest-config.js';
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export { envDtsTemplate } from './env-dts.js';
|
|
2
|
-
export { integrationTestTemplate } from './integration-test.js';
|
|
3
1
|
export { mockRequestTemplate } from './mock-request.js';
|
|
4
2
|
export { setupGlobalsTemplate } from './setup-globals.js';
|
|
5
|
-
export { swellClientTemplate } from './swell-client.js';
|
|
6
3
|
export { tsconfigTemplate } from './tsconfig.js';
|
|
7
4
|
export { unitTestTemplate } from './unit-test.js';
|
|
8
5
|
export { vitestConfigTemplate } from './vitest-config.js';
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export function mockRequestTemplate(options) {
|
|
2
2
|
return `\
|
|
3
|
-
import { env } from "cloudflare:test";
|
|
4
|
-
import { createSwellClient } from "./swell-client";
|
|
5
|
-
|
|
6
3
|
export interface MockRequestOptions {
|
|
7
4
|
data?: SwellData;
|
|
8
5
|
query?: { [key: string]: string };
|
|
@@ -13,7 +10,6 @@ export interface MockRequestOptions {
|
|
|
13
10
|
swell?: Partial<SwellAPI>;
|
|
14
11
|
store?: Partial<SwellStore>;
|
|
15
12
|
appId?: string;
|
|
16
|
-
useRealSwell?: boolean;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
function createDefaultSwellMock(): Partial<SwellAPI> {
|
|
@@ -42,7 +38,6 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
|
|
|
42
38
|
swell,
|
|
43
39
|
store,
|
|
44
40
|
appId,
|
|
45
|
-
useRealSwell = false,
|
|
46
41
|
} = options;
|
|
47
42
|
|
|
48
43
|
const requestHeaders = new Headers(headers);
|
|
@@ -58,7 +53,7 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
|
|
|
58
53
|
body: method === "GET" ? undefined : JSON.stringify(data),
|
|
59
54
|
});
|
|
60
55
|
|
|
61
|
-
const storeId = store?.id ||
|
|
56
|
+
const storeId = store?.id || "test-store";
|
|
62
57
|
|
|
63
58
|
const resolvedStore = {
|
|
64
59
|
id: storeId,
|
|
@@ -72,10 +67,9 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
|
|
|
72
67
|
},
|
|
73
68
|
};
|
|
74
69
|
|
|
75
|
-
const swellClient =
|
|
76
|
-
swell || (useRealSwell ? createSwellClient() : createDefaultSwellMock());
|
|
70
|
+
const swellClient = swell || createDefaultSwellMock();
|
|
77
71
|
|
|
78
|
-
const resolvedAppId = appId ||
|
|
72
|
+
const resolvedAppId = appId || "${options.appId}";
|
|
79
73
|
|
|
80
74
|
const req: Partial<SwellRequest> & { appId: string; storeId: string } = {
|
|
81
75
|
originalRequest,
|
|
@@ -91,7 +85,7 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
|
|
|
91
85
|
publicKey: null,
|
|
92
86
|
store: resolvedStore,
|
|
93
87
|
session,
|
|
94
|
-
apiHost:
|
|
88
|
+
apiHost: "",
|
|
95
89
|
logParams: undefined,
|
|
96
90
|
swell: swellClient as SwellAPI,
|
|
97
91
|
rawBody: method === "GET" ? "" : JSON.stringify(data),
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function vitestConfigTemplate(options: TestTemplateOptions): string;
|
|
1
|
+
export declare function vitestConfigTemplate(): string;
|
|
@@ -1,123 +1,27 @@
|
|
|
1
1
|
import { FUNCTION_COMPATIBILITY_DATE } from '../../../constants.js';
|
|
2
|
-
export function vitestConfigTemplate(
|
|
2
|
+
export function vitestConfigTemplate() {
|
|
3
3
|
return `\
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const envStoreId = process.env.SWELL_STORE_ID;
|
|
19
|
-
const envSessionId = process.env.SWELL_SESSION_ID;
|
|
20
|
-
const envApiBaseUrl = process.env.SWELL_API_BASE_URL;
|
|
21
|
-
|
|
22
|
-
if (envStoreId && envSessionId) {
|
|
23
|
-
return {
|
|
24
|
-
storeId: envStoreId,
|
|
25
|
-
sessionId: envSessionId,
|
|
26
|
-
apiBaseUrl: envApiBaseUrl || \`https://\${envStoreId}.swell.store/admin/api\`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (isCI) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
"CI environment detected but SWELL_STORE_ID and SWELL_SESSION_ID are not set. " +
|
|
33
|
-
"Add these as CI secrets/variables to run integration tests.",
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const home = homedir();
|
|
38
|
-
const configPath = path.resolve(home, ".swell", "config.json");
|
|
39
|
-
if (!existsSync(configPath)) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
\`Swell CLI config not found at \${configPath}. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.\`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const rawConfig = readFileSync(configPath, "utf-8");
|
|
46
|
-
|
|
47
|
-
interface CliConfig {
|
|
48
|
-
defaultStore?: string;
|
|
49
|
-
stores?: { storeId: string; sessionId?: string }[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let configJson: CliConfig;
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
configJson = JSON.parse(rawConfig) as CliConfig;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
\`Unable to parse Swell CLI config at \${configPath}: \${String(error)}\`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const defaultStore = configJson.defaultStore;
|
|
63
|
-
const stores = configJson.stores || [];
|
|
64
|
-
|
|
65
|
-
const store = defaultStore
|
|
66
|
-
? stores.find((item) => item.storeId === defaultStore)
|
|
67
|
-
: undefined;
|
|
68
|
-
|
|
69
|
-
if (!defaultStore || !store?.sessionId) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
"No active Swell CLI session found. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.",
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const envPath = path.join(home, ".swell", "env.json");
|
|
76
|
-
let apiBaseUrl = \`https://\${defaultStore}.swell.store/admin/api\`;
|
|
77
|
-
|
|
78
|
-
if (existsSync(envPath)) {
|
|
79
|
-
try {
|
|
80
|
-
const rawEnv = readFileSync(envPath, "utf-8");
|
|
81
|
-
const envJson = JSON.parse(rawEnv) as { ADMIN_API_BASE_URL?: string };
|
|
82
|
-
if (envJson.ADMIN_API_BASE_URL) {
|
|
83
|
-
apiBaseUrl = envJson.ADMIN_API_BASE_URL.replace("\${STORE_ID}", defaultStore);
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
// Ignore env parsing errors and fall back to default
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
storeId: defaultStore,
|
|
92
|
-
sessionId: store.sessionId!,
|
|
93
|
-
apiBaseUrl,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const sdkAuth = loadSwellAuth();
|
|
98
|
-
|
|
99
|
-
export default defineWorkersConfig({
|
|
4
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
5
|
+
import { defineConfig } from "vitest/config";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [
|
|
9
|
+
cloudflareTest({
|
|
10
|
+
miniflare: {
|
|
11
|
+
compatibilityDate: "${FUNCTION_COMPATIBILITY_DATE}",
|
|
12
|
+
// Reports a rejection as unhandled only if nothing has handled it by the end of the
|
|
13
|
+
// microtask queue. Without it, a rejection handled one step later fails the run
|
|
14
|
+
compatibilityFlags: ["unhandled_rejection_after_microtask_checkpoint"],
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
100
18
|
test: {
|
|
101
19
|
globals: true,
|
|
20
|
+
// The Workers runtime cannot read Vitest's module cache from disk
|
|
21
|
+
fsModuleCache: false,
|
|
102
22
|
include: ["test/**/*.test.ts"],
|
|
103
23
|
exclude: ["node_modules", "docs"],
|
|
104
24
|
setupFiles: ["./test/setup-globals.ts"],
|
|
105
|
-
poolOptions: {
|
|
106
|
-
workers: {
|
|
107
|
-
singleWorker: true,
|
|
108
|
-
isolatedStorage: true,
|
|
109
|
-
miniflare: {
|
|
110
|
-
compatibilityDate: "${FUNCTION_COMPATIBILITY_DATE}",
|
|
111
|
-
bindings: {
|
|
112
|
-
SWELL_STORE_ID: sdkAuth.storeId,
|
|
113
|
-
SWELL_SESSION_ID: sdkAuth.sessionId,
|
|
114
|
-
SWELL_API_BASE_URL: sdkAuth.apiBaseUrl,
|
|
115
|
-
SWELL_APP_ID: "${options.appId}",
|
|
116
|
-
SWELL_ENVIRONMENT: "test",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
25
|
},
|
|
122
26
|
});
|
|
123
27
|
`;
|
|
@@ -13,6 +13,8 @@ export interface ScaffoldResult {
|
|
|
13
13
|
createdFiles: string[];
|
|
14
14
|
/** Files that were skipped because they already exist */
|
|
15
15
|
skippedFiles: string[];
|
|
16
|
+
/** Files of an earlier scaffold that were removed */
|
|
17
|
+
removedFiles: string[];
|
|
16
18
|
/** Whether package.json was updated */
|
|
17
19
|
packageJsonUpdated: boolean;
|
|
18
20
|
/** Warning messages to display to user */
|
package/dist/lib/create/tests.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { filePathExistsAsync, writeFile } from '../apps/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { APPS_SDK_VERSION } from './component.js';
|
|
5
|
+
import { mockRequestTemplate, setupGlobalsTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
|
|
5
6
|
const TEST_DEV_DEPENDENCIES = {
|
|
6
|
-
//
|
|
7
|
-
'@cloudflare/vitest-
|
|
8
|
-
vitest: '
|
|
7
|
+
// npm 10 cannot resolve vitest 4.1 next to the plugin; vitest 5 needs Node 22.12 or later
|
|
8
|
+
'@cloudflare/vitest-plugin': '1.4.x',
|
|
9
|
+
vitest: '5.0.x',
|
|
9
10
|
'@swell/app-types': 'latest',
|
|
10
|
-
'@swell/apps-sdk':
|
|
11
|
+
'@swell/apps-sdk': APPS_SDK_VERSION,
|
|
11
12
|
};
|
|
13
|
+
// Earlier scaffolds used the deprecated pool package and a vitest.config.ts,
|
|
14
|
+
// which Vitest would load ahead of vitest.config.mts
|
|
15
|
+
const LEGACY_POOL_PACKAGE = '@cloudflare/vitest-pool-workers';
|
|
16
|
+
const LEGACY_VITEST_CONFIG = 'vitest.config.ts';
|
|
17
|
+
// The plugin is ESM-only and scaffolded apps are not "type": "module"
|
|
18
|
+
const VITEST_CONFIG = 'vitest.config.mts';
|
|
19
|
+
// Files of earlier scaffolds that have no place in the current one
|
|
20
|
+
const RETIRED_FILES = [
|
|
21
|
+
LEGACY_VITEST_CONFIG,
|
|
22
|
+
'test/env.d.ts',
|
|
23
|
+
'test/helpers/swell-client.ts',
|
|
24
|
+
'test/integration/example.test.ts',
|
|
25
|
+
];
|
|
12
26
|
async function writeTextFile(filePath, contents, overwrite) {
|
|
13
27
|
if (!overwrite && (await filePathExistsAsync(filePath))) {
|
|
14
28
|
return { created: false, skipped: true };
|
|
@@ -46,9 +60,16 @@ async function updatePackageJson(appPath) {
|
|
|
46
60
|
}
|
|
47
61
|
const devDeps = (pkg.devDependencies || {});
|
|
48
62
|
const deps = (pkg.dependencies || {});
|
|
49
|
-
devDeps[
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
if (devDeps[LEGACY_POOL_PACKAGE] || deps[LEGACY_POOL_PACKAGE]) {
|
|
64
|
+
// The plugin does not support the vitest version the pool package required
|
|
65
|
+
for (const section of [deps, devDeps]) {
|
|
66
|
+
delete section[LEGACY_POOL_PACKAGE];
|
|
67
|
+
delete section.vitest;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
devDeps['@cloudflare/vitest-plugin'] =
|
|
71
|
+
devDeps['@cloudflare/vitest-plugin'] ||
|
|
72
|
+
TEST_DEV_DEPENDENCIES['@cloudflare/vitest-plugin'];
|
|
52
73
|
devDeps.vitest = devDeps.vitest || TEST_DEV_DEPENDENCIES.vitest;
|
|
53
74
|
if (!devDeps['@swell/app-types'] && !deps['@swell/app-types']) {
|
|
54
75
|
devDeps['@swell/app-types'] = TEST_DEV_DEPENDENCIES['@swell/app-types'];
|
|
@@ -72,6 +93,29 @@ async function updatePackageJson(appPath) {
|
|
|
72
93
|
};
|
|
73
94
|
}
|
|
74
95
|
}
|
|
96
|
+
async function hasLegacySetup(appPath) {
|
|
97
|
+
if (await filePathExistsAsync(path.join(appPath, LEGACY_VITEST_CONFIG))) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(await fs.readFile(path.join(appPath, 'package.json'), 'utf8'));
|
|
102
|
+
return Boolean(pkg.devDependencies?.[LEGACY_POOL_PACKAGE] ||
|
|
103
|
+
pkg.dependencies?.[LEGACY_POOL_PACKAGE]);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// The app tsconfig.json covers functions only; the config file runs in Node
|
|
110
|
+
async function excludeVitestConfig(appPath) {
|
|
111
|
+
const tsconfigPath = path.join(appPath, 'tsconfig.json');
|
|
112
|
+
if (!(await filePathExistsAsync(tsconfigPath)))
|
|
113
|
+
return;
|
|
114
|
+
const tsconfig = await fs.readFile(tsconfigPath, 'utf8');
|
|
115
|
+
const next = tsconfig.replace(`"${LEGACY_VITEST_CONFIG}"`, `"${VITEST_CONFIG}"`);
|
|
116
|
+
if (next !== tsconfig)
|
|
117
|
+
await fs.writeFile(tsconfigPath, next, 'utf8');
|
|
118
|
+
}
|
|
75
119
|
async function validatePrerequisites(appPath) {
|
|
76
120
|
const warnings = [];
|
|
77
121
|
const tsconfigPath = path.join(appPath, 'tsconfig.json');
|
|
@@ -85,6 +129,7 @@ export async function createTestsScaffold(options) {
|
|
|
85
129
|
const result = {
|
|
86
130
|
createdFiles: [],
|
|
87
131
|
skippedFiles: [],
|
|
132
|
+
removedFiles: [],
|
|
88
133
|
packageJsonUpdated: false,
|
|
89
134
|
warnings: [],
|
|
90
135
|
};
|
|
@@ -100,14 +145,21 @@ export async function createTestsScaffold(options) {
|
|
|
100
145
|
result.skippedFiles.push(relativePath);
|
|
101
146
|
}
|
|
102
147
|
};
|
|
103
|
-
|
|
148
|
+
if (overwrite) {
|
|
149
|
+
const exists = await Promise.all(RETIRED_FILES.map((file) => filePathExistsAsync(path.join(appPath, file))));
|
|
150
|
+
result.removedFiles = RETIRED_FILES.filter((_, index) => exists[index]);
|
|
151
|
+
await Promise.all(result.removedFiles.map((file) => fs.rm(path.join(appPath, file))));
|
|
152
|
+
}
|
|
153
|
+
else if (await hasLegacySetup(appPath)) {
|
|
154
|
+
result.warnings.push(`This app has an earlier test setup (${LEGACY_VITEST_CONFIG} with ${LEGACY_POOL_PACKAGE}, which is deprecated). Nothing was changed. Run \`swell create tests --overwrite\` to move it to @cloudflare/vitest-plugin; this replaces the scaffolded test files.`);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
await trackFile(VITEST_CONFIG, vitestConfigTemplate());
|
|
158
|
+
await excludeVitestConfig(appPath);
|
|
104
159
|
await trackFile('test/tsconfig.json', tsconfigTemplate());
|
|
105
|
-
await trackFile('test/env.d.ts', envDtsTemplate());
|
|
106
160
|
await trackFile('test/setup-globals.ts', setupGlobalsTemplate());
|
|
107
|
-
await trackFile('test/helpers/swell-client.ts', swellClientTemplate());
|
|
108
161
|
await trackFile('test/helpers/mock-request.ts', mockRequestTemplate({ appId }));
|
|
109
162
|
await trackFile('test/unit/example.test.ts', unitTestTemplate());
|
|
110
|
-
await trackFile('test/integration/example.test.ts', integrationTestTemplate());
|
|
111
163
|
const pkgResult = await updatePackageJson(appPath);
|
|
112
164
|
result.packageJsonUpdated = pkgResult.updated;
|
|
113
165
|
if (pkgResult.warning) {
|
|
@@ -42,7 +42,10 @@ export interface ComponentRecord {
|
|
|
42
42
|
file_path?: string;
|
|
43
43
|
values?: {
|
|
44
44
|
[key: string]: unknown;
|
|
45
|
-
extension?: string
|
|
45
|
+
extension?: string | {
|
|
46
|
+
id: string;
|
|
47
|
+
type?: string;
|
|
48
|
+
};
|
|
46
49
|
};
|
|
47
50
|
[key: string]: unknown;
|
|
48
51
|
}
|
|
@@ -202,6 +205,10 @@ export declare function stripHookPrefix(event: string): string;
|
|
|
202
205
|
export declare function eventsCoveredByFunctions(functions: Pick<FunctionRecord, 'model'>[]): Set<string>;
|
|
203
206
|
/** Required events not covered by any of the bound functions. */
|
|
204
207
|
export declare function missingRequiredEvents(type: ExtensionType, functions: Pick<FunctionRecord, 'model'>[]): string[];
|
|
208
|
+
/**
|
|
209
|
+
* The extension id a component binds to: a string (legacy) or `{ id }` (object)
|
|
210
|
+
*/
|
|
211
|
+
export declare function componentExtensionId(values: ComponentRecord['values']): string | undefined;
|
|
205
212
|
/** Lift a component record into the synthesized `BoundComponent` shape. */
|
|
206
213
|
export declare function liftBoundComponent(record: ComponentRecord): BoundComponent;
|
|
207
214
|
/** Lift a function record into the synthesized `BoundFunction` shape. */
|
|
@@ -127,12 +127,22 @@ export function missingRequiredEvents(type, functions) {
|
|
|
127
127
|
const covered = eventsCoveredByFunctions(functions);
|
|
128
128
|
return requiredEventsFor(type).filter((ev) => !covered.has(ev));
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* The extension id a component binds to: a string (legacy) or `{ id }` (object)
|
|
132
|
+
*/
|
|
133
|
+
export function componentExtensionId(values) {
|
|
134
|
+
const extension = values?.extension;
|
|
135
|
+
if (typeof extension === 'string') {
|
|
136
|
+
return extension;
|
|
137
|
+
}
|
|
138
|
+
return typeof extension?.id === 'string' ? extension.id : undefined;
|
|
139
|
+
}
|
|
130
140
|
/** Lift a component record into the synthesized `BoundComponent` shape. */
|
|
131
141
|
export function liftBoundComponent(record) {
|
|
132
142
|
return {
|
|
133
143
|
id: record.id,
|
|
134
144
|
name: record.name,
|
|
135
|
-
extension: record.values
|
|
145
|
+
extension: componentExtensionId(record.values),
|
|
136
146
|
file_path: record.file_path,
|
|
137
147
|
};
|
|
138
148
|
}
|
|
@@ -686,5 +696,5 @@ export function functionMatchesExtension(fn, extId) {
|
|
|
686
696
|
return fn.extension === extId;
|
|
687
697
|
}
|
|
688
698
|
export function componentMatchesExtension(comp, extId) {
|
|
689
|
-
return comp.values
|
|
699
|
+
return componentExtensionId(comp.values) === extId;
|
|
690
700
|
}
|
|
@@ -101,6 +101,7 @@ class SwellRequest {
|
|
|
101
101
|
this.accessToken = req.headers.get('Swell-Access-Token');
|
|
102
102
|
this.publicKey = req.headers.get('Swell-Public-Key');
|
|
103
103
|
this.store = this.parseJson(req.headers.get('Swell-Store-Details'));
|
|
104
|
+
this.app = this.parseJson(req.headers.get('Swell-App-Details'));
|
|
104
105
|
this.session = this.parseJson(req.headers.get('Swell-Session'));
|
|
105
106
|
this.logParams = this.parseJson(req.headers.get('Swell-Request-Log'));
|
|
106
107
|
this.apiHost = req.headers.get('Swell-API-Host') || 'https://api.schema.io';
|
package/oclif.manifest.json
CHANGED
|
@@ -1664,7 +1664,8 @@
|
|
|
1664
1664
|
"description": "Create an app component in the components folder.",
|
|
1665
1665
|
"examples": [
|
|
1666
1666
|
"$ swell create component",
|
|
1667
|
-
"$ swell create component ColorPicker -d \"Brand color picker\" -y"
|
|
1667
|
+
"$ swell create component ColorPicker -d \"Brand color picker\" -y",
|
|
1668
|
+
"$ swell create component MyComponent --extension <extension-id> --target <target-id> -y"
|
|
1668
1669
|
],
|
|
1669
1670
|
"flags": {
|
|
1670
1671
|
"app-path": {
|
|
@@ -1683,12 +1684,26 @@
|
|
|
1683
1684
|
"multiple": false,
|
|
1684
1685
|
"type": "option"
|
|
1685
1686
|
},
|
|
1687
|
+
"extension": {
|
|
1688
|
+
"description": "Extension id from swell.json: makes the component the UI of that extension",
|
|
1689
|
+
"name": "extension",
|
|
1690
|
+
"hasDynamicHelp": false,
|
|
1691
|
+
"multiple": false,
|
|
1692
|
+
"type": "option"
|
|
1693
|
+
},
|
|
1686
1694
|
"overwrite": {
|
|
1687
1695
|
"description": "Overwrite existing file",
|
|
1688
1696
|
"name": "overwrite",
|
|
1689
1697
|
"allowNo": false,
|
|
1690
1698
|
"type": "boolean"
|
|
1691
1699
|
},
|
|
1700
|
+
"target": {
|
|
1701
|
+
"description": "Target id where the component renders; repeat for more targets",
|
|
1702
|
+
"name": "target",
|
|
1703
|
+
"hasDynamicHelp": false,
|
|
1704
|
+
"multiple": true,
|
|
1705
|
+
"type": "option"
|
|
1706
|
+
},
|
|
1692
1707
|
"yes": {
|
|
1693
1708
|
"char": "y",
|
|
1694
1709
|
"description": "Skip prompts, require all arguments",
|
|
@@ -1706,7 +1721,7 @@
|
|
|
1706
1721
|
"strict": true,
|
|
1707
1722
|
"summary": "Create an app component in the components folder.",
|
|
1708
1723
|
"helpMeta": {
|
|
1709
|
-
"usageDirect": "<name> [-d <description>] -y"
|
|
1724
|
+
"usageDirect": "<name> [-d <description>] [--extension <extension-id>] [--target <target-id>...] -y"
|
|
1710
1725
|
},
|
|
1711
1726
|
"isESM": true,
|
|
1712
1727
|
"relativePath": [
|
|
@@ -2346,7 +2361,7 @@
|
|
|
2346
2361
|
"create:tests": {
|
|
2347
2362
|
"aliases": [],
|
|
2348
2363
|
"args": {},
|
|
2349
|
-
"description": "Initialize a vitest + Cloudflare Workers test setup
|
|
2364
|
+
"description": "Initialize a vitest + Cloudflare Workers test setup for app functions.",
|
|
2350
2365
|
"examples": [
|
|
2351
2366
|
"$ swell create tests",
|
|
2352
2367
|
"$ swell create tests --overwrite"
|
|
@@ -3670,5 +3685,5 @@
|
|
|
3670
3685
|
]
|
|
3671
3686
|
}
|
|
3672
3687
|
},
|
|
3673
|
-
"version": "2.9.
|
|
3688
|
+
"version": "2.9.29"
|
|
3674
3689
|
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function envDtsTemplate(): string;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export function envDtsTemplate() {
|
|
2
|
-
return `\
|
|
3
|
-
declare module "cloudflare:test" {
|
|
4
|
-
interface ProvidedEnv {
|
|
5
|
-
SWELL_STORE_ID: string;
|
|
6
|
-
SWELL_SESSION_ID: string;
|
|
7
|
-
SWELL_API_BASE_URL: string;
|
|
8
|
-
SWELL_APP_ID: string;
|
|
9
|
-
SWELL_ENVIRONMENT?: string;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export {};
|
|
14
|
-
`;
|
|
15
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function integrationTestTemplate(): string;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export function integrationTestTemplate() {
|
|
2
|
-
return `\
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
import { createSwellClient } from "../helpers/swell-client";
|
|
5
|
-
|
|
6
|
-
describe("Store settings (read-only)", () => {
|
|
7
|
-
it("loads general store settings using CLI auth", async () => {
|
|
8
|
-
const swell = createSwellClient();
|
|
9
|
-
|
|
10
|
-
const general = await swell.get("/settings/general");
|
|
11
|
-
|
|
12
|
-
expect(general).toBeDefined();
|
|
13
|
-
expect(general.id).toBe("general");
|
|
14
|
-
expect(general.features).toBeDefined();
|
|
15
|
-
expect(typeof general.features.carts).toBe("boolean");
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
`;
|
|
19
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function swellClientTemplate(): string;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
export function swellClientTemplate() {
|
|
2
|
-
return `\
|
|
3
|
-
import { env } from "cloudflare:test";
|
|
4
|
-
|
|
5
|
-
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
6
|
-
|
|
7
|
-
type BasicSwellClient = Pick<
|
|
8
|
-
SwellAPI,
|
|
9
|
-
"get" | "post" | "put" | "delete" | "settings"
|
|
10
|
-
>;
|
|
11
|
-
|
|
12
|
-
async function makeRequest(
|
|
13
|
-
method: HttpMethod,
|
|
14
|
-
url: string,
|
|
15
|
-
data?: any
|
|
16
|
-
): Promise<any> {
|
|
17
|
-
const baseUrl = env.SWELL_API_BASE_URL || "https://api.swell.store";
|
|
18
|
-
const sessionId = env.SWELL_SESSION_ID;
|
|
19
|
-
const environment = env.SWELL_ENVIRONMENT || "test";
|
|
20
|
-
|
|
21
|
-
if (!sessionId) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
"Missing SWELL_SESSION_ID binding. Run \\\`swell login\\\` or set it explicitly for tests.",
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let endpointUrl = String(url).startsWith("/") ? url.substring(1) : String(url);
|
|
28
|
-
if (!endpointUrl.startsWith("data/")) {
|
|
29
|
-
endpointUrl = \`data/\${endpointUrl}\`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let fullUrl = \`\${baseUrl}/\${endpointUrl}\`;
|
|
33
|
-
const headers: HeadersInit = {
|
|
34
|
-
"Content-Type": "application/json;charset=UTF-8",
|
|
35
|
-
"User-Agent": "swell-app-tests/1.0",
|
|
36
|
-
"X-Session": sessionId,
|
|
37
|
-
"Swell-Env": environment,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const options: RequestInit = {
|
|
41
|
-
method,
|
|
42
|
-
headers,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
if (data) {
|
|
46
|
-
if (method === "GET") {
|
|
47
|
-
const params = new URLSearchParams();
|
|
48
|
-
|
|
49
|
-
function append(prefix: string, value: any) {
|
|
50
|
-
if (value === null || value === undefined) return;
|
|
51
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
52
|
-
for (const [k, v] of Object.entries(value)) {
|
|
53
|
-
append(\`\${prefix}[\${k}]\`, v);
|
|
54
|
-
}
|
|
55
|
-
} else if (Array.isArray(value)) {
|
|
56
|
-
for (const v of value) {
|
|
57
|
-
append(\`\${prefix}[]\`, v);
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
params.append(prefix, String(value));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
for (const [key, value] of Object.entries(data)) {
|
|
65
|
-
append(key, value);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const query = params.toString();
|
|
69
|
-
if (query) {
|
|
70
|
-
fullUrl += \`?\${query}\`;
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
options.body = JSON.stringify(data);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const response = await fetch(fullUrl, options);
|
|
78
|
-
const text = await response.text();
|
|
79
|
-
|
|
80
|
-
let result: any;
|
|
81
|
-
try {
|
|
82
|
-
result = text ? JSON.parse(text) : null;
|
|
83
|
-
} catch {
|
|
84
|
-
result = text;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new (globalThis as any).SwellError(result || text || "Request failed", {
|
|
89
|
-
status: response.status,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (result?.errors) {
|
|
94
|
-
throw new (globalThis as any).SwellError(result.errors, {
|
|
95
|
-
status: 400,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function createSwellClient(): BasicSwellClient {
|
|
103
|
-
return {
|
|
104
|
-
get(url: string, query?: any) {
|
|
105
|
-
return makeRequest("GET", url, query);
|
|
106
|
-
},
|
|
107
|
-
post(url: string, data?: any) {
|
|
108
|
-
return makeRequest("POST", url, data);
|
|
109
|
-
},
|
|
110
|
-
put(url: string, data?: any) {
|
|
111
|
-
return makeRequest("PUT", url, data);
|
|
112
|
-
},
|
|
113
|
-
delete(url: string, data?: any) {
|
|
114
|
-
return makeRequest("DELETE", url, data);
|
|
115
|
-
},
|
|
116
|
-
settings(id?: string) {
|
|
117
|
-
const appId = id || env.SWELL_APP_ID;
|
|
118
|
-
if (!appId) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"Missing app id. Pass an id to settings() or set SWELL_APP_ID binding.",
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return makeRequest("GET", \`/settings/\${appId}\`);
|
|
124
|
-
},
|
|
125
|
-
} as BasicSwellClient;
|
|
126
|
-
}
|
|
127
|
-
`;
|
|
128
|
-
}
|