Skip to content

[FEATURE] Strict component blueprint support #20862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '<%= modulePrefix %>/tests/helpers';
import { render } from '@ember/test-helpers';
import <%= componentName %> from '<%= pkgName %>/components/<%= componentPathName %>';

module('<%= friendlyTestDescription %>', function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
// Updating values is achieved using autotracking, just like in app code. For example:
// class State { @tracked myProperty = 0; }; const state = new State();
// and update using state.myProperty = 1; await rerender();
// Handle any actions with function myAction(val) { ... };

await render(<template><%= selfCloseComponent(componentName) %></template>);

assert.dom().hasText('');

// Template block usage:
await render(<template>
<%= openComponent(componentName) %>
template block text
<%= closeComponent(componentName) %>
</template>);

assert.dom().hasText('template block text');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '<%= modulePrefix %>/tests/helpers';
import { render } from '@ember/test-helpers';
import <%= componentName %> from '<%= pkgName %>/components/<%= componentPathName %>';

module('<%= friendlyTestDescription %>', function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
// Updating values is achieved using autotracking, just like in app code. For example:
// class State { @tracked myProperty = 0; }; const state = new State();
// and update using state.myProperty = 1; await rerender();
// Handle any actions with function myAction(val) { ... };

await render(<template><%= selfCloseComponent(componentName) %></template>);

assert.dom().hasText('');

// Template block usage:
await render(<template>
<%= openComponent(componentName) %>
template block text
<%= closeComponent(componentName) %>
</template>);

assert.dom().hasText('template block text');
});
});
40 changes: 39 additions & 1 deletion blueprints/component-test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ function invocationFor(options) {
return parts.map((p) => stringUtil.classify(p)).join('::');
}

function invocationForStrictComponentAuthoringFormat(options) {
let parts = options.entity.name.split('/');
let componentName = parts[parts.length - 1];
return stringUtil.classify(componentName);
}

module.exports = {
description: 'Generates a component integration or unit test.',

Expand All @@ -36,6 +42,17 @@ module.exports = {
{ unit: 'unit' },
],
},
{
name: 'component-authoring-format',
type: ['loose', 'strict'],
default: 'loose',
aliases: [
{ loose: 'loose' },
{ strict: 'strict' },
{ 'template-tag': 'strict' },
{ tt: 'strict' },
],
},
],

fileMapTokens: function () {
Expand All @@ -55,6 +72,23 @@ module.exports = {
};
},

files() {
let files = this._super.files.apply(this, arguments);

if (this.options.componentAuthoringFormat === 'strict') {
const strictFilesToRemove =
this.options.isTypeScriptProject || this.options.typescript ? '.gjs' : '.gts';
files = files.filter(
(file) =>
!(file.endsWith('.js') || file.endsWith('.ts') || file.endsWith(strictFilesToRemove))
);
} else {
files = files.filter((file) => !(file.endsWith('.gjs') || file.endsWith('.gts')));
}

return files;
},

locals: function (options) {
let dasherizedModuleName = stringUtil.dasherize(options.entity.name);
let componentPathName = dasherizedModuleName;
Expand All @@ -74,7 +108,10 @@ module.exports = {
? "import { hbs } from 'ember-cli-htmlbars';"
: "import hbs from 'htmlbars-inline-precompile';";

let templateInvocation = invocationFor(options);
let templateInvocation =
this.options.componentAuthoringFormat === 'strict'
? invocationForStrictComponentAuthoringFormat(options)
: invocationFor(options);
let componentName = templateInvocation;
let openComponent = (descriptor) => `<${descriptor}>`;
let closeComponent = (descriptor) => `</${descriptor}>`;
Expand All @@ -92,6 +129,7 @@ module.exports = {
selfCloseComponent,
friendlyTestDescription,
hbsImportStatement,
pkgName: options.project.pkg.name,
};
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% if (componentClass === '@glimmer/component') {%>import Component from '@glimmer/component';

export default class <%= classifiedModuleName %> extends Component {
<template>
{{yield}}
</template>
}<%} else {%><template>
{{yield}}
</template><%}%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% if (componentClass === '@glimmer/component') {%>import Component from '@glimmer/component';

<%= componentSignature %>
export default class <%= classifiedModuleName %> extends Component<<%= classifiedModuleName %>Signature> {
<template>
{{yield}}
</template>
}<%} else {%>import type { TOC } from '@ember/component/template-only';

<%= componentSignature %>
<template>
{{yield}}
</template> satisfies TOC<<%= classifiedModuleName %>Signature>;<%}%>
56 changes: 49 additions & 7 deletions blueprints/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const chalk = require('chalk');
const stringUtil = require('ember-cli-string-utils');
const getPathOption = require('ember-cli-get-component-path-option');
const normalizeEntityName = require('ember-cli-normalize-entity-name');
const SilentError = require('silent-error');
const { generateComponentSignature } = require('../-utils');

const typescriptBlueprintPolyfill = require('ember-cli-typescript-blueprint-polyfill');
Expand Down Expand Up @@ -40,6 +41,17 @@ module.exports = {
default: 'flat',
aliases: [{ fs: 'flat' }, { ns: 'nested' }],
},
{
name: 'component-authoring-format',
type: ['loose', 'strict'],
default: 'loose',
aliases: [
{ loose: 'loose' },
{ strict: 'strict' },
{ 'template-tag': 'strict' },
{ tt: 'strict' },
],
},
],

init() {
Expand All @@ -58,6 +70,18 @@ module.exports = {
options.componentClass = '';
}

if (options.componentAuthoringFormat === 'strict') {
if (options.componentClass === '@ember/component') {
throw new SilentError(
'The "@ember/component" component class cannot be used in combination with the "--strict" flag'
);
}

if (options.componentClass === '') {
options.componentClass = '@ember/component/template-only';
}
}

return this._super.install.apply(this, arguments);
},

Expand All @@ -78,14 +102,16 @@ module.exports = {
afterInstall(options) {
this._super.afterInstall.apply(this, arguments);

this.skippedJsFiles.forEach((file) => {
let mapped = this.mapFile(file, this.savedLocals);
this.ui.writeLine(` ${chalk.yellow('skip')} ${mapped}`);
});
if (options.componentAuthoringFormat === 'loose') {
this.skippedJsFiles.forEach((file) => {
let mapped = this.mapFile(file, this.savedLocals);
this.ui.writeLine(` ${chalk.yellow('skip')} ${mapped}`);
});

if (this.skippedJsFiles.size > 0) {
let command = `ember generate component-class ${options.entity.name}`;
this.ui.writeLine(` ${chalk.cyan('tip')} to add a class, run \`${command}\``);
if (this.skippedJsFiles.size > 0) {
let command = `ember generate component-class ${options.entity.name}`;
this.ui.writeLine(` ${chalk.cyan('tip')} to add a class, run \`${command}\``);
}
}
},

Expand Down Expand Up @@ -135,6 +161,21 @@ module.exports = {
}
});
}
if (this.options.componentAuthoringFormat === 'strict') {
const strictFilesToRemove =
this.options.isTypeScriptProject || this.options.typescript ? '.gjs' : '.gts';
files = files.filter(
(file) =>
!(
file.endsWith('.js') ||
file.endsWith('.ts') ||
file.endsWith('.hbs') ||
file.endsWith(strictFilesToRemove)
)
);
} else {
files = files.filter((file) => !(file.endsWith('.gjs') || file.endsWith('.gts')));
}

return files;
},
Expand Down Expand Up @@ -172,6 +213,7 @@ module.exports = {
}

return {
classifiedModuleName,
importTemplate,
importComponent,
componentSignature,
Expand Down
38 changes: 38 additions & 0 deletions node-tests/blueprints/component-test-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ describe('Blueprint: component-test', function () {
});
});

it('component-test foo --strict', function () {
return emberGenerateDestroy(['component-test', 'foo', '--strict'], (_file) => {
expect(_file('tests/integration/components/foo-test.gjs')).to.equal(
fixture('component-test/app.gjs')
);
});
});

it('component-test foo --strict --typescript', function () {
return emberGenerateDestroy(
['component-test', 'foo', '--strict', '--typescript'],
(_file) => {
expect(_file('tests/integration/components/foo-test.gts')).to.equal(
fixture('component-test/app.gts')
);
}
);
});

it('component-test x-foo --unit', function () {
return emberGenerateDestroy(['component-test', 'x-foo', '--unit'], (_file) => {
expect(_file('tests/unit/components/x-foo-test.js')).to.equal(
Expand Down Expand Up @@ -65,6 +84,25 @@ describe('Blueprint: component-test', function () {
);
});
});

it('component-test foo --strict', function () {
return emberGenerateDestroy(['component-test', 'foo', '--strict'], (_file) => {
expect(_file('tests/integration/components/foo-test.gjs')).to.equal(
fixture('component-test/addon.gjs')
);
});
});

it('component-test foo --strict --typescript', function () {
return emberGenerateDestroy(
['component-test', 'foo', '--strict', '--typescript'],
(_file) => {
expect(_file('tests/integration/components/foo-test.gts')).to.equal(
fixture('component-test/addon.gts')
);
}
);
});
});

describe('in in-repo-addon', function () {
Expand Down
Loading
Loading