Skip to content
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

Implement lit init element #3248

Merged
merged 13 commits into from Nov 14, 2022
6 changes: 6 additions & 0 deletions .changeset/wild-pillows-carry.md
@@ -0,0 +1,6 @@
---
'@lit-labs/cli': minor
'@lit-labs/gen-utils': minor
---

Implemented lit init element command
1 change: 1 addition & 0 deletions .prettierignore
Expand Up @@ -146,6 +146,7 @@ packages/labs/cli/index.d.ts
packages/labs/cli/index.d.ts.map
packages/labs/cli/test-gen/

packages/labs/cli/test-goldens/
packages/labs/cli-localize/lib/
packages/labs/cli-localize/node_modules/

Expand Down
1 change: 1 addition & 0 deletions packages/labs/cli/.prettierignore
@@ -0,0 +1 @@
/test-goldens/
3 changes: 2 additions & 1 deletion packages/labs/cli/package.json
Expand Up @@ -62,7 +62,8 @@
"command": "tsc --pretty --noEmit",
"files": [
"tsconfig.json",
"src/**/*"
"src/**/*",
"test-goldens/**/*"
],
"output": []
},
Expand Down
8 changes: 7 additions & 1 deletion packages/labs/cli/src/lib/commands/help.ts
Expand Up @@ -88,7 +88,13 @@ export const makeHelpCommand = (cli: LitCli): ResolvedCommand => {
],

async run(options: CommandOptions, console: Console) {
const commandNames = options['command'] as Array<string> | null;
let commandNames = options['command'] as Array<string> | string | null;

if (typeof commandNames === 'string') {
commandNames = commandNames?.split(' ') ?? [];
}

commandNames = commandNames?.map((c) => c.trim()) ?? null;

if (commandNames == null) {
console.debug('no command given, printing general help...', {options});
Expand Down
82 changes: 82 additions & 0 deletions packages/labs/cli/src/lib/commands/init.ts
@@ -0,0 +1,82 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {Command, CommandOptions} from '../command.js';
import {run} from '../init/element-starter/index.js';
import {LitCli} from '../lit-cli.js';

export type Language = 'ts' | 'js';
export interface InitCommandOptions {
lang: Language;
name: string;
dir: string;
}

export const makeInitCommand = (cli: LitCli): Command => {
return {
kind: 'resolved',
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
name: 'init',
description: 'Initialize a Lit project',
subcommands: [
{
kind: 'resolved',
name: 'element',
description: 'Generate a shareable element starter package',
options: [
e111077 marked this conversation as resolved.
Show resolved Hide resolved
{
name: 'lang',
defaultValue: 'js',
description:
'Which language to use for the element. Supported languages: js, ts',
},
{
name: 'name',
defaultValue: 'my-element',
description:
'Tag name of the Element to generate (must include a hyphen).',
},
{
name: 'dir',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the gen command, we currently call this out, and all things equal would be nice to have common naming for common things between commands. Let's change one or the other to be common.

{
name: 'out',
defaultValue: './gen',
description: 'Folder to output generated packages to.',
},

I don't love out, but since gen also has an input directory (package), dir is probably too ambiguous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will follow up with another PR rn

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #3456

defaultValue: '.',
description: 'Directory in which to generate the element package.',
},
],
async run(options: CommandOptions, console: Console) {
const name = options.name as string;
/*
* This is a basic check to ensure that the name is a valid custom
* element name. Will make sure you you start off with a character and
* at least one hyphen plus more characters. Will not check for the
* following invalid use cases:
* - starting with a digit
*
* Will not allow the following valid use cases:
* - including a unicode character as not the first character
*/
const customElementMatch = name.match(/\w+(-\w+)+/g);
if (!customElementMatch || customElementMatch[0] !== name) {
throw new Error(
`"${name}" is not a valid custom-element name. (Must include a hyphen and ascii characters)`
);
}
return await run(
options as unknown as InitCommandOptions,
console,
cli
);
},
},
],
async run(_options: CommandOptions, console: Console) {
// by default run the element command
return await run(
{lang: 'js', name: 'my-element', dir: '.'},
console,
cli
);
},
};
};
52 changes: 52 additions & 0 deletions packages/labs/cli/src/lib/init/element-starter/index.ts
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {FileTree, writeFileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
import {generateTsconfig} from './templates/tsconfig.json.js';
import {generatePackageJson} from './templates/package.json.js';
import {generateIndex} from './templates/demo/index.html.js';
import {generateGitignore} from './templates/gitignore.js';
import {generateNpmignore} from './templates/npmignore.js';
import {generateElement} from './templates/lib/element.js';
import {CommandResult} from '../../command.js';
import {InitCommandOptions} from '../../commands/init.js';
import {LitCli} from '../../lit-cli.js';
import path from 'path';

export const generateLitElementStarter = async (
options: InitCommandOptions
): Promise<FileTree> => {
const {name, lang} = options;
let files = {
...generatePackageJson(name, lang),
...generateIndex(name),
...generateGitignore(lang),
...generateNpmignore(),
...generateElement(name, lang),
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
};
if (lang === 'ts') {
files = {
...files,
...generateTsconfig(),
};
}
return files;
};

export const run = async (
options: InitCommandOptions,
console: Console,
cli: LitCli
): Promise<CommandResult> => {
const files = await generateLitElementStarter(options);
const outPath = path.join(cli.cwd, options.dir, options.name);
await writeFileTree(outPath, files);
const relativePath = path.relative(cli.cwd, outPath);
console.log(`Created sharable element in ${relativePath}/.`);
return {
exitCode: 0,
};
};
@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
import {html} from '@lit-labs/gen-utils/lib/str-utils.js';

export const generateIndex = (elementName: string): FileTree => {
return {
demo: {
'index.html': html`<!DOCTYPE html>
<html lang="en">
<head>
<title>${elementName} demo</title>
<script type="module" src="../lib/${elementName}.js"></script>
</head>
<body>
<${elementName}></${elementName}>
</body>
</html>`,
},
};
};
@@ -0,0 +1,19 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
import {Language} from '../../../commands/init.js';

export const generateGitignore = (lang: Language): FileTree => {
return {
'.gitignore': `node_modules${
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
lang !== 'ts'
? ''
: `
lib`
}`,
};
};
@@ -0,0 +1,107 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
e111077 marked this conversation as resolved.
Show resolved Hide resolved
import {
javascript,
kabobToPascalCase,
} from '@lit-labs/gen-utils/lib/str-utils.js';
import {Language} from '../../../../commands/init.js';

export const generateElement = (
elementName: string,
language: Language
): FileTree => {
const directory = language === 'js' ? 'lib' : 'src';
e111077 marked this conversation as resolved.
Show resolved Hide resolved
return {
[directory]: {
[`${elementName}.${language}`]:
language === 'js'
? generateTemplate(elementName, language)
: generateTemplate(elementName, language),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can get rid of the ternary now?

},
};
};

const generateTemplate = (elementName: string, lang: Language) => {
const className = kabobToPascalCase(elementName);
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
return javascript`import {LitElement, html, css} from 'lit';${
lang === 'js'
? ''
: `
import {property, customElement} from 'lit/decorators.js';`
}

/**
* An example element.
*
* @fires count-changed - Indicates when the count changes
* @slot - This element has a slot
* @csspart button - The button
* @cssprop --${elementName}-font-size - The button's font size
*/${
lang === 'js'
? ''
: `
@customElement('${elementName}')`
}
export class ${className} extends LitElement {
static styles = css\`
:host {
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
display: block;
border: solid 1px gray;
padding: 16px;
}

button {
font-size: var(--${elementName}-font-size, 16px);
}
\`;

${
lang === 'js'
? `static properties = {
/**
* The number of times the button has been clicked.
* @type {number}
*/
count: {type: Number},
}

constructor() {
super();
this.count = 0;
}`
: `/**
* The number of times the button has been clicked.
*/
@property({type: Number})
count = 0;`
}

render() {
return html\`
<h1>Hello World</h1>
<button @click=\${this._onClick} part="button">
Click Count: \${this.count}
</button>
<slot></slot>
\`;
}

${lang === 'js' ? '' : 'protected '}_onClick() {
this.count++;
const event = new Event('count-changed', {bubbles: true});
this.dispatchEvent(event);
}
}
${
lang === 'js'
? `
customElements.define('${elementName}', ${className});`
: ``
}`;
};
@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';

export const generateNpmignore = (): FileTree => {
return {
'.npmignore': `node_modules
.vscode
README.md
index.html
src`,
};
};