Skip to content

Commit d458dcc

Browse files
committed
feat: Added build --as-needed to watch and rebuild
1 parent fe54ee6 commit d458dcc

File tree

6 files changed

+357
-105
lines changed

6 files changed

+357
-105
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"dependencies": {
3838
"babel-polyfill": "6.7.4",
3939
"bunyan": "1.8.0",
40+
"debounce": "1.0.0",
4041
"es6-error": "2.1.0",
4142
"es6-promisify": "4.0.0",
4243
"firefox-profile": "0.3.12",
@@ -47,6 +48,7 @@
4748
"source-map-support": "0.4.0",
4849
"stream-to-promise": "1.1.0",
4950
"tmp": "0.0.28",
51+
"watchpack": "1.0.1",
5052
"yargs": "4.6.0",
5153
"zip-dir": "1.0.2"
5254
},

src/cmd/build.js

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import minimatch from 'minimatch';
44
import {createWriteStream} from 'fs';
55
import streamToPromise from 'stream-to-promise';
66

7+
import defaultSourceWatcher from '../watcher';
78
import {zipDir} from '../util/zip-dir';
89
import getValidatedManifest from '../util/manifest';
910
import {prepareArtifactsDir} from '../util/artifacts';
@@ -12,49 +13,74 @@ import {createLogger} from '../util/logger';
1213
const log = createLogger(__filename);
1314

1415

15-
export default function build(
16-
{sourceDir, artifactsDir}: Object,
17-
{manifestData, fileFilter}: Object = {}): Promise {
16+
function defaultPackageCreator(
17+
{manifestData, sourceDir, fileFilter, artifactsDir}) {
1818

19-
log.info(`Building web extension from ${sourceDir}`);
19+
return new Promise(
20+
(resolve) => {
21+
if (manifestData) {
22+
log.debug(`Using manifest id=${manifestData.applications.gecko.id}`);
23+
resolve(manifestData);
24+
} else {
25+
resolve(getValidatedManifest(sourceDir));
26+
}
27+
})
28+
.then((manifestData) => {
29+
return zipDir(
30+
sourceDir, {
31+
filter: (...args) => fileFilter.wantFile(...args),
32+
})
33+
.then((buffer) => {
34+
let packageName = safeFileName(
35+
`${manifestData.name}-${manifestData.version}.xpi`);
36+
let extensionPath = path.join(artifactsDir, packageName);
37+
let stream = createWriteStream(extensionPath);
38+
let promisedStream = streamToPromise(stream);
39+
40+
stream.write(buffer, () => stream.end());
41+
42+
return promisedStream
43+
.then(() => {
44+
log.info(`Your web extension is ready: ${extensionPath}`);
45+
return {extensionPath};
46+
});
47+
});
48+
});
49+
}
2050

21-
let resolveManifest;
22-
if (manifestData) {
23-
log.debug(`Using manifest id=${manifestData.applications.gecko.id}`);
24-
resolveManifest = Promise.resolve(manifestData);
25-
} else {
26-
resolveManifest = getValidatedManifest(sourceDir);
27-
}
2851

29-
if (!fileFilter) {
30-
fileFilter = new FileFilter();
31-
}
52+
export default function build(
53+
{sourceDir, artifactsDir, asNeeded}: Object,
54+
{manifestData, fileFilter=new FileFilter(),
55+
onSourceChange=defaultSourceWatcher,
56+
packageCreator=defaultPackageCreator}
57+
: Object = {}): Promise {
3258

33-
return resolveManifest
34-
.then((manifestData) =>
35-
Promise.all([
36-
prepareArtifactsDir(artifactsDir),
37-
zipDir(sourceDir, {
38-
filter: (...args) => fileFilter.wantFile(...args),
39-
}),
40-
])
41-
.then((results) => {
42-
let [artifactsDir, buffer] = results;
43-
let packageName = safeFileName(
44-
`${manifestData.name}-${manifestData.version}.xpi`);
45-
let extensionPath = path.join(artifactsDir, packageName);
46-
let stream = createWriteStream(extensionPath);
47-
let promisedStream = streamToPromise(stream);
48-
49-
stream.write(buffer, () => stream.end());
50-
51-
return promisedStream
52-
.then(() => {
53-
log.info(`Your web extension is ready: ${extensionPath}`);
54-
return {extensionPath};
55-
});
56-
})
57-
);
59+
const rebuildAsNeeded = asNeeded; // alias for `build --as-needed`
60+
log.info(`Building web extension from ${sourceDir}`);
61+
62+
const createPackage = () => packageCreator({
63+
manifestData, sourceDir, fileFilter, artifactsDir,
64+
});
65+
66+
return prepareArtifactsDir(artifactsDir)
67+
.then(() => createPackage())
68+
.then((result) => {
69+
if (rebuildAsNeeded) {
70+
log.info('Rebuilding when files change...');
71+
onSourceChange({
72+
sourceDir, artifactsDir,
73+
onChange: () => {
74+
return createPackage().catch((error) => {
75+
log.error(error.stack);
76+
throw error;
77+
});
78+
},
79+
shouldWatchFile: (...args) => fileFilter.wantFile(...args),
80+
});
81+
}
82+
return result;
83+
});
5884
}
5985

6086

@@ -86,7 +112,7 @@ export class FileFilter {
86112
wantFile(path: string): boolean {
87113
for (const test of this.filesToIgnore) {
88114
if (minimatch(path, test)) {
89-
log.debug(`Not including file ${path} in ZIP archive`);
115+
log.debug(`FileFilter: ignoring file ${path}`);
90116
return false;
91117
}
92118
}

src/program.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,12 @@ Example: $0 --help run.
142142
program
143143
.command('build',
144144
'Create a web extension package from source',
145-
commands.build, {})
145+
commands.build, {
146+
'as-needed': {
147+
describe: 'Watch for file changes and re-build as needed',
148+
type: 'boolean',
149+
},
150+
})
146151
.command('sign',
147152
'Sign the web extension so it can be installed in Firefox',
148153
commands.sign, {

src/watcher.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* @flow */
2+
import Watchpack from 'watchpack';
3+
import debounce from 'debounce';
4+
5+
import {createLogger} from './util/logger';
6+
7+
const log = createLogger(__filename);
8+
9+
10+
export default function onSourceChange(
11+
{sourceDir, artifactsDir, onChange, shouldWatchFile}: Object): Watchpack {
12+
// TODO: For network disks, we would need to add {poll: true}.
13+
const watcher = new Watchpack();
14+
15+
const onFileChange = (filePath) => {
16+
proxyFileChanges({artifactsDir, onChange, filePath, shouldWatchFile});
17+
};
18+
const executeImmediately = true;
19+
watcher.on('change', debounce(onFileChange, 1000, executeImmediately));
20+
21+
log.debug(`Watching for file changes in ${sourceDir}`);
22+
watcher.watch([], [sourceDir], Date.now());
23+
24+
// TODO: support windows See:
25+
// http://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
26+
process.on('SIGINT', () => watcher.close());
27+
return watcher;
28+
}
29+
30+
31+
export function proxyFileChanges(
32+
{artifactsDir, onChange, filePath, shouldWatchFile=() => true}
33+
: Object) {
34+
if (filePath.indexOf(artifactsDir) === 0 || !shouldWatchFile(filePath)) {
35+
log.debug(`Ignoring change to: ${filePath}`);
36+
} else {
37+
log.info(`Changed: ${filePath}`);
38+
log.debug(`Last change detection: ${(new Date()).toTimeString()}`);
39+
onChange();
40+
}
41+
}

0 commit comments

Comments
 (0)