Skip to content

Commit 71e243b

Browse files
authored
Merge pull request #166 from Web3-API/feature/w3-build-watch
Watch mode for `w3 build` command
2 parents f88b96f + ff1aba0 commit 71e243b

9 files changed

Lines changed: 221 additions & 46 deletions

File tree

demos/simple-storage/protocol/src/query/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function getData(input: Input_getData): u32 {
55
const res = Ethereum_Query.callView({
66
address: input.address,
77
method: "function get() view returns (uint256)",
8-
args: []
8+
args: [],
99
});
1010

1111
return U32.parseInt(res);

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"assemblyscript": "0.17.14",
3535
"axios": "0.19.2",
3636
"chalk": "4.1.0",
37+
"chokidar": "^3.5.1",
3738
"fs-extra": "9.0.1",
3839
"gluegun": "4.6.1",
3940
"graphql-tag": "2.11.0",

packages/cli/src/commands/build.ts

Lines changed: 109 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
/* eslint-disable prefer-const */
2-
import { Compiler, Project, SchemaComposer } from "../lib";
2+
import {
3+
Compiler,
4+
Project,
5+
SchemaComposer,
6+
Watcher,
7+
WatchEvent,
8+
watchEventName,
9+
} from "../lib";
310
import { fixParameters } from "../lib/helpers/parameters";
411
import { publishToIPFS } from "../lib/publishers/ipfs-publisher";
512

613
import chalk from "chalk";
714
import axios from "axios";
15+
import readline from "readline";
816
import { GluegunToolbox } from "gluegun";
917

1018
const HELP = `
@@ -15,11 +23,9 @@ Options:
1523
-i, --ipfs [<node>] Upload build results to an IPFS node (default: dev-server's node)
1624
-o, --output-dir <path> Output directory for build results (default: build/)
1725
-e, --test-ens <[address,]domain> Publish the package to a test ENS domain locally (requires --ipfs)
26+
-w, --watch Automatically rebuild when changes are made (default: false)
1827
`;
1928

20-
// TODO: add to the above options when implemented
21-
// -w, --watch Regenerate types when web3api files change (default: false)
22-
2329
export default {
2430
alias: ["b"],
2531
description: "Builds a Web3API and (optionally) uploads it to IPFS",
@@ -145,56 +151,117 @@ export default {
145151
schemaComposer,
146152
});
147153

148-
let result = false;
154+
const execute = async (): Promise<boolean> => {
155+
compiler.clearCache();
156+
const result = await compiler.compile();
149157

150-
/*if (watch) {
151-
// TODO: https://github.com/Web3-API/prototype/issues/98
152-
// compiler.watchAndCompile();
153-
} else*/ {
154-
result = await compiler.compile();
155-
if (result === false) {
156-
process.exitCode = 1;
157-
return;
158+
if (!result) {
159+
return result;
158160
}
159-
}
160161

161-
const uris: string[][] = [];
162+
const uris: string[][] = [];
163+
164+
// publish to IPFS
165+
if (ipfs) {
166+
const cid = await publishToIPFS(outputDir, ipfs);
167+
168+
print.success(`IPFS { ${cid} }`);
169+
uris.push(["Web3API IPFS", `ipfs://${cid}`]);
170+
171+
if (testEns) {
172+
if (!ensAddress) {
173+
uris.push(["ENS Registry", `${ethProvider}/${ensAddress}`]);
174+
}
162175

163-
// publish to IPFS
164-
if (ipfs) {
165-
const cid = await publishToIPFS(outputDir, ipfs);
176+
// ask the dev server to publish the CID to ENS
177+
const { data } = await axios.get(
178+
"http://localhost:4040/register-ens",
179+
{
180+
params: {
181+
domain: ensDomain,
182+
cid,
183+
},
184+
}
185+
);
166186

167-
print.success(`IPFS { ${cid} }`);
168-
uris.push(["Web3API IPFS", `ipfs://${cid}`]);
187+
if (data.success) {
188+
uris.push(["Web3API ENS", `${testEns} => ${cid}`]);
189+
} else {
190+
print.error(
191+
`ENS Resolution Failed { ${testEns} => ${cid} }\n` +
192+
`Ethereum Provider: ${ethProvider}\n` +
193+
`ENS Address: ${ensAddress}`
194+
);
195+
}
169196

170-
if (testEns) {
171-
if (!ensAddress) {
172-
uris.push(["ENS Registry", `${ethProvider}/${ensAddress}`]);
197+
return data.success;
173198
}
199+
}
174200

175-
// ask the dev server to publish the CID to ENS
176-
const { data } = await axios.get("http://localhost:4040/register-ens", {
177-
params: {
178-
domain: ensDomain,
179-
cid,
180-
},
201+
if (uris.length) {
202+
print.success("URI Viewers:");
203+
print.table(uris);
204+
return true;
205+
} else {
206+
return false;
207+
}
208+
};
209+
210+
if (!watch) {
211+
const result = await execute();
212+
213+
if (!result) {
214+
process.exitCode = 1;
215+
return;
216+
}
217+
} else {
218+
// Execute
219+
await execute();
220+
221+
const keyPressListener = () => {
222+
// Watch for escape key presses
223+
print.info(`Watching: ${project.manifestDir}`);
224+
print.info("Exit: [CTRL + C], [ESC], or [Q]");
225+
readline.emitKeypressEvents(process.stdin);
226+
process.stdin.on("keypress", async (str, key) => {
227+
if (
228+
key.name == "escape" ||
229+
key.name == "q" ||
230+
(key.name == "c" && key.ctrl)
231+
) {
232+
await watcher.stop();
233+
process.kill(process.pid, "SIGINT");
234+
}
181235
});
182236

183-
if (data.success) {
184-
uris.push(["Web3API ENS", `${testEns} => ${cid}`]);
185-
} else {
186-
print.error(
187-
`ENS Resolution Failed { ${testEns} => ${cid} }\n` +
188-
`Ethereum Provider: ${ethProvider}\n` +
189-
`ENS Address: ${ensAddress}`
190-
);
237+
if (process.stdin.setRawMode) {
238+
process.stdin.setRawMode(true);
191239
}
192-
}
193-
}
194240

195-
if (uris.length) {
196-
print.success("URI Viewers:");
197-
print.table(uris);
241+
process.stdin.resume();
242+
};
243+
244+
keyPressListener();
245+
246+
// Watch the directory
247+
const watcher = new Watcher();
248+
249+
watcher.start(project.manifestDir, {
250+
ignored: [outputDir + "/**", project.manifestDir + "/**/w3/**"],
251+
ignoreInitial: true,
252+
execute: async (events: WatchEvent[]) => {
253+
// Log all of the events encountered
254+
for (const event of events) {
255+
print.info(`${watchEventName(event.type)}: ${event.path}`);
256+
}
257+
258+
// Execute the build
259+
await execute();
260+
261+
// Process key presses
262+
keyPressListener();
263+
},
264+
});
198265
}
199266

200267
process.exitCode = 0;

packages/cli/src/lib/Compiler.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export class Compiler {
3636
}
3737
}
3838

39+
public clearCache(): void {
40+
this._config.project.clearCache();
41+
this._config.schemaComposer.clearCache();
42+
}
43+
3944
private async _compileWeb3Api(verbose?: boolean) {
4045
const { outputDir, project, schemaComposer } = this._config;
4146

@@ -44,7 +49,6 @@ export class Compiler {
4449
this._cleanDir(this._config.outputDir);
4550

4651
const manifest = await project.getManifest();
47-
4852
// Get the fully composed schema
4953
const composed = await schemaComposer.getComposedSchemas();
5054

packages/cli/src/lib/Project.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ export class Project {
2525
return !!this._config.quiet;
2626
}
2727

28-
async getManifest(): Promise<Manifest> {
28+
public async getManifest(): Promise<Manifest> {
2929
if (!this._manifest) {
3030
this._manifest = await loadManifest(this.manifestPath, this.quiet);
3131
}
3232

3333
return Promise.resolve(this._manifest);
3434
}
35+
36+
public clearCache(): void {
37+
this._manifest = undefined;
38+
}
3539
}

packages/cli/src/lib/SchemaComposer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ export class SchemaComposer {
9898
return this._composerOutput;
9999
}
100100

101+
public clearCache(): void {
102+
this._composerOutput = undefined;
103+
}
104+
101105
private async _fetchExternalSchema(
102106
uri: string,
103107
manifest: Manifest

packages/cli/src/lib/Watcher.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import chokidar from "chokidar";
2+
3+
export type WatchEventType =
4+
| "add"
5+
| "addDir"
6+
| "change"
7+
| "unlink"
8+
| "unlinkDir";
9+
10+
export const watchEventNames: Record<WatchEventType, string> = {
11+
add: "File Added",
12+
addDir: "Folder Added",
13+
change: "File Changed",
14+
unlink: "File Removed",
15+
unlinkDir: "Folder Removed",
16+
};
17+
18+
export function watchEventName(type: WatchEventType): string {
19+
return watchEventNames[type];
20+
}
21+
22+
export interface WatchEvent {
23+
type: WatchEventType;
24+
path: string;
25+
}
26+
27+
export interface WatchOptions extends chokidar.WatchOptions {
28+
execute: (events: WatchEvent[]) => Promise<void>;
29+
}
30+
31+
interface WatchSession {
32+
stop: () => Promise<void>;
33+
directory: string;
34+
}
35+
36+
export class Watcher {
37+
private _session: WatchSession | undefined;
38+
39+
public start(directory: string, options: WatchOptions): void {
40+
if (this._session) {
41+
throw Error(
42+
`Watcher session is already in progress. Directory: ${this._session.directory}`
43+
);
44+
}
45+
46+
const watcher = chokidar.watch(directory, options);
47+
let backlog: WatchEvent[] = [];
48+
49+
// Watch all file system events
50+
watcher.on("all", (type: WatchEventType, path: string) => {
51+
// Add the event to the backlog if it doesn't exist
52+
if (!backlog.some((e) => e.path == path && e.type == type)) {
53+
backlog.push({ type, path });
54+
}
55+
});
56+
57+
// Process the event backlog on a given interval
58+
let instance: ReturnType<typeof setInterval>;
59+
const interval = options.interval || 1000;
60+
61+
const updateLoop = async () => {
62+
if (backlog.length > 0) {
63+
// Reset the interval
64+
clearInterval(instance);
65+
66+
// Execute
67+
await options.execute(backlog);
68+
69+
// Reset the backlog
70+
backlog = [];
71+
72+
// Start a new interval
73+
instance = setInterval(updateLoop, interval);
74+
}
75+
};
76+
77+
instance = setInterval(updateLoop, interval);
78+
79+
this._session = {
80+
stop: async () => {
81+
await watcher.close();
82+
clearInterval(instance);
83+
},
84+
directory,
85+
};
86+
}
87+
88+
public async stop(): Promise<void> {
89+
if (this._session) {
90+
await this._session.stop();
91+
this._session = undefined;
92+
}
93+
}
94+
}

packages/cli/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./Project";
22
export * from "./Compiler";
33
export * from "./CodeGenerator";
44
export * from "./SchemaComposer";
5+
export * from "./Watcher";

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5997,7 +5997,7 @@ chokidar@^2.0.4, chokidar@^2.1.8:
59975997
optionalDependencies:
59985998
fsevents "^1.2.7"
59995999

6000-
chokidar@^3.3.0, chokidar@^3.4.1:
6000+
chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.5.1:
60016001
version "3.5.1"
60026002
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
60036003
integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==

0 commit comments

Comments
 (0)