Skip to content

Commit

Permalink
CB-11623 added symlinking option
Browse files Browse the repository at this point in the history
  • Loading branch information
carynbear committed Aug 4, 2016
1 parent da2e856 commit 79cace5
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 17 deletions.
96 changes: 79 additions & 17 deletions index.js
Expand Up @@ -189,6 +189,12 @@ module.exports = function(dir, optionalId, optionalName, cfg, extEvents) {
var isGit;
var isNPM;

//If symlink, don't fetch
if (!!cfg.lib.www.link) {
events.emit('verbose', 'Symlinking assets.');
return Q(cfg.lib.www.url);
}

events.emit('verbose', 'Copying assets."');
isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url)));
Expand Down Expand Up @@ -247,32 +253,41 @@ module.exports = function(dir, optionalId, optionalName, cfg, extEvents) {
}

try {

// Copy files from template to project
if (cfg.lib.www.template)
copyTemplateFiles(import_from_path, dir, isSubDir);

// If following were not copied from template, copy from stock app hello world
ifNotCopied(paths.www, path.join(dir, 'www'));
ifNotCopied(paths.hooks, path.join(dir, 'hooks'));
var configXmlExists = projectConfig(dir);
// If --link, link merges, hooks, www, and config.xml (and/or copy to root)
if (!!cfg.lib.www.link)
linkFromTemplate(import_from_path, dir);

// If following were not copied/linked from template, copy from stock app hello world
copyIfNotExists(paths.www, path.join(dir, 'www'));
copyIfNotExists(paths.hooks, path.join(dir, 'hooks'));
var configXmlExists = projectConfig(dir); //moves config to root if in www
if (paths.configXml && !configXmlExists) {
shell.cp(paths.configXml, path.join(dir, 'config.xml'));
}
} catch (e) {
if (!dirAlreadyExisted) {
shell.rm('-rf', dir);
}
if (process.platform.slice(0, 3) == 'win' && e.code == 'EPERM') {
throw new CordovaError('Symlinks on Windows require Administrator privileges');
}
throw e;
}

// Update package.json name and version fields.
if (fs.existsSync(path.join(dir, 'package.json'))) {
var pkgjson = require(path.resolve(dir, 'package.json'));
var pkgjsonPath = path.join(dir, 'package.json');
// Update package.json name and version fields
if (fs.existsSync(pkgjsonPath)) {
var pkgjson = require(pkgjsonPath);
if (cfg.name) {
pkgjson.name = cfg.name.toLowerCase();
}
pkgjson.version = '1.0.0';
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkgjson, null, 4), 'utf8');
fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8');
}

// Create basic project structure.
Expand All @@ -282,25 +297,28 @@ module.exports = function(dir, optionalId, optionalName, cfg, extEvents) {
if (!fs.existsSync(path.join(dir, 'plugins')))
shell.mkdir(path.join(dir, 'plugins'));

// Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
var configPath = projectConfig(dir);
var conf = new ConfigParser(configPath);
if (cfg.id) conf.setPackageName(cfg.id);
if (cfg.name) conf.setName(cfg.name);
conf.setVersion('1.0.0');
conf.write();
var configPath = path.join(dir, 'config.xml');
// only update config.xml if not a symlink
if(!fs.lstatSync(configPath).isSymbolicLink()) {
// Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
var conf = new ConfigParser(configPath);
if (cfg.id) conf.setPackageName(cfg.id);
if (cfg.name) conf.setName(cfg.name);
conf.setVersion('1.0.0');
conf.write();
}
}).then(function(){
cleanupEvents();
});
};

/**
* Recursively copies folder to destination if folder is not found in destination.
* Recursively copies folder to destination if folder is not found in destination (including symlinks).
* @param {string} src for copying
* @param {string} dst for copying
* @return No return value
*/
function ifNotCopied(src, dst) {
function copyIfNotExists(src, dst) {
if (!fs.existsSync(dst) && src) {
shell.mkdir(dst);
shell.cp('-R', path.join(src, '*'), dst);
Expand Down Expand Up @@ -410,3 +428,47 @@ function writeToConfigJson(project_root, opts, autoPersist) {
return json;
}
}

/**
* Removes existing files and symlinks them if they exist.
* Symlinks folders: www, merges, hooks
* Symlinks file: config.xml (but only if it exists outside of the www folder)
* If config.xml exists inside of template/www, COPY (not link) it to project/
* */
function linkFromTemplate(templateDir, projectDir) {
var linkSrc, linkDst, linkFolders, copySrc, copyDst;
function rmlinkSync(src, dst, type) {
if (src && dst) {
if (fs.existsSync(dst)) {
shell.rm('-rf', dst);
}
if (fs.existsSync(src)) {
fs.symlinkSync(src, dst, type);
}
}
}
// if template is a www dir
if (path.basename(templateDir) === 'www') {
linkSrc = path.resolve(templateDir);
linkDst = path.join(projectDir, 'www');
rmlinkSync(linkSrc, linkDst, 'dir');
copySrc = path.join(templateDir, 'config.xml');
} else {
linkFolders = ['www', 'merges', 'hooks'];
// Link each folder
for (var i = 0; i < linkFolders.length; i++) {
linkSrc = path.join(templateDir, linkFolders[i]);
linkDst = path.join(projectDir, linkFolders[i]);
rmlinkSync(linkSrc, linkDst, 'dir');
}
linkSrc = path.join(templateDir, 'config.xml');
linkDst = path.join(projectDir, 'config.xml');
rmlinkSync(linkSrc, linkDst, 'file');
copySrc = path.join(templateDir, 'www', 'config.xml');
}
// if template/www/config.xml then copy to project/config.xml
copyDst = path.join(projectDir, 'config.xml');
if (!fs.existsSync(copyDst) && fs.existsSync(copySrc)) {
shell.cp(copySrc, projectDir);
}
}
183 changes: 183 additions & 0 deletions spec/create.spec.js
Expand Up @@ -23,6 +23,7 @@ var helpers = require('./helpers'),
events = require('cordova-common').events,
ConfigParser = require('cordova-common').ConfigParser,
create = require('../index'),
fs = require('fs'),
CordovaLogger = require('cordova-common').CordovaLogger.get().setLevel('error');

var tmpDir = helpers.tmpDir('create_test');
Expand Down Expand Up @@ -328,4 +329,186 @@ describe('create end-to-end', function() {
.fin(done);
}, 60000);

describe('when --link-to is provided', function() {
it('when passed www folder should not move www/config.xml, only copy and update', function(done) {
function checkSymWWW() {
// Check if top level dirs exist.
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
dirs.forEach(function(d) {
expect(path.join(project, d)).toExist();
});
expect(path.join(project, 'hooks', 'README.md')).toExist();

// Check if www files exist.
expect(path.join(project, 'www', 'index.html')).toExist();

// Check www/config exists
expect(path.join(project, 'www', 'config.xml')).toExist();
// Check www/config.xml was not updated.
var configXml = new ConfigParser(path.join(project, 'www', 'config.xml'));
expect(configXml.packageName()).toEqual('io.cordova.hellocordova');
expect(configXml.version()).toEqual('0.0.1');
expect(configXml.description()).toEqual('this is the correct config.xml');

// Check that config.xml was copied to project/config.xml
expect(path.join(project, 'config.xml')).toExist();
configXml = new ConfigParser(path.join(project, 'config.xml'));
expect(configXml.description()).toEqual('this is the correct config.xml');
// Check project/config.xml was updated.
expect(configXml.packageName()).toEqual(appId);
expect(configXml.version()).toEqual('1.0.0');

// Check that we got no package.json
expect(path.join(project, 'package.json')).not.toExist();

// Check that www is really a symlink,
// and project/config.xml , hooks and merges are not
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(project, 'hooks')).isSymbolicLink()).not.toBe(true);
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).not.toBe(true);
}
var config = {
lib: {
www: {
template: true,
url: path.join(__dirname, 'templates', 'config_in_www', 'www'),
version: '',
link: true
}
}
};
project = project + '4';
return create(project, appId, appName, config)
.then(checkSymWWW)
.fail(function(err) {
if(process.platform.slice(0, 3) == 'win') {
// Allow symlink error if not in admin mode
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
} else {
if (err) {
console.log(err.stack);
}
expect(err).toBeUndefined();
}
})
.fin(done);
}, 60000);

it('with subdirectory should not update symlinked project/config.xml', function(done) {
function checkSymSubDir() {
// Check if top level dirs exist.
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
dirs.forEach(function(d) {
expect(path.join(project, d)).toExist();
});
expect(path.join(project, 'hooks', 'README.md')).toExist();

//index.js and template subdir folder should not exist (inner files should be copied to the project folder)
expect(path.join(project, 'index.js')).not.toExist();
expect(path.join(project, 'template')).not.toExist();

// Check if www files exist.
expect(path.join(project, 'www', 'index.html')).toExist();

// Check that www, and config.xml is really a symlink
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).toBe(true);

// Check that config.xml was not updated. (symlinked config does not get updated!)
var configXml = new ConfigParser(path.join(project, 'config.xml'));
expect(configXml.packageName()).toEqual('io.cordova.hellocordova');
expect(configXml.version()).toEqual('0.0.1');

// Check that we got the right config.xml
expect(configXml.description()).toEqual('this is the correct config.xml');

// Check that we got package.json (the correct one) and it was changed
var pkjson = require(path.join(project, 'package.json'));
expect(pkjson.name).toEqual(appName.toLowerCase());
expect(pkjson.valid).toEqual('true');
}
var config = {
lib: {
www: {
template: true,
url: path.join(__dirname, 'templates', 'withsubdirectory_package_json'),
version: '',
link: true
}
}
};
project = project + '5';
return create(project, appId, appName, config)
.then(checkSymSubDir)
.fail(function(err) {
if(process.platform.slice(0, 3) == 'win') {
// Allow symlink error if not in admin mode
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
} else {
if (err) {
console.log(err.stack);
}
expect(err).toBeUndefined();
}
})
.fin(done);
}, 60000);

it('with no config should create one and update it', function(done) {
function checkSymNoConfig() {
// Check if top level dirs exist.
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
dirs.forEach(function(d) {
expect(path.join(project, d)).toExist();
});
expect(path.join(project, 'hooks', 'hooks.file')).toExist();
expect(path.join(project, 'merges', 'merges.file')).toExist();

// Check if www files exist.
expect(path.join(project, 'www', 'index.html')).toExist();

// Check that config.xml was updated.
var configXml = new ConfigParser(path.join(project, 'config.xml'));
expect(configXml.packageName()).toEqual(appId);

// Check that www, hooks, merges are really a symlink; config is not
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(project, 'hooks')).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(project, 'merges')).isSymbolicLink()).toBe(true);
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).not.toBe(true);
}

var config = {
lib: {
www: {
template: true,
url: path.join(__dirname, 'templates', 'noconfig'),
version: '',
link: true
}
}
};
project = project + '6';
return create(project, appId, appName, config)
.then(checkSymNoConfig)
.fail(function(err) {
if(process.platform.slice(0, 3) == 'win') {
// Allow symlink error if not in admin mode
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
} else {
if (err) {
console.log(err.stack);
}
expect(err).toBeUndefined();
}
})
.fin(done);
}, 60000);

});





});
Empty file.
Empty file.
49 changes: 49 additions & 0 deletions spec/templates/noconfig/www/index.html
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<html>
<head>
<!--
Customize this policy to fit your own app's needs. For more guidance, see:
https://github.com/apache/cordova-plugin-whitelist/blob/master/README.md#content-security-policy
Some notes:
* gap: is required only on iOS (when using UIWebView) and is needed for JS->native communication
* https://ssl.gstatic.com is required only on Android and is needed for TalkBack to function properly
* Disables use of inline scripts in order to mitigate risk of XSS vulnerabilities. To change this:
* Enable inline JS: add 'unsafe-inline' to default-src
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<link rel="stylesheet" type="text/css" href="css/index.css">
<title>Hello World</title>
</head>
<body>
<div class="app">
<h1>Apache Cordova</h1>
<div id="deviceready" class="blink">
<p class="event listening">Connecting to Device</p>
<p class="event received">Device is Ready</p>
</div>
</div>
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/index.js"></script>
</body>
</html>

0 comments on commit 79cace5

Please sign in to comment.