Skip to content

Commit 5e417b2

Browse files
authored
Completely refactor build system (#205)
The goals of this refactoring are the following: - Improve code quality of the build system - Prepare code base for adding code coverage - Prepare code base for exposing the build system as a package export, so platforms can build their `cordova.js` during their build (or creation) process. This would make the JS build using coho obsolete. - Prepare code base to remove dependency on Grunt The build process now lives under `build-tools` (was `tasks`). It does not depend on Grunt anymore but is written in plain Node.js. The original Grunt interface for building (as used by coho) was preserved and is implemented in `Gruntfile.js` using the new build system. The `platformName` option and support for getting platform paths from the `cordova-platforms` key in `package.json` have been removed from the Grunt interface, but neither of those are used in coho. The logic that is specific to the test build has been extracted from the rest of the build. It now lives in `test/build.js` and is run automatically during `npm test`. The following dependencies have been added: - execa - fs-extra - globby I have taken extra care to preserve the exact format of the built file. This means that the correctness of the refactoring can be verified by simply diffing the build artifacts in `pkg` created with and without this change.
1 parent 4f6abd6 commit 5e417b2

21 files changed

+424
-568
lines changed

Gruntfile.js

+34-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19+
20+
const path = require('path');
21+
const { build, collectModules } = require('./build-tools');
22+
1923
module.exports = function (grunt) {
2024

2125
grunt.initConfig({
@@ -24,20 +28,46 @@ module.exports = function (grunt) {
2428
'android': {},
2529
'ios': {},
2630
'osx': {},
27-
'test': {},
2831
'windows': { useWindowsLineEndings: true },
2932
'browser': {},
3033
'electron': {}
3134
},
3235
clean: ['pkg']
3336
});
3437

38+
// custom tasks
39+
grunt.registerMultiTask('compile', 'Packages cordova.js', function () {
40+
const done = this.async();
41+
42+
const platformPath = path.resolve(`../cordova-${this.target}`);
43+
const platformPkgPath = path.join(platformPath, 'package');
44+
const platformModulesPath = path.join(platformPath, 'cordova-js-src');
45+
46+
build({
47+
platformName: this.target,
48+
platformVersion: grunt.option('platformVersion') ||
49+
require(platformPkgPath).version,
50+
extraModules: collectModules(platformModulesPath)
51+
})
52+
.then(cordovaJs => {
53+
// if we are using windows line endings, we will also add the BOM
54+
if (this.data.useWindowsLineEndings) {
55+
cordovaJs = '\ufeff' + cordovaJs.split(/\r?\n/).join('\r\n');
56+
}
57+
58+
// Write out the bundle
59+
const baseName = `cordova.${this.target}.js`;
60+
const fileName = path.join('pkg', baseName);
61+
grunt.file.write(fileName, cordovaJs);
62+
63+
console.log(`Generated ${fileName}`);
64+
})
65+
.then(done, done);
66+
});
67+
3568
// external tasks
3669
grunt.loadNpmTasks('grunt-contrib-clean');
3770

38-
// custom tasks
39-
grunt.loadTasks('tasks');
40-
4171
// defaults
4272
grunt.registerTask('default', ['compile']);
4373
};

build-tools/build.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const execa = require('execa');
21+
const bundle = require('./bundle');
22+
const scripts = require('./scripts');
23+
const modules = require('./modules');
24+
const { pkgRoot } = require('./common');
25+
26+
module.exports = function build (userConfig) {
27+
const config = Object.assign({ preprocess: x => x }, userConfig);
28+
29+
return Promise.all([
30+
scripts(config),
31+
modules(config),
32+
getCommitId()
33+
])
34+
.then(([ scripts, modules, commitId ]) => {
35+
Object.assign(config, { commitId });
36+
return bundle(scripts, modules, config);
37+
});
38+
};
39+
40+
function getCommitId () {
41+
return execa.stdout('git', ['rev-parse', 'HEAD'], { cwd: pkgRoot });
42+
}

build-tools/bundle.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
module.exports = function bundle (scripts, modules, config) {
21+
const context = Object.assign({
22+
modules: modules.map(m => m.contents).join('\n'),
23+
includeScript: name => scripts[name].contents
24+
}, config);
25+
26+
return bundleTemplate(context);
27+
};
28+
29+
const bundleTemplate = ({
30+
commitId,
31+
platformName,
32+
platformVersion,
33+
includeScript,
34+
modules
35+
}) => `
36+
// Platform: ${platformName}
37+
// ${commitId}
38+
/*
39+
Licensed to the Apache Software Foundation (ASF) under one
40+
or more contributor license agreements. See the NOTICE file
41+
distributed with this work for additional information
42+
regarding copyright ownership. The ASF licenses this file
43+
to you under the Apache License, Version 2.0 (the
44+
"License"); you may not use this file except in compliance
45+
with the License. You may obtain a copy of the License at
46+
\x20
47+
http://www.apache.org/licenses/LICENSE-2.0
48+
\x20
49+
Unless required by applicable law or agreed to in writing,
50+
software distributed under the License is distributed on an
51+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
52+
KIND, either express or implied. See the License for the
53+
specific language governing permissions and limitations
54+
under the License.
55+
*/
56+
;(function() {
57+
var PLATFORM_VERSION_BUILD_LABEL = '${platformVersion}';
58+
${includeScript('require')}
59+
${modules}
60+
window.cordova = require('cordova');
61+
${includeScript('bootstrap')}
62+
})();
63+
`.trim();

build-tools/common.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const fs = require('fs-extra');
21+
const path = require('path');
22+
const globby = require('globby');
23+
24+
const pkgRoot = path.join(__dirname, '..');
25+
26+
module.exports = {
27+
pkgRoot,
28+
29+
values (obj) {
30+
return Object.keys(obj).map(key => obj[key]);
31+
},
32+
33+
readContents (f) {
34+
return fs.readFile(f.path, 'utf8')
35+
.then(contents => Object.assign({}, f, { contents }));
36+
},
37+
38+
// Strips the license header.
39+
// Basically only the first multi-line comment up to to the closing */
40+
stripLicenseHeader (f) {
41+
const LICENSE_REGEX = /^\s*\/\*[\s\S]+?\*\/\s*/;
42+
const withoutLicense = f.contents.replace(LICENSE_REGEX, '');
43+
return Object.assign({}, f, { contents: withoutLicense });
44+
},
45+
46+
// TODO format path relative to pkg.json
47+
prependFileComment (f) {
48+
const comment = `// file: ${f.path}`;
49+
const contents = [comment, f.contents].join('\n');
50+
return Object.assign({}, f, { contents });
51+
},
52+
53+
collectModules (dir) {
54+
return globby.sync(['**/*.js'], { cwd: dir })
55+
.map(fileName => ({
56+
path: path.relative(pkgRoot, path.join(dir, fileName)),
57+
moduleId: fileName.slice(0, -3)
58+
}))
59+
.map(file => ({ [file.moduleId]: file }))
60+
.reduce((result, fragment) => Object.assign(result, fragment), {});
61+
}
62+
};

build-tools/index.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
module.exports = {
21+
build: require('./build'),
22+
collectModules: require('./common').collectModules
23+
};

build-tools/modules.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const path = require('path');
21+
22+
const {
23+
readContents,
24+
stripLicenseHeader,
25+
prependFileComment,
26+
values,
27+
pkgRoot,
28+
collectModules
29+
} = require('./common');
30+
31+
module.exports = function modules (config) {
32+
for (const m of values(config.extraModules)) {
33+
if (m.path.startsWith('../')) {
34+
m.path = path.resolve(m.path);
35+
}
36+
}
37+
38+
const commonModules = collectCommonModules();
39+
const modules = values(Object.assign(commonModules, config.extraModules));
40+
modules.sort((a, b) => a.moduleId.localeCompare(b.moduleId));
41+
return Promise.all(modules.map(modulePipeline(config)));
42+
};
43+
44+
function collectCommonModules () {
45+
const modules = collectModules(path.join(pkgRoot, 'src/common'));
46+
modules[''] = {
47+
moduleId: '',
48+
path: path.relative(pkgRoot, path.join(pkgRoot, 'src/cordova.js'))
49+
};
50+
return modules;
51+
}
52+
53+
function modulePipeline (config) {
54+
return f => Promise.resolve(f)
55+
.then(readContents)
56+
.then(config.preprocess)
57+
.then(stripLicenseHeader)
58+
.then(addModuleNamespace('cordova'))
59+
.then(wrapInModuleContext)
60+
.then(prependFileComment);
61+
}
62+
63+
function addModuleNamespace (ns) {
64+
return m => {
65+
const moduleId = path.posix.join(ns, m.moduleId);
66+
return Object.assign({}, m, { moduleId });
67+
};
68+
}
69+
70+
function wrapInModuleContext (f) {
71+
const contents = moduleTemplate({ id: f.moduleId, body: f.contents });
72+
return Object.assign({}, f, { contents });
73+
}
74+
75+
const moduleTemplate = ({ id, body }) => `
76+
define("${id}", function(require, exports, module) {
77+
78+
${body}
79+
});
80+
`.trimLeft();

build-tools/scripts.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const path = require('path');
21+
const {
22+
readContents,
23+
stripLicenseHeader,
24+
prependFileComment,
25+
values,
26+
pkgRoot,
27+
collectModules
28+
} = require('./common');
29+
30+
module.exports = function scripts (config) {
31+
const scripts = values(collectScripts());
32+
return Promise.all(scripts.map(scriptPipeline(config)))
33+
.then(indexByModuleId);
34+
};
35+
36+
function collectScripts () {
37+
const scripts = collectModules(path.join(pkgRoot, 'src/scripts'));
38+
for (const script of ['require', 'bootstrap']) {
39+
if (script in scripts) continue;
40+
throw new Error(`Could not find required script '${script}.js'`);
41+
}
42+
return scripts;
43+
}
44+
45+
function scriptPipeline (config) {
46+
return f => Promise.resolve(f)
47+
.then(readContents)
48+
.then(config.preprocess)
49+
.then(stripLicenseHeader)
50+
.then(prependEmptyLine)
51+
.then(prependFileComment);
52+
}
53+
54+
function prependEmptyLine (f) {
55+
return Object.assign({}, f, { contents: '\n' + f.contents });
56+
}
57+
58+
function indexByModuleId (files) {
59+
return files
60+
.reduce((acc, f) => Object.assign(acc, { [f.moduleId]: f }), {});
61+
}

0 commit comments

Comments
 (0)