@swell/cli 2.3.2 → 2.3.3

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.
@@ -297,6 +297,8 @@ ENVIRONMENT = "development"
297
297
  // Get all functions in this app
298
298
  const functions = await this.getAppFunctions();
299
299
  if (functions.length === 0) {
300
+ // Create routing server to proxy requests to frontend even without functions
301
+ await this.createFunctionRouter(serverPort);
300
302
  spinner.stop();
301
303
  return;
302
304
  }
@@ -308,7 +310,7 @@ ENVIRONMENT = "development"
308
310
  this.watchForChanges({
309
311
  onChange: this.onChangeFunctionWatcher.bind(this),
310
312
  });
311
- // Create a routing server that proxies requests to function servers
313
+ // Create routing server after function servers are ready
312
314
  await this.createFunctionRouter(serverPort);
313
315
  spinner.succeed(`App function server running on port ${serverPort}\n`);
314
316
  this.log(`${style.appConfigName(`Functions:`)}`);
@@ -18,7 +18,6 @@ export default class Logs extends SwellCommand {
18
18
  static summary: string;
19
19
  private output;
20
20
  run(): Promise<void>;
21
- __run(): Promise<void>;
22
21
  private getLogs;
23
22
  private getLogsAndWriteToStream;
24
23
  }
@@ -1,5 +1,4 @@
1
1
  import { Flags } from '@oclif/core';
2
- import ora from 'ora';
3
2
  import { LineOutput, LoggedItem, TableOutput } from '../lib/logs/index.js';
4
3
  import { SwellCommand } from '../swell-command.js';
5
4
  // the columns available to display in the table
@@ -19,6 +18,23 @@ const OUTPUT_COLUMNS = [
19
18
  const OUTPUT_COLUMNS_DEFAULTS = ['date', 'request', 'data', 'status', 'time'];
20
19
  // the interval to poll the API when following logs
21
20
  const FOLLOW_POLLING_INTERVAL = 1000 * 2; // 2 seconds
21
+ /**
22
+ * Converts $in value to array
23
+ * @param value value to convert
24
+ * @param op operator
25
+ * @param convertToNumber true if string value should be converted to number
26
+ * @returns converted value
27
+ */
28
+ function convertOperatorValue(value, op, convertToNumber = false) {
29
+ if (Array.isArray(value)) {
30
+ return value;
31
+ }
32
+ const converted = convertToNumber ? Number.parseInt(value, 10) : value;
33
+ if (op !== '$in') {
34
+ return converted;
35
+ }
36
+ return [converted];
37
+ }
22
38
  /**
23
39
  * Builds the request options to send to the API based on the flags passed
24
40
  * to the command.
@@ -36,7 +52,7 @@ function buildLogRequestBody(flags) {
36
52
  // some filters are simple and can be mapped directly to the API
37
53
  const logFilters = [
38
54
  { filter: 'app_id', flag: 'app', operator: '$in' },
39
- { filter: 'message.status', flag: 'status', operator: '$in' },
55
+ { filter: 'message.status', flag: 'status', operator: '$in', number: true },
40
56
  { filter: 'message.type', flag: 'type', operator: '$in' },
41
57
  ];
42
58
  // map the flags to the API query
@@ -44,7 +60,9 @@ function buildLogRequestBody(flags) {
44
60
  if (flags[filter.flag]) {
45
61
  andConditions.push({
46
62
  [filter.filter]: filter.operator
47
- ? { [filter.operator]: flags[filter.flag] }
63
+ ? {
64
+ [filter.operator]: convertOperatorValue(flags[filter.flag], filter.operator, filter.number),
65
+ }
48
66
  : flags[filter.flag],
49
67
  });
50
68
  }
@@ -145,7 +163,7 @@ export default class Logs extends SwellCommand {
145
163
  char: 'p',
146
164
  default: false,
147
165
  description: 'note that this flag will take more space in the terminal and require more time to load',
148
- summary: 'pretty print json data',
166
+ summary: 'pretty print json data in table output',
149
167
  }),
150
168
  search: Flags.string({
151
169
  char: 's',
@@ -163,10 +181,6 @@ export default class Logs extends SwellCommand {
163
181
  static summary = 'Output or stream store logs to the terminal.';
164
182
  output;
165
183
  async run() {
166
- const spinner = ora();
167
- spinner.fail('This command is temporarily disabled. Check back soon.');
168
- }
169
- async __run() {
170
184
  const { flags } = await this.parse(Logs);
171
185
  // indentify the columns to display
172
186
  const columns = flags.columns.split(',');
@@ -175,24 +189,30 @@ export default class Logs extends SwellCommand {
175
189
  flags.output === 'table'
176
190
  ? new TableOutput(columns, flags.pretty)
177
191
  : new LineOutput(columns, flags.pretty);
178
- // first run for getting the logs
179
- // we keep track of the last date we received so we can get logs after that
180
- flags.startPolling = await this.getLogsAndWriteToStream(flags);
181
192
  // if following, poll the API every FOLLOW_POLLING_INTERVAL seconds and
182
193
  // write new logs to the stream
183
194
  if (flags.follow) {
195
+ // first get the latest log to use its date
196
+ // we keep track of the last date we received so we can get logs after that
197
+ const previousNumber = flags.number;
198
+ flags.number = 1; // get the latest log
199
+ flags.startPolling = await this.getLogsAndWriteToStream(flags, false);
200
+ flags.number = previousNumber;
184
201
  setInterval(async () => {
185
202
  // when polling, we want to get logs after the last date we received
186
203
  flags.startPolling = await this.getLogsAndWriteToStream(flags);
187
204
  }, FOLLOW_POLLING_INTERVAL);
188
205
  }
206
+ else {
207
+ await this.getLogsAndWriteToStream(flags);
208
+ }
189
209
  }
190
210
  async getLogs(flags) {
191
211
  const body = buildLogRequestBody(flags);
192
212
  const response = await this.api.post({ adminPath: `/data/$get/:logs` }, { body });
193
213
  return response?.results?.reverse() || [];
194
214
  }
195
- async getLogsAndWriteToStream(flags) {
215
+ async getLogsAndWriteToStream(flags, show = true) {
196
216
  const logs = await this.getLogs(flags);
197
217
  let lastDate = '';
198
218
  if (!this.output) {
@@ -200,12 +220,13 @@ export default class Logs extends SwellCommand {
200
220
  }
201
221
  if (logs.length > 0) {
202
222
  lastDate = logs.at(-1).date;
203
- for (const log of logs) {
204
- this.output.write(this.output.prepareData(new LoggedItem(log)));
223
+ if (show) {
224
+ for (const log of logs) {
225
+ this.output.write(this.output.prepareData(new LoggedItem(log)));
226
+ }
205
227
  }
206
228
  }
207
- // if the user is following logs, we want to return the last date we
208
- // received
209
- return lastDate || flags.startPolling;
229
+ // if the user is following logs, we want to return the last date we received
230
+ return lastDate || flags.startPolling || new Date().toISOString();
210
231
  }
211
232
  }
@@ -89,6 +89,12 @@ export declare class AppConfigFunction extends AppConfig {
89
89
  isRootFunction(): boolean;
90
90
  preparePostData(postData: any): Promise<any>;
91
91
  }
92
+ export declare class AppConfigComponent extends AppConfig {
93
+ hasValues: boolean;
94
+ type: ConfigType;
95
+ isRootComponent(): boolean;
96
+ preparePostData(postData: any): Promise<any>;
97
+ }
92
98
  export declare class AppConfigAsset extends AppConfigDefault {
93
99
  hasValues: boolean;
94
100
  type: ConfigType;
@@ -3,7 +3,7 @@ import isEmpty from 'lodash/isEmpty.js';
3
3
  import { detectFilenameMime } from 'mime-detect';
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
- import { bundleFunction } from '../bundle.js';
6
+ import { bundleFunction, bundleComponent } from '../bundle.js';
7
7
  import { AllConfigPaths, ConfigType, filePathExists, hashFile, } from './index.js';
8
8
  export class IgnoringFileError extends Error {
9
9
  constructor(message) {
@@ -117,6 +117,9 @@ export class AppConfig {
117
117
  case ConfigType.THEME: {
118
118
  return new AppConfigTheme(attrs);
119
119
  }
120
+ case ConfigType.COMPONENT: {
121
+ return new AppConfigComponent(attrs);
122
+ }
120
123
  default: {
121
124
  // the default type is file
122
125
  const defaultConfig = new AppConfigDefault(attrs);
@@ -275,6 +278,43 @@ export class AppConfigFunction extends AppConfig {
275
278
  return postData;
276
279
  }
277
280
  }
281
+ export class AppConfigComponent extends AppConfig {
282
+ hasValues = true;
283
+ type = ConfigType.COMPONENT;
284
+ isRootComponent() {
285
+ return (this.isRootConfig('components') &&
286
+ (this.filePath.endsWith('.jsx') || this.filePath.endsWith('.tsx')));
287
+ }
288
+ async preparePostData(postData) {
289
+ if (!this.isRootComponent()) {
290
+ return postData;
291
+ }
292
+ try {
293
+ // get file contents and if it's empty ignore
294
+ const fileData = this.prepareFileData();
295
+ if (!fileData) {
296
+ return;
297
+ }
298
+ const { code, config } = await bundleComponent(this.filePath);
299
+ if (!config) {
300
+ throw new IgnoringFileError('Component must export a `config` object.');
301
+ }
302
+ // Save the original file and the bundled version
303
+ postData.file = {
304
+ data: fileData,
305
+ };
306
+ postData.build_file = {
307
+ content_type: 'application/javascript',
308
+ data: code,
309
+ };
310
+ postData.values = config;
311
+ }
312
+ catch (error) {
313
+ throw new FunctionProcessingError(`Unable to compile component ${this.name}`, error);
314
+ }
315
+ return postData;
316
+ }
317
+ }
278
318
  // Assets do not get installed but saved as plain files
279
319
  export class AppConfigAsset extends AppConfigDefault {
280
320
  hasValues = false;
@@ -93,6 +93,7 @@ export declare enum ConfigType {
93
93
  CONTENT = "content",
94
94
  FILE = "file",
95
95
  FRONTEND = "frontend",
96
+ COMPONENT = "component",
96
97
  FUNCTION = "function",
97
98
  MODEL = "model",
98
99
  NOTIFICATION = "notification",
@@ -26,6 +26,7 @@ export var ConfigType;
26
26
  ConfigType["CONTENT"] = "content";
27
27
  ConfigType["FILE"] = "file";
28
28
  ConfigType["FRONTEND"] = "frontend";
29
+ ConfigType["COMPONENT"] = "component";
29
30
  ConfigType["FUNCTION"] = "function";
30
31
  ConfigType["MODEL"] = "model";
31
32
  ConfigType["NOTIFICATION"] = "notification";
@@ -44,6 +45,7 @@ const ConfigTypeBatchOrder = [
44
45
  ConfigType.WEBHOOK,
45
46
  ConfigType.THEME,
46
47
  ConfigType.FRONTEND,
48
+ ConfigType.COMPONENT,
47
49
  ConfigType.FILE,
48
50
  ];
49
51
  // All available configs
@@ -51,6 +53,7 @@ const AllConfigTypes = [
51
53
  'ASSET',
52
54
  'CONTENT',
53
55
  'FRONTEND',
56
+ 'COMPONENT',
54
57
  'FUNCTION',
55
58
  'MODEL',
56
59
  'NOTIFICATION',
@@ -1 +1,2 @@
1
1
  export declare function bundleFunction(filePath: string): Promise<any>;
2
+ export declare function bundleComponent(filePath: string): Promise<any>;
@@ -36,6 +36,45 @@ export async function bundleFunction(filePath) {
36
36
  throw new Error(`Unable to compile function ${filePath}: ${error.message}`);
37
37
  }
38
38
  }
39
+ export async function bundleComponent(filePath) {
40
+ try {
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ const stdinLoader = filePath.endsWith('.tsx') ? 'tsx' : 'jsx';
43
+ const buildResult = await esbuild.build({
44
+ stdin: {
45
+ contents: `
46
+ ${content}
47
+ import { render, h } from "preact";
48
+ export const preact = { render, h };
49
+ `,
50
+ loader: stdinLoader,
51
+ resolveDir: path.dirname(filePath),
52
+ },
53
+ bundle: true,
54
+ minify: true,
55
+ platform: 'browser',
56
+ format: 'iife',
57
+ globalName: 'Component',
58
+ loader: {
59
+ '.ts': 'ts',
60
+ '.jsx': 'jsx',
61
+ '.tsx': 'tsx',
62
+ },
63
+ jsxFactory: 'h',
64
+ jsxFragment: 'Fragment',
65
+ write: false,
66
+ logLevel: 'silent',
67
+ });
68
+ const { outputFiles: [{ text: code }], } = buildResult;
69
+ // eslint-disable-next-line no-new-func
70
+ const evalFn = new Function(`${code}\nreturn { ...Component }`);
71
+ const { config } = evalFn();
72
+ return { code, config };
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Unable to compile component ${filePath}: ${error.message}`);
76
+ }
77
+ }
39
78
  function getSwellFunctionWrapper() {
40
79
  const __filename = fileURLToPath(import.meta.url);
41
80
  const __dirname = path.dirname(__filename);
@@ -79,7 +79,7 @@ export class TableOutput extends Output {
79
79
  width: 6,
80
80
  },
81
81
  };
82
- return columns[column];
82
+ return columns[column] || { width: 6 };
83
83
  }
84
84
  prepareColumnDisplay(column, extraWidth) {
85
85
  const display = this.displayOptions(column);
@@ -792,14 +792,14 @@ export class PushAppCommand extends RemoteAppCommand {
792
792
  let deploymentUrl;
793
793
  let interactiveError = false;
794
794
  let pagesProjectError = false;
795
+ let outputBuffer = '';
795
796
  this.log(`\nDeploying to Cloudflare...\n`);
796
797
  try {
797
798
  await this.execFrontend(`npx wrangler deploy`, (string) => {
798
- // Dependent on wrangler output
799
- // Parse the deployment URL from the wrangler output.
800
- const match = string.match(/^\s*(https:\/\/\S+\.workers\.dev)\s*[\S\s]*?Current Version ID: [\w-]+/m);
801
- if (match && match.length > 0) {
802
- deploymentUrl = match[1];
799
+ // Accumulate output for URL parsing after command completes
800
+ outputBuffer += string;
801
+ // Suppress workers.dev URL from output (users should use swell domain)
802
+ if (string.match(/^\s*https:\/\/\S+\.workers\.dev\s*$/m)) {
803
803
  return false;
804
804
  }
805
805
  // Check for Pages project error
@@ -834,6 +834,12 @@ export class PushAppCommand extends RemoteAppCommand {
834
834
  throw error;
835
835
  }
836
836
  }
837
+ // Parse deployment URL from accumulated output
838
+ // Match the workers.dev URL that appears on its own line after "Deployed ... triggers"
839
+ const urlMatch = outputBuffer.match(/^\s*(https:\/\/\S+\.workers\.dev)\s*$/m);
840
+ if (urlMatch && urlMatch[1]) {
841
+ deploymentUrl = urlMatch[1];
842
+ }
837
843
  if (!deploymentUrl) {
838
844
  this.error('Unable to retrieve deployment URL.');
839
845
  }
@@ -190,7 +190,7 @@
190
190
  "char": "p",
191
191
  "description": "note that this flag will take more space in the terminal and require more time to load",
192
192
  "name": "pretty",
193
- "summary": "pretty print json data",
193
+ "summary": "pretty print json data in table output",
194
194
  "allowNo": false,
195
195
  "type": "boolean"
196
196
  },
@@ -999,7 +999,7 @@
999
999
  "name": "targetPath"
1000
1000
  }
1001
1001
  },
1002
- "description": "Pull all app files, a specific file, or a specific configuration\ntype from an app in your store's test environment to your local machine.\n\nIf APPID is not specified, you will be prompted with a list of apps to choose from.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/\nfrontend/",
1002
+ "description": "Pull all app files, a specific file, or a specific configuration\ntype from an app in your store's test environment to your local machine.\n\nIf APPID is not specified, you will be prompted with a list of apps to choose from.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\ncomponents/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/\nfrontend/",
1003
1003
  "examples": [
1004
1004
  "swell app pull",
1005
1005
  "swell app pull example_app",
@@ -1048,7 +1048,7 @@
1048
1048
  "name": "file"
1049
1049
  }
1050
1050
  },
1051
- "description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png) and swell.json.\n- If a file is specified, only that file will be pushed to the store.\n- If a directory is specified, only files in that directory will be pushed.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/",
1051
+ "description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png) and swell.json.\n- If a file is specified, only that file will be pushed to the store.\n- If a directory is specified, only files in that directory will be pushed.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\ncomponents/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/",
1052
1052
  "examples": [
1053
1053
  "swell app push",
1054
1054
  "swell app push content",
@@ -2875,5 +2875,5 @@
2875
2875
  ]
2876
2876
  }
2877
2877
  },
2878
- "version": "2.3.2"
2878
+ "version": "2.3.3"
2879
2879
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [