Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
wycats committed Oct 10, 2024
1 parent 0f9b3fc commit 2f5e698
Show file tree
Hide file tree
Showing 23 changed files with 13,054 additions and 9,751 deletions.
2 changes: 2 additions & 0 deletions packages/@ember/template-compiler/index.ts
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 packages/@ember/template-compiler/lib/compile-options.ts
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 packages/@ember/template-compiler/lib/dasherize-component-name.ts
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 packages/@ember/template-compiler/lib/plugins/assert-against-attrs.ts
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';
}
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}`
);
}
},
},
};
}
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}`;
}
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.`;
}
Loading

0 comments on commit 2f5e698

Please sign in to comment.