diff --git a/packages/macros/src/glimmer/ast-transform.ts b/packages/macros/src/glimmer/ast-transform.ts index 8fb071e96..b1e8832a0 100644 --- a/packages/macros/src/glimmer/ast-transform.ts +++ b/packages/macros/src/glimmer/ast-transform.ts @@ -2,7 +2,14 @@ import literal from './literal'; import getConfig from './get-config'; import dependencySatisfies from './dependency-satisfies'; import { maybeAttrs } from './macro-maybe-attrs'; -import { macroIfBlock, macroIfExpression, macroIfMustache } from './macro-condition'; +import { + macroIfBlock, + macroIfExpression, + macroIfMustache, + macroUnlessBlock, + macroUnlessExpression, + macroUnlessMustache, +} from './macro-condition'; import { failBuild } from './fail-build'; import { RewrittenPackageCache } from '@embroider/shared-internals'; @@ -181,6 +188,9 @@ export function makeSecondTransform() { if (node.path.original === 'if') { return macroIfBlock(node); } + if (node.path.original === 'unless') { + return macroUnlessBlock(node); + } }, SubExpression(node: any) { if (node.path.type !== 'PathExpression') { @@ -192,6 +202,9 @@ export function makeSecondTransform() { if (node.path.original === 'if') { return macroIfExpression(node, env.syntax.builders); } + if (node.path.original === 'unless') { + return macroUnlessExpression(node, env.syntax.builders); + } if (node.path.original === 'macroFailBuild') { failBuild(node); } @@ -208,6 +221,16 @@ export function makeSecondTransform() { return false; } } + if ( + modifier.path.type === 'SubExpression' && + modifier.path.path.type === 'PathExpression' && + modifier.path.path.original === 'unless' + ) { + modifier.path = macroUnlessExpression(modifier.path, env.syntax.builders); + if (modifier.path.type === 'UndefinedLiteral') { + return true; + } + } if (modifier.path.type !== 'PathExpression') { return true; } @@ -231,6 +254,9 @@ export function makeSecondTransform() { if (node.path.original === 'if') { return macroIfMustache(node, env.syntax.builders); } + if (node.path.original === 'unless') { + return macroUnlessMustache(node, env.syntax.builders); + } if (node.path.original === 'macroFailBuild') { failBuild(node); } diff --git a/packages/macros/src/glimmer/macro-condition.ts b/packages/macros/src/glimmer/macro-condition.ts index 0e1c0c931..2bf488720 100644 --- a/packages/macros/src/glimmer/macro-condition.ts +++ b/packages/macros/src/glimmer/macro-condition.ts @@ -63,3 +63,67 @@ export function macroIfMustache(node: any, builders: any) { return builders.mustache(result); } + +export function macroUnlessBlock(node: any) { + let condition = node.params[0]; + + if (!condition || condition.type !== 'SubExpression' || condition.path.original !== 'macroCondition') { + return node; + } + + if (condition.params.length !== 1) { + throw new Error(`macroCondition requires one arguments, you passed ${node.params.length}`); + } + + let result = evaluate(condition.params[0]); + if (!result.confident) { + throw new Error(`argument to macroCondition must be statically analyzable`); + } + + if (result.value) { + if (node.inverse) { + return node.inverse.body; + } else { + return []; + } + } else { + return node.program.body; + } +} + +export function macroUnlessExpression(node: any, builders: any) { + let condition = node.params[0]; + + if (!condition || condition.type !== 'SubExpression' || condition.path.original !== 'macroCondition') { + return node; + } + + if (condition.params.length !== 1) { + throw new Error(`macroCondition requires one arguments, you passed ${node.params.length}`); + } + + let result = evaluate(condition.params[0]); + if (!result.confident) { + throw new Error(`argument to macroCondition must be statically analyzable`); + } + + if (result.value) { + return node.params[2] || builders.undefined(); + } else { + return node.params[1]; + } +} + +export function macroUnlessMustache(node: any, builders: any) { + let result = macroUnlessExpression(node, builders); + + if (result === node) { + return node; + } + + if (result.type === 'SubExpression') { + return builders.mustache(result.path, result.params, result.hash); + } + + return builders.mustache(result); +} diff --git a/tests/fixtures/macro-test/tests/integration/components/macro-unless-test.js b/tests/fixtures/macro-test/tests/integration/components/macro-unless-test.js new file mode 100644 index 000000000..70141f277 --- /dev/null +++ b/tests/fixtures/macro-test/tests/integration/components/macro-unless-test.js @@ -0,0 +1,139 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { helper } from '@ember/component/helper'; + +module('Integration | Macro | macroCondition + {{unless}}', function (hooks) { + setupRenderingTest(hooks); + + test('macroCondition in content position when true', async function (assert) { + await render(hbs`{{#unless (macroCondition true)}}red{{else}}blue{{/unless}}`); + assert.equal(this.element.textContent.trim(), 'blue'); + }); + + test('macroCondition in content position when false', async function (assert) { + await render(hbs`{{#unless (macroCondition false)}}red{{else}}blue{{/unless}}`); + assert.equal(this.element.textContent.trim(), 'red'); + }); + + test('macroCondition in content position when true with no alternate', async function (assert) { + await render(hbs`{{#unless (macroCondition true)}}red{{/unless}}`); + assert.equal(this.element.textContent.trim(), ''); + }); + + test('macroCondition in subexpression position when true', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, 'blue'); + }) + ); + await render(hbs`{{my-assertion (unless (macroCondition true) 'red' 'blue') }}`); + }); + + test('macroCondition inside string', async function (assert) { + assert.expect(1); + await render(hbs`
`); + assert.ok(this.element.querySelector('.target').matches('.blue')); + }); + + test('macroCondition in subexpression position when false', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, 'red'); + }) + ); + await render(hbs`{{my-assertion (unless (macroCondition false) 'red' 'blue') }}`); + }); + + test('macroCondition in subexpression position when true with no alternate', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, undefined); + }) + ); + await render(hbs`{{my-assertion (unless (macroCondition true) 'red') }}`); + }); + + test('macroCondition composes with other macros, true case', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, 'blue'); + }) + ); + await render( + hbs`{{my-assertion (unless (macroCondition (macroDependencySatisfies 'ember-source' '*')) 'red' 'blue') }}` + ); + }); + + test('macroCondition composes with other macros, false case', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, 'red'); + }) + ); + await render( + hbs`{{my-assertion (unless (macroCondition (macroDependencySatisfies 'ember-source' '10.x')) 'red' 'blue') }}` + ); + }); + + test('macroCondition composes with self', async function (assert) { + assert.expect(1); + this.owner.register( + 'helper:my-assertion', + helper(function ([value]) { + assert.strictEqual(value, 'red'); + }) + ); + await render(hbs`{{my-assertion (unless (macroCondition false) (unless (macroCondition true) 'green' 'red') 'blue') }}`); + }); + + test('macroCondition in modifier position when false', async function (assert) { + assert.expect(1); + this.doThing = function () { + assert.ok(true, 'it ran'); + }; + await render( + hbs('', { + insertRuntimeErrors: true, + }) + ); + await click('button'); + }); + + test('macroCondition in modifier position when true', async function (assert) { + assert.expect(1); + this.doThing = function () { + assert.ok(true, 'it ran'); + }; + await render( + hbs('', { + insertRuntimeErrors: true, + }) + ); + await click('button'); + }); + + test('macroCondition in modifier position when true with no alternate', async function (assert) { + assert.expect(0); + this.doThing = function () { + assert.ok(true, 'it ran'); + }; + await render( + hbs('', { + insertRuntimeErrors: true, + }) + ); + await click('button'); + }); +});