}
76 | */
77 | async template(templateName, data) {
78 | return Template.load(
79 | `${templateName}.${this.options.templateFileExtension}`,
80 | this.options,
81 | {
82 | template: templateName,
83 | ...data
84 | }
85 | );
86 | }
87 | }
88 |
89 | Macaw.defaultOptions = defaultOptions;
90 |
91 | module.exports = Macaw;
92 |
--------------------------------------------------------------------------------
/packages/engine/src/lib/Template.js:
--------------------------------------------------------------------------------
1 | const showdown = require("showdown");
2 | const mjml2html = require("mjml");
3 | const { twig } = require("twig");
4 |
5 | const fm = require("./Frontmatter");
6 |
7 | /**
8 | * Constructor for the Macaw template instance.
9 | * Rather than initiating this class directly, use the `template` factory
10 | * method in the `Macaw` class.
11 | *
12 | * @param {Object} options Macaw instance options
13 | * @param {Object} data Variables to pass along to renderer
14 | */
15 | class Template {
16 | /**
17 | * Constructor.
18 | *
19 | * @param {Object} options Macaw instance options
20 | * @param {Object} data Variables to pass along to renderer
21 | */
22 | constructor(options, data) {
23 | this.options = options;
24 | this.data = data;
25 | this.includesCache = {};
26 | }
27 |
28 | async resolveIncludes(str) {
29 | const regex = /{\s?%\s+?includes?\s+(['"])([^'"]+)(['"])\s+?%\s?\}/;
30 | while (str.match(regex)) {
31 | const result = str.match(regex);
32 | const { index } = result;
33 | const [match, , includePath] = result;
34 | const fullIncludePath = `${this.options.layoutsDirectory}/partials/${includePath}.mjml`;
35 | this.includesCache[fullIncludePath] = await this.options.storage.getItem(
36 | fullIncludePath
37 | );
38 | str = `${str.substr(
39 | 0,
40 | index
41 | )}${str.substr(
42 | index + match.length
43 | )}`;
44 | }
45 | return str;
46 | }
47 |
48 | applyIncludes(str) {
49 | const regex = /(\s+?)?(\s+?<\/p>)?/;
50 | while (str.match(regex)) {
51 | const result = str.match(regex);
52 | const { index } = result;
53 | const [match, , fullIncludePath] = result;
54 | str = `${str.substr(0, index)}${
55 | this.includesCache[fullIncludePath]
56 | }${str.substr(index + match.length)}`;
57 | }
58 |
59 | return str;
60 | }
61 |
62 | /**
63 | * Load a template file and its layout from the storage engine.
64 | *
65 | * @param {string} templatePath The template path
66 | * @returns {Promise}
67 | */
68 | async loadFile(templatePath) {
69 | // Load file from storage
70 | const content = await this.options.storage.getItem(templatePath);
71 |
72 | // Get Frontmatter
73 | const { attributes, body } = await fm(content, this.data, [
74 | this.resolveIncludes.bind(this),
75 | this.applyIncludes.bind(this)
76 | ]);
77 | this.data = { ...attributes, ...this.data };
78 |
79 | // Resolve include tags in markdown body
80 | this.markdown = await this.resolveIncludes(body);
81 |
82 | if (!this.data.layout) {
83 | this.data.layout = "default";
84 | }
85 |
86 | this.layoutFilePath = `${this.options.layoutsDirectory}/${this.data.layout}.mjml`;
87 | this.mjml = await this.resolveIncludes(
88 | await this.options.storage.getItem(this.layoutFilePath)
89 | );
90 | }
91 |
92 | /**
93 | * Parse the template and return raw HTML output.
94 | *
95 | * @returns {string} Parsed template HTML
96 | */
97 | render() {
98 | const markdown = twig({ data: this.markdown }).render(this.data);
99 | const converter = new showdown.Converter(this.options.markdown);
100 | const body = twig({
101 | data: this.applyIncludes(converter.makeHtml(markdown))
102 | }).render(this.data);
103 |
104 | const mjml = twig({ data: this.applyIncludes(this.mjml) }).render({
105 | ...this.data,
106 | body
107 | });
108 | const { html, errors } = mjml2html(mjml, {
109 | ...this.options.mjml
110 | });
111 |
112 | if (errors.length) {
113 | const err = new Error(
114 | errors.reduce(
115 | (errors, error) => `${errors}\n - ${error.formattedMessage}`,
116 | "Invalid MJML:"
117 | )
118 | );
119 | err.errors = errors;
120 | throw err;
121 | }
122 |
123 | return html;
124 | }
125 |
126 | /**
127 | * Send the template via the `provider` specified in the Macaw options.
128 | *
129 | * Every provider requires their own set of `sendOptions`, so have a look
130 | * at the README file for the provider to find out what to pass along.
131 | *
132 | * @param {Object} sendOptions Options to be passed to provider
133 | * @returns {Promise} Response from provider
134 | */
135 | send(sendOptions) {
136 | if (!this.options.provider) {
137 | throw Error("No provider set to send email with.");
138 | }
139 |
140 | const html = this.render();
141 | return this.options.provider.send({
142 | ...sendOptions,
143 | data: this.data,
144 | html
145 | });
146 | }
147 | }
148 |
149 | /**
150 | * Static template loading helper.
151 | *
152 | * @param {string} templatePath Path of the template in the storage engine
153 | * @param {Object} options Macaw instance options
154 | * @param {Object} data Variables to pass along to renderer
155 | * @returns {Promise}
156 | */
157 | Template.load = async (templatePath, options, data) => {
158 | const template = new Template(options, data);
159 | await template.loadFile(templatePath);
160 | return template;
161 | };
162 |
163 | module.exports = Template;
164 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-custom-layout.md:
--------------------------------------------------------------------------------
1 | ---
2 | subject: Hello, world.
3 | layout: custom
4 | ---
5 |
6 | Hello, {{name}}!
7 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-invalid-frontmatter.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | Hello, {{name}}!
4 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-invalid-mjml.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: invalid
3 | ---
4 |
5 | Hello, {{name}}!
6 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-no-frontmatter.md:
--------------------------------------------------------------------------------
1 | Hello, {{name}}!
2 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-partial.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: twig-test
3 | subject: world
4 | ---
5 |
6 | Hello, {% includes 'name' %}!
7 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-twig-frontmatter-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: twig-test
3 | fromEmail: {{ email ?: 'mark@example.com' }}
4 | fromName: {{ (email ?: 'mark@example.com') | split('@', 2) | first | title }}
5 | subject: Hello!
6 | ---
7 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-twig-frontmatter-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: twig-test
3 | fromEmail: { % includes 'from-email' % }
4 | subject: Hello!
5 | ---
6 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-twig-frontmatter.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: twig-test
3 | subject: Hello {{ name }}!
4 | ---
5 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/example-twig-subject.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: twig-test
3 | subject: world
4 | ---
5 |
6 | Hello, {{name}}!
7 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/custom.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{subject}}
4 | {{preview}}
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{title}}
12 |
13 |
14 | {{subtitle}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{body | raw}}
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/default.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{subject}}
4 | {{preview}}
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{title}}
12 |
13 |
14 | {{subtitle}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{body | raw}}
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/invalid.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{subject}}
4 | {{preview}}
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{title}}
12 |
13 |
14 | {{subtitle}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{body | raw}}
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/partials/from-email.mjml:
--------------------------------------------------------------------------------
1 | {% spaceless %}
2 | {% if name %}
3 | {{ name | lower }}@acme.inc
4 | {% else %}
5 | noreply@acme.inc
6 | {% endif %}
7 | {% endspaceless %}
8 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/partials/name.mjml:
--------------------------------------------------------------------------------
1 | {{ name }}
2 |
--------------------------------------------------------------------------------
/packages/engine/tests/emails/layouts/twig-test.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{subject}}
4 | {{preview}}
5 |
6 |
7 |
8 |
9 |
10 |
11 | Twig, {{subject}}!
12 |
13 |
14 | {{subtitle}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{body | raw}}
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/engine/tests/index.test.js:
--------------------------------------------------------------------------------
1 | const index = require("../src/index");
2 | const Macaw = require("../src/lib/Macaw");
3 |
4 | test("index initiates engine", () => {
5 | const engine = index({
6 | templatesDirectory: "tests/emails"
7 | });
8 |
9 | expect(engine).toBeInstanceOf(Macaw);
10 | });
11 |
12 | test("index initiates engine with options", () => {
13 | const options = { option1: "test", templatesDirectory: "tests/emails" };
14 | const engine = index(options);
15 |
16 | expect(engine).toBeInstanceOf(Macaw);
17 | expect(engine.options).toEqual({
18 | ...Macaw.defaultOptions,
19 | ...options
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/engine/tests/lib/Macaw.test.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const Macaw = require("../../src/lib/Macaw");
4 | const Template = require("../../src/lib/Template");
5 |
6 | test("engine fails with non-existing default templates directory", () => {
7 | const callConstructor = () => {
8 | new Macaw();
9 | };
10 |
11 | expect(callConstructor).toThrow(/templates directory does not exist/i);
12 | });
13 |
14 | test("engine fails with non-existing custom templates directory", () => {
15 | const callConstructor = () => {
16 | new Macaw({
17 | templatesDirectory: "test/emails-nonexisting"
18 | });
19 | };
20 |
21 | expect(callConstructor).toThrow(/templates directory does not exist/i);
22 | });
23 |
24 | test("engine allows custom templates directory", () => {
25 | const engine = new Macaw({
26 | templatesDirectory: "tests/emails"
27 | });
28 |
29 | expect(engine.options.templatesDirectory).toEqual("tests/emails");
30 | });
31 |
32 | test("engine is able to load template", async () => {
33 | const engine = new Macaw({
34 | templatesDirectory: "tests/emails"
35 | });
36 |
37 | const template = await engine.template("example-no-frontmatter");
38 |
39 | expect(template).toBeInstanceOf(Template);
40 | expect(template.options).toEqual(engine.options);
41 | });
42 |
--------------------------------------------------------------------------------
/packages/engine/tests/lib/Template.test.js:
--------------------------------------------------------------------------------
1 | const Template = require("../../src/lib/Template");
2 |
3 | const storage = require("@macaw-email/storage-fs")();
4 | storage.setOptions({
5 | templatesDirectory: "./tests/emails"
6 | });
7 |
8 | const defaultOptions = {
9 | layoutsDirectory: "layouts",
10 | storage: storage
11 | };
12 |
13 | test("template without frontmatter uses default layout", async () => {
14 | const template = await Template.load(
15 | "example-no-frontmatter.md",
16 | defaultOptions,
17 | {}
18 | );
19 |
20 | expect(template.layoutFilePath).toEqual("layouts/default.mjml");
21 | });
22 |
23 | test("template can specify custom layout", async () => {
24 | const template = await Template.load(
25 | "example-custom-layout.md",
26 | defaultOptions,
27 | {}
28 | );
29 |
30 | expect(template.layoutFilePath).toEqual("layouts/custom.mjml");
31 | });
32 |
33 | test("template throws error if file doesn't exist", async () => {
34 | await expect(
35 | Template.load("random-file.md", defaultOptions, {})
36 | ).rejects.toThrow(/no such file/i);
37 | });
38 |
39 | test("template renders with vars", async () => {
40 | const template = await Template.load(
41 | "example-no-frontmatter.md",
42 | defaultOptions,
43 | { name: "John" }
44 | );
45 |
46 | const html = template.render();
47 |
48 | expect(html).toContain("Hello, John!");
49 | });
50 |
51 | test("template renders frontmatter with twig", async () => {
52 | const template = await Template.load(
53 | "example-twig-frontmatter.md",
54 | defaultOptions,
55 | { name: "Bob" }
56 | );
57 |
58 | expect(template.data.subject).toEqual("Hello Bob!");
59 | });
60 |
61 | test("template renders frontmatter with twig (complex)", async () => {
62 | const template = await Template.load(
63 | "example-twig-frontmatter-2.md",
64 | defaultOptions,
65 | { email: "bob@andrews.com" }
66 | );
67 |
68 | expect(template.data.fromName).toEqual("Bob");
69 | expect(template.data.fromEmail).toEqual("bob@andrews.com");
70 | });
71 |
72 | test("template renders frontmatter with twig (defaults)", async () => {
73 | const template = await Template.load(
74 | "example-twig-frontmatter-2.md",
75 | defaultOptions,
76 | {}
77 | );
78 |
79 | expect(template.data.fromName).toEqual("Mark");
80 | expect(template.data.fromEmail).toEqual("mark@example.com");
81 | });
82 |
83 | test("template renders with partial", async () => {
84 | const template = await Template.load("example-partial.md", defaultOptions, {
85 | name: "Peter"
86 | });
87 |
88 | const html = template.render();
89 |
90 | expect(html).toContain("Hello, Peter");
91 | });
92 |
93 | test("template renders with partial in frontmatter", async () => {
94 | const template = await Template.load(
95 | "example-twig-frontmatter-3.md",
96 | defaultOptions,
97 | {
98 | name: "Peter"
99 | }
100 | );
101 |
102 | expect(template.data.fromEmail).toEqual("peter@acme.inc");
103 |
104 | const template2 = await Template.load(
105 | "example-twig-frontmatter-3.md",
106 | defaultOptions,
107 | {}
108 | );
109 |
110 | expect(template2.data.fromEmail).toEqual("noreply@acme.inc");
111 | });
112 |
113 | test("template renders layout with vars", async () => {
114 | const template = await Template.load(
115 | "example-twig-subject.md",
116 | defaultOptions,
117 | { name: "John" }
118 | );
119 |
120 | const html = template.render();
121 |
122 | expect(html).toContain("Twig, world!");
123 | });
124 |
125 | test("template gracefully handles missing vars", async () => {
126 | // TODO: not sure if I actually agree with this behaviour
127 | const template = await Template.load(
128 | "example-twig-subject.md",
129 | defaultOptions,
130 | {}
131 | );
132 |
133 | const html = template.render();
134 |
135 | expect(html).toContain("Twig, world!");
136 | expect(html).toContain("Hello, !");
137 | });
138 |
139 | test("template send calls provider send function", async () => {
140 | const mockSend = jest.fn();
141 | const template = await Template.load(
142 | "example-no-frontmatter.md",
143 | {
144 | ...defaultOptions,
145 | provider: {
146 | send: mockSend
147 | }
148 | },
149 | { name: "John" }
150 | );
151 |
152 | const html = template.render();
153 | const to = {
154 | name: "John",
155 | email: "john@example.com"
156 | };
157 |
158 | template.send({
159 | to
160 | });
161 |
162 | expect(mockSend).toBeCalledTimes(1);
163 | expect(mockSend).toBeCalledWith({ html, to, data: template.data });
164 | });
165 |
166 | test("template throws error on invalid mjml", async () => {
167 | const template = await Template.load(
168 | "example-invalid-mjml.md",
169 | defaultOptions,
170 | { name: "John" }
171 | );
172 |
173 | expect(template.data.layout).toEqual("invalid");
174 | expect(() => template.render()).toThrow(/invalid mjml/i);
175 | });
176 |
177 | test("template send throws error if no provider set", async () => {
178 | const template = await Template.load(
179 | "example-no-frontmatter.md",
180 | defaultOptions,
181 | { name: "John" }
182 | );
183 |
184 | const callSend = () => {
185 | template.send({
186 | to: {
187 | name: "John",
188 | email: "john@example.com"
189 | }
190 | });
191 | };
192 |
193 | expect(callSend).toThrow(/no provider set/i);
194 | });
195 |
196 | test("template with invalid frontmatter uses default layout", async () => {
197 | const template = await Template.load(
198 | "example-invalid-frontmatter.md",
199 | defaultOptions,
200 | {}
201 | );
202 |
203 | expect(template.layoutFilePath).toEqual("layouts/default.mjml");
204 | });
205 |
--------------------------------------------------------------------------------
/packages/macaw/.npmignore:
--------------------------------------------------------------------------------
1 | yarn.lock
2 |
--------------------------------------------------------------------------------
/packages/macaw/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require("@macaw-email/cli");
4 |
--------------------------------------------------------------------------------
/packages/macaw/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require("@macaw-email/engine");
2 |
--------------------------------------------------------------------------------
/packages/macaw/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "macaw",
3 | "version": "1.7.0",
4 | "description": "Scalable email tooling for transactional emails.",
5 | "main": "./index.js",
6 | "author": "Thomas Schoffelen ",
7 | "license": "MIT",
8 | "homepage": "https://macaw.email",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/macaw-email/macaw.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/macaw-email/macaw/issues"
15 | },
16 | "keywords": [
17 | "emails",
18 | "templates",
19 | "templating",
20 | "email",
21 | "mailer",
22 | "email-templates"
23 | ],
24 | "engines": {
25 | "node": ">=10"
26 | },
27 | "bin": "./cli.js",
28 | "scripts": {
29 | "prepublishOnly": "cp ../../README.md README.md",
30 | "postpublish": "rm -rf README.md"
31 | },
32 | "dependencies": {
33 | "@macaw-email/cli": "^1.7.0",
34 | "@macaw-email/engine": "^1.7.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/preview-ui/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/packages/preview-ui/.npmignore:
--------------------------------------------------------------------------------
1 | tests/
2 | coverage/
3 | src/
4 | public/
5 | build/precache-manifest.*.js
6 | build/service-worker.js
7 | yarn.lock
8 |
--------------------------------------------------------------------------------
/packages/preview-ui/README.md:
--------------------------------------------------------------------------------
1 | # Preview UI for Macaw
2 |
3 | The preview UI for Macaw helps view the compiled mjml file, markdown and JSON data. It is built with [react](https://reactjs.org/), and fetches the updated templates using [socket.io](https://socket.io/) from the macaw cli.
4 |
5 |
6 | 
7 |
8 | **[Disclamer] This package is intended to be consumed internally by the macaw cli**
9 |
10 | ## Development setup
11 |
12 | These instructions will get you a copy of the Preview UI running ready for development. All commands assume starting at the repositories root folder
13 |
14 | First in the cli folder install the dependencies
15 | ```bash
16 | cd packages/cli && yarn install
17 | ```
18 |
19 | Second run the macaw cli pointing it towards the template directory
20 |
21 | ```bash
22 | yarn start preview --source templates
23 | ```
24 |
25 | Then in a new terminal promt install the dependencies for the preview UI
26 |
27 | ```bash
28 | cd packages/preview-ui && yarn install
29 | ```
30 |
31 | Finally start the UI in development mode
32 | ```
33 | yarn start
34 | ```
35 |
36 | The development server will start on port 3000. The preview-ui automatically connects to the [socket.io](socket.io) server started by the cli.
37 |
--------------------------------------------------------------------------------
/packages/preview-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@macaw-email/preview-ui",
3 | "version": "1.6.0",
4 | "description": "Email template preview app for Macaw.",
5 | "main": "build/index.html",
6 | "author": "Thomas Schoffelen ",
7 | "license": "MIT",
8 | "homepage": "https://macaw.email",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/macaw-email/macaw.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/macaw-email/macaw/issues"
15 | },
16 | "devDependencies": {
17 | "codemirror": "^5.58.2",
18 | "jsonlint-mod": "^1.7.5",
19 | "react": "^16.12.0",
20 | "react-codemirror2": "^6.0.0",
21 | "react-dom": "^16.12.0",
22 | "react-scripts": "3.4.0",
23 | "socket.io-client": "^2.3.0"
24 | },
25 | "scripts": {
26 | "prepublishOnly": "react-scripts build",
27 | "start": "react-scripts start",
28 | "build": "react-scripts build"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app",
32 | "rules": {
33 | "jsx-a11y/anchor-is-valid": "off"
34 | }
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | },
48 | "dependencies": {
49 | "file-saver": "^2.0.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/preview-ui/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tschoffelen/macaw/02f2a30fb1bc140c46a78bcf0421a1e093c7e17d/packages/preview-ui/preview.png
--------------------------------------------------------------------------------
/packages/preview-ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tschoffelen/macaw/02f2a30fb1bc140c46a78bcf0421a1e093c7e17d/packages/preview-ui/public/favicon.ico
--------------------------------------------------------------------------------
/packages/preview-ui/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Email template preview
8 |
9 |
10 | You need to enable JavaScript to run this app.
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/preview-ui/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, createRef } from "react";
2 | import { UnControlled as CodeMirror } from "react-codemirror2";
3 | import { saveAs } from "file-saver";
4 | import socketio from "socket.io-client";
5 | import "codemirror/mode/javascript/javascript";
6 | import "codemirror/lib/codemirror.css";
7 | import "codemirror/addon/lint/lint";
8 | import "codemirror/addon/lint/lint.css";
9 | import "codemirror/addon/lint/json-lint";
10 | import "codemirror/theme/material-darker.css";
11 |
12 | import "./index.css";
13 |
14 | const jsonlint = require("jsonlint-mod");
15 | window.jsonlint = jsonlint;
16 |
17 | const socket = socketio(
18 | window.location.host.replace(":3000", ":4000").replace(":3001", ":4000")
19 | );
20 |
21 | let defaultData = {
22 | subject: "Hello, world",
23 | to: { name: "John", email: "john@example.com" }
24 | };
25 |
26 | const defaultJson = JSON.stringify(defaultData, null, 2);
27 |
28 | function App() {
29 | const [initialJson, setInitialJson] = useState(defaultJson);
30 | const [json, setJson] = useState(defaultJson);
31 | const [mode, setMode] = useState("responsive");
32 | const [html, setHtml] = useState("");
33 | const [template, setTemplate] = useState(localStorage.lastTemplate || "");
34 | const [templates, setTemplates] = useState([]);
35 | const iframe = createRef();
36 |
37 | useEffect(() => {
38 | socket.on("templates", data => {
39 | setTemplates(data);
40 | if (data.length && (!template || !data.includes(template))) {
41 | setTemplate(data[0]);
42 | }
43 | });
44 | if (!templates) {
45 | socket.emit("templates");
46 | }
47 | }, [template, templates]);
48 | useEffect(() => {
49 | socket.on("render", html => {
50 | setHtml(html);
51 | if (!iframe.current) {
52 | return;
53 | }
54 | iframe.current.contentWindow.document.body.innerHTML = html;
55 | });
56 | socket.on("render-error", message => {
57 | if (!iframe.current) {
58 | return;
59 | }
60 | // TODO: make render errors look nicer
61 | iframe.current.contentWindow.document.body.innerHTML = `
62 | Render error
63 | ${message}
64 | `;
65 | });
66 | if (template) {
67 | try {
68 | const data = JSON.parse(json);
69 | socket.emit("data", [template, data]);
70 | } catch (e) {
71 | console.log("Invalid JSON payload:", e);
72 | }
73 | }
74 | }, [template, json, iframe]);
75 | useEffect(() => {
76 | try {
77 | if (localStorage[`lastJson${template}`]) {
78 | defaultData = JSON.parse(localStorage[`lastJson${template}`]);
79 | }
80 | const defaultJson = JSON.stringify(defaultData, null, 2);
81 | setInitialJson(defaultJson);
82 | setJson(defaultJson);
83 | } catch (e) {
84 | // do nothing
85 | }
86 | }, [template]);
87 |
88 | const downloadAsHtml = () => {
89 | const contents = `
90 |
98 | ${html}
99 | `;
100 |
101 | const blob = new Blob([contents], { type: "text/plain;charset=utf-8" });
102 | saveAs(blob, `${template}.html`);
103 | };
104 |
105 | return (
106 | <>
107 |
108 |
109 |
110 |
111 |
112 |
113 |
117 |
121 |
125 |
129 |
133 |
137 |
141 |
145 |
149 |
153 |
157 |
158 |
159 | Email template preview
160 |
161 |
162 |
163 | Select template
164 |
165 |
{
167 | setTemplate(e.target.value);
168 | localStorage.lastTemplate = e.target.value;
169 | }}
170 | >
171 |
172 | Choose a template
173 |
174 | {templates.map(templ => (
175 |
180 | {templ}
181 |
182 | ))}
183 |
184 |
185 |
190 |
191 |
192 |
193 |
194 | Options
195 |
196 |
197 | Download
198 |
199 |
200 |
setMode("responsive")}
202 | className={`responsive ${
203 | mode === "responsive" ? "active" : ""
204 | }`}
205 | >
206 |
207 |
212 |
217 |
218 |
219 |
220 |
221 |
setMode("tablet")}
223 | className={`tablet ${mode === "tablet" ? "active" : ""}`}
224 | >
225 |
226 |
231 |
232 |
237 |
238 |
239 |
240 |
setMode("mobile")}
242 | className={`mobile ${mode === "mobile" ? "active" : ""}`}
243 | >
244 |
245 |
250 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | JSON data
264 | {
267 | setJson(value);
268 | localStorage[`lastJson${template}`] = value;
269 | }}
270 | options={{
271 | mode: "application/json",
272 | theme: "material-darker",
273 | line: true,
274 | lint: true,
275 | json: true
276 | }}
277 | />
278 |
279 |
280 |
281 |
282 |
292 |
293 | >
294 | );
295 | }
296 |
297 | export default App;
298 |
--------------------------------------------------------------------------------
/packages/preview-ui/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #root,
4 | select {
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
6 | sans-serif, Apple Color Emoji, Segoe UI Emoji;
7 | font-size: 13px;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | * {
13 | box-sizing: border-box;
14 | }
15 |
16 | html,
17 | body,
18 | #root {
19 | padding: 0;
20 | margin: 0;
21 | height: 100%;
22 | line-height: 1;
23 | letter-spacing: 0.2px;
24 | word-wrap: break-word;
25 | position: sticky;
26 | overflow: hidden;
27 | }
28 |
29 | #root {
30 | display: flex;
31 | }
32 |
33 | h1,
34 | h3 {
35 | margin: 0;
36 | padding: 0;
37 | user-select: none;
38 | }
39 |
40 | h1 {
41 | color: #fff;
42 | font-size: 15px;
43 | font-weight: 700;
44 | line-height: 32px;
45 | }
46 |
47 | .logo svg {
48 | float: left;
49 | margin-right: 16px;
50 | }
51 |
52 | h3 {
53 | font-size: 13px;
54 | font-weight: 600;
55 | color: #8b8c91;
56 | padding-bottom: 10px;
57 | }
58 |
59 | aside {
60 | background: #171a22;
61 | flex: 1;
62 | flex-basis: 320px;
63 | min-width: 280px;
64 | max-width: 320px;
65 | height: 100%;
66 | overflow-x: hidden;
67 | overflow-y: auto;
68 | }
69 |
70 | section {
71 | border-bottom: 2px solid #22252d;
72 | padding: 24px;
73 | }
74 |
75 | section:last-child {
76 | border-bottom: 0;
77 | }
78 |
79 | .select {
80 | position: relative;
81 | margin-bottom: -4px;
82 | user-select: none;
83 | }
84 |
85 | .select select,
86 | button.styled-button {
87 | display: block;
88 | appearance: none;
89 | width: calc(100% + 8px);
90 | box-sizing: border-box;
91 | border: 4px solid #2a2e38;
92 | border-radius: 10px;
93 | background: #fff;
94 | height: 48px;
95 | margin: 0 -4px;
96 | padding: 0 16px;
97 | padding-right: 32px;
98 | color: #171a23;
99 | font-weight: 600;
100 | outline: none;
101 | }
102 |
103 | .select svg {
104 | position: absolute;
105 | right: 16px;
106 | top: 50%;
107 | margin-top: -2px;
108 | pointer-events: none;
109 | }
110 |
111 | .options {
112 | display: flex;
113 | }
114 |
115 | .options .styled-button {
116 | width: auto;
117 | padding: 0 16px;
118 | margin-right: 18px;
119 | }
120 |
121 | .toggle {
122 | border: 4px solid #2a2e38;
123 | background: #2a2e38;
124 | border-radius: 10px;
125 | display: flex;
126 | flex: 1;
127 | margin: 0 -4px;
128 | user-select: none;
129 | }
130 |
131 | .toggle button {
132 | appearance: none;
133 | background: transparent;
134 | border: 0;
135 | outline: 0;
136 | flex: 1;
137 | flex-basis: 33%;
138 | height: 40px;
139 | text-align: center;
140 | border-radius: 6px;
141 | font-size: 11px;
142 | font-weight: 600;
143 | color: #fff;
144 | opacity: 0.8;
145 | cursor: pointer;
146 | position: relative;
147 | }
148 |
149 | .toggle button,
150 | .toggle button * {
151 | transition: all 0.1s ease;
152 | }
153 |
154 | .toggle button.active {
155 | background: #fff;
156 | color: #171a23;
157 | opacity: 1;
158 | cursor: default;
159 | }
160 |
161 | .toggle button svg {
162 | zoom: 0.8;
163 | }
164 |
165 | .toggle button svg path[fill-rule="nonzero"] {
166 | fill: #fff !important;
167 | }
168 |
169 | .toggle button.active svg path[fill-rule="nonzero"] {
170 | fill: #171a23 !important;
171 | }
172 |
173 | .cm-s-material-darker.CodeMirror {
174 | background-color: #2a2e38;
175 | border-radius: 10px;
176 | line-height: 1.6;
177 | margin: 0 -4px -4px;
178 | height: auto;
179 | min-height: 240px;
180 | }
181 |
182 | .CodeMirror-lines {
183 | padding: 10px;
184 | }
185 |
186 | .CodeMirror-lint-tooltip {
187 | background: #000;
188 | border: 0;
189 | border-radius: 10px;
190 | color: #fff;
191 | padding: 20px;
192 | box-shadow: 0 0 32px rgba(0, 0, 0, 0.32);
193 | }
194 |
195 | main {
196 | display: flex;
197 | flex: 4;
198 | height: 100%;
199 | box-shadow: 0 0 32px rgba(0, 0, 0, 0.1);
200 | background: #f1f2f3;
201 | justify-content: center;
202 | align-items: center;
203 | }
204 |
205 | main iframe {
206 | width: 100%;
207 | height: 100%;
208 | background: #fff;
209 | }
210 |
211 | .frame {
212 | background: #d8d7e0;
213 | box-shadow: 0 0 32px rgba(0, 0, 0, 0.12), inset 0 0 4px rgba(0, 0, 0, 0.01);
214 | margin: 64px;
215 | display: flex;
216 | flex: 1;
217 | height: 85vh;
218 | transition: all 0.3s ease-in-out;
219 | }
220 |
221 | .frame.responsive {
222 | border-radius: 7px;
223 | overflow: hidden;
224 | padding: 4px;
225 | padding-top: 30px;
226 | }
227 |
228 | .frame.tablet {
229 | max-width: 1024px;
230 | max-height: 768px;
231 | min-width: 768px;
232 | min-height: 640px;
233 | padding: 20px 80px;
234 | border-radius: 40px;
235 | }
236 |
237 | .frame.mobile {
238 | max-width: 360px;
239 | max-height: 90vh;
240 | min-width: 320px;
241 | min-height: 667px;
242 | padding: 80px 20px;
243 | border-radius: 40px;
244 | }
245 |
246 | .frame-inner {
247 | display: flex;
248 | flex: 1;
249 | background: #fff;
250 | }
251 |
--------------------------------------------------------------------------------
/packages/preview-ui/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import App from "./App";
5 |
6 | ReactDOM.render( , document.getElementById("root"));
7 |
--------------------------------------------------------------------------------
/packages/provider-sendgrid/.npmignore:
--------------------------------------------------------------------------------
1 | tests/
2 | coverage/
3 | yarn.lock
4 |
--------------------------------------------------------------------------------
/packages/provider-sendgrid/README.md:
--------------------------------------------------------------------------------
1 | # Sendgrid provider for Macaw
2 |
3 | **[Macaw](https://macaw.email/) is a simple library to streamline email templating.**
4 |
5 | ## Quickstart
6 |
7 | Please start by looking at the [Macaw documentation](https://macaw.email/).
8 |
9 | First install the Sendgrid provider package:
10 |
11 | ```
12 | yarn add @macaw-email/provider-sendgrid
13 | ```
14 |
15 | When initiating your instance of Macaw, pass in Sendgrid as your provider:
16 |
17 | ```js
18 | const sendgrid = require("@macaw-email/provider-sendgrid");
19 |
20 | const mailer = macaw({
21 | provider: sendgrid({ apiKey: "aaaaa-bbbbbbb-ccccccc-ddddddd" })
22 | });
23 | ```
24 |
25 | You can find your API key in the Sendgrid developer console.
26 |
27 | Then you can load a template and send it:
28 |
29 | ```js
30 | const template = await mailer.template("monthly-newsletter", {
31 | greeting: "Hello, world"
32 | });
33 |
34 | await template.send({
35 | subject: "Hello, world!",
36 | to: {
37 | name: "Thomas Schoffelen",
38 | email: "thomas@schof.co"
39 | },
40 | from: {
41 | name: "Mark from Startup X",
42 | email: "noreply@startup-x.com"
43 | }
44 | });
45 | ```
46 |
47 | The `template.send()` function accepts any parameters that are accepted by the [Sendgrid Node API](https://github.com/sendgrid/sendgrid-nodejs/blob/master/use-cases/kitchen-sink.md). It requires at least a `subject`, `to` and `from` field to be set.
48 |
--------------------------------------------------------------------------------
/packages/provider-sendgrid/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@macaw-email/provider-sendgrid",
3 | "version": "1.5.0",
4 | "description": "Sendgrid provider adapter for Macaw.",
5 | "main": "src/index.js",
6 | "author": "Thomas Schoffelen ",
7 | "license": "MIT",
8 | "homepage": "https://macaw.email",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/macaw-email/macaw.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/macaw-email/macaw/issues"
15 | },
16 | "engines": {
17 | "node": ">=10"
18 | },
19 | "scripts": {
20 | "test": "jest"
21 | },
22 | "dependencies": {
23 | "@sendgrid/mail": "^6.5.2"
24 | },
25 | "devDependencies": {
26 | "jest": "^25.1.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/provider-sendgrid/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ apiKey }) => {
2 | if (!apiKey) {
3 | throw Error("Missing required parameter `apiKey` for Sendgrid provider.");
4 | }
5 |
6 | const sendgrid = require("@sendgrid/mail");
7 | sendgrid.setApiKey(apiKey);
8 |
9 | return {
10 | sendgrid,
11 | send: function sendViaSendgrid(options) {
12 | let { data, html, category, categories, ...providerOptions } = options;
13 |
14 | categories = [
15 | ...(data.template ? [data.template] : []),
16 | ...(categories || [])
17 | ];
18 | if (data.template && data.template.includes("-")) {
19 | categories.push(data.template.split("-", 1)[0]);
20 | }
21 |
22 | return sendgrid.send({
23 | subject: providerOptions.subject || data.subject,
24 | to: providerOptions.to || data.to,
25 | from: providerOptions.from || data.from,
26 | categories,
27 | ...providerOptions,
28 | html
29 | });
30 | }
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/packages/provider-sendgrid/tests/index.test.js:
--------------------------------------------------------------------------------
1 | jest.mock("@sendgrid/mail");
2 | const sendgrid = require("@sendgrid/mail");
3 |
4 | const provider = require("../src/index");
5 |
6 | test("sets api key", () => {
7 | provider({ apiKey: "aaaa" });
8 | expect(sendgrid.setApiKey).toBeCalledWith("aaaa");
9 | });
10 |
11 | test("throws when no api key is specified", () => {
12 | expect(() => provider({})).toThrow();
13 | });
14 |
15 | test("passes through html", () => {
16 | const providerOptions = {
17 | data: {},
18 | html: "Hello, world!
"
19 | };
20 |
21 | const sender = provider({ apiKey: "aaaa" });
22 | sender.send(providerOptions);
23 | expect(sendgrid.setApiKey).toBeCalledWith("aaaa");
24 | expect(sendgrid.send).toBeCalledWith({
25 | html: "Hello, world!
",
26 | categories: []
27 | });
28 | });
29 |
30 | test("sets expected categories", () => {
31 | const providerOptions = {
32 | data: {
33 | template: "intro-customer-email"
34 | },
35 | html: "Hello, world!
"
36 | };
37 |
38 | const sender = provider({ apiKey: "aaaa" });
39 | sender.send(providerOptions);
40 | expect(sendgrid.setApiKey).toBeCalledWith("aaaa");
41 | expect(sendgrid.send).toBeCalledWith({
42 | categories: ["intro-customer-email", "intro"],
43 | html: "Hello, world!
"
44 | });
45 | });
46 |
47 | test("sets expected custom categories", () => {
48 | const providerOptions = {
49 | data: {
50 | template: "intro-customer"
51 | },
52 | categories: ["payment"],
53 | html: "Hello, world!
"
54 | };
55 |
56 | const sender = provider({ apiKey: "aaaa" });
57 | sender.send(providerOptions);
58 | expect(sendgrid.setApiKey).toBeCalledWith("aaaa");
59 | expect(sendgrid.send).toBeCalledWith({
60 | categories: ["intro-customer", "payment", "intro"],
61 | html: "Hello, world!
"
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/packages/storage-fs/README.md:
--------------------------------------------------------------------------------
1 | # Macaw local filesystem storage
2 |
3 | **[Macaw](https://macaw.email/) is a simple library to streamline email templating.**
4 |
5 | Please check the [Macaw documentation](https://macaw.email/) for more information.
6 |
--------------------------------------------------------------------------------
/packages/storage-fs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@macaw-email/storage-fs",
3 | "version": "1.5.0",
4 | "description": "Default storage engine for Macaw.",
5 | "main": "src/index.js",
6 | "author": "Thomas Schoffelen ",
7 | "license": "MIT",
8 | "homepage": "https://macaw.email",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/macaw-email/macaw.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/macaw-email/macaw/issues"
15 | },
16 | "engines": {
17 | "node": ">=10"
18 | },
19 | "scripts": {
20 | "test": "jest"
21 | },
22 | "devDependencies": {
23 | "jest": "^25.1.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/storage-fs/src/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | module.exports = () => {
5 | let options = {
6 | templatesDirectory: "emails"
7 | };
8 |
9 | const setOptions = opt => {
10 | options = { ...options, ...opt };
11 | options.templatesDirectory = path.resolve(options.templatesDirectory);
12 |
13 | if (!fs.existsSync(options.templatesDirectory)) {
14 | throw Error(
15 | `Templates directory does not exist: ${options.templatesDirectory}`
16 | );
17 | }
18 | };
19 |
20 | const getItem = async fileName =>
21 | new Promise((resolve, reject) => {
22 | const pathName =
23 | fileName.indexOf("/") === 0
24 | ? fileName
25 | : path.join(options.templatesDirectory, fileName);
26 |
27 | fs.readFile(pathName, "utf8", (err, contents) => {
28 | if (err) {
29 | return reject(err);
30 | }
31 |
32 | if (!contents) {
33 | return reject(new Error(`File is empty: ${fileName}.`));
34 | }
35 |
36 | resolve(contents);
37 | });
38 | });
39 |
40 | return {
41 | setOptions,
42 | getItem
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/packages/storage-fs/tests/fixtures/empty-file.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tschoffelen/macaw/02f2a30fb1bc140c46a78bcf0421a1e093c7e17d/packages/storage-fs/tests/fixtures/empty-file.txt
--------------------------------------------------------------------------------
/packages/storage-fs/tests/fixtures/file.txt:
--------------------------------------------------------------------------------
1 | Test file!
2 |
--------------------------------------------------------------------------------
/packages/storage-fs/tests/index.test.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const storage = require("../src/index");
4 |
5 | test("throws error when setting options with invalid default dir", () => {
6 | const callConstructor = () => {
7 | storage().setOptions({});
8 | };
9 |
10 | expect(callConstructor).toThrow(/templates directory does not exist/i);
11 | });
12 |
13 | test("throws error when setting options with invalid custom dir", () => {
14 | const callConstructor = () => {
15 | storage().setOptions({
16 | templatesDirectory: "test/emails-nonexisting"
17 | });
18 | };
19 |
20 | expect(callConstructor).toThrow(/templates directory does not exist/i);
21 | });
22 |
23 | test("returns promise on file read", () => {
24 | const instance = storage();
25 | instance.setOptions({
26 | templatesDirectory: "tests/fixtures"
27 | });
28 |
29 | expect(instance.getItem("file.txt")).toBeInstanceOf(Promise);
30 | });
31 |
32 | test("reads file with relative path", async () => {
33 | const instance = storage();
34 | instance.setOptions({
35 | templatesDirectory: "tests/fixtures"
36 | });
37 |
38 | const data = await instance.getItem("file.txt");
39 |
40 | expect(data).toEqual("Test file!\n");
41 | });
42 |
43 | test("reads file with absolute path", async () => {
44 | const instance = storage();
45 | instance.setOptions({
46 | templatesDirectory: "tests/fixtures"
47 | });
48 |
49 | const data = await instance.getItem(path.resolve("tests/fixtures/file.txt"));
50 |
51 | expect(data).toEqual("Test file!\n");
52 | });
53 |
54 | test("throw error if file does not exist", async () => {
55 | const instance = storage();
56 | instance.setOptions({
57 | templatesDirectory: "tests/fixtures"
58 | });
59 |
60 | await expect(instance.getItem("random-file.md")).rejects.toThrow(
61 | /no such file/
62 | );
63 | });
64 |
65 | test("throw error if file is empty", async () => {
66 | const instance = storage();
67 | instance.setOptions({
68 | templatesDirectory: "tests/fixtures"
69 | });
70 |
71 | await expect(instance.getItem("empty-file.txt")).rejects.toThrow(/empty/);
72 | });
73 |
--------------------------------------------------------------------------------
/packages/storage-s3/README.md:
--------------------------------------------------------------------------------
1 | # Macaw AWS S3 storage
2 |
3 | **[Macaw](https://macaw.email/) is a simple library to streamline email templating.**
4 |
5 | Please check the [Macaw documentation](https://macaw.email/) for more information.
6 |
--------------------------------------------------------------------------------
/packages/storage-s3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@macaw-email/storage-s3",
3 | "version": "1.5.0",
4 | "description": "AWS S3 storage engine for Macaw.",
5 | "main": "src/index.js",
6 | "author": "Thomas Schoffelen ",
7 | "license": "MIT",
8 | "homepage": "https://macaw.email",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/macaw-email/macaw.git"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/macaw-email/macaw/issues"
15 | },
16 | "engines": {
17 | "node": ">=10"
18 | },
19 | "scripts": {
20 | "test": "jest"
21 | },
22 | "dependencies": {
23 | "aws-sdk": "^2.658.0",
24 | "aws-sdk-mock": "^5.1.0"
25 | },
26 | "devDependencies": {
27 | "jest": "^25.1.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/storage-s3/src/index.js:
--------------------------------------------------------------------------------
1 | const AWS = require("aws-sdk");
2 |
3 | module.exports = (bucketName, s3Options = undefined, aws = AWS) => {
4 | if (!bucketName) {
5 | throw new Error("Invalid bucket name specified.");
6 | }
7 |
8 | const s3 = new aws.S3(s3Options);
9 |
10 | const getItem = async fileName => {
11 | const params = {
12 | Bucket: bucketName,
13 | Key: fileName
14 | };
15 |
16 | const data = await s3.getObject(params).promise();
17 | const contents = data.Body.toString();
18 |
19 | if (!contents) {
20 | throw new Error(`File is empty: ${fileName}.`);
21 | }
22 |
23 | return contents;
24 | };
25 |
26 | return {
27 | setOptions: _ => {},
28 | getItem
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/packages/storage-s3/tests/index.test.js:
--------------------------------------------------------------------------------
1 | const AWSMock = require("aws-sdk-mock");
2 | const AWS = require("aws-sdk");
3 |
4 | const storage = require("../src/index");
5 |
6 | AWSMock.setSDKInstance(AWS);
7 | AWSMock.mock("S3", "getObject", ({ Bucket, Key }, callback) => {
8 | if (Bucket === "test-bucket" && Key === "file.txt") {
9 | callback(null, { Body: new Buffer("Test file!\n") });
10 | }
11 | if (Bucket === "test-bucket" && Key === "empty.txt") {
12 | callback(null, { Body: new Buffer("") });
13 | }
14 | callback(new Error("File not found"));
15 | });
16 |
17 | test("throws if no bucket name specified", async () => {
18 | const fn = () => {
19 | storage();
20 | };
21 |
22 | await expect(fn).toThrow(/Invalid bucket name specified/);
23 | });
24 |
25 | test("returns promise on file read", () => {
26 | const instance = storage("test-bucket", {}, AWS);
27 | instance.setOptions({});
28 |
29 | expect(instance.getItem("file.txt")).toBeInstanceOf(Promise);
30 | });
31 |
32 | test("reads file with relative path", async () => {
33 | const instance = storage("test-bucket", {}, AWS);
34 |
35 | const data = await instance.getItem("file.txt");
36 |
37 | expect(data).toEqual("Test file!\n");
38 | });
39 |
40 | test("throw error if file does not exist", async () => {
41 | const instance = storage("test-bucket", {}, AWS);
42 |
43 | await expect(instance.getItem("random-file.md")).rejects.toThrow();
44 | });
45 |
46 | test("throw error if file is empty", async () => {
47 | const instance = storage("test-bucket", {}, AWS);
48 |
49 | await expect(instance.getItem("empty.txt")).rejects.toThrow();
50 | });
51 |
--------------------------------------------------------------------------------
/packages/uglify-js-noop/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tschoffelen/macaw/02f2a30fb1bc140c46a78bcf0421a1e093c7e17d/packages/uglify-js-noop/index.js
--------------------------------------------------------------------------------
/packages/uglify-js-noop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uglify-js",
3 | "main": "index.js",
4 | "private": "true"
5 | }
6 |
--------------------------------------------------------------------------------