diff --git a/.versions b/.versions index fb30cb9..11148e6 100644 --- a/.versions +++ b/.versions @@ -6,7 +6,7 @@ ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.10.0 ecmascript-runtime-server@0.9.0 fetch@0.1.1 -frozeman:build-client@0.4.3 +frozeman:build-client@1.0.0 inter-process-messaging@0.1.1 meteor@1.9.3 minifier-css@1.5.0 diff --git a/README.md b/README.md index 6f94837..a068a8b 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,29 @@ # Meteor Build Client -This tool builds and bundles the client part of a Meteor app with a simple index.html, -so it can be hosted on any server or even loaded via the `file://` protocol. +This tool builds and bundles the client part of a Meteor app with a simple `index.html`, so it can be hosted on any server or even loaded via the `file://` protocol. ## Installation - $ [sudo] npm install -g meteor-build-client +```shell +npm install -g meteor-build-client +``` ## Usage - // cd into your meteor app - $ cd myApp +```shell +// cd into your meteor app +cd /my/app + +// run meteor-build-client +meteor-build-client ../output/directory +``` - // run meteor-build-client - $ meteor-build-client ../myOutputFolder +## Important notes: -**Warning** the content of the output folder will be deleted before building the new output! So dont do things like -`$ meteor-build-client /home`! +- __Warning__: the content of the output folder will be deleted before building the new output! So don't do things like `meteor-build-client /home`! +- __Do not use dynamic imports!__ e.g. `import('/eager/file');`; +- By default this package link __legacy__ ES5 bundle build. ### Output @@ -33,70 +39,86 @@ The content of the output folder could look as follows: For a list of options see: - $ meteor-build-client --help +```shell +meteor-build-client --help +``` ### Passing a settings.json You can pass an additional settings file using the `--settings` or `-s` option: - $ meteor-build-client ../myOutputFolder -s ../settings.json +```shell +meteor-build-client ../output/directory -s ../settings.json +``` **Note** Only the `public` property of that JSON file will be add to the `Meteor.settings` property. - ### App URL Additionally you can set the `ROOT_URL` of your app using the `--url` or `-u` option: - $ meteor-build-client ../myOutputFolder -u http://myserver.com - -If you pass `"default"`, your app will try to connect to the server where the application was served from. +```shell +meteor-build-client ../output/directory -u https://myserver.com +``` -If this option was not set, it will set the server to `""` (empty string) and will add a `Meteor.disconnect()` after Meteor was loaded. +If you pass `"default"`, your app will try to connect to the server where the application was served from. If this option was not set, it will set the server to `""` (empty string) and will add a `Meteor.disconnect()` after Meteor was loaded. ### Absolute or relative paths -If you want to be able to start you app by simply opening the index.html (using the `file://` protocol), -you need to link your files relative. You can do this by setting the `--path` or `-p` option: +If you want to be able to start you app by simply opening the index.html (using the `file://` protocol), you need to link your files relative. You can do this by setting the `--path` or `-p` option: - $ meteor-build-client ../myOutputFolder -p "" +```shell +meteor-build-client ../output/directory -p "" +``` The default path value is `"/"`. *Note* When set a path value, it will also replace this path in you Meteor CSS file, so that fonts etc link correctly. - ### Using your own build folder -If you want to use your own build folder by running `meteor build` yourself, specify the `--usebuild` flag and meteor-build-client will not run the meteor build command for you. It will expect that the build folder be located up a directory from the app folder (A sibling to /app, Meteor's default location). +To use pre-build Meteor application, built using `meteor build` command manually, specify the `--usebuild ` flag and `meteor-build-client` will not run the `meteor build` command. -### Using custom templates +### Recommended packages for client-only build -If you want to provide a custom template for the initial HTML provide an HTML file with the `--template` or `-t` option: +If you're building server-less standalone web application we recommend to replace `meteor-base` with `meteor` and `webapp` packages. - $ meteor-build-client ../myOutputFolder -t ../myTemplate.html +```diff +@@ .meteor/packages +- meteor-base ++ meteor ++ webapp +``` -The template file need to contain the following placholders: `{{> head}}`, `{{> css}}` and `{{> scripts}}`. -The following example adds a simple loading text to the initial HTML file (Your app should later take care of removing the loading text): +### Template + +Following Meteor's recommended usage of `` and `` this tags will be replaced with links to generated CSS and JS files respectively. Optionally, use `{{url-to-meteor-bundled-css}}` as a placeholder for URL to generated CSS file. We encourage to use `static-html` (*for non-Blaze projects*) or `blaze-html-templates` (*for Blaze projects*) package for creating bare HTML template in your app, minimal example: + +```html + + + + + + + My Meteor App + + + + + +``` + +Where `` will be replaced with `` element to generated CSS file(s) and `{{url-to-meteor-bundled-css}}` will be replaced with URL to generated CSS file. ```html - - - - {{> head}} - - - -

Loading...

- - {{> css}} - {{> scripts}} - - + + + + ``` -By linking a file from your `public` folder (e.g. `loadingScreen.css`) and moving the `{{> css}}` and `{{> scripts}}` placeholder to the end of the `` tag, -you can simply style your loading screen. -Because the small CSS file (`loadingScreen.css`) and the body content will be loaded *before* the Meteor app script, the the user sees the nice Loading text. + +Where `` will be replaced with `'); - }) - scripts = scripts.join("\n "); - - // add the meteor runtime config - settings = { - 'meteorRelease': starJson.meteorRelease, - 'ROOT_URL_PATH_PREFIX': '', - meteorEnv: { NODE_ENV: 'production' }, - autoupdate: { versions : {}}, - // 'DDP_DEFAULT_CONNECTION_URL': program.url || '', // will reload infinite if Meteor.disconnect is not called - // 'appId': process.env.APP_ID || null, - // 'autoupdateVersion': null, // "ecf7fcc2e3d4696ea099fdd287dfa56068a692ec" - // 'autoupdateVersionRefreshable': null, // "c5600e68d4f2f5b920340f777e3bfc4297127d6e" - // 'autoupdateVersionCordova': null - }; - // on url = "default", we dont set the ROOT_URL, so Meteor chooses the app serving url for its DDP connection - if(program.url !== 'default') - settings.ROOT_URL = program.url || ''; - - - if(settingsJson.public) - settings.PUBLIC_SETTINGS = settingsJson.public; - - scripts = scripts.replace('__meteor_runtime_config__', ''); - - // add Meteor.disconnect() when no server is given - if(!program.url) - scripts += "\n"+' '; - - content = content.replace(/{{ *> *scripts *}}/, scripts); - - // write the index.html - return fs.writeFileAsync(path.join(buildPath, 'index.html'), content); - }); - }, - cleanUp: function(callback) { - return Q.try(function() { - // remove files - deleteFolderRecursive(path.join(buildPath, 'bundle')); - fs.unlinkSync(path.join(buildPath, 'program.json')); - try{ - fs.unlinkSync(path.join(buildPath, 'head.html')); - } catch (e){ - console.log("Didn't unlink head.html; doesn't exist."); - } + // ADD HEAD + content = content.replace(RE.templates.head, head); + content = content.replace(RE.templates.body, body); + + // get the css and js files + const files = { + css: [], + js: [] + }; + + _.each(fs.readdirSync(outputPath), (file) => { + if (RE.fileName.js.test(file)) { + files.js.push(file); + } else if (RE.fileName.css.test(file)) { + files.css.push(file); + } + }); + + let primaryCSSfile = files.css[0]; + + // --debug case + if (program.debug) { + const json = fs.readFileSync(path.resolve(path.join(outputPath, 'program.json')), {encoding: 'utf-8'}); + const prog = JSON.parse(json); + + _.each(prog.manifest, (item) => { + if (item.type === 'js' && item.url) { + files.js.push(item.path.replace(RE.path.app, '') + '?hash=' + item.hash); + } else if (item.type === 'css' && item.url) { + // for css file cases, do not append hash. + files.css.push(item.path.replace(RE.path.app, '')); + + if (item.url.includes('meteor_css_resource=true')) { + primaryCSSfile = item.path.replace(RE.path.app, ''); + } + } }); - } -} + + print('[DEBUG] Files:', files, {primaryCSSfile}); + } + + // MAKE PATHS ABSOLUTE + if(_.isString(program.path)) { + // fix paths in the CSS file + if(!_.isEmpty(files.css)) { + _.each(files.css, (css, i) => { + var cssFile = fs.readFileSync(path.join(outputPath, css), { encoding: 'utf8' }); + cssFile = cssFile.replace(/url\(\'\//g, `url('${program.path}`).replace(/url\(\//g, `url(${program.path}`); + fs.unlinkSync(path.join(outputPath, css)); + fs.writeFileSync(path.join(outputPath, css), cssFile, { encoding: 'utf8' }); + + files.css[i] = `${program.path}${css}`; + }); + } + + if(!_.isEmpty(files.js)) { + _.each(files.js, (jsFile, i) => { + files.js[i] = `${program.path}${jsFile}`; + }); + } + + if (primaryCSSfile) { + primaryCSSfile = `${program.path}${primaryCSSfile}`; + } + } else { + if(!_.isEmpty(files.css)) { + _.each(files.css, (cssFile, i) => { + files.css[i] = `/${cssFile}`; + }); + } + + if(!_.isEmpty(files.js)) { + _.each(files.js, (jsFile, i) => { + files.js[i] = `/${jsFile}`; + }); + } + + if (primaryCSSfile) { + primaryCSSfile = `/${primaryCSSfile}`; + } + } + + // ADD CSS + var css = []; + _.each(files.css, (cssFile) => { + css.push(``); + }); + css = css.join(''); + + if (RE.styles.template.test(content)) { + content = content.replace(RE.styles.template, css); + } else if (RE.styles.tag.test(content)) { + content = content.replace(RE.styles.tag, css); + } + + if (RE.styles.url.test(content) && primaryCSSfile) { + content = content.replace(RE.styles.url, primaryCSSfile); + } + + // ADD the SCRIPT files + var scripts = ['__meteor_runtime_config__']; + _.each(files.js, (jsFile) => { + scripts.push(``); + }); + scripts = scripts.join(''); + + // add the meteor runtime config + const settings = { + 'meteorRelease': starJson.meteorRelease, + 'ROOT_URL_PATH_PREFIX': process.env.ROOT_URL_PATH_PREFIX || '', + meteorEnv: { + NODE_ENV: 'production' + }, + autoupdate: { versions: {}}, + // 'DDP_DEFAULT_CONNECTION_URL': program.url || '', // will reload infinite if Meteor.disconnect is not called + // 'appId': process.env.APP_ID || null, + // 'autoupdateVersion': null, // "ecf7fcc2e3d4696ea099fdd287dfa56068a692ec" + // 'autoupdateVersionRefreshable': null, // "c5600e68d4f2f5b920340f777e3bfc4297127d6e" + // 'autoupdateVersionCordova': null + }; + + // on url = "default", we dont set the ROOT_URL, so Meteor chooses the app serving url for its DDP connection + if (program.url !== 'default') { + settings.ROOT_URL = program.url || ''; + } + + if (settingsJson.public) { + settings.PUBLIC_SETTINGS = settingsJson.public; + } + + scripts = scripts.replace('__meteor_runtime_config__', ``); + + // add Meteor.disconnect() when no server is given + if (!program.url) { + scripts += ''; + } + + if (RE.scripts.template.test(content)) { + content = content.replace(RE.scripts.template, scripts); + } else if (RE.scripts.tag.test(content)) { + content = content.replace(RE.scripts.tag, scripts); + } + + // write the index.html + return fs.writeFileAsync(path.join(outputPath, 'index.html'), content).then(() => { + if (!program.url) { + return fs.mkdirAsync(path.join(outputPath, 'sockjs')) + .then(() => fs.writeFileAsync(path.join(outputPath, 'sockjs/info'), '{"websocket": false}', { encoding: 'utf8' })).catch(() => { + print('sockjs/info not created or already exists'); + return true; + }); + } + return true; + }); + }); + }, + cleanUp(program) { + return Q.try(function() { + // remove files + if (!program.usebuild) { + deleteFolderRecursive(path.join(buildPath, 'bundle')); + } + + try { + fs.unlinkSync(path.join(outputPath, 'program.json')); + } catch (e){ + if (program.debug) { + print('FYI: Didn\'t unlink program.json; doesn\'t exist.'); + } + } + + try{ + fs.unlinkSync(path.join(outputPath, 'head.html')); + } catch (e){ + if (program.debug) { + print('FYI: Didn\'t unlink head.html; doesn\'t exist.'); + } + } + + try{ + fs.unlinkSync(path.join(outputPath, 'body.html')); + } catch (e){ + if (program.debug) { + print('FYI: Didn\'t unlink body.html; doesn\'t exist.'); + } + } + }); + } +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f20fa7f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,107 @@ +{ + "name": "meteor-build-client", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "buffered-spawn": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/buffered-spawn/-/buffered-spawn-3.3.2.tgz", + "integrity": "sha1-l7mEbE5EaqIzILSpTFIJ7dMtrLs=", + "requires": { + "cross-spawn": "^4.0.0" + } + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "underscore": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", + "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } +} diff --git a/package.js b/package.js index 5eb936e..f626329 100644 --- a/package.js +++ b/package.js @@ -1,12 +1,11 @@ Package.describe({ - name: "frozeman:build-client", - summary: "Placeholder package for meteor-build-client (npm). Do not install!", - version: "0.4.3", - git: "https://github.com/frozeman/meteor-build-client" + name: 'frozeman:build-client', + summary: 'Placeholder package for meteor-build-client (npm). Do not install!', + version: '1.0.0', + git: 'https://github.com/frozeman/meteor-build-client' }); - Package.onUse(function (api) { - api.versionsFrom('1.2'); - api.use('standard-minifiers'); + api.versionsFrom('1.2'); + api.use('standard-minifiers'); }); diff --git a/package.json b/package.json index 16927d5..0a2b56a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meteor-build-client", - "version": "0.4.3", + "version": "1.0.0", "description": "A bundles the client part of a Meteor app.", "author": "Fabian Vogelsteller ", "contributors": [ @@ -32,8 +32,8 @@ "dependencies": { "bluebird": "^3.4.6", "buffered-spawn": "^3.3.2", - "commander": "^2.8.1", - "simple-spinner": "0.0.3", + "commander": "^5.1.0", + "fs-extra": "^9.0.1", "underscore": "^1.8.3" }, "devDependencies": {} diff --git a/queue.js b/queue.js deleted file mode 100644 index 8b73c95..0000000 --- a/queue.js +++ /dev/null @@ -1,55 +0,0 @@ -// FIFO -module.exports = function() { - var self = this; - var invokations = []; - var paused = true; - var maxLength = 0; - - self.progress = function(count, total) { - // console.log(count + ' of ' + total); - }; - - self.reset = function() { - paused = true; - invokations = []; - }; - - self.add = function(f) { - if (paused) { - if (typeof f !== 'function') { - throw new Error('queue requires function'); - } - invokations.push(f); - } - }; - - self.next = function(text) { - if (text) { - self.reset(); - console.log(' ' + text.red); - } - - if (!paused) { - - - if (invokations.length) { - var f = invokations.shift(); - // Update the progress - self.progress(invokations.length, maxLength); - setTimeout(function() { - // Run function - f(self.next); - }, 0); - } - - } - }; - - self.run = function() { - paused = false; - maxLength = invokations.length; - // Update the progress - self.progress(invokations.length, maxLength); - self.next(); - }; -}; \ No newline at end of file