Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create draft app command #53

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/app/lib/cli/commands/app/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Env, Filesystem, Http, Path, Session, Tasks } from '@youcan/cli-kit';
import { AppCommand } from '@/util/theme-command';
import type { InitialAppConfig } from '@/types';

class Dev extends AppCommand {
async run(): Promise<any> {
const session = await Session.authenticate(this);
const path = Path.resolve(Path.cwd(), this.configFileName());

await Tasks.run<{ config?: InitialAppConfig }>([
{
title: 'Loading app configuration..',
async task(context, _) {
if (!await Filesystem.exists(path)) {
throw new Error('Could not find the app\'s configuration file.');
}

context.config = await Filesystem.readJsonFile<InitialAppConfig>(path);
},
},
{
title: 'Creating draft app..',
skip(ctx) {
return ctx.config!.id != null;
},
async task(context, _) {
const res = await Http.post<Record<string, any>>(`${Env.apiHostname()}/apps/draft/create`, {
headers: { Authorization: `Bearer ${session.access_token}` },
body: JSON.stringify({ name: context.config!.name }),
});

context.config = {
name: res.name,
id: res.id,
url: res.url,
oauth: {
scopes: res.scopes,
client_id: res.client_id,
},
};

await Filesystem.writeJsonFile(path, context.config);
},
},
]);
}
}

export default Dev;
1 change: 1 addition & 0 deletions packages/app/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CONFIGURATION_FILE_NAME = 'youcan.app.json';
11 changes: 11 additions & 0 deletions packages/app/lib/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Flags } from '@oclif/core';
import { Path } from '@youcan/cli-kit';

export const APP_FLAGS = {
path: Flags.string({
env: 'YC_FLAG_PATH',
default: async () => Path.cwd(),
parse: async input => Path.resolve(input),
description: 'The path to your app directory.',
}),
};
14 changes: 14 additions & 0 deletions packages/app/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface InitialAppConfig {
[key: string]: unknown
name: string
}

export type AppConfig = {
id: string
url: string

oauth: {
client_id: string
scopes: string[]
}
} & InitialAppConfig;
8 changes: 8 additions & 0 deletions packages/app/lib/util/theme-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Cli } from '@youcan/cli-kit';
import { CONFIGURATION_FILE_NAME } from '@/constants';

export abstract class AppCommand extends Cli.Command {
protected configFileName() {
return CONFIGURATION_FILE_NAME;
}
}
35 changes: 35 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@youcan/app",
"type": "module",
"version": "1.1.0-beta.0",
"description": "OCLIF plugin for building apps",
"author": "YouCan <[email protected]> (https://youcan.shop)",
"license": "MIT",
"keywords": [
"youcan",
"youcan-cli",
"youcan-app"
],
"files": [
"dist",
"oclif.manifest.json"
],
"scripts": {
"build": "shx rm -rf dist && tsc --noEmit && rollup --config rollup.config.js",
"dev": "rollup --config rollup.config.js --watch",
"release": "pnpm publish --access public",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@oclif/core": "^2.15.0",
"@youcan/cli-kit": "workspace:*"
},
"devDependencies": {
"@oclif/plugin-legacy": "^1.3.0",
"@types/node": "^18.18.0",
"shx": "^0.3.4"
},
"oclif": {
"commands": "./dist/cli/commands"
}
}
18 changes: 18 additions & 0 deletions packages/app/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fileURLToPath } from 'node:url';
import typescript from 'rollup-plugin-typescript2';
import { nodeExternals } from 'rollup-plugin-node-externals';
import { glob } from 'glob';

export default {
input: glob.sync('lib/**/*.ts').map(f => fileURLToPath(new URL(f, import.meta.url))),
plugins: [
nodeExternals(),
typescript({ tsconfig: 'tsconfig.json' }),
],
output: {
dir: 'dist',
exports: 'named',
format: 'es',
preserveModules: true,
},
};
16 changes: 16 additions & 0 deletions packages/app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "lib",
"paths": {
"@/*": [
"./lib/*"
]
}
},
"include": [
"lib",
"rollup.config.js"
]
}
1 change: 1 addition & 0 deletions packages/cli-kit/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * as Github from './node/github';
export * as System from './node/system';
export * as Config from './node/config';
export * as Crypto from './node/crypto';
export * as Session from './node/session';
export * as Callback from './node/callback';
export * as Filesystem from './node/filesystem';

Expand Down
26 changes: 26 additions & 0 deletions packages/cli-kit/lib/node/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import FilesystemPromises from 'fs/promises';
import type { Mode, OpenMode, PathLike } from 'fs';
import { temporaryDirectoryTask } from 'tempy';
import FsExtra from 'fs-extra';

Expand Down Expand Up @@ -28,3 +29,28 @@ interface MoveFileOptions {
export async function move(src: string, dest: string, options: MoveFileOptions = {}): Promise<void> {
await FsExtra.move(src, dest, options);
}

export async function readFile(
path: PathLike,
options: { encoding?: BufferEncoding; flag?: OpenMode } = { encoding: 'utf-8', flag: 'r' },
): Promise<Buffer | string> {
return await FilesystemPromises.readFile(path, options);
}

export async function writeFile(
path: PathLike,
data: string,
options: { encoding: BufferEncoding; flag: Mode } = { encoding: 'utf-8', flag: 'w' },
): Promise<void> {
return await FilesystemPromises.writeFile(path, data, options);
}

export async function readJsonFile<T = Record<string, unknown>>(path: PathLike): Promise<T> {
const file = await readFile(path);

return JSON.parse(file instanceof Buffer ? file.toString() : file);
}

export async function writeJsonFile(path: PathLike, data: Record<string, unknown>): Promise<void> {
return writeFile(path, JSON.stringify(data, null, 4));
}
3 changes: 2 additions & 1 deletion packages/cli-kit/lib/node/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ async function agent() {

export const DEFAULT_HTTP_CLIENT_OPTIONS = {
headers: {
Accept: 'application/json',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
agent: await agent(),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { Callback, type Cli, Crypto, Env, Http, System } from '@youcan/cli-kit';
import { Callback, type Cli, Config, Crypto, Env, Http, System } from '..';

const LS_PORT = 3000;
const LS_HOST = 'localhost';

export interface StoreSession {
id: string
slug: string
access_token: string
}

export interface StoreResponse {
interface StoreResponse {
slug: string
store_id: string
is_active: boolean
is_email_verified: boolean
}

export async function isSessionValid(session: StoreSession): Promise<boolean> {
async function isSessionValid(session: StoreSession): Promise<boolean> {
try {
const store = await Http.get<{ is_active: boolean }>(
`${Env.apiHostname()}/me`,
Expand All @@ -30,7 +24,7 @@ export async function isSessionValid(session: StoreSession): Promise<boolean> {
}
}

export async function exchange(code: string) {
async function exchange(code: string) {
const params = {
code,
client_id: Env.oauthClientId(),
Expand All @@ -50,7 +44,7 @@ export async function exchange(code: string) {
return result.access_token;
}

export async function authorize(command: Cli.Command, state: string = Crypto.randomHex(30)) {
async function authorize(command: Cli.Command, state: string = Crypto.randomHex(30)) {
const AUTHORIZATION_URL = Env.sellerAreaHostname();

if (!await System.isPortAvailable(LS_PORT)) {
Expand All @@ -63,7 +57,9 @@ export async function authorize(command: Cli.Command, state: string = Crypto.ran
message: `Would you like to terminate ${await System.getPortProcessName(LS_PORT)}?`,
});

!confirmed && command.output.error('Exiting..');
if (!confirmed) {
throw new Error('Exiting..');
}

await System.killPortProcess(LS_PORT);
}
Expand All @@ -90,3 +86,57 @@ export async function authorize(command: Cli.Command, state: string = Crypto.ran

return result.code;
}

export interface StoreSession {
id: string
slug: string
access_token: string
}

export async function authenticate(command: Cli.Command): Promise<StoreSession> {
const existingSession = Config
.manager({ projectName: 'youcan-cli' })
.get('store_session');

if (existingSession && await isSessionValid(existingSession)) {
return existingSession;
}

const accessToken = await exchange(await authorize(command));

const { stores } = await Http.get<{ stores: StoreResponse[] }>(
`${Env.apiHostname()}/stores`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);

const active = stores.filter(s => s.is_active);
if (!active.length) {
throw new Error('No active stores found');
}

const { selected } = await command.prompt({
type: 'select',
name: 'selected',
message: 'Select a store to log into',
choices: active.map(s => ({ title: s.slug, value: s.store_id })),
});

const store = stores.find(s => s.store_id === selected)!;

const { token: storeAccessToken } = await Http.post<{ token: string }>(
`${Env.apiHostname()}/switch-store/${store.store_id}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);

const session = {
slug: store.slug,
id: store.store_id,
access_token: storeAccessToken,
};

Config
.manager({ projectName: 'youcan-cli' })
.set('store_session', session);

return session;
}
12 changes: 6 additions & 6 deletions packages/cli-kit/lib/node/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { exit } from 'process';
import { Loader } from '@/internal/node/ui';

export interface Task<T = unknown> {
Expand All @@ -15,16 +16,16 @@ async function runTask<T>(task: Task<T>, ctx: T) {
return await task.task(ctx, task);
}

export async function run<T = unknown>(tasks: Task[]) {
export async function run<T = unknown>(tasks: Task<T>[]) {
const context: T = {} as T;

for (const task of tasks) {
for await (const task of tasks) {
await Loader.exec(task.title, async (loader) => {
try {
const subtasks = await runTask(task, context);
const subtasks = await runTask<T>(task, context);

if (Array.isArray(subtasks) && subtasks.length > 0 && subtasks.every(t => 'task' in t)) {
for (const subtask of subtasks) {
for await (const subtask of subtasks) {
await runTask(subtask, context);
}
}
Expand All @@ -33,8 +34,7 @@ export async function run<T = unknown>(tasks: Task[]) {
}
catch (err) {
loader.error(String(err));

throw err;
exit(1);
}
});
}
Expand Down
Loading