Skip to content
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"private": true,
"dependencies": {
"@koa/cors": "^3.3.0",
"chokidar": "^3.5.3",
"bcryptjs": "^2.4.3",
"bytes": "^3.1.2",
"class-validator": "^0.13.2",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ program
'./vulcan.yaml'
)
.option('-p --port <port>', 'server port', '3000')
.option('-w --watch', 'watch file changes', false)
.action(async (options) => {
await handleStart(options);
});
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ const defaultOptions: BuildCommandOptions = {
config: './vulcan.yaml',
};

export const mergeBuildDefaultOption = (
options: Partial<BuildCommandOptions>
) => {
return {
...defaultOptions,
...options,
} as BuildCommandOptions;
};

export const buildVulcan = async (options: BuildCommandOptions) => {
const configPath = path.resolve(process.cwd(), options.config);
const config: any = jsYAML.load(await fs.readFile(configPath, 'utf-8'));
Expand All @@ -36,9 +45,5 @@ export const buildVulcan = async (options: BuildCommandOptions) => {
export const handleBuild = async (
options: Partial<BuildCommandOptions>
): Promise<void> => {
options = {
...defaultOptions,
...options,
};
await buildVulcan(options as BuildCommandOptions);
await buildVulcan(mergeBuildDefaultOption(options));
};
35 changes: 27 additions & 8 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as jsYAML from 'js-yaml';
import { promises as fs } from 'fs';
import * as path from 'path';
import { addShutdownJob, localModulePath, logger } from '../utils';
import {
addShutdownJob,
localModulePath,
logger,
removeShutdownJob,
} from '../utils';

export interface ServeCommandOptions {
config: string;
Expand All @@ -13,6 +18,15 @@ const defaultOptions: ServeCommandOptions = {
port: 3000,
};

export const mergeServeDefaultOption = (
options: Partial<ServeCommandOptions>
) => {
return {
...defaultOptions,
...options,
} as ServeCommandOptions;
};

export const serveVulcan = async (options: ServeCommandOptions) => {
const configPath = path.resolve(process.cwd(), options.config);
const config: any = jsYAML.load(await fs.readFile(configPath, 'utf-8'));
Expand All @@ -25,19 +39,24 @@ export const serveVulcan = async (options: ServeCommandOptions) => {
const server = new VulcanServer(config);
await server.start();
logger.info(`Server is listening at port ${config.port || 3000}.`);
addShutdownJob(async () => {

const closeServerJob = async () => {
logger.info(`Stopping server...`);
await server.close();
logger.info(`Server stopped`);
});
};
addShutdownJob(closeServerJob);

return {
stopServer: async () => {
removeShutdownJob(closeServerJob);
await closeServerJob();
},
};
};

export const handleServe = async (
options: Partial<ServeCommandOptions>
): Promise<void> => {
options = {
...defaultOptions,
...options,
};
await serveVulcan(options as ServeCommandOptions);
await serveVulcan(mergeServeDefaultOption(options));
};
128 changes: 123 additions & 5 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,127 @@
import { BuildCommandOptions, handleBuild } from './build';
import { handleServe, ServeCommandOptions } from './serve';
import {
BuildCommandOptions,
buildVulcan,
mergeBuildDefaultOption,
} from './build';
import {
mergeServeDefaultOption,
ServeCommandOptions,
serveVulcan,
} from './serve';
import * as chokidar from 'chokidar';
import * as jsYAML from 'js-yaml';
import { promises as fs } from 'fs';
import * as path from 'path';
import { addShutdownJob, logger } from '../utils';

const callAfterFulfilled = (func: () => Promise<void>) => {
let busy = false;
let waitQueue: (() => void)[] = [];
const runJob = () => {
const currentPromises = waitQueue;
waitQueue = [];
busy = true;
func().finally(() => {
currentPromises.forEach((resolve) => resolve());
busy = false;
if (waitQueue.length > 0) runJob();
});
};
const callback = () =>
new Promise<void>((resolve) => {
waitQueue.push(resolve);
if (!busy) runJob();
});
return callback;
};

export interface StartCommandOptions {
watch: boolean;
config: string;
}

const defaultOptions: StartCommandOptions = {
config: './vulcan.yaml',
watch: false,
};

export const mergeStartDefaultOption = (
options: Partial<StartCommandOptions>
) => {
return {
...defaultOptions,
...options,
} as StartCommandOptions;
};

export const handleStart = async (
options: Partial<BuildCommandOptions & ServeCommandOptions>
options: Partial<
StartCommandOptions & BuildCommandOptions & ServeCommandOptions
>
): Promise<void> => {
await handleBuild(options);
await handleServe(options);
const buildOptions = mergeBuildDefaultOption(options);
const serveOptions = mergeServeDefaultOption(options);
const startOptions = mergeStartDefaultOption(options);

const configPath = path.resolve(process.cwd(), startOptions.config);
const config: any = jsYAML.load(await fs.readFile(configPath, 'utf-8'));

let stopServer: (() => Promise<any>) | undefined;

const restartServer = async () => {
if (stopServer) await stopServer();
try {
await buildVulcan(buildOptions);
stopServer = (await serveVulcan(serveOptions)).stopServer;
} catch (e) {
// Ignore the error to keep watch process works
if (!startOptions.watch) throw e;
}
};

await restartServer();

if (startOptions.watch) {
const pathsToWatch: string[] = [];

// YAML files
const schemaReader = config['schema-parser']?.['reader'];
if (schemaReader === 'LocalFile') {
pathsToWatch.push(
`${path
.resolve(config['schema-parser']?.['folderPath'])
.split(path.sep)
.join('/')}/**/*.yaml`
);
} else {
logger.warn(
`We can't watch with schema parser reader: ${schemaReader}, ignore it.`
);
}

// SQL files
const templateProvider = config['template']?.['provider'];
if (templateProvider === 'LocalFile') {
pathsToWatch.push(
`${path
.resolve(config['template']?.['folderPath'])
.split(path.sep)
.join('/')}/**/*.sql`
);
} else {
logger.warn(
`We can't watch with template provider: ${templateProvider}, ignore it.`
);
}

const restartWhenFulfilled = callAfterFulfilled(restartServer);
const watcher = chokidar
.watch(pathsToWatch, { ignoreInitial: true })
.on('all', () => restartWhenFulfilled());
addShutdownJob(async () => {
logger.info(`Stop watching changes...`);
await watcher.close();
});
logger.info(`Start watching changes...`);
}
};
5 changes: 5 additions & 0 deletions packages/cli/src/utils/shutdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const addShutdownJob = (job: JobBeforeShutdown) => {
shutdownJobs.push(job);
};

export const removeShutdownJob = (job: JobBeforeShutdown) => {
const index = shutdownJobs.indexOf(job);
if (index >= 0) shutdownJobs.splice(index, 1);
};

export const runShutdownJobs = async () => {
logger.info('Ctrl-C signal caught, stopping services...');
await Promise.all(shutdownJobs.map((job) => job()));
Expand Down
24 changes: 23 additions & 1 deletion packages/cli/test/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ beforeAll(async () => {
}, 30000);

afterAll(async () => {
await fs.rm(projectRoot, { recursive: true, force: true });
await fs.rm(projectRoot, { recursive: true, force: true, maxRetries: 5 });
});

afterEach(async () => {
Expand Down Expand Up @@ -83,6 +83,28 @@ it('Start command should build the project and start Vulcan server', async () =>
await runShutdownJobs();
});

it('Start command with watch option should watch the file changes', (done) => {
// Arrange
const agent = supertest(`http://localhost:${testingServerPort}`);
const testYamlPath = path.resolve(projectRoot, 'sqls', 'test.yaml');

// Act
program
.parseAsync(['node', 'vulcan', 'start', '-w'])
// Wait for server start
.then(() => new Promise((resolve) => setTimeout(resolve, 1000)))
// Write some invalid configs, let server fail to start
.then(() => fs.writeFile(testYamlPath, 'url: /user/:id\ns', 'utf-8'))
// Wait for serer restart
.then(() => new Promise((resolve) => setTimeout(resolve, 1000)))
.then(() => expect(agent.get('/doc')).rejects.toThrow())
.finally(() => {
runShutdownJobs()
.then(() => fs.rm(testYamlPath))
.then(() => done());
});
}, 5000);

it('Version command should execute without error', async () => {
// Action, Assert
await expect(
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2157,7 +2157,7 @@ charenc@0.0.2:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==

chokidar@^3.5.1:
chokidar@^3.5.1, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
Expand Down