@youcan/app 2.2.0 → 2.3.1

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.
@@ -1,14 +1,13 @@
1
- import { AppCommand } from '@/util/theme-command';
1
+ import { AppCommand } from '@/util/app-command';
2
2
  declare class Dev extends AppCommand {
3
3
  static description: string;
4
- private app;
5
- private session;
6
4
  private readonly hotKeys;
7
5
  run(): Promise<any>;
6
+ private prepareNetworkOptions;
8
7
  reloadWorkers(): Promise<void>;
9
8
  private runWorkers;
10
- private syncAppConfig;
11
9
  private prepareDevProcesses;
10
+ private buildEnvironmentVariables;
12
11
  private openAppPreview;
13
12
  }
14
13
  export default Dev;
@@ -1,13 +1,11 @@
1
- import { Session, Tasks, UI, Env, Http, Filesystem, Path, System } from '@youcan/cli-kit';
2
- import { AppCommand } from '../../../util/theme-command.js';
1
+ import { Session, Tasks, UI, System, Services, Filesystem, Path, Env, Http } from '@youcan/cli-kit';
2
+ import { bootTunnelWorker, bootAppWorker, bootWebWorker, bootExtensionWorker } from '../../services/dev/workers/index.js';
3
+ import { AppCommand } from '../../../util/app-command.js';
3
4
  import { load } from '../../../util/app-loader.js';
4
5
  import { APP_CONFIG_FILENAME } from '../../../constants.js';
5
- import { bootAppWorker, bootWebWorker, bootExtensionWorker } from '../../services/dev/workers/index.js';
6
6
 
7
7
  class Dev extends AppCommand {
8
8
  static description = 'Run the app in dev mode';
9
- app;
10
- session;
11
9
  hotKeys = [
12
10
  {
13
11
  keyboardKey: 'p',
@@ -25,60 +23,77 @@ class Dev extends AppCommand {
25
23
  this.app = await load();
26
24
  const { workers } = await Tasks.run({ cmd: this, workers: [] }, [
27
25
  {
28
- title: 'Syncing app configuration..',
29
- task: async () => await this.syncAppConfig(),
26
+ title: 'Preparing network options...',
27
+ task: async (ctx) => {
28
+ ctx.workers.push(await this.prepareNetworkOptions());
29
+ },
30
+ },
31
+ {
32
+ title: 'Syncing app configuration...',
33
+ task: async () => { await this.syncAppConfig(); },
30
34
  },
31
35
  {
32
36
  title: 'Preparing dev processes...',
33
37
  task: async (ctx) => {
34
- ctx.workers = await this.prepareDevProcesses();
38
+ ctx.workers.push(...await this.prepareDevProcesses());
35
39
  },
36
40
  },
37
41
  ]);
38
42
  UI.renderDevOutput({ hotKeys: this.hotKeys, cmd: this });
39
43
  this.runWorkers(workers);
40
44
  }
45
+ async prepareNetworkOptions() {
46
+ const port = await System.getNextAvailablePort(3000);
47
+ this.app.network_config = {
48
+ app_port: port,
49
+ app_url: `http://localhost:${port}`
50
+ };
51
+ const worker = await bootTunnelWorker(this, this.app, new Services.Cloudflared());
52
+ this.app.config = {
53
+ ...this.app.config,
54
+ app_url: worker.getUrl(),
55
+ redirect_urls: this.app.config.redirect_urls?.length > 0
56
+ ? this.app.config.redirect_urls.map(r => new URL(new URL(r).pathname, worker.getUrl()).toString())
57
+ : [new URL('/auth/callback', worker.getUrl()).toString()]
58
+ };
59
+ await Filesystem.writeJsonFile(Path.join(this.app.root, APP_CONFIG_FILENAME), this.app.config);
60
+ return worker;
61
+ }
41
62
  async reloadWorkers() {
42
63
  this.controller = new AbortController();
64
+ // Preserve network config.
65
+ const networkConfig = this.app.network_config;
43
66
  this.app = await load();
67
+ this.app.network_config = networkConfig;
44
68
  await this.syncAppConfig();
45
69
  await this.runWorkers(await this.prepareDevProcesses());
46
70
  }
47
71
  async runWorkers(workers) {
48
- await Promise.all(workers.map(worker => worker.run())).catch(_ => { });
49
- }
50
- async syncAppConfig() {
51
- const endpoint = this.app.config.id == null
52
- ? `${Env.apiHostname()}/apps/create`
53
- : `${Env.apiHostname()}/apps/${this.app.config.id}/update`;
54
- const res = await Http.post(endpoint, {
55
- headers: { Authorization: `Bearer ${this.session.access_token}` },
56
- body: JSON.stringify({
57
- name: this.app.config.name,
58
- app_url: this.app.config.app_url,
59
- redirect_urls: this.app.config.redirect_urls,
60
- }),
61
- });
62
- this.app.config = {
63
- name: res.name,
64
- id: res.id,
65
- app_url: res.app_url,
66
- redirect_urls: res.redirect_urls,
67
- oauth: {
68
- scopes: res.scopes,
69
- client_id: res.client_id,
70
- },
71
- };
72
- await Filesystem.writeJsonFile(Path.join(this.app.root, APP_CONFIG_FILENAME), this.app.config);
72
+ await Promise.all(workers.map(worker => worker.run())).catch((_) => { });
73
73
  }
74
74
  async prepareDevProcesses() {
75
75
  const promises = [
76
76
  bootAppWorker(this, this.app),
77
77
  ];
78
- this.app.webs.forEach(web => promises.unshift(bootWebWorker(this, this.app, web)));
78
+ this.app.webs.forEach(web => promises.unshift(bootWebWorker(this, this.app, web, this.buildEnvironmentVariables())));
79
79
  this.app.extensions.forEach(ext => promises.unshift(bootExtensionWorker(this, this.app, ext)));
80
80
  return Promise.all(promises);
81
81
  }
82
+ buildEnvironmentVariables() {
83
+ if (!this.app.remote_config) {
84
+ throw new Error('remote app config not loaded');
85
+ }
86
+ if (!this.app.network_config) {
87
+ throw new Error('app network config is not set');
88
+ }
89
+ return {
90
+ YOUCAN_API_KEY: this.app.remote_config.client_id,
91
+ YOUCAN_API_SECRET: this.app.remote_config.client_secret,
92
+ YOUCAN_API_SCOPES: this.app.remote_config.scopes.join(','),
93
+ APP_URL: this.app.network_config.app_url,
94
+ PORT: this.app.network_config.app_port.toString(),
95
+ };
96
+ }
82
97
  async openAppPreview() {
83
98
  const endpointUrl = `${Env.apiHostname()}/apps/${this.app.config.id}/authorization-url`;
84
99
  const { url } = await Http.get(endpointUrl);
@@ -0,0 +1,7 @@
1
+ import { AppCommand } from '@/util/app-command';
2
+ declare class EnvShow extends AppCommand {
3
+ static description: string;
4
+ run(): Promise<any>;
5
+ private printEnvironmentVariables;
6
+ }
7
+ export default EnvShow;
@@ -0,0 +1,29 @@
1
+ import { Session, Tasks, Color } from '@youcan/cli-kit';
2
+ import { AppCommand } from '../../../../util/app-command.js';
3
+ import { load } from '../../../../util/app-loader.js';
4
+
5
+ class EnvShow extends AppCommand {
6
+ static description = 'Display app environment variables';
7
+ async run() {
8
+ this.app = await load();
9
+ this.session = await Session.authenticate(this);
10
+ await Tasks.run({}, [
11
+ {
12
+ title: 'Syncing app configuration..',
13
+ task: async () => { await this.syncAppConfig(); },
14
+ },
15
+ ]);
16
+ await this.printEnvironmentVariables();
17
+ }
18
+ async printEnvironmentVariables() {
19
+ if (!this.app.remote_config) {
20
+ throw new Error('remote app config not loaded');
21
+ }
22
+ this.log();
23
+ this.log(`${Color.yellow('YOUCAN_API_KEY')}=%s`, this.app.remote_config.client_id);
24
+ this.log(`${Color.yellow('YOUCAN_API_SECRET')}=%s`, this.app.remote_config.client_secret);
25
+ this.log(`${Color.yellow('YOUCAN_API_SCOPES')}=%s`, this.app.remote_config.scopes.join(','));
26
+ }
27
+ }
28
+
29
+ export { EnvShow as default };
@@ -1,4 +1,4 @@
1
- import { AppCommand } from '@/util/theme-command';
1
+ import { AppCommand } from '@/util/app-command';
2
2
  declare class GenerateExtension extends AppCommand {
3
3
  static description: string;
4
4
  run(): Promise<any>;
@@ -1,5 +1,5 @@
1
1
  import { Path, Filesystem, String, Tasks } from '@youcan/cli-kit';
2
- import { AppCommand } from '../../../../util/theme-command.js';
2
+ import { AppCommand } from '../../../../util/app-command.js';
3
3
  import extensions from '../../../services/generate/extensions/index.js';
4
4
  import { ensureExtensionDirectoryExists, initThemeExtension } from '../../../services/generate/generate.js';
5
5
  import { APP_CONFIG_FILENAME } from '../../../../constants.js';
@@ -1,4 +1,4 @@
1
- import { AppCommand } from '@/util/theme-command';
1
+ import { AppCommand } from '@/util/app-command';
2
2
  export default class Install extends AppCommand {
3
3
  static description: string;
4
4
  run(): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { Session, Tasks, Http, Env, System } from '@youcan/cli-kit';
2
2
  import { load } from '../../../util/app-loader.js';
3
- import { AppCommand } from '../../../util/theme-command.js';
3
+ import { AppCommand } from '../../../util/app-command.js';
4
4
 
5
5
  class Install extends AppCommand {
6
6
  static description = 'Generate an app installation URL';
@@ -11,8 +11,7 @@ class AppWorker extends Worker.Abstract {
11
11
  this.app = app;
12
12
  this.logger = new Worker.Logger('app', 'green');
13
13
  }
14
- async boot() {
15
- }
14
+ async boot() { }
16
15
  async run() {
17
16
  await this.command.output.wait(500);
18
17
  this.logger.write('watching for config updates...');
@@ -1,11 +1,14 @@
1
- import type { Cli, Worker } from '@youcan/cli-kit';
1
+ import type { Cli, Services, Worker } from '@youcan/cli-kit';
2
2
  import WebWorker from './web-worker';
3
3
  import AppWorker from './app-worker';
4
+ import TunnelWorker from './tunnel-worker';
4
5
  import type { App, Extension, Web } from '@/types';
5
6
  import type DevCommand from '@/cli/commands/app/dev';
7
+ import type { AppCommand } from '@/util/app-command';
6
8
  export interface ExtensionWorkerCtor {
7
9
  new (command: Cli.Command, app: App, extension: Extension): Worker.Interface;
8
10
  }
9
11
  export declare function bootAppWorker(command: DevCommand, app: App): Promise<AppWorker>;
10
12
  export declare function bootExtensionWorker(command: Cli.Command, app: App, extension: Extension): Promise<Worker.Interface>;
11
- export declare function bootWebWorker(command: Cli.Command, app: App, web: Web): Promise<WebWorker>;
13
+ export declare function bootWebWorker(command: Cli.Command, app: App, web: Web, env: Record<string, string>): Promise<WebWorker>;
14
+ export declare function bootTunnelWorker(command: AppCommand, app: App, tunnel: Services.Cloudflared): Promise<TunnelWorker>;
@@ -1,6 +1,7 @@
1
1
  import ThemeExtensionWorker from './theme-extension-worker.js';
2
2
  import WebWorker from './web-worker.js';
3
3
  import AppWorker from './app-worker.js';
4
+ import TunnelWorker from './tunnel-worker.js';
4
5
 
5
6
  const EXTENSION_WORKERS = {
6
7
  theme: ThemeExtensionWorker,
@@ -16,10 +17,15 @@ async function bootExtensionWorker(command, app, extension) {
16
17
  await worker.boot();
17
18
  return worker;
18
19
  }
19
- async function bootWebWorker(command, app, web) {
20
- const worker = new WebWorker(command, app, web);
20
+ async function bootWebWorker(command, app, web, env) {
21
+ const worker = new WebWorker(command, app, web, env);
22
+ await worker.boot();
23
+ return worker;
24
+ }
25
+ async function bootTunnelWorker(command, app, tunnel) {
26
+ const worker = new TunnelWorker(command, app, tunnel);
21
27
  await worker.boot();
22
28
  return worker;
23
29
  }
24
30
 
25
- export { bootAppWorker, bootExtensionWorker, bootWebWorker };
31
+ export { bootAppWorker, bootExtensionWorker, bootTunnelWorker, bootWebWorker };
@@ -0,0 +1,16 @@
1
+ import { Worker } from '@youcan/cli-kit';
2
+ import type { Services } from '@youcan/cli-kit';
3
+ import type { App } from '@/types';
4
+ import type { AppCommand } from '@/util/app-command';
5
+ export default class TunnelWorker extends Worker.Abstract {
6
+ private command;
7
+ private app;
8
+ private tunnelService;
9
+ private readonly logger;
10
+ private url;
11
+ constructor(command: AppCommand, app: App, tunnelService: Services.Cloudflared);
12
+ boot(): Promise<void>;
13
+ run(): Promise<void>;
14
+ private checkForError;
15
+ getUrl(): string;
16
+ }
@@ -0,0 +1,53 @@
1
+ import { Worker, System } from '@youcan/cli-kit';
2
+
3
+ class TunnelWorker extends Worker.Abstract {
4
+ command;
5
+ app;
6
+ tunnelService;
7
+ logger;
8
+ url = null;
9
+ constructor(command, app, tunnelService) {
10
+ super();
11
+ this.command = command;
12
+ this.app = app;
13
+ this.tunnelService = tunnelService;
14
+ this.logger = new Worker.Logger('tunnel', 'dim');
15
+ }
16
+ async boot() {
17
+ if (!this.app.network_config) {
18
+ throw new Error('app network config is not set');
19
+ }
20
+ this.logger.write('start tunneling the app');
21
+ await this.tunnelService.tunnel(this.app.network_config.app_port);
22
+ let attempts = 0;
23
+ while (!this.url && attempts <= 28) {
24
+ const url = this.tunnelService.getUrl();
25
+ if (url) {
26
+ this.url = url;
27
+ this.app.network_config.app_url = this.url;
28
+ this.logger.write(`tunneled url obtained: \`${url}\``);
29
+ }
30
+ await System.sleep(0.5);
31
+ }
32
+ if (!this.url) {
33
+ this.logger.write('could not establish a tunnel, using localhost instead');
34
+ }
35
+ }
36
+ async run() {
37
+ setInterval(() => this.checkForError, 500);
38
+ }
39
+ checkForError() {
40
+ const error = this.tunnelService.getError();
41
+ if (error) {
42
+ throw new Error(`tunnel stopped: ${error}`);
43
+ }
44
+ }
45
+ getUrl() {
46
+ if (!this.url) {
47
+ throw new Error('app url not set');
48
+ }
49
+ return this.url;
50
+ }
51
+ }
52
+
53
+ export { TunnelWorker as default };
@@ -1,11 +1,12 @@
1
1
  import { type Cli, Worker } from '@youcan/cli-kit';
2
2
  import type { App, Web } from '@/types';
3
3
  export default class WebWorker extends Worker.Abstract {
4
- private command;
5
- private app;
6
- private web;
4
+ private readonly command;
5
+ private readonly app;
6
+ private readonly web;
7
+ private readonly env;
7
8
  private logger;
8
- constructor(command: Cli.Command, app: App, web: Web);
9
+ constructor(command: Cli.Command, app: App, web: Web, env: Record<string, string>);
9
10
  boot(): Promise<void>;
10
11
  run(): Promise<void>;
11
12
  }
@@ -4,12 +4,14 @@ class WebWorker extends Worker.Abstract {
4
4
  command;
5
5
  app;
6
6
  web;
7
+ env;
7
8
  logger;
8
- constructor(command, app, web) {
9
+ constructor(command, app, web, env) {
9
10
  super();
10
11
  this.command = command;
11
12
  this.app = app;
12
13
  this.web = web;
14
+ this.env = env;
13
15
  this.logger = new Worker.Logger(this.web.config.name || 'web', 'blue');
14
16
  }
15
17
  async boot() {
@@ -20,6 +22,7 @@ class WebWorker extends Worker.Abstract {
20
22
  stdout: this.logger,
21
23
  signal: this.command.controller.signal,
22
24
  stderr: new Worker.Logger(this.web.config.name || 'web', 'red'),
25
+ env: this.env,
23
26
  });
24
27
  }
25
28
  }
package/dist/types.d.ts CHANGED
@@ -11,6 +11,16 @@ export type AppConfig = {
11
11
  scopes: string[];
12
12
  };
13
13
  } & InitialAppConfig;
14
+ export interface RemoteAppConfig {
15
+ id: string;
16
+ name: string;
17
+ app_url: string;
18
+ owner_id: string;
19
+ client_id: string;
20
+ client_secret: string;
21
+ redirect_urls: string[];
22
+ scopes: string[];
23
+ }
14
24
  export interface ExtensionConfig {
15
25
  [key: string]: unknown;
16
26
  type: string;
@@ -54,6 +64,11 @@ export interface App {
54
64
  root: string;
55
65
  webs: Web[];
56
66
  config: AppConfig;
67
+ remote_config?: RemoteAppConfig;
68
+ network_config?: {
69
+ app_url: string;
70
+ app_port: number;
71
+ };
57
72
  extensions: Extension[];
58
73
  }
59
74
  export interface ExtensionFileDescriptor {
@@ -0,0 +1,8 @@
1
+ import type { Session } from '@youcan/cli-kit';
2
+ import { Cli } from '@youcan/cli-kit';
3
+ import type { App } from '@/types';
4
+ export declare abstract class AppCommand extends Cli.Command {
5
+ protected app: App;
6
+ protected session: Session.StoreSession;
7
+ syncAppConfig(): Promise<App>;
8
+ }
@@ -0,0 +1,35 @@
1
+ import { Cli, Env, Http, Filesystem, Path } from '@youcan/cli-kit';
2
+ import { APP_CONFIG_FILENAME } from '../constants.js';
3
+
4
+ class AppCommand extends Cli.Command {
5
+ app;
6
+ session;
7
+ async syncAppConfig() {
8
+ const endpoint = this.app.config.id == null
9
+ ? `${Env.apiHostname()}/apps/create`
10
+ : `${Env.apiHostname()}/apps/${this.app.config.id}/update`;
11
+ const res = await Http.post(endpoint, {
12
+ headers: { Authorization: `Bearer ${this.session.access_token}` },
13
+ body: JSON.stringify({
14
+ name: this.app.config.name,
15
+ app_url: this.app.config.app_url,
16
+ redirect_urls: this.app.config.redirect_urls
17
+ }),
18
+ });
19
+ this.app.config = {
20
+ name: res.name,
21
+ id: res.id,
22
+ app_url: res.app_url,
23
+ redirect_urls: res.redirect_urls,
24
+ oauth: {
25
+ scopes: res.scopes,
26
+ client_id: res.client_id,
27
+ },
28
+ };
29
+ await Filesystem.writeJsonFile(Path.join(this.app.root, APP_CONFIG_FILENAME), this.app.config);
30
+ this.app.remote_config = res;
31
+ return this.app;
32
+ }
33
+ }
34
+
35
+ export { AppCommand };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/app",
3
3
  "type": "module",
4
- "version": "2.2.0",
4
+ "version": "2.3.1",
5
5
  "description": "OCLIF plugin for building apps",
6
6
  "author": "YouCan <contact@youcan.shop> (https://youcan.shop)",
7
7
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "@oclif/core": "^2.15.0",
19
19
  "dayjs": "^1.11.10",
20
- "@youcan/cli-kit": "2.2.0"
20
+ "@youcan/cli-kit": "2.3.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@oclif/plugin-legacy": "^1.3.0",
@@ -25,7 +25,12 @@
25
25
  "shx": "^0.3.4"
26
26
  },
27
27
  "oclif": {
28
- "commands": "./dist/cli/commands"
28
+ "commands": "./dist/cli/commands",
29
+ "topics": {
30
+ "app:env": {
31
+ "description": "Manage app environment variables"
32
+ }
33
+ }
29
34
  },
30
35
  "scripts": {
31
36
  "build": "shx rm -rf dist && tsc --noEmit && rollup --config rollup.config.js",
@@ -1,3 +0,0 @@
1
- import { Cli } from '@youcan/cli-kit';
2
- export declare abstract class AppCommand extends Cli.Command {
3
- }
@@ -1,6 +0,0 @@
1
- import { Cli } from '@youcan/cli-kit';
2
-
3
- class AppCommand extends Cli.Command {
4
- }
5
-
6
- export { AppCommand };