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

Auto-updating development mode on file changes #19

Merged
merged 10 commits into from
Jan 21, 2020
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ zeplin connect [options]
|--------------|---------------------------------------------------|-------------------------|
| -f, --file | Path to components configuration file | .zeplin/components.json |
| -d, --dev | Activate development mode | false |
| --no-watch | Disable watching file changes on development mode | false |
| -p, --plugin | npm package name of a Zeplin CLI `connect` plugin | |
| -h, --help | Output usage information | |
| --verbose | Enable verbose logs | |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@hapi/joi": "^16.1.7",
"axios": "^0.19.0",
"chalk": "^3.0.0",
"chokidar": "^3.3.1",
"ci-info": "^2.0.0",
"commander": "^4.1.0",
"endent": "^1.3.0",
Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ const connectCommand = program.command("connect")
.description("Connect components to code")
.option("-f, --file <file>", "Full path to components file", createCollector(), defaults.commands.connect.filePaths)
.option("-d, --dev", "Activate development mode", defaults.commands.connect.devMode)
.option("--no-watch", "Disable watch files on development mode", defaults.commands.connect.devModeWatch)
.option("-p, --plugin <plugin>", "npm package name of a Zeplin CLI connect plugin", createCollector(), [])
.action(commandRunner(async options => {
const connectOptions: ConnectOptions = {
configFiles: options.file,
devMode: options.dev,
devModePort: defaults.commands.connect.port,
devModeWatch: options.watch,
plugins: options.plugin
};

Expand Down
128 changes: 96 additions & 32 deletions src/commands/connect/index.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,119 @@
import chalk from "chalk";
import chokidar from "chokidar";
import dedent from "ts-dedent";

import logger from "../../util/logger";
import path from "path";
import { indent } from "../../util/text";
import { getComponentConfigFiles } from "./config";
import { ConnectedBarrelComponents } from "./interfaces/api";
import { connectComponentConfigFiles } from "./plugin";
import { ConnectDevServer } from "./server";
import { ConnectedComponentsService } from "./service";
import { indent } from "../../util/text";
import logger from "../../util/logger";

export interface ConnectOptions {
configFiles: string[];
devMode: boolean;
devModePort: number;
plugins: string[];
}
const getComponentFilePaths = (connectedBarrels: ConnectedBarrelComponents[]): string[] =>
connectedBarrels.map(f =>
f.connectedComponents.map(c => path.resolve(c.path))
yuqu marked this conversation as resolved.
Show resolved Hide resolved
).reduce((a, b) => [...a, ...b], []);

export async function connect(options: ConnectOptions): Promise<void> {
try {
logger.debug(`connect options: ${JSON.stringify(options)}`);
const connectComponents = async (options: Pick<ConnectOptions, "configFiles" | "plugins">): Promise<ConnectedBarrelComponents[]> => {
const {
configFiles,
plugins
} = options;

const {
configFiles,
plugins,
devMode,
devModePort
} = options;
const componentConfigFiles = await getComponentConfigFiles(configFiles, plugins);

const componentConfigFiles = await getComponentConfigFiles(configFiles, plugins);
logger.debug(`component config files: ${JSON.stringify(componentConfigFiles)}`);

logger.debug(`component config files: ${JSON.stringify(componentConfigFiles)}`);
const connectedBarrels = await connectComponentConfigFiles(componentConfigFiles);

const connectedBarrels = await connectComponentConfigFiles(componentConfigFiles);
logger.debug(`connected barrels output: ${JSON.stringify(connectedBarrels)}`);

logger.debug(`connected barrels output: ${JSON.stringify(connectedBarrels)}`);
return connectedBarrels;
};

if (devMode) {
logger.info("Starting development server…");
const startDevServer = async (
options: Pick<ConnectOptions, "configFiles" | "devModePort" | "devModeWatch" | "plugins">,
connectedBarrels: ConnectedBarrelComponents[]
): Promise<void> => {
const {
configFiles,
devModePort,
devModeWatch,
plugins
} = options;

const devServer = new ConnectDevServer(connectedBarrels);
logger.info("Starting development server…");

await devServer.start(devModePort);
const devServer = new ConnectDevServer(connectedBarrels);

logger.info(`Development server is started on port ${devModePort}!`);
} else {
logger.info("Connecting all connected components into Zeplin…");
await devServer.start(devModePort);

logger.info(chalk.green(`Development server is started.`));

if (devModeWatch) {
let componentFiles = getComponentFilePaths(connectedBarrels);

const watcher = chokidar.watch(
[...configFiles, ...componentFiles],
yuqu marked this conversation as resolved.
Show resolved Hide resolved
{
cwd: process.cwd(),
persistent: true,
awaitWriteFinish: true
}
);

watcher.on("change", async filePath => {
logger.info((chalk.yellow(`\nFile change detected ${filePath}.\n`)));

try {
const updatedConnectedBarrels = await connectComponents({ configFiles, plugins });

const service = new ConnectedComponentsService();
watcher.unwatch(componentFiles);

await service.uploadConnectedBarrels(connectedBarrels);
devServer.updateConnectedBarrels(updatedConnectedBarrels);

logger.info("🦄 Components successfully connected to components in Zeplin.");
componentFiles = getComponentFilePaths(updatedConnectedBarrels);

watcher.add(componentFiles);
} catch (error) {
logger.error(chalk.red(dedent`
Could not update components.
${error}
`));
}
});
}
};

const upload = async (connectedBarrels: ConnectedBarrelComponents[]): Promise<void> => {
logger.info("Connecting all connected components into Zeplin…");

const service = new ConnectedComponentsService();

await service.uploadConnectedBarrels(connectedBarrels);

logger.info("🦄 Components successfully connected to components in Zeplin.");
};

export interface ConnectOptions {
configFiles: string[];
devMode: boolean;
devModePort: number;
devModeWatch: boolean;
plugins: string[];
}

export async function connect(options: ConnectOptions): Promise<void> {
try {
logger.debug(`connect options: ${JSON.stringify(options)}`);

const connectedBarrels = await connectComponents(options);

if (options.devMode) {
await startDevServer(options, connectedBarrels);
} else {
await upload(connectedBarrels);
}
} catch (error) {
error.message = dedent`
Expand Down
5 changes: 5 additions & 0 deletions src/commands/connect/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const connectComponentConfig = async (
const pluginPromises = plugins.map(async plugin => {
try {
if (plugin.supports(component)) {
logger.debug(`${plugin.name} supports ${component.path}. Processing…`);
const componentData = await plugin.process(component);

data.push({
Expand All @@ -110,6 +111,10 @@ const connectComponentConfig = async (
componentData.links?.forEach(link =>
urlPaths.push(processLink(link))
);

logger.debug(`${plugin.name} processed ${component.path}: ${componentData}`);
} else {
logger.debug(`${plugin.name} does not support ${component.path}.`);
}
} catch (err) {
throw new CLIError(dedent`
Expand Down
49 changes: 42 additions & 7 deletions src/commands/connect/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import express from "express";
import { ConnectedBarrelComponents, ConnectedComponent } from "../interfaces/api";
import { CLIError } from "../../../errors";
import { Server } from "http";
import { OK } from "http-status-codes";
import { Socket } from "net";
import { CLIError } from "../../../errors";
import logger from "../../../util/logger";
import { ConnectedBarrelComponents, ConnectedComponent } from "../interfaces/api";

export class ConnectDevServer {
connectedBarrels: ConnectedBarrelComponents[] = [];
stopped = false;
server: Server | undefined;
connections: Socket[] = [];

constructor(connectedBarrels: ConnectedBarrelComponents[]) {
this.connectedBarrels = connectedBarrels;
Expand All @@ -18,7 +24,15 @@ export class ConnectDevServer {
return found ? found.connectedComponents : null;
}

start(port: number): Promise<void> {
updateConnectedBarrels(connectedBarrels: ConnectedBarrelComponents[]): void {
this.connectedBarrels = connectedBarrels;
}

start(port: number): Promise<Server> {
if (this.server && this.server.listening) {
return Promise.resolve(this.server);
}

const app = express();

// CORS
Expand All @@ -37,16 +51,37 @@ export class ConnectDevServer {
return res.status(OK).json({ connectedComponents });
});

const promise = new Promise<void>((resolve, reject): void => {
app.listen(port, resolve)
return new Promise<Server>((resolve, reject): void => {
this.server = app.listen(port)
.on("listening", () => {
logger.debug(`Started dev server on port ${port}`);
resolve(this.server);
})
.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
reject(new CLIError(`Port ${port} is already in use.`));
}
})
.on("connection", connection => {
this.connections.push(connection);
connection.on("close", () =>
(this.connections = this.connections.filter(curr => curr !== connection))
);
});
});
}

stop(): Promise<void> {
logger.debug("Stopping dev server.");

return promise;
this.stopped = true;
this.connections.forEach(conn => conn.end());

return new Promise((resolve): void => {
this.server?.close(() => {
logger.debug("Stopped dev server.");
resolve();
});
});
}
}

1 change: 1 addition & 0 deletions src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const defaults = {
connect: {
filePaths: [".zeplin/components.json"],
devMode: false,
devModeWatch: true,
port: 9756
}
},
Expand Down