Skip to content

Make babel-template nicer in a bunch of ways#6492

Merged
loganfsmyth merged 11 commits intobabel:masterfrom
loganfsmyth:better-template
Oct 18, 2017
Merged

Make babel-template nicer in a bunch of ways#6492
loganfsmyth merged 11 commits intobabel:masterfrom
loganfsmyth:better-template

Conversation

@loganfsmyth
Copy link
Member

Q                       A
Fixed Issues?
Patch: Bug Fix?
Major: Breaking Change? Y, minimally for the common usecases though
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR
Any Dependency Changes?

Breaking changes:

  • Numeric placeholders like "$0" and "$1" ... expect you to either pass an array with those indices, or an object with the full name as a property, rather than using positional arguments. e.g.
    template("$0 + $1")([ t.numericLiteral(4), t.numericLiteral(5) ])
    // OR
    template("$0 + $1")({ $0: t.numericLiteral(4), $1: t.numericLiteral(5) })
    
    where before you did
    template("$0 + $1")(t.numericLiteral(4), t.numericLiteral(5))
    
    (without the array brackets).
  • Auto-expanding an array of values into an array was removed as a feature, e.g.
    template(`[THING]`)({thing: [t.identifier("foo"), t.identifier("bar")]});
    
    will throw instead of producing [foo, bar].
  • Searching for placeholders is performed up front and cached, rather than running when template is instantiated. This means placeholder names need to be obvious up front. This breaks the following:
    • Template placeholders that used characters other than /[_$A-Z0-9]/ will need to pass a custom placeholderPattern or rename the placeholder. There are no cases of this in Babel's core plugins.
    • Templates that included identifiers matching that pattern, but not meant for replacement, will start throwing exceptions about a missing replacement value. The only case of this in Babel's core plugins was in the helpers, which don't have any placeholders anyway, so disabling pattern matching was the best fix.

New features:

  • Better performance, since the templates aren't traversed anymore at instantiation time. Instantiation just iterates over an existing list of placeholders to swap out.
  • Templates can pass in strings as replacements, which will automatically be wrapped into Identifier nodes, or into StringLiteral nodes if the placeholder was a string.
  • Templates created via tagged literals are cached, so you can do
    var tpl = template`4 + 5`;
    var ast = tpl();
    
    will essentially just perform a clone operation, because the AST will only be parsed the first time the template is encountered.
  • More utilities are exposed for templates, so you can enforce better return types, e.g. template.expression("4"), template.statement("function foo(){}"), template.statements(..), template.program(""), and template.smart (the current default).
  • Templates that just want to instantiate an AST immediately can skip the intermediate function and associated new Error and stack processing by using .ast on the template function:
    var ast = template.ast`foo;`
    

@loganfsmyth loganfsmyth added the PR: Breaking Change 💥 A type of pull request used for our changelog categories for next major release label Oct 16, 2017
@loganfsmyth loganfsmyth requested a review from Jessidhia October 16, 2017 21:23
NAMESPACE: t.identifier(data.name),
IMPORT_NAME: t.identifier(importName),
template`
Object.defineProperty(EXPORTS, "EXPORT_NAME", {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could also opt to use template.ast on a lot of these and do

Object.defineProperty(${metadata.exportName}, "${exportName}", {

but I kind of felt like it hurt readability of some of the templates.

MODULE_NAME: moduleName,

AMD_ARGUMENTS: t.arrayExpression(amdArgs),
COMMONJS_ARGUMENTS: commonjsArgs,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nice thing about adding validation is that it catches all you're dumb mistakes :D

@babel-bot
Copy link
Collaborator

babel-bot commented Oct 16, 2017

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/5400/

"main": "lib/index.js",
"dependencies": {
"babel-code-frame": "7.0.0-beta.3",
"babel-traverse": "7.0.0-beta.3",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This severs the cyclic dependency that I filed. By making babel-template only depend on babel-types, we avoid risking cycles that we'd otherwise get if we split things out of babel-traverse but still wanted to use them as dependencies in babel-traverse.

? buildNameListCheck({
EXPORTS_LIST: t.identifier(metadata.exportNameListName),
})
? template`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this also a template.statement or it doesn't matter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is essentially autodetect statement or statements so I really didn't need to change the other cases either. Guess I just didn't do this one.

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this PR 🎉

// error.stack does not exists in IE <= 9
rootStack = error.stack
.split("\n")
.slice(3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment which explains why 3? It looks too magic to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally :)


export type Formatter<T> = {
code: string => string,
validate: BabelNodeFile => void,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are BabelNode* types imported in this file? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd get Flowtype set up properly and have them live in babel-types, but for now they live in https://github.com/babel/babel/blob/master/lib/types.js

validate: (ast: BabelNodeFile) => {
const { program } = ast;
if (program.directives && program.directives.length > 0) {
throw new Error("Unexpected directives found");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that this code is invalid?

// I want an expression statement which contains a string literal
const ast = template.statement`"string";`();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you'd have to do

template.statement`("string");`();

if you wanted to get a string expression statement.

It's the same behavior we have right now, it's just that now it actually throws an error instead of silently discarding the string :P

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo Oct 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should wrap the code in { ... } so that my example is parsed as an expression statement. Otherwise it is inconsistent with template.statements, which wraps the input.

e.g.

template.statements`"string";`(); // [ ExpressionStatement {} ]
template.statement`"string";`(); // Error! 💥 

Or probably (better) we should use () => { /* input */ } as the wrapper in the statements formatter, so that directives are always parsed as directives.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They both throw at the moment, but I can certainly tweak this logic so strings are always interpreted as statements and not directives, if you think it'll be more ideal. I can see how the name of the functions might make that more surprising.

Copy link
Member

@existentialism existentialism left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Oct 17, 2017

@loganfsmyth I just tested this PR locally: is this the expected behavior? Shouldn't template.statements return the BlockStatement's body?

const ast = template.statements.ast`foo; bar;`;
console.log(ast);
[ { type: 'BlockStatement',
    start: undefined,
    end: undefined,
    loc: undefined,
    body: [ [Object], [Object] ],
    directives: [] } ]

Copy link
Member

@xtuc xtuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@loganfsmyth is the best!

@loganfsmyth
Copy link
Member Author

@nicolo-ribaudo Oh dang, you're right. I actually forgot that I'd wrapped statements in {} for parsing. I agree those should be consistent. I'm actually going to change that behavior so that smart, statement and statements all automatically treat strings as statements instead of directives, since that seems most consistent.

fn: (Array<BabelNodeStatement>) => T,
): Formatter<T> {
return {
code: str => `"==@babel/template==";;\n${str}`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious for thoughts on this. Since we want to force things to be statements and not directives, the easiest thing is just to prepend a ; to the string. I felt like it'd be a little weird for people to get errors like

SyntaxError: Unexpected token, expected ; (2:4)
  1 | ;
> 2 | omg 45
    |    ^

so I threw a little header in there so it's clearer, e.g.

SyntaxError: Unexpected token, expected ; (2:4)
  1 | "==@babel/template==";;
> 2 | omg 45
    |    ^

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this certainly gives at least some context

@loganfsmyth loganfsmyth merged commit 97a217d into babel:master Oct 18, 2017
@loganfsmyth loganfsmyth deleted the better-template branch October 18, 2017 21:14
@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 5, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 5, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Breaking Change 💥 A type of pull request used for our changelog categories for next major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants