tatethurston / embedded-typescript

Type safe embedded TypeScript templates
MIT License
45 stars 1 forks source link

Embedded TypeScript

Type safe TypeScript templates


What is this? 🧐

A type safe templating system for TypeScript. Templates are compiled to TypeScript files that you then import for type safe string generation.

This templating system draws inspiration from ERB, EJS, handlebars and mustache. This project embraces the "just JavaScript" spirit of ejs and adds some of the helpful white space semantics of mustache.

Checkout the examples or play with embedded-typescript in your browser.

Installation & Usage πŸ“¦

  1. Add this package to your project:

    npm install embedded-typescript or yarn add embedded-typescript

Motivation

Hello undefined!

When using a typed language, I want my templates to be type checked. For most cases, template literals work well. If I'm writing HTML/XML, JSX works well. When I'm writing text templates, template literals quickly become difficult to maintain as the template complexity grows. I can switch to EJS, handlebars, mustache, etc, but then I lose the type safety I had with template literals. Sometimes I want the expressiveness of a templating language without losing type safety. For those cases, I wrote embedded-typescript.

Syntax

Syntax Name Description
--- CODE --- Header Defines code that should live outside of the generated render function. Use this to define Props and any imports, exports or constants.
<%= EXPRESSION %> Expression Inserts the value of an expression. If the expression generates multiple lines, the indentation level is preserved across all resulting lines.
<% CODE %> Statement Executes code, but does not insert a value.
TEXT Text Text literals are inserted as is. All white space is preserved.

Examples πŸš€

Minimal

  1. Write a template file: my-template.ets:
---
interface Props {
  users: {
    name: string;
  }[]
}
---
<% props.users.forEach(function(user) { %>
Name: <%= user.name %>
<% }) %>
  1. Run the compiler: npx ets. This will compile any files with the .ets extension. my-template.ets.ts will be generated.

  2. Import the generated .ets.ts file wherever you'd like to render your template:

import render from "./my-template.ets";

/* will output:
Name: Alice
Name: Bob
*/

console.log(render({ users: [{ name: "Alice" }, { name: "Bob" }] }));

Note that the arguments to your template function are type checked. You define the arguments to your template function by defining a type or interface named Props.

Partials

Embedded TypeScript preserves the indentation wherever an expression tag (<%= EXPRESSION %>) is used. This means there isn't any special syntax for partials, and ets templates nest as you would expect.

  1. Write a "partial" user-partial.ets:
---
interface Props {
  name: string;
  email: string;
  phone: string;
}
---
Name: <%= props.user.name %>
Email: <%= props.user.email %>
Phone: <%= props.user.phone %>

Note there is nothing special about user-partial.ets, it's just an ets template. We're using the -partial suffix purely for illustration.

  1. Import your "partial" into another ets template my-template-2.ets:
---
import renderUser, { Props as User } from './user-partial.ets';

interface Props {
  users: User[];
}

const example =
`1
2
3
4`;
---
<% if (props.users.length > 0) { %>
Here is a list of users:

  <% props.users.forEach(function(user) { %>
  <%= renderUser(user) %>
  <% }) %>

<% } %>
The indentation level is preserved for the rendered 'partial'.

There isn't anything special about the 'partial'. Here we used another `.ets` template, but any
expression yeilding a multiline string would be treated the same.

  <%= example %>

The end!
  1. Run the compiler: npx ets.

  2. Import the generated my-template-2.ets.ts file wherever you'd like to render your template:

import render from "./my-template-2.ets";

/* will output:
Here is a list of users:

  Name: Tate
  Email: tate@tate.com
  Phone: 888-888-8888

  Name: Emily
  Email: emily@emily.com
  Phone: 777-777-7777

The indentation level is preserved for the rendered 'partial'.

There isn't anything special about the 'partial'. Here we used another `ets` template, but any
expression yielding a multi-line string would be treated the same.

  1
  2
  3
  4

The end!
*/

console.log(
  render({
    users: [
      { name: "Tate", phone: "888-888-8888", email: "tate@tate.com" },
      { name: "Emily", phone: "777-777-7777", email: "emily@emily.com" },
    ],
  })
);

Note that indentation was preserved for all lines rendered by user-partial.ets and all lines of the example variable. Any expression yielding a multi-line string rendered inside an expresssion block (<%= EXPRESSION %>) will apply the indentation across each line.

More Examples

For more examples, take a look at the e2e directory. The *.ets.ts files are generated by the compiler from the *.ets template files. The corresponding *${NAME}.test.ts shows example usage and output.

Understanding Error Messages

The compiler will output errors when it encounters invalid syntax:

error: Unexpected closing tag '%>'
   --> ./template-1.ets:4:41
    |
4   | <% users.forEach(function(user) { %>%>
    |                                     ^
    |                                     |
...

The first line is a description of the error that was encountered.

The second line is location of the error, in path:line:column notation.

The next 5 lines provide visual context for the error.

Notable deviations from prior art

This tool specifically targets text templating, rather than HTML templating. Think: code generation, text message content (emails or SMS), etc. HTML templating is possible with this tool, but I would generally recommend JSX instead of embedded-typescript for HTML.

The templating system does not perform any HTML escaping. You can import any self authored or 3rd party HTML escaping utilities in your template, and call that directly on any untrusted input:

---
import htmlescape from 'htmlescape';

interface Props {
  users: { name: string}[];
}
---
<% props.users.forEach(function(user) { %>
<p>Name: <%= htmlescape(user.name) %></p>
<% }) %>

I'm not aware of any other templating systems that preserve indentation for partials and multi-line strings like Embedded TypeScript. Many templating libraries target HTML so this is not surprising, but I've found this functionality useful for text templates.

Highlights

🎁 Zero run time dependencies

Configuration πŸ› 

Embedded TypeScript aims to be zero config, but can be configured by creating an ets.config.mjs (or .js or .cjs) file in your project root.

Name Description Type
source The root directory. `.ets` files will be searched under this directory. Embedded TypeScript will recursively search all subdirectories for `.ets` files. Defaults to the project root. Example: Search for `.ets` files under a directory named `src` // ets.config.mjs ```js /** @type {import('ets').Config} */ export default { source: "src", }; ``` string (filepath)

Contributing πŸ‘«

PR's and issues welcomed! For more guidance check out CONTRIBUTING.md

Licensing πŸ“ƒ

See the project's MIT License.