Skip to content

Commit

Permalink
chore: script to create new setup for atomic component (#4913)
Browse files Browse the repository at this point in the history
https://coveord.atlassian.net/browse/KIT-3908

needs #4914 to work correctly so if you want to test it, test it from
there.

A new script to generate file structure and basic files for new lit
atomic components. It is used like this :


`npx nx run atomic:generate-component --name=atomic-ball
--output=src/components/search/atomic-ball`

The output arg is always relative to the atomic package root.


This is to help us during the migration and it will keep changing to
make it better.
  • Loading branch information
alexprudhomme authored Jan 30, 2025
1 parent 87681ca commit f872a49
Show file tree
Hide file tree
Showing 209 changed files with 731 additions and 558 deletions.
8 changes: 2 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/atomic/.storybook/main.mts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ const resolveStorybookUtils: PluginImpl = () => {
return {
name: 'resolve-storybook-utils',
async resolveId(source: string, importer, options) {
if (source.startsWith('@coveo/atomic-storybook-utils')) {
if (source.startsWith('@/storybook-utils')) {
return this.resolve(
source.replace(
'@coveo/atomic-storybook-utils',
path.resolve(__dirname, '../storybookUtils')
'@/storybook-utils',
path.resolve(__dirname, '../storybook-utils')
),
importer,
options
Expand Down
38 changes: 28 additions & 10 deletions packages/atomic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,6 @@ To run all the test in Firefox:
npm run e2e:firefox
```

### Separate test for Hosted Search Page

To test the current Atomic build against the hosted search pages for Trials, use the following commands:

```sh
npm run e2e:hsp:watch
npm run e2e:hsp
npm run e2e:hsp:firefox
```

## Utilities

### Stencil decorators
Expand Down Expand Up @@ -143,3 +133,31 @@ export class AtomicResultComponent {
}
}
```

## Generate Component Command

To generate a new component, use the following command:

```bash
npx nx run atomic:generate-component --name=<component-name> --output=<path-to-output-directory>
```

The `output` argument is optional. If not provided, it will default to `src/components/commerce`.

For example, to generate a component named `atomic-ball`, run:

```bash
npx nx run atomic:generate-component --name=ball
```

This will create the necessary component files under the default path `src/components/commerce/atomic-ball`.

If you'd like to specify a different path, you can use the `--output` flag. For example, to generate the component under `src/components/search`, run:

```bash
npx nx run atomic:generate-component --name=ball --output=src/components/search
```

You can also use `--name=atomic-ball` if you'd like, but the script will automatically add the "atomic" prefix if necessary.

This will create the component in the specified directory (`src/components/search/atomic-ball` in this case), or use the default `src/components/commerce/atomic-ball` if no `output` is provided.
2 changes: 1 addition & 1 deletion packages/atomic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
},
"devDependencies": {
"@axe-core/playwright": "4.10.1",
"@coveo/atomic-storybook-utils": "file:./storybookUtils",
"@coveo/release": "1.0.0",
"@custom-elements-manifest/analyzer": "0.10.4",
"@nx/vite": "20.3.1",
Expand Down Expand Up @@ -124,6 +123,7 @@
"cypress-axe": "1.5.0",
"cypress-repeat": "2.3.8",
"cypress-split": "1.5.0",
"handlebars": "4.7.8",
"html-loader-jest": "0.2.1",
"jest": "29.7.0",
"jest-cli": "29.7.0",
Expand Down
21 changes: 21 additions & 0 deletions packages/atomic/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,27 @@
"command": "rm -f ./dist/atomic/loader/package.json",
"cwd": "packages/atomic"
}
},
"generate-component": {
"executor": "nx:run-commands",
"options": {
"command": "node ./scripts/generate-component.js {args.name} {args.output}",
"cwd": "{projectRoot}"
},
"inputs": [],
"schema": {
"properties": {
"name": {
"type": "string",
"description": "The name of the component to generate"
},
"output": {
"type": "string",
"description": "The output directory for the generated files"
}
},
"required": ["name", "output"]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h1 {
color: #3490dc;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Canvas, ArgTypes, Meta, Title, Description} from '@storybook/blocks';
import * as {{namePascalCase}}Stories from './{{name}}.new.stories';

{/* Storybook provides a number of blocs to construct documentation pages */}
{/* https://storybook.js.org/docs/writing-docs/doc-blocks#available-blocks */}

<Meta of={ {{namePascalCase}}Stories } />

<Title />
<Description />

<Canvas of={ {{namePascalCase}}Stories.Default } />

## Properties
<ArgTypes />

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
import {renderComponent} from '@/storybook-utils/common/render-component';
import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper';
// import {wrapInCommerceInterface} from '@/storybook-utils/commerce/commerce-interface-wrapper';
import type {Meta, StoryObj as Story} from '@storybook/web-components';

// Wrap it in whatever interface/component you need
const {decorator, play} = wrapInSearchInterface();
// const {decorator, play} = wrapInCommerceInterface();

const meta: Meta = {
component: '{{name}}',
title: '{{namePascalCase}}',
id: '{{name}}',
render: renderComponent,
decorators: [decorator],
parameters,
play,
};

export default meta;

export const Default: Story = {
name: '{{name}}',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {within} from 'shadow-dom-testing-library';
import {describe, test, expect, beforeAll, afterAll} from 'vitest';
import './{{name}}';
import { {{namePascalCase}} } from './{{name}}';

describe('{{namePascalCase}}', () => {
let element: {{namePascalCase}};
beforeAll(async () => {
element = document.createElement('{{name}}');
document.body.appendChild(element);
});

afterAll(() => {
document.body.removeChild(element);
});

test('should render the component', async () => {
expect(element.shadowRoot).toBeTruthy();
const text = await within(element).findByShadowText('Hello World');
expect(text).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {TailwindLitElement} from '@/src/utils/tailwind.element';
import {CSSResultGroup, html, unsafeCSS} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import styles from './{{name}}.css';

/**
* The {{name}} is a component that does something.
*/
@customElement('{{name}}')
export class {{namePascalCase}} extends TailwindLitElement {
static styles: CSSResultGroup = [
TailwindLitElement.styles,
unsafeCSS(styles),
];

/**
* The name of the {{name}}
*/
@property() name = 'World';

render() {
return html`<div>
<h1>{{name}}</h1>
<p>Hello ${this.name}</p>
</div>`;
}
}

declare global {
interface HTMLElementTagNameMap {
'{{name}}': {{namePascalCase}};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {test, expect} from './fixture';

test.describe('{{namePascalCase}}', () => {
test.beforeEach(async ({ {{shorterName}} }) => {
await {{shorterName}}.load();
});

test('should be accessible', async ({makeAxeBuilder}) => {
const accessibilityResults = await makeAxeBuilder().analyze();
expect(accessibilityResults.violations.length).toEqual(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {test as base} from '@playwright/test';
import {AxeFixture, makeAxeBuilder} from '@/playwright-utils/base-fixture';
import { {{namePascalCase}}PageObject } from './page-object';

type Fixtures = {
{{shorterName}}: {{namePascalCase}}PageObject;
};

export const test = base.extend<Fixtures & AxeFixture>({
makeAxeBuilder,
{{shorterName}}: async ({page}, use) => {
await use(new {{namePascalCase}}PageObject(page));
},
});

export {expect} from '@playwright/test';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {Page} from '@playwright/test';
import {BasePageObject} from '@/playwright-utils/lit-base-page-object';

export class {{namePascalCase}}PageObject extends BasePageObject {
constructor(page: Page) {
super(page, '{{name}}');
}
}
102 changes: 102 additions & 0 deletions packages/atomic/scripts/generate-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import fs from 'fs-extra';
import handlebars from 'handlebars';
import path from 'path';
import prettier from 'prettier';

const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const kebabToPascal = (str) => str.split('-').map(capitalize).join('');

async function formatWithPrettier(content, filePath) {
try {
const options = await prettier.resolveConfig(filePath);
return prettier.format(content, {...options, filepath: filePath});
} catch (error) {
console.warn(`Failed to format ${filePath} with Prettier`, error);
return content;
}
}

async function generateFiles(name, outputDir) {
const templatesDir = path.resolve(
import.meta.dirname,
'generate-component-templates'
);
const resolvedOutputDir = path.resolve(outputDir);
const namePascalCase = kebabToPascal(name);
const shorterName = name.replace(/^atomic-/, '').toLowerCase();

const files = [
{template: 'component.ts.hbs', output: `${name}.ts`},
{template: 'component.mdx.hbs', output: `${name}.mdx`},
{
template: 'component.new.stories.tsx.hbs',
output: `${name}.new.stories.tsx`,
},
{template: 'component.css.hbs', output: `${name}.css`},
{template: 'component.spec.ts.hbs', output: `${name}.spec.ts`},
{template: 'e2e/component.e2e.ts.hbs', output: `e2e/${name}.e2e.ts`},
{template: 'e2e/fixture.ts.hbs', output: `e2e/fixture.ts`},
{template: 'e2e/page-object.ts.hbs', output: `e2e/page-object.ts`},
];

for (const file of files) {
const templatePath = path.join(templatesDir, file.template);

const outputPath = path.join(
resolvedOutputDir,
file.output.replace('noop', name)
);

// Does not overwrite existing files
if (await fs.pathExists(outputPath)) {
console.log(`Skipped (already exists): ${outputPath}`);
continue;
}

const templateContent = await fs.readFile(templatePath, 'utf8');
const compiled = handlebars.compile(templateContent);
let content = compiled({name, namePascalCase, shorterName});

// Format each file with Prettier
content = await formatWithPrettier(content, outputPath);

await fs.ensureDir(path.dirname(outputPath));
await fs.writeFile(outputPath, content, 'utf8');
console.log(`Created: ${outputPath}`);
}
}

const [componentName, outputDir] = process.argv.slice(2);

// Ensure the component name is prefixed with 'atomic-' if it's not already there
const normalizedComponentName = componentName.startsWith('atomic-')
? componentName
: `atomic-${componentName}`;

let resolvedOutputDir;

if (outputDir) {
// Use the provided output dir and add the component name
resolvedOutputDir = path.join(outputDir, normalizedComponentName);
} else {
// Default to src/components/commerce/<component-name> if no output dir is provided
resolvedOutputDir = path.resolve(
'src',
'components',
'commerce',
normalizedComponentName
);
}

if (!componentName) {
console.error(
'Usage: node generate-component.js <component-name> [<output-dir>]'
);
process.exit(1);
}

generateFiles(normalizedComponentName, resolvedOutputDir).catch(console.error);

// add the import to the lazy index file
// add the import to the index file
// change the output arg to always start with search/commerce/insight/ipx/recommendations
Loading

0 comments on commit f872a49

Please sign in to comment.