├── .babelrc ├── .gitignore ├── .npmignore ├── test ├── fixtures │ ├── call-expression │ │ ├── .babelrc │ │ ├── actual.js │ │ └── expected.js │ └── tagged-template-expression │ │ ├── .babelrc │ │ ├── actual.js │ │ └── expected.js └── index.js ├── package.json ├── README.md └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | .nyc_output 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .npmignore 3 | node_modules 4 | *.log 5 | src 6 | test 7 | -------------------------------------------------------------------------------- /test/fixtures/call-expression/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["../../../src"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/tagged-template-expression/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["../../../src"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/call-expression/actual.js: -------------------------------------------------------------------------------- 1 | import hbs from 'handlebars-inline-precompile'; 2 | 3 | hbs('Hello World!'); 4 | -------------------------------------------------------------------------------- /test/fixtures/tagged-template-expression/actual.js: -------------------------------------------------------------------------------- 1 | import hbs from 'handlebars-inline-precompile'; 2 | 3 | hbs`Hello World!`; 4 | -------------------------------------------------------------------------------- /test/fixtures/call-expression/expected.js: -------------------------------------------------------------------------------- 1 | import _Handlebars2 from 'handlebars/runtime'; 2 | 3 | _Handlebars2.template({ 4 | "compiler": [7, ">= 4.0.0"], 5 | "main": function (container, depth0, helpers, partials, data) { 6 | return "Hello World!"; 7 | }, 8 | "useData": true 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixtures/tagged-template-expression/expected.js: -------------------------------------------------------------------------------- 1 | import _Handlebars2 from 'handlebars/runtime'; 2 | 3 | _Handlebars2.template({ 4 | "compiler": [7, ">= 4.0.0"], 5 | "main": function (container, depth0, helpers, partials, data) { 6 | return "Hello World!"; 7 | }, 8 | "useData": true 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-handlebars-inline-precompile", 3 | "description": "", 4 | "version": "2.1.1", 5 | "repository": "thejameskyle/babel-plugin-handlebars-inline-precompile", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "devDependencies": { 9 | "babel-cli": "^6.2.0", 10 | "babel-core": "^6.2.1", 11 | "babel-preset-es2015": "^6.1.18", 12 | "babel-register": "^6.2.0", 13 | "mocha": "^2.2.5", 14 | "nyc": "^5.3.0", 15 | "rimraf": "^2.5.0" 16 | }, 17 | "scripts": { 18 | "coverage": "nyc npm test", 19 | "clean": "rimraf lib", 20 | "prebuild": "npm run clean", 21 | "build": "babel src -d lib", 22 | "prepublish": "npm run build", 23 | "test": "mocha --compilers js:babel-register" 24 | }, 25 | "keywords": [ 26 | "babel-plugin" 27 | ], 28 | "dependencies": { 29 | "handlebars": "^4.0.5", 30 | "resolve-cwd": "^1.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-handlebars-inline-precompile 2 | 3 | Precompile inline Handlebars templates. 4 | 5 | ## Example 6 | 7 | **In** 8 | 9 | ```js 10 | import hbs from 'handlebars-inline-precompile'; 11 | 12 | hbs`Hello World!`; 13 | ``` 14 | 15 | **Out** 16 | 17 | ```js 18 | import _Handlebars from 'handlebars/runtime'; 19 | 20 | _Handlebars.template({ /* A bunch of crazy template stuff */ }) 21 | ``` 22 | 23 | ## Installation 24 | 25 | ```sh 26 | $ npm install babel-plugin-handlebars-inline-precompile 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Via `.babelrc` (Recommended) 32 | 33 | **.babelrc** 34 | 35 | ```json 36 | { 37 | "plugins": ["handlebars-inline-precompile"] 38 | } 39 | ``` 40 | 41 | ### Via CLI 42 | 43 | ```sh 44 | $ babel --plugins handlebars-inline-precompile script.js 45 | ``` 46 | 47 | ### Via Node API 48 | 49 | ```javascript 50 | require("babel-core").transform("code", { 51 | plugins: ["handlebars-inline-precompile"] 52 | }); 53 | ``` 54 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import assert from "assert"; 4 | import * as babel from "babel-core"; 5 | import plugin from "../src/index"; 6 | 7 | function trim(str) { 8 | return str.replace(/^\s+|\s+$/, ""); 9 | } 10 | 11 | function attempt (code) { 12 | return () => babel.transform(code, { 13 | babelrc: false, 14 | filename: "index.js", 15 | sourceRoot: __dirname, 16 | plugins: [plugin] 17 | }).code; 18 | } 19 | 20 | function check (msg) { 21 | const preface = "index.js: "; 22 | return err => err instanceof SyntaxError && err.message.slice(0, preface.length) === preface && err.message.slice(preface.length) === msg; 23 | } 24 | 25 | describe("precompiles inline templates", () => { 26 | const fixturesDir = path.join(__dirname, "fixtures"); 27 | 28 | fs.readdirSync(fixturesDir).map((caseName) => { 29 | it(`works for ${caseName.split("-").join(" ")}`, () => { 30 | const fixtureDir = path.join(fixturesDir, caseName); 31 | const actual = babel.transformFileSync( 32 | path.join(fixtureDir, "actual.js") 33 | ).code; 34 | const expected = fs.readFileSync(path.join(fixtureDir, "expected.js")).toString(); 35 | 36 | assert.equal(trim(actual), trim(expected)); 37 | }); 38 | }); 39 | }); 40 | 41 | describe("importing anything other than the default", () => { 42 | it("throws a SyntaxError", () => { 43 | assert.throws( 44 | attempt("import { foo } from 'handlebars-inline-precompile'"), 45 | check(`Only \`import hbs from 'handlebars-inline-precompile'\` is supported. You used: \`import { foo } from 'handlebars-inline-precompile'\``)); 46 | }); 47 | }); 48 | 49 | describe("`hbs` is called with 0 arguments", () => { 50 | it("throws a SyntaxError", () => { 51 | assert.throws( 52 | attempt("import hbs from 'handlebars-inline-precompile'; hbs()"), 53 | check("hbs should be invoked with a single argument: the template string")); 54 | }); 55 | }); 56 | 57 | describe("`hbs` is called with a non-string argument", () => { 58 | it("throws a SyntaxError", () => { 59 | assert.throws( 60 | attempt("import hbs from 'handlebars-inline-precompile'; hbs(42)"), 61 | check("hbs should be invoked with a single argument: the template string")); 62 | }); 63 | }); 64 | 65 | describe("`hbs` is called with more than 1 argument", () => { 66 | it("throws a SyntaxError", () => { 67 | assert.throws( 68 | attempt("import hbs from 'handlebars-inline-precompile'; hbs('foo', 'bar')"), 69 | check("hbs should be invoked with a single argument: the template string")); 70 | }); 71 | }); 72 | 73 | describe("`hbs` is used as a tagged template expression, with a template string containing placeholders", () => { 74 | it("throws a SyntaxError", () => { 75 | assert.throws( 76 | attempt("import hbs from 'handlebars-inline-precompile'; hbs`${6 * 7 === 42}`"), 77 | check("placeholders inside a tagged template string are not supported")); 78 | }); 79 | }); 80 | 81 | describe("other tagged template expressions occur", () => { 82 | it("ignores them", () => { 83 | assert.doesNotThrow(attempt("function foo () {}; foo`bar`")) 84 | }) 85 | }); 86 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import resolveCwd from 'resolve-cwd' 2 | 3 | // Use local handlebars (if installed as a peer) rather than the version that 4 | // came with this plugin. Allows a newer handlebars to be used without needing 5 | // to upgrade this package. 6 | const Handlebars = require(resolveCwd('handlebars') || 'handlebars') 7 | 8 | export default function({ types: t }) { 9 | const IMPORT_NAME = 'handlebars-inline-precompile'; 10 | const IMPORT_PROP = '_handlebarsImportSpecifier'; 11 | 12 | function isReferenceToImport(node, file) { 13 | return t.isIdentifier(node, { 14 | name: file[IMPORT_PROP] && file[IMPORT_PROP].input 15 | }); 16 | } 17 | 18 | // Precompile template and replace node. 19 | function compile(path, template, importName) { 20 | let precompiled = Handlebars.precompile(template); 21 | path.replaceWithSourceString(`${importName}.template(${precompiled})`); 22 | } 23 | 24 | return { 25 | visitor: { 26 | 27 | /** 28 | * Find the import declaration for `hbs`. 29 | */ 30 | 31 | ImportDeclaration(path, file) { 32 | const { node, scope } = path; 33 | // Filter out anything other than the `hbs` module. 34 | if (!t.isLiteral(node.source, { value: IMPORT_NAME })) { 35 | return; 36 | } 37 | 38 | let first = node.specifiers && node.specifiers[0]; 39 | 40 | // Throw an error if using anything other than the default import. 41 | if (!t.isImportDefaultSpecifier(first)) { 42 | let usedImportStatement = file.file.code.slice(node.start, node.end); 43 | throw path.buildCodeFrameError(`Only \`import hbs from '${IMPORT_NAME}'\` is supported. You used: \`${usedImportStatement}\``); 44 | } 45 | 46 | const { name } = file.addImport('handlebars/runtime', 'default', scope.generateUid('Handlebars')); 47 | path.remove(); 48 | 49 | // Store the import name to lookup references elsewhere. 50 | file[IMPORT_PROP] = { 51 | input: first.local.name, 52 | output: name 53 | }; 54 | }, 55 | 56 | /** 57 | * Look for places where `hbs` is called normally. 58 | */ 59 | 60 | CallExpression(path, file) { 61 | const { node } = path; 62 | 63 | // filter out anything other than `hbs`. 64 | if (!isReferenceToImport(node.callee, file)) { 65 | return; 66 | } 67 | 68 | let template = node.arguments.length > 0 && node.arguments[0].value; 69 | 70 | // `hbs` should be called as `hbs('template')`. 71 | if ( 72 | node.arguments.length !== 1 || 73 | typeof template !== 'string' 74 | ) { 75 | throw path.buildCodeFrameError(`${node.callee.name} should be invoked with a single argument: the template string`); 76 | } 77 | 78 | compile(path, template, file[IMPORT_PROP].output); 79 | }, 80 | 81 | /** 82 | * Look for places where `hbs` is called as a tagged template. 83 | */ 84 | 85 | TaggedTemplateExpression(path, file) { 86 | const { node } = path; 87 | 88 | // filter out anything other than `hbs`. 89 | if (!isReferenceToImport(node.tag, file)) { 90 | return; 91 | } 92 | 93 | // hbs`${template}` is not supported. 94 | if (node.quasi.expressions.length) { 95 | throw path.buildCodeFrameError('placeholders inside a tagged template string are not supported'); 96 | } 97 | 98 | let template = node.quasi.quasis.map(quasi => quasi.value.cooked).join(''); 99 | 100 | compile(path, template, file[IMPORT_PROP].output); 101 | } 102 | } 103 | }; 104 | } 105 | --------------------------------------------------------------------------------