Skip to content
Draft
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@
"prettier": "^2.1.2",
"typescript": "^4.3.2"
}
}
}
9 changes: 2 additions & 7 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,8 @@ const args = ((): CLIArgs & { _unknown: Array<string> } => {
parsedArgs['dev'] = true;
}

// Initialize default working directory
if (parsedArgs['workdir'] === '') {
parsedArgs['workdir'] = process.cwd();
}

// Initialize default project name
if (parsedArgs['projectName'] === '') {
// Initialize default project name only when workdir was provided
if (parsedArgs['projectName'] === '' && parsedArgs['workdir'] !== '') {
parsedArgs['projectName'] = basename(parsedArgs['workdir']);
}

Expand Down
17 changes: 14 additions & 3 deletions src/cli/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,24 @@ export const error = (message: string, exitCode = 1): never => {
return process.exit(exitCode);
};
export const apiError = (err: ProtocolError): never => {
const response = err.response as
| { status?: number; data?: unknown }
| undefined;
const status =
response && typeof response.status === 'number'
? response.status
: undefined;
const data = response?.data;
const statusStr = status !== undefined ? String(status) : '';
const dataStr =
data !== undefined && data !== null
? String(data)
: String(err.message ?? '');
// eslint-disable-next-line no-console
console.error(
chalk.redBright.bold('X') +
chalk.redBright(
` Server responded with error code: ${
err.response?.status || ''
} ${err.response?.data as string}`
` Server responded with error code: ${statusStr} ${dataStr}`.trim()
)
);
return process.exit(1);
Expand Down
9 changes: 5 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ void (async () => {
if (args['logout']) return logout();

const config = await startup(args['confDir']);
const api: APIInterface = API(
config.token as string,
args['dev'] ? config.devURL : config.baseURL
);
const baseURL =
process.env.TEST_DEPLOY_LOCAL === 'true' || args['dev']
? config.devURL
: config.baseURL;
const api: APIInterface = API(config.token as string, baseURL);

await validateToken(api);

Expand Down
6 changes: 5 additions & 1 deletion src/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

import { auth } from './auth';
import args from './cli/args';
import { Config, defaultPath, load } from './config';
import { Config, defaultPath, load, save } from './config';

const devToken = 'local'; // Use some random token in order to proceed

export const startup = async (confDir: string | undefined): Promise<Config> => {
const config = await load(confDir || defaultPath);
const token = args['dev'] ? devToken : await auth(config);

if (args['dev'] && token === devToken) {
await save({ token }, confDir || defaultPath);
}

return Object.assign(config, { token });
};
32 changes: 21 additions & 11 deletions src/test/cli.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ import {
runCLI
} from './cli';

// Strip ANSI escape codes so assertions match plain text (CLI uses chalk)
const ansiCsiRe = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
const stripAnsi = (s: string | unknown): string =>
String(s).replace(ansiCsiRe, '');

describe('Integration CLI (Deploy)', function () {
this.timeout(2000000);

const url = 'https://github.com/metacall/examples';
const addRepoSuffix = 'metacall-examples';
// FaaS uses repositoryName(url) = last path segment, so "examples" not "metacall-examples"
const addRepoSuffix =
url
.split('/')
.pop()
?.replace(/\.git$/, '') ?? 'metacall-examples';

const workDirSuffix = 'time-app-web';
const filePath = join(
Expand Down Expand Up @@ -132,7 +142,7 @@ describe('Integration CLI (Deploy)', function () {
[keys.enter, 'n', keys.enter, keys.kill]
).promise;

ok(String(result).includes('i Deploying...\n'));
ok(stripAnsi(result).includes('i Deploying...\n'));

strictEqual(await deployed(addRepoSuffix), true);
return result;
Expand Down Expand Up @@ -168,7 +178,7 @@ describe('Integration CLI (Deploy)', function () {
const result = await runCLI(['--delete'], [keys.enter, keys.enter])
.promise;

ok(String(result).includes('i Deploy Delete Succeed\n'));
ok(stripAnsi(result).includes('i Deploy Delete Succeed\n'));

strictEqual(await deleted(addRepoSuffix), true);

Expand All @@ -182,7 +192,7 @@ describe('Integration CLI (Deploy)', function () {
[keys.enter, 'n', keys.enter, keys.kill]
).promise;

ok(String(result).includes(`i Deploying ${filePath}...\n`));
ok(stripAnsi(result).includes(`i Deploying ${filePath}...\n`));

strictEqual(await deployed(workDirSuffix), true);
return result;
Expand All @@ -193,7 +203,7 @@ describe('Integration CLI (Deploy)', function () {
const result = await runCLI(['--delete'], [keys.enter, keys.enter])
.promise;

ok(String(result).includes('i Deploy Delete Succeed\n'));
ok(stripAnsi(result).includes('i Deploy Delete Succeed\n'));

strictEqual(await deleted(workDirSuffix), true);

Expand All @@ -214,7 +224,7 @@ describe('Integration CLI (Deploy)', function () {
]
).promise;

ok(String(result).includes('i Deploying...\n'));
ok(stripAnsi(result).includes('i Deploying...\n'));

strictEqual(await deployed(addRepoSuffix), true);
return result;
Expand All @@ -225,7 +235,7 @@ describe('Integration CLI (Deploy)', function () {
const result = await runCLI(['--delete'], [keys.enter, keys.enter])
.promise;

ok(String(result).includes('i Deploy Delete Succeed\n'));
ok(stripAnsi(result).includes('i Deploy Delete Succeed\n'));

strictEqual(await deleted(workDirSuffix), true);

Expand All @@ -247,7 +257,7 @@ describe('Integration CLI (Deploy)', function () {
[keys.enter, keys.kill]
).promise;

ok(String(result).includes(`i Deploying ${projectPath}...\n`));
ok(stripAnsi(result).includes(`i Deploying ${projectPath}...\n`));

strictEqual(await deployed('env'), true);
return result;
Expand All @@ -258,7 +268,7 @@ describe('Integration CLI (Deploy)', function () {
const result = await runCLI(['--delete'], [keys.enter, keys.enter])
.promise;

ok(String(result).includes('i Deploy Delete Succeed\n'));
ok(stripAnsi(result).includes('i Deploy Delete Succeed\n'));

strictEqual(await deleted('env'), true);

Expand All @@ -276,7 +286,7 @@ describe('Integration CLI (Deploy)', function () {
[keys.enter, 'n', keys.enter, keys.kill]
).promise;

ok(String(result).includes(`i Deploying ${filePath}...\n`));
ok(stripAnsi(result).includes(`i Deploying ${filePath}...\n`));

strictEqual(await deployed(workDirSuffix), true);

Expand Down Expand Up @@ -326,7 +336,7 @@ describe('Integration CLI (Deploy)', function () {
const result = await runCLI(['--delete'], [keys.enter, keys.enter])
.promise;

ok(String(result).includes('i Deploy Delete Succeed\n'));
ok(stripAnsi(result).includes('i Deploy Delete Succeed\n'));

strictEqual(await deleted(workDirSuffix), true);

Expand Down
70 changes: 52 additions & 18 deletions src/test/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,31 +88,53 @@ export const runWithInput = (
}, 3000);
};

const stderrChunks: Buffer[] = [];
child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk));

const isRealError = (stderrText: string): boolean => {
const trimmed = stderrText.trim();
if (!trimmed) return false;
// Ignore Node deprecation warnings (e.g. util.isArray, DEP0044)
if (/DeprecationWarning|DEP\d+|util\.isArray/i.test(trimmed))
return false;
if (/\(node:\d+\)\s*\[DEP/.test(trimmed)) return false;
// Real errors: CLI error prefix or HTTP/request errors
return (
trimmed.startsWith('X ') ||
trimmed.startsWith('! ') ||
trimmed.startsWith('Error:') ||
/Request failed|status code \d{3}/.test(trimmed)
);
};

return {
promise: new Promise((resolve, reject) => {
child.stderr?.once('data', err => {
child.stdin?.end();

if (childTimeout) {
clearTimeout(childTimeout);
inputs = [];
}
reject(String(err));
});

child.on('error', reject);

loop(inputs);

child.stdout?.pipe(
concat(result => {
if (killTimeout) {
clearTimeout(killTimeout);
if (killTimeout) clearTimeout(killTimeout);
if (childTimeout) clearTimeout(childTimeout);
const stderrText = Buffer.concat(stderrChunks).toString();
if (isRealError(stderrText)) {
child.stdin?.end();
reject(stderrText);
} else {
resolve(result.toString());
}

resolve(result.toString());
})
);

// If process exits before stdout ends, settle with buffered stderr
child.on('close', (code, signal) => {
if (childTimeout) clearTimeout(childTimeout);
if (code !== 0 && signal !== 'SIGTERM') {
const stderrText = Buffer.concat(stderrChunks).toString();
if (isRealError(stderrText)) reject(stderrText);
}
});
}),
child
};
Expand All @@ -130,7 +152,11 @@ export const keys = Object.freeze({

export const deployed = async (suffix: string): Promise<boolean> => {
const config = await startup(args['confDir']);
const api = API(config.token as string, config.baseURL);
const baseURL =
process.env.TEST_DEPLOY_LOCAL === 'true'
? config.devURL
: config.baseURL;
const api = API(config.token as string, baseURL);

const sleep = (ms: number): Promise<ReturnType<typeof setTimeout>> =>
new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -162,7 +188,11 @@ export const deployed = async (suffix: string): Promise<boolean> => {

export const deleted = async (suffix: string): Promise<boolean> => {
const config = await startup(args['confDir']);
const api = API(config.token as string, config.baseURL);
const baseURL =
process.env.TEST_DEPLOY_LOCAL === 'true'
? config.devURL
: config.baseURL;
const api = API(config.token as string, baseURL);

const sleep = (ms: number): Promise<ReturnType<typeof setTimeout>> =>
new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -198,10 +228,14 @@ export const generateRandomString = (length: number): string => {
};

export const runCLI = (args: string[], inputs: string[]) => {
if (process.env.TEST_DEPLOY_LOCAL === 'true') {
const useLocal = process.env.TEST_DEPLOY_LOCAL === 'true';
if (useLocal) {
args.push('--dev');
}
return runWithInput('dist/index.js', args, inputs);
const env: Record<string, string> = useLocal
? { TEST_DEPLOY_LOCAL: 'true' }
: {};
return runWithInput('dist/index.js', args, inputs, env);
};

export const clearCache = async (): Promise<void> => {
Expand Down