Skip to content

Commit 79cace5

Browse files
committed
CB-11623 added symlinking option
1 parent da2e856 commit 79cace5

File tree

5 files changed

+311
-17
lines changed

5 files changed

+311
-17
lines changed

index.js

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ module.exports = function(dir, optionalId, optionalName, cfg, extEvents) {
189189
var isGit;
190190
var isNPM;
191191

192+
//If symlink, don't fetch
193+
if (!!cfg.lib.www.link) {
194+
events.emit('verbose', 'Symlinking assets.');
195+
return Q(cfg.lib.www.url);
196+
}
197+
192198
events.emit('verbose', 'Copying assets."');
193199
isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
194200
isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url)));
@@ -247,32 +253,41 @@ module.exports = function(dir, optionalId, optionalName, cfg, extEvents) {
247253
}
248254

249255
try {
256+
250257
// Copy files from template to project
251258
if (cfg.lib.www.template)
252259
copyTemplateFiles(import_from_path, dir, isSubDir);
253260

254-
// If following were not copied from template, copy from stock app hello world
255-
ifNotCopied(paths.www, path.join(dir, 'www'));
256-
ifNotCopied(paths.hooks, path.join(dir, 'hooks'));
257-
var configXmlExists = projectConfig(dir);
261+
// If --link, link merges, hooks, www, and config.xml (and/or copy to root)
262+
if (!!cfg.lib.www.link)
263+
linkFromTemplate(import_from_path, dir);
264+
265+
// If following were not copied/linked from template, copy from stock app hello world
266+
copyIfNotExists(paths.www, path.join(dir, 'www'));
267+
copyIfNotExists(paths.hooks, path.join(dir, 'hooks'));
268+
var configXmlExists = projectConfig(dir); //moves config to root if in www
258269
if (paths.configXml && !configXmlExists) {
259270
shell.cp(paths.configXml, path.join(dir, 'config.xml'));
260271
}
261272
} catch (e) {
262273
if (!dirAlreadyExisted) {
263274
shell.rm('-rf', dir);
264275
}
276+
if (process.platform.slice(0, 3) == 'win' && e.code == 'EPERM') {
277+
throw new CordovaError('Symlinks on Windows require Administrator privileges');
278+
}
265279
throw e;
266280
}
267281

268-
// Update package.json name and version fields.
269-
if (fs.existsSync(path.join(dir, 'package.json'))) {
270-
var pkgjson = require(path.resolve(dir, 'package.json'));
282+
var pkgjsonPath = path.join(dir, 'package.json');
283+
// Update package.json name and version fields
284+
if (fs.existsSync(pkgjsonPath)) {
285+
var pkgjson = require(pkgjsonPath);
271286
if (cfg.name) {
272287
pkgjson.name = cfg.name.toLowerCase();
273288
}
274289
pkgjson.version = '1.0.0';
275-
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkgjson, null, 4), 'utf8');
290+
fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8');
276291
}
277292

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

285-
// Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
286-
var configPath = projectConfig(dir);
287-
var conf = new ConfigParser(configPath);
288-
if (cfg.id) conf.setPackageName(cfg.id);
289-
if (cfg.name) conf.setName(cfg.name);
290-
conf.setVersion('1.0.0');
291-
conf.write();
300+
var configPath = path.join(dir, 'config.xml');
301+
// only update config.xml if not a symlink
302+
if(!fs.lstatSync(configPath).isSymbolicLink()) {
303+
// Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
304+
var conf = new ConfigParser(configPath);
305+
if (cfg.id) conf.setPackageName(cfg.id);
306+
if (cfg.name) conf.setName(cfg.name);
307+
conf.setVersion('1.0.0');
308+
conf.write();
309+
}
292310
}).then(function(){
293311
cleanupEvents();
294312
});
295313
};
296314

297315
/**
298-
* Recursively copies folder to destination if folder is not found in destination.
316+
* Recursively copies folder to destination if folder is not found in destination (including symlinks).
299317
* @param {string} src for copying
300318
* @param {string} dst for copying
301319
* @return No return value
302320
*/
303-
function ifNotCopied(src, dst) {
321+
function copyIfNotExists(src, dst) {
304322
if (!fs.existsSync(dst) && src) {
305323
shell.mkdir(dst);
306324
shell.cp('-R', path.join(src, '*'), dst);
@@ -410,3 +428,47 @@ function writeToConfigJson(project_root, opts, autoPersist) {
410428
return json;
411429
}
412430
}
431+
432+
/**
433+
* Removes existing files and symlinks them if they exist.
434+
* Symlinks folders: www, merges, hooks
435+
* Symlinks file: config.xml (but only if it exists outside of the www folder)
436+
* If config.xml exists inside of template/www, COPY (not link) it to project/
437+
* */
438+
function linkFromTemplate(templateDir, projectDir) {
439+
var linkSrc, linkDst, linkFolders, copySrc, copyDst;
440+
function rmlinkSync(src, dst, type) {
441+
if (src && dst) {
442+
if (fs.existsSync(dst)) {
443+
shell.rm('-rf', dst);
444+
}
445+
if (fs.existsSync(src)) {
446+
fs.symlinkSync(src, dst, type);
447+
}
448+
}
449+
}
450+
// if template is a www dir
451+
if (path.basename(templateDir) === 'www') {
452+
linkSrc = path.resolve(templateDir);
453+
linkDst = path.join(projectDir, 'www');
454+
rmlinkSync(linkSrc, linkDst, 'dir');
455+
copySrc = path.join(templateDir, 'config.xml');
456+
} else {
457+
linkFolders = ['www', 'merges', 'hooks'];
458+
// Link each folder
459+
for (var i = 0; i < linkFolders.length; i++) {
460+
linkSrc = path.join(templateDir, linkFolders[i]);
461+
linkDst = path.join(projectDir, linkFolders[i]);
462+
rmlinkSync(linkSrc, linkDst, 'dir');
463+
}
464+
linkSrc = path.join(templateDir, 'config.xml');
465+
linkDst = path.join(projectDir, 'config.xml');
466+
rmlinkSync(linkSrc, linkDst, 'file');
467+
copySrc = path.join(templateDir, 'www', 'config.xml');
468+
}
469+
// if template/www/config.xml then copy to project/config.xml
470+
copyDst = path.join(projectDir, 'config.xml');
471+
if (!fs.existsSync(copyDst) && fs.existsSync(copySrc)) {
472+
shell.cp(copySrc, projectDir);
473+
}
474+
}

spec/create.spec.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var helpers = require('./helpers'),
2323
events = require('cordova-common').events,
2424
ConfigParser = require('cordova-common').ConfigParser,
2525
create = require('../index'),
26+
fs = require('fs'),
2627
CordovaLogger = require('cordova-common').CordovaLogger.get().setLevel('error');
2728

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

332+
describe('when --link-to is provided', function() {
333+
it('when passed www folder should not move www/config.xml, only copy and update', function(done) {
334+
function checkSymWWW() {
335+
// Check if top level dirs exist.
336+
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
337+
dirs.forEach(function(d) {
338+
expect(path.join(project, d)).toExist();
339+
});
340+
expect(path.join(project, 'hooks', 'README.md')).toExist();
341+
342+
// Check if www files exist.
343+
expect(path.join(project, 'www', 'index.html')).toExist();
344+
345+
// Check www/config exists
346+
expect(path.join(project, 'www', 'config.xml')).toExist();
347+
// Check www/config.xml was not updated.
348+
var configXml = new ConfigParser(path.join(project, 'www', 'config.xml'));
349+
expect(configXml.packageName()).toEqual('io.cordova.hellocordova');
350+
expect(configXml.version()).toEqual('0.0.1');
351+
expect(configXml.description()).toEqual('this is the correct config.xml');
352+
353+
// Check that config.xml was copied to project/config.xml
354+
expect(path.join(project, 'config.xml')).toExist();
355+
configXml = new ConfigParser(path.join(project, 'config.xml'));
356+
expect(configXml.description()).toEqual('this is the correct config.xml');
357+
// Check project/config.xml was updated.
358+
expect(configXml.packageName()).toEqual(appId);
359+
expect(configXml.version()).toEqual('1.0.0');
360+
361+
// Check that we got no package.json
362+
expect(path.join(project, 'package.json')).not.toExist();
363+
364+
// Check that www is really a symlink,
365+
// and project/config.xml , hooks and merges are not
366+
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
367+
expect(fs.lstatSync(path.join(project, 'hooks')).isSymbolicLink()).not.toBe(true);
368+
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).not.toBe(true);
369+
}
370+
var config = {
371+
lib: {
372+
www: {
373+
template: true,
374+
url: path.join(__dirname, 'templates', 'config_in_www', 'www'),
375+
version: '',
376+
link: true
377+
}
378+
}
379+
};
380+
project = project + '4';
381+
return create(project, appId, appName, config)
382+
.then(checkSymWWW)
383+
.fail(function(err) {
384+
if(process.platform.slice(0, 3) == 'win') {
385+
// Allow symlink error if not in admin mode
386+
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
387+
} else {
388+
if (err) {
389+
console.log(err.stack);
390+
}
391+
expect(err).toBeUndefined();
392+
}
393+
})
394+
.fin(done);
395+
}, 60000);
396+
397+
it('with subdirectory should not update symlinked project/config.xml', function(done) {
398+
function checkSymSubDir() {
399+
// Check if top level dirs exist.
400+
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
401+
dirs.forEach(function(d) {
402+
expect(path.join(project, d)).toExist();
403+
});
404+
expect(path.join(project, 'hooks', 'README.md')).toExist();
405+
406+
//index.js and template subdir folder should not exist (inner files should be copied to the project folder)
407+
expect(path.join(project, 'index.js')).not.toExist();
408+
expect(path.join(project, 'template')).not.toExist();
409+
410+
// Check if www files exist.
411+
expect(path.join(project, 'www', 'index.html')).toExist();
412+
413+
// Check that www, and config.xml is really a symlink
414+
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
415+
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).toBe(true);
416+
417+
// Check that config.xml was not updated. (symlinked config does not get updated!)
418+
var configXml = new ConfigParser(path.join(project, 'config.xml'));
419+
expect(configXml.packageName()).toEqual('io.cordova.hellocordova');
420+
expect(configXml.version()).toEqual('0.0.1');
421+
422+
// Check that we got the right config.xml
423+
expect(configXml.description()).toEqual('this is the correct config.xml');
424+
425+
// Check that we got package.json (the correct one) and it was changed
426+
var pkjson = require(path.join(project, 'package.json'));
427+
expect(pkjson.name).toEqual(appName.toLowerCase());
428+
expect(pkjson.valid).toEqual('true');
429+
}
430+
var config = {
431+
lib: {
432+
www: {
433+
template: true,
434+
url: path.join(__dirname, 'templates', 'withsubdirectory_package_json'),
435+
version: '',
436+
link: true
437+
}
438+
}
439+
};
440+
project = project + '5';
441+
return create(project, appId, appName, config)
442+
.then(checkSymSubDir)
443+
.fail(function(err) {
444+
if(process.platform.slice(0, 3) == 'win') {
445+
// Allow symlink error if not in admin mode
446+
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
447+
} else {
448+
if (err) {
449+
console.log(err.stack);
450+
}
451+
expect(err).toBeUndefined();
452+
}
453+
})
454+
.fin(done);
455+
}, 60000);
456+
457+
it('with no config should create one and update it', function(done) {
458+
function checkSymNoConfig() {
459+
// Check if top level dirs exist.
460+
var dirs = ['hooks', 'platforms', 'plugins', 'www'];
461+
dirs.forEach(function(d) {
462+
expect(path.join(project, d)).toExist();
463+
});
464+
expect(path.join(project, 'hooks', 'hooks.file')).toExist();
465+
expect(path.join(project, 'merges', 'merges.file')).toExist();
466+
467+
// Check if www files exist.
468+
expect(path.join(project, 'www', 'index.html')).toExist();
469+
470+
// Check that config.xml was updated.
471+
var configXml = new ConfigParser(path.join(project, 'config.xml'));
472+
expect(configXml.packageName()).toEqual(appId);
473+
474+
// Check that www, hooks, merges are really a symlink; config is not
475+
expect(fs.lstatSync(path.join(project, 'www')).isSymbolicLink()).toBe(true);
476+
expect(fs.lstatSync(path.join(project, 'hooks')).isSymbolicLink()).toBe(true);
477+
expect(fs.lstatSync(path.join(project, 'merges')).isSymbolicLink()).toBe(true);
478+
expect(fs.lstatSync(path.join(project, 'config.xml')).isSymbolicLink()).not.toBe(true);
479+
}
480+
481+
var config = {
482+
lib: {
483+
www: {
484+
template: true,
485+
url: path.join(__dirname, 'templates', 'noconfig'),
486+
version: '',
487+
link: true
488+
}
489+
}
490+
};
491+
project = project + '6';
492+
return create(project, appId, appName, config)
493+
.then(checkSymNoConfig)
494+
.fail(function(err) {
495+
if(process.platform.slice(0, 3) == 'win') {
496+
// Allow symlink error if not in admin mode
497+
expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
498+
} else {
499+
if (err) {
500+
console.log(err.stack);
501+
}
502+
expect(err).toBeUndefined();
503+
}
504+
})
505+
.fin(done);
506+
}, 60000);
507+
508+
});
509+
510+
511+
512+
513+
331514
});

spec/templates/noconfig/hooks/hooks.file

Whitespace-only changes.

spec/templates/noconfig/merges/merges.file

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE html>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<html>
21+
<head>
22+
<!--
23+
Customize this policy to fit your own app's needs. For more guidance, see:
24+
https://github.com/apache/cordova-plugin-whitelist/blob/master/README.md#content-security-policy
25+
Some notes:
26+
* gap: is required only on iOS (when using UIWebView) and is needed for JS->native communication
27+
* https://ssl.gstatic.com is required only on Android and is needed for TalkBack to function properly
28+
* Disables use of inline scripts in order to mitigate risk of XSS vulnerabilities. To change this:
29+
* Enable inline JS: add 'unsafe-inline' to default-src
30+
-->
31+
<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 *">
32+
<meta name="format-detection" content="telephone=no">
33+
<meta name="msapplication-tap-highlight" content="no">
34+
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
35+
<link rel="stylesheet" type="text/css" href="css/index.css">
36+
<title>Hello World</title>
37+
</head>
38+
<body>
39+
<div class="app">
40+
<h1>Apache Cordova</h1>
41+
<div id="deviceready" class="blink">
42+
<p class="event listening">Connecting to Device</p>
43+
<p class="event received">Device is Ready</p>
44+
</div>
45+
</div>
46+
<script type="text/javascript" src="cordova.js"></script>
47+
<script type="text/javascript" src="js/index.js"></script>
48+
</body>
49+
</html>

0 commit comments

Comments
 (0)