-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
13,054 additions
and
9,751 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { template } from './lib/template'; | ||
export type { EmberPrecompileOptions } from './lib/types'; |
115 changes: 115 additions & 0 deletions
115
packages/@ember/template-compiler/lib/compile-options.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { assert } from '@ember/debug'; | ||
import { | ||
RESOLUTION_MODE_TRANSFORMS, | ||
STRICT_MODE_KEYWORDS, | ||
STRICT_MODE_TRANSFORMS, | ||
} from './plugins/index'; | ||
import type { EmberPrecompileOptions, PluginFunc } from './types'; | ||
import COMPONENT_NAME_SIMPLE_DASHERIZE_CACHE from './dasherize-component-name'; | ||
|
||
let USER_PLUGINS: PluginFunc[] = []; | ||
|
||
function malformedComponentLookup(string: string) { | ||
return string.indexOf('::') === -1 && string.indexOf(':') > -1; | ||
} | ||
|
||
function buildCompileOptions(_options: EmberPrecompileOptions): EmberPrecompileOptions { | ||
let moduleName = _options.moduleName; | ||
|
||
let options: EmberPrecompileOptions & Partial<EmberPrecompileOptions> = { | ||
meta: {}, | ||
isProduction: false, | ||
plugins: { ast: [] }, | ||
..._options, | ||
moduleName, | ||
customizeComponentName(tagname: string): string { | ||
assert( | ||
`You tried to invoke a component named <${tagname} /> in "${ | ||
moduleName ?? '[NO MODULE]' | ||
}", but that is not a valid name for a component. Did you mean to use the "::" syntax for nested components?`, | ||
!malformedComponentLookup(tagname) | ||
); | ||
|
||
return COMPONENT_NAME_SIMPLE_DASHERIZE_CACHE.get(tagname); | ||
}, | ||
}; | ||
|
||
if ('eval' in options) { | ||
const localScopeEvaluator = options.eval as (value: string) => unknown; | ||
const globalScopeEvaluator = (value: string) => new Function(`return ${value};`)(); | ||
|
||
options.lexicalScope = (variable: string) => { | ||
if (inScope(variable, localScopeEvaluator)) { | ||
return !inScope(variable, globalScopeEvaluator); | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
delete options.eval; | ||
} | ||
|
||
if ('locals' in options && !options.locals) { | ||
// Glimmer's precompile options declare `locals` like: | ||
// locals?: string[] | ||
// but many in-use versions of babel-plugin-htmlbars-inline-precompile will | ||
// set locals to `null`. This used to work but only because glimmer was | ||
// ignoring locals for non-strict templates, and now it supports that case. | ||
delete options.locals; | ||
} | ||
|
||
// move `moduleName` into `meta` property | ||
if (options.moduleName) { | ||
let meta = options.meta; | ||
assert('has meta', meta); // We just set it | ||
meta.moduleName = options.moduleName; | ||
} | ||
|
||
if (options.strictMode) { | ||
options.keywords = STRICT_MODE_KEYWORDS; | ||
} | ||
|
||
return options; | ||
} | ||
|
||
function transformsFor(options: EmberPrecompileOptions): readonly PluginFunc[] { | ||
return options.strictMode ? STRICT_MODE_TRANSFORMS : RESOLUTION_MODE_TRANSFORMS; | ||
} | ||
|
||
export default function compileOptions( | ||
_options: Partial<EmberPrecompileOptions> = {} | ||
): EmberPrecompileOptions { | ||
let options = buildCompileOptions(_options); | ||
let builtInPlugins = transformsFor(options); | ||
|
||
if (!_options.plugins) { | ||
options.plugins = { ast: [...USER_PLUGINS, ...builtInPlugins] }; | ||
} else { | ||
let potententialPugins = [...USER_PLUGINS, ...builtInPlugins]; | ||
assert('expected plugins', options.plugins); | ||
let pluginsToAdd = potententialPugins.filter((plugin) => { | ||
assert('expected plugins', options.plugins); | ||
return options.plugins.ast.indexOf(plugin) === -1; | ||
}); | ||
options.plugins.ast = [...options.plugins.ast, ...pluginsToAdd]; | ||
} | ||
|
||
return options; | ||
} | ||
|
||
type Evaluator = (value: string) => unknown; | ||
|
||
function inScope(variable: string, evaluator: Evaluator): boolean { | ||
try { | ||
return evaluator(`typeof ${variable} !== "undefined"`) === true; | ||
} catch (e) { | ||
// This occurs when attempting to evaluate a reserved word using eval (`eval('typeof let')`). | ||
// If the variable is a reserved word, it's definitely not in scope, so return false. | ||
if (e && e instanceof SyntaxError) { | ||
return false; | ||
} | ||
|
||
// If it's another kind of error, don't swallow it. | ||
throw e; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/@ember/template-compiler/lib/dasherize-component-name.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Cache } from '@ember/-internals/utils'; | ||
|
||
/* | ||
This diverges from `Ember.String.dasherize` so that`<XFoo />` can resolve to `x-foo`. | ||
`Ember.String.dasherize` would resolve it to `xfoo`.. | ||
*/ | ||
const SIMPLE_DASHERIZE_REGEXP = /[A-Z]|::/g; | ||
const ALPHA = /[A-Za-z0-9]/; | ||
|
||
export default new Cache<string, string>(1000, (key) => | ||
key.replace(SIMPLE_DASHERIZE_REGEXP, (char, index) => { | ||
if (char === '::') { | ||
return '/'; | ||
} | ||
|
||
if (index === 0 || !ALPHA.test(key[index - 1]!)) { | ||
return char.toLowerCase(); | ||
} | ||
|
||
return `-${char.toLowerCase()}`; | ||
}) | ||
); |
119 changes: 119 additions & 0 deletions
119
packages/@ember/template-compiler/lib/plugins/assert-against-attrs.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { assert, deprecate } from '@ember/debug'; | ||
import type { AST, ASTPlugin } from '@glimmer/syntax'; | ||
import calculateLocationDisplay from '../system/calculate-location-display'; | ||
import type { EmberASTPluginEnvironment } from '../types'; | ||
|
||
/** | ||
@module ember | ||
*/ | ||
|
||
/** | ||
A Glimmer2 AST transformation that asserts against | ||
```handlebars | ||
{{attrs.foo.bar}} | ||
``` | ||
...as well as `{{#if attrs.foo}}`, `{{deeply (nested attrs.foobar.baz)}}`. | ||
@private | ||
@class AssertAgainstAttrs | ||
*/ | ||
|
||
export default function assertAgainstAttrs(env: EmberASTPluginEnvironment): ASTPlugin { | ||
let { builders: b } = env.syntax; | ||
let moduleName = env.meta?.moduleName; | ||
|
||
let stack: string[][] = [[]]; | ||
|
||
function updateBlockParamsStack(blockParams: string[]) { | ||
let parent = stack[stack.length - 1]; | ||
assert('has parent', parent); | ||
stack.push(parent.concat(blockParams)); | ||
} | ||
|
||
return { | ||
name: 'assert-against-attrs', | ||
|
||
visitor: { | ||
Template: { | ||
enter(node: AST.Template) { | ||
updateBlockParamsStack(node.blockParams); | ||
}, | ||
exit() { | ||
stack.pop(); | ||
}, | ||
}, | ||
|
||
Block: { | ||
enter(node: AST.Block) { | ||
updateBlockParamsStack(node.blockParams); | ||
}, | ||
exit() { | ||
stack.pop(); | ||
}, | ||
}, | ||
|
||
ElementNode: { | ||
enter(node: AST.ElementNode) { | ||
updateBlockParamsStack(node.blockParams); | ||
}, | ||
exit() { | ||
stack.pop(); | ||
}, | ||
}, | ||
|
||
PathExpression(node: AST.PathExpression): AST.Node | void { | ||
if (isAttrs(node, stack[stack.length - 1]!)) { | ||
assert( | ||
`Using {{attrs}} to reference named arguments is not supported. {{${ | ||
node.original | ||
}}} should be updated to {{@${node.original.slice(6)}}}. ${calculateLocationDisplay( | ||
moduleName, | ||
node.loc | ||
)}` | ||
); | ||
} else if (isThisDotAttrs(node)) { | ||
// When removing this, ensure `{{this.attrs.foo}}` is left as-is, without triggering | ||
// any assertions/deprecations. It's perfectly legal to reference `{{this.attrs.foo}}` | ||
// in the template since it is a real property on the backing class – it will give you | ||
// a `MutableCell` wrapper object, but maybe that's what you want. And in any case, | ||
// there is no compelling to special case that property access. | ||
deprecate( | ||
`Using {{this.attrs}} to reference named arguments has been deprecated. {{${ | ||
node.original | ||
}}} should be updated to {{@${node.original.slice(11)}}}. ${calculateLocationDisplay( | ||
moduleName, | ||
node.loc | ||
)}`, | ||
false, | ||
{ | ||
id: 'attrs-arg-access', | ||
url: 'https://deprecations.emberjs.com/v3.x/#toc_attrs-arg-access', | ||
until: '6.0.0', | ||
for: 'ember-source', | ||
since: { | ||
available: '3.26.0', | ||
enabled: '3.26.0', | ||
}, | ||
} | ||
); | ||
|
||
return b.path(`@${node.original.slice(11)}`, node.loc); | ||
} | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
function isAttrs(node: AST.PathExpression, symbols: string[]) { | ||
return ( | ||
node.head.type === 'VarHead' && | ||
node.head.name === 'attrs' && | ||
symbols.indexOf(node.head.name) === -1 | ||
); | ||
} | ||
|
||
function isThisDotAttrs(node: AST.PathExpression) { | ||
return node.head.type === 'ThisHead' && node.tail[0] === 'attrs'; | ||
} |
37 changes: 37 additions & 0 deletions
37
packages/@ember/template-compiler/lib/plugins/assert-against-named-outlets.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { assert } from '@ember/debug'; | ||
import type { AST, ASTPlugin } from '@glimmer/syntax'; | ||
import calculateLocationDisplay from '../system/calculate-location-display'; | ||
import type { EmberASTPluginEnvironment } from '../types'; | ||
|
||
/** | ||
@module ember | ||
*/ | ||
|
||
/** | ||
Prevents usage of named outlets, a legacy concept in Ember removed in 4.0. | ||
@private | ||
@class AssertAgainstNamedOutlets | ||
*/ | ||
export default function assertAgainstNamedOutlets(env: EmberASTPluginEnvironment): ASTPlugin { | ||
let moduleName = env.meta?.moduleName; | ||
|
||
return { | ||
name: 'assert-against-named-outlets', | ||
|
||
visitor: { | ||
MustacheStatement(node: AST.MustacheStatement) { | ||
if ( | ||
node.path.type === 'PathExpression' && | ||
node.path.original === 'outlet' && | ||
node.params[0] | ||
) { | ||
let sourceInformation = calculateLocationDisplay(moduleName, node.loc); | ||
assert( | ||
`Named outlets were removed in Ember 4.0. See https://deprecations.emberjs.com/v3.x#toc_route-render-template for guidance on alternative APIs for named outlet use cases. ${sourceInformation}` | ||
); | ||
} | ||
}, | ||
}, | ||
}; | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/@ember/template-compiler/lib/plugins/assert-input-helper-without-block.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { assert } from '@ember/debug'; | ||
import type { AST, ASTPlugin } from '@glimmer/syntax'; | ||
import calculateLocationDisplay from '../system/calculate-location-display'; | ||
import type { EmberASTPluginEnvironment } from '../types'; | ||
import { isPath } from './utils'; | ||
|
||
export default function errorOnInputWithContent(env: EmberASTPluginEnvironment): ASTPlugin { | ||
let moduleName = env.meta?.moduleName; | ||
|
||
return { | ||
name: 'assert-input-helper-without-block', | ||
|
||
visitor: { | ||
BlockStatement(node: AST.BlockStatement) { | ||
if (isPath(node.path) && node.path.original === 'input') { | ||
assert(assertMessage(moduleName, node)); | ||
} | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
function assertMessage(moduleName: string | undefined, node: AST.BlockStatement): string { | ||
let sourceInformation = calculateLocationDisplay(moduleName, node.loc); | ||
|
||
return `The {{input}} helper cannot be used in block form. ${sourceInformation}`; | ||
} |
47 changes: 47 additions & 0 deletions
47
packages/@ember/template-compiler/lib/plugins/assert-reserved-named-arguments.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { assert } from '@ember/debug'; | ||
import type { AST, ASTPlugin } from '@glimmer/syntax'; | ||
import calculateLocationDisplay from '../system/calculate-location-display'; | ||
import type { EmberASTPluginEnvironment } from '../types'; | ||
|
||
export default function assertReservedNamedArguments(env: EmberASTPluginEnvironment): ASTPlugin { | ||
let moduleName = env.meta?.moduleName; | ||
|
||
return { | ||
name: 'assert-reserved-named-arguments', | ||
|
||
visitor: { | ||
// In general, we don't assert on the invocation side to avoid creating migration | ||
// hazards (e.g. using angle bracket to invoke a classic component that uses | ||
// `this.someReservedName`. However, we want to avoid leaking special internal | ||
// things, such as `__ARGS__`, so those would need to be asserted on both sides. | ||
|
||
AttrNode({ name, loc }: AST.AttrNode) { | ||
if (name === '@__ARGS__') { | ||
assert(`${assertMessage(name)} ${calculateLocationDisplay(moduleName, loc)}`); | ||
} | ||
}, | ||
|
||
HashPair({ key, loc }: AST.HashPair) { | ||
if (key === '__ARGS__') { | ||
assert(`${assertMessage(key)} ${calculateLocationDisplay(moduleName, loc)}`); | ||
} | ||
}, | ||
|
||
PathExpression({ original, loc }: AST.PathExpression) { | ||
if (isReserved(original)) { | ||
assert(`${assertMessage(original)} ${calculateLocationDisplay(moduleName, loc)}`); | ||
} | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
const RESERVED = ['@arguments', '@args', '@block', '@else']; | ||
|
||
function isReserved(name: string): boolean { | ||
return RESERVED.indexOf(name) !== -1 || Boolean(name.match(/^@[^a-z]/)); | ||
} | ||
|
||
function assertMessage(name: string): string { | ||
return `'${name}' is reserved.`; | ||
} |
Oops, something went wrong.