diff --git a/.eslintrc b/.eslintrc index 57a0db7..8065a0e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,14 +3,10 @@ "globals": { "Mailer": true, - "Utils": true, "juice": false, "FlowRouter": false, "Router": false, - "TemplateHelpers": true, "SSR": false, - "Picker": false, - "Routing": true, - "Templates": true + "Picker": false } } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f28beab --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +PORT=3100 +TEST_DRIVER=dispatch:mocha + +lint: + @./node_modules/.bin/eslint . + +test-package: + @meteor test-packages ./ --driver-package $(TEST_DRIVER) --once --port $(PORT) + +test-app: + @cd example && npm test -- --port $(PORT) && cd - + +test-app-watch: + @cd example && npm run test:watch -- --port $(PORT) + +test-watch: + @TEST_WATCH=1 meteor test-packages ./ --driver-package $(TEST_DRIVER) --port $(PORT) + +.PHONY: test, test-watch diff --git a/README.md b/README.md index 41daacd..c5732bb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Meteor Emails +[![CircleCI](https://circleci.com/gh/lookback/meteor-emails.svg?style=shield)](https://circleci.com/gh/lookback/meteor-emails) + `lookback:emails` is a Meteor package that makes it easier to build, test and debug rich HTML emails. Usually, building HTML emails yourself is tedious. On top of that, add the need for data integration and thus a template language (for sending out daily digest emails, for instance). We wanted a way to preview the email in the browser *with real data* in order to quickly iterate on the design, instead of alternating between code editor and email client. @@ -562,7 +564,7 @@ route: { PRs and help is welcomed. -## Develop +### Develop Clone repo, and run: @@ -570,9 +572,27 @@ Clone repo, and run: npm install ``` -to install dev dev dependencies. We're using ESLint for linting. +to install dev dev dependencies. We're using ESLint for linting. Lint with: + +``` +npm run lint +``` + +Run tests with: + +``` +npm test +``` + +or have test watching with: + +``` +npm run test:watch +``` + +You'll find tests in the `lib` directory along with the source files. -## Things to do +### Things to do - [ ] Tests. @@ -580,4 +600,4 @@ Also see [open issues](https://github.com/lookback/meteor-emails/issues). *** -Made by [Lookback](http://github.com/lookback) +Made by [Johan](http://johanbrook.com) in [Lookback](http://github.com/lookback) diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..5f27da5 --- /dev/null +++ b/circle.yml @@ -0,0 +1,17 @@ +machine: + node: + version: 5.8.0 + +dependencies: + cache_directories: + - "~/.meteor" + - "~/.npm" + override: + # -- CACHE METEOR -- + # Restore the meteor symlink + - if [ -d ~/.meteor ]; then sudo ln -s ~/.meteor/meteor /usr/local/bin/meteor; fi + # Install Meteor (if not restored from cache) + - if [ ! -e ~/.meteor/meteor ]; then curl https://install.meteor.com | /bin/sh; fi + - npm install --no-progress + - meteor npm install --no-progress: + pwd: example diff --git a/example/.meteor/.finished-upgraders b/example/.meteor/.finished-upgraders index dacc2c0..aa60704 100644 --- a/example/.meteor/.finished-upgraders +++ b/example/.meteor/.finished-upgraders @@ -11,3 +11,5 @@ notices-for-facebook-graph-api-2 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package +1.4.0-remove-old-dev-bundle-link +1.4.1-add-shell-server-package diff --git a/example/.meteor/packages b/example/.meteor/packages index b7da44d..42991ec 100644 --- a/example/.meteor/packages +++ b/example/.meteor/packages @@ -4,14 +4,18 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -ecmascript +ecmascript@0.5.9 lookback:emails #chrisbutler:node-sass@3.2.0 -meteor-base +meteor-base@1.0.4 blaze-html-templates -reload +reload@1.1.11 spacebars kadira:flow-router #iron:router -standard-minifier-css -standard-minifier-js +standard-minifier-css@1.3.2 +standard-minifier-js@1.2.1 + +practicalmeteor:chai +dispatch:mocha +shell-server diff --git a/example/.meteor/release b/example/.meteor/release index 72980bc..c260846 100644 --- a/example/.meteor/release +++ b/example/.meteor/release @@ -1 +1 @@ -METEOR@1.4.1.1 +METEOR@1.4.2 diff --git a/example/.meteor/versions b/example/.meteor/versions index 17147ac..1a4dbb4 100644 --- a/example/.meteor/versions +++ b/example/.meteor/versions @@ -1,69 +1,76 @@ allow-deny@1.0.5 -autoupdate@1.2.10 -babel-compiler@6.8.3 -babel-runtime@0.1.9_1 -base64@1.0.9 -binary-heap@1.0.9 -blaze@2.1.8 -blaze-html-templates@1.0.4 -blaze-tools@1.0.9 -boilerplate-generator@1.0.9 -caching-compiler@1.0.5_1 -caching-html-compiler@1.0.6 -callback-hook@1.0.9 -check@1.2.3 +autoupdate@1.2.11 +babel-compiler@6.13.0 +babel-runtime@0.1.12 +base64@1.0.10 +binary-heap@1.0.10 +blaze@2.1.9 +blaze-html-templates@1.0.5 +blaze-tools@1.0.10 +boilerplate-generator@1.0.11 +caching-compiler@1.1.8 +caching-html-compiler@1.0.7 +callback-hook@1.0.10 +check@1.2.4 +coffeescript@1.0.17 ddp@1.2.5 -ddp-client@1.2.8_1 -ddp-common@1.2.6 -ddp-server@1.2.8_1 +ddp-client@1.2.9 +ddp-common@1.2.7 +ddp-server@1.2.10 deps@1.0.12 -diff-sequence@1.0.6 -ecmascript@0.4.6_1 -ecmascript-runtime@0.2.11_1 -ejson@1.0.12 -email@1.0.14_1 -geojson-utils@1.0.9 +diff-sequence@1.0.7 +dispatch:mocha@0.0.9 +ecmascript@0.5.9 +ecmascript-runtime@0.3.15 +ejson@1.0.13 +email@1.0.16 +geojson-utils@1.0.10 hot-code-push@1.0.4 -html-tools@1.0.10 -htmljs@1.0.10 -http@1.1.7 -id-map@1.0.8 -jquery@1.11.9 +html-tools@1.0.11 +htmljs@1.0.11 +http@1.1.8 +id-map@1.0.9 +jquery@1.11.10 kadira:flow-router@2.12.1 livedata@1.0.18 -logging@1.0.13_1 +logging@1.0.14 lookback:emails@0.7.5 -meteor@1.1.15_1 +meteor@1.6.0 meteor-base@1.0.4 meteorhacks:picker@1.0.3 meteorhacks:ssr@2.2.0 -minifier-css@1.1.12_1 -minifier-js@1.1.12_1 -minimongo@1.0.17 -modules@0.6.4 -modules-runtime@0.6.4_1 +minifier-css@1.2.15 +minifier-js@1.2.15 +minimongo@1.0.18 +modules@0.7.7 +modules-runtime@0.7.7 mongo@1.1.9_1 -mongo-id@1.0.5 -npm-mongo@1.4.44_1 -observe-sequence@1.0.12 -ordered-dict@1.0.8 -promise@0.7.2_1 +mongo-id@1.0.6 +npm-mongo@1.4.45 +observe-sequence@1.0.14 +ordered-dict@1.0.9 +practicalmeteor:chai@2.1.0_1 +practicalmeteor:mocha-core@1.0.1 +promise@0.8.8 random@1.0.10 reactive-dict@1.1.8 -reactive-var@1.0.10 -reload@1.1.10 -retry@1.0.8 +reactive-var@1.0.11 +reload@1.1.11 +retry@1.0.9 routepolicy@1.0.11 sacha:juice@0.1.4 -spacebars@1.0.12 -spacebars-compiler@1.0.12 -standard-minifier-css@1.0.7_1 -standard-minifier-js@1.0.7_1 -templating@1.1.12_1 -templating-tools@1.0.4 -tracker@1.0.14 -ui@1.0.11 -underscore@1.0.9 -url@1.0.10 -webapp@1.2.9_1 +shell-server@0.2.1 +spacebars@1.0.13 +spacebars-compiler@1.0.13 +standard-minifier-css@1.3.2 +standard-minifier-js@1.2.1 +templating@1.2.15 +templating-compiler@1.2.15 +templating-runtime@1.2.15 +templating-tools@1.0.5 +tracker@1.1.1 +ui@1.0.12 +underscore@1.0.10 +url@1.0.11 +webapp@1.2.11 webapp-hashing@1.0.9 diff --git a/example/package.json b/example/package.json index efb15a8..e7e87e3 100644 --- a/example/package.json +++ b/example/package.json @@ -2,7 +2,8 @@ "name": "example", "version": "1.0.0", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "meteor test --driver-package dispatch:mocha --once --full-app", + "test:watch": "TEST_WATCH=1 meteor test --port 5012 --driver-package dispatch:mocha --full-app" }, "author": "Johan Brook", "license": "MIT", diff --git a/example/packages/lookback:emails b/example/packages/emails similarity index 100% rename from example/packages/lookback:emails rename to example/packages/emails diff --git a/example/server/lib/template-helpers.js b/example/server/lib/template-helpers.js index 613ccae..687faed 100644 --- a/example/server/lib/template-helpers.js +++ b/example/server/lib/template-helpers.js @@ -1,4 +1,4 @@ -TemplateHelpers = { +export default { enumerate(arr, limit, oxfordComma) { if (arr) { @@ -24,7 +24,7 @@ TemplateHelpers = { let suffix = ' and '; if (oxfordComma === true - || (typeof oxfordComma === 'number' && length >= oxfordComma)) { + || typeof oxfordComma === 'number' && length >= oxfordComma) { suffix = `, ${suffix}`; } diff --git a/example/server/lib/templates.js b/example/server/lib/templates.js index dd05cd3..5a770e1 100644 --- a/example/server/lib/templates.js +++ b/example/server/lib/templates.js @@ -1,22 +1,20 @@ -Templates = {}; +export default { + sample: { + path: 'sample-email/template.html', // Relative to the 'private' dir. + scss: 'sample-email/style.scss', // Mail specific SCSS. -Templates.sample = { - path: 'sample-email/template.html', // Relative to the 'private' dir. - scss: 'sample-email/style.scss', // Mail specific SCSS. + helpers: { + capitalizedName() { + return this.name.charAt(0).toUpperCase() + this.name.slice(1); + } + }, - helpers: { - capitalizedName() { - return this.name.charAt(0).toUpperCase() + this.name.slice(1); - } - }, - - route: { - path: '/sample/:name', - data: function(params) { - return { + route: { + path: '/sample/:name', + data: (params) => ({ name: params.name, names: ['Johan', 'John', 'Paul', 'Ringo'] - }; + }) } } }; diff --git a/example/server/server.app-test.js b/example/server/server.app-test.js new file mode 100644 index 0000000..771a2c5 --- /dev/null +++ b/example/server/server.app-test.js @@ -0,0 +1,29 @@ +/* eslint-env mocha */ +import { Mailer } from 'meteor/lookback:emails'; +import { assert } from 'meteor/practicalmeteor:chai'; + +describe('Render email', () => { + + it('should render an email', () => { + const string = Mailer.render('sample', { + name: 'johan', + names: ['Johan', 'John', 'Paul', 'Ringo'] + }); + + assert.isString(string); + assert.match(string, new RegExp('

Hi Johan', 'gm'), 'includes heading with capitalized name'); + }); + + it('should render with a layout', () => { + const searchFor = 'Paul is dead'; + + const string = Mailer.render('sample', { + name: 'johan', + names: ['Johan', 'John', 'Paul', 'Ringo'], + preview: searchFor + }); + + assert.isString(string); + assert.match(string, new RegExp(`${searchFor}`, 'gm'), 'includes a element from layout.html'); + }); +}); diff --git a/example/server/server.js b/example/server/server.js index 53e2d02..dc3575a 100644 --- a/example/server/server.js +++ b/example/server/server.js @@ -1,3 +1,6 @@ +import TemplateHelpers from './lib/template-helpers'; +import Templates from './lib/templates'; + if (!process.env.MAIL_URL) { process.env.MAIL_URL = Meteor.settings.MAIL_URL; } diff --git a/export.js b/export.js new file mode 100644 index 0000000..e1bcdb0 --- /dev/null +++ b/export.js @@ -0,0 +1,3 @@ +import { Mailer as theModule } from './lib/mailer'; + +Mailer = theModule; diff --git a/lib/mailer.js b/lib/mailer.js index ec6d8ec..5e4853a 100644 --- a/lib/mailer.js +++ b/lib/mailer.js @@ -5,6 +5,9 @@ // See the [GitHub repo](https://github.com/lookback/meteor-emails) for README. // Made by Johan Brook for [Lookback](https://github.com/lookback). +import RoutingMiddleware from './routing'; +import TemplateHelpers from './template-helpers'; +import Utils from './utils'; const TAG = 'mailer'; @@ -19,7 +22,7 @@ const TAG = 'mailer'; // - `disabled`, optionally disable the actual email sending. Useful for E2E testing. // Defaults to `false`. // - `addRoutes`, should we add preview and send routes? Defaults to `true` in development. -Mailer = { +export const Mailer = { settings: { silent: false, routePrefix: 'emails', @@ -300,7 +303,7 @@ Mailer.init = function(opts) { const obj = _.extend(this, factory(opts)); if (obj.settings.addRoutes) { - obj.use(Routing); + obj.use(RoutingMiddleware); } obj.init(); diff --git a/lib/mailer.test.js b/lib/mailer.test.js new file mode 100644 index 0000000..f30538d --- /dev/null +++ b/lib/mailer.test.js @@ -0,0 +1,10 @@ +/* eslint-env mocha */ + +import Mailer from './mailer'; +import { assert } from 'meteor/practicalmeteor:chai'; + +describe('Mailer', () => { + it('should be defined', () => { + assert.isObject(Mailer); + }); +}); diff --git a/lib/routing.js b/lib/routing.js index 2cbb425..3897678 100644 --- a/lib/routing.js +++ b/lib/routing.js @@ -7,16 +7,17 @@ // It will apply the returned data from a `data` function on the // provided `route` prop from the template. +import Utils from './utils'; + const CONTENT_TYPES = { html: 'text/html', text: 'text/plain' }; -const arrayOrString = (str) => { - return Array.isArray(str) ? str : str.split(','); -}; +const arrayOrString = (str) => + Array.isArray(str) ? str : str.split(','); -Routing = (template, settings, render, compile) => { +export default function Routing(template, settings, render, compile) { check(template, Object); check(template.name, String); @@ -123,7 +124,7 @@ Routing = (template, settings, render, compile) => { res.writeHead(200); const reallySentEmail = !!process.env.MAIL_URL; msg = reallySentEmail - ? `Sent test email to ${to}` + ((cc) ? ` and cc: ${cc}` : '') + ((bcc) ? `, and bcc: ${bcc}` : '') + ? `Sent test email to ${to}` + ((cc) ? ` and cc: ${cc}` : '') + ((bcc) ? `, and bcc: ${bcc}` : '') // eslint-disable-line : 'Sent email to STDOUT'; } @@ -151,4 +152,4 @@ Routing = (template, settings, render, compile) => { Picker.route(path, (params, req, res) => action(req, res, params, template)); }); -}; +} diff --git a/lib/template-helpers.js b/lib/template-helpers.js index 53435ef..90329fb 100644 --- a/lib/template-helpers.js +++ b/lib/template-helpers.js @@ -1,7 +1,9 @@ +import Utils from './utils'; + // # Template helpers // // Built-in template helpers. -TemplateHelpers = { +export default { // `baseUrl` gives you a full absolute URL from a relative path. // // {{ baseUrl '/some-path' }} => http://root-domain.com/some-path @@ -17,7 +19,8 @@ TemplateHelpers = { const theRouter = Package['iron:router'] ? Router : FlowRouter; if (theRouter && theRouter.path) { - return Utils.joinUrl(Mailer.settings.baseUrl, theRouter.path.call(theRouter, routeName, params.hash)); + return Utils.joinUrl(Mailer.settings.baseUrl, + theRouter.path.call(theRouter, routeName, params.hash)); } Utils.Logger.warn(`We noticed that neither Iron Router nor FlowRouter is installed, thus 'emailUrlFor' can't render a path to the route '${routeName}.`); diff --git a/lib/utils.js b/lib/utils.js index b0bf80d..e2a878a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -27,15 +27,13 @@ const sass = (function() { * @param {String} filePath * @return {Boolean} */ -const fileExists = function (filePath) { +const fileExists = (filePath) => { try { return fs.statSync(filePath).isFile(); + } catch (err) { + return false; } - catch (err) - { - return false; - } -} +}; const TAG = 'mailer-utils'; @@ -78,7 +76,7 @@ let ROOT = privateDir && Path.join(privateDir, 'programs', 'server', 'assets', ' ROOT = ROOT || developmentPrivateDir(); -Utils = { +const Utils = { // Takes an HTML string and outputs a text version of it. Catches and logs errors. toText(html, opts = {}) { try { @@ -168,11 +166,11 @@ Utils = { }, readFile(relativePathFromApp) { - let file - if (Meteor.isAppTest - || Meteor.isTest) { - // note: - // * this DOES WORK with Meteor.isTest (=unit test mode) + let file; + + if (Meteor.isAppTest || Meteor.isTest) { + // Note: + // * this does work with Meteor.isTest (=unit test mode) // * Meteor.isAppTest is NOT TESTED yet (=in-app test mode) // // background-info: we had NO luck with "Assets.absoluteFilePath(relativePathFromApp)", @@ -185,9 +183,7 @@ Utils = { } try { - return fs.readFileSync(file, { - encoding: 'utf8' - }); + return fs.readFileSync(file, { encoding: 'utf8' }); } catch (ex) { throw new Meteor.Error(500, `Could not find file: ${file}`, ex.message); } @@ -200,17 +196,18 @@ Utils = { ? 'Please run `meteor npm install --save node-sass` in your app to add sass support.' : 'Please run `meteor add chrisbutler:node-sass` to add sass support.'; - Utils.Logger.warn(`Could not find sass module. Sass support is opt-in since lookback:emails@0.5.0. + Utils.Logger + .warn(`Could not find sass module. Sass support is opt-in since lookback:emails@0.5.0. ${packageToRecommend}`, TAG); return Utils.readFile(scss); } - let file - if (Meteor.isAppTest - || Meteor.isTest) { - // note: - // * this DOES WORK with Meteor.isTest (=unit test mode) + let file; + + if (Meteor.isAppTest || Meteor.isTest) { + // Note: + // * this does work with Meteor.isTest (=unit test mode) // * Meteor.isAppTest is NOT TESTED yet (=in-app test mode) file = Path.join(process.cwd(), 'assets', 'app', scss); } else { @@ -221,7 +218,7 @@ ${packageToRecommend}`, TAG); if (fileExists(file)) { try { return sass.renderSync({ - file: file, + file, sourceMap: false }).css.toString(); } catch (ex) { @@ -232,3 +229,5 @@ ${packageToRecommend}`, TAG); return ''; // fallback: on error, or if file does NOT exist } }; + +export default Utils; diff --git a/package.js b/package.js index c139ab0..c07edc4 100644 --- a/package.js +++ b/package.js @@ -23,7 +23,7 @@ Package.onUse(function(api) { ], where, { weak: true }); api.use([ - 'ecmascript@0.1.5', + 'ecmascript', 'check', 'underscore', 'email', @@ -33,11 +33,20 @@ Package.onUse(function(api) { ], where); api.addFiles([ - 'lib/utils.js', - 'lib/template-helpers.js', - 'lib/routing.js', - 'lib/mailer.js' + 'export.js' ], where); api.export('Mailer', where); }); + +Package.onTest(function(api) { + api.use([ + 'ecmascript', + 'underscore', + 'dispatch:mocha', + 'practicalmeteor:chai', + 'lookback:emails', + ], 'server'); + + api.mainModule('tests.js', 'server'); +}); diff --git a/package.json b/package.json index b701462..4c9575c 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,12 @@ "type": "git", "url": "git+https://github.com/lookback/meteor-emails.git" }, + "author": "Johan Brook", "scripts": { - "lint": "./node_modules/.bin/eslint ." + "lint": "make lint", + "pretest": "npm run lint", + "test": "make test-app && make test-package", + "test:watch": "make test-watch" }, "license": "MIT", "bugs": { diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..37cc25d --- /dev/null +++ b/tests.js @@ -0,0 +1,2 @@ +// Run tests on individual modules +import './lib/mailer.test.js';