├── .eslintignore
├── .eslintrc.js
├── .github
├── pull_request_template.md
└── workflows
│ ├── release.yml
│ ├── verify-node.yml
│ └── verify-windows.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── README.md
├── babel.config.js
├── package.json
├── renovate.json
├── src
├── Generator.js
├── core.js
├── create.js
└── generators
│ ├── app-lit-element-ts
│ ├── index.js
│ └── templates
│ │ ├── custom-elements.json
│ │ ├── my-app.stories.ts
│ │ ├── my-app.test.ts
│ │ ├── my-app.ts
│ │ ├── open-wc-logo.svg
│ │ ├── package.json
│ │ ├── static-demoing
│ │ └── .storybook
│ │ │ └── main.js
│ │ ├── static-testing
│ │ └── web-test-runner.config.js
│ │ ├── static
│ │ ├── README.md
│ │ ├── index.html
│ │ └── web-dev-server.config.js
│ │ └── tsconfig.json
│ ├── app-lit-element
│ ├── index.js
│ └── templates
│ │ ├── custom-elements.json
│ │ ├── my-app.js
│ │ ├── my-app.stories.js
│ │ ├── my-app.test.js
│ │ ├── open-wc-logo.svg
│ │ ├── package.json
│ │ ├── static-demoing
│ │ └── .storybook
│ │ │ └── main.js
│ │ ├── static-testing
│ │ └── web-test-runner.config.js
│ │ └── static
│ │ ├── README.md
│ │ ├── index.html
│ │ └── web-dev-server.config.js
│ ├── app
│ ├── executeViaOptions.js
│ ├── gatherMixins.js
│ ├── header.js
│ └── index.js
│ ├── building-rollup-ts
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── rollup.config.js
│ ├── building-rollup
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── rollup.config.js
│ ├── common-repo
│ ├── index.js
│ └── templates
│ │ ├── gitignore
│ │ ├── package.json
│ │ └── static
│ │ ├── .editorconfig
│ │ ├── .vscode
│ │ └── extensions.json
│ │ └── LICENSE
│ ├── demoing-storybook-ts
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ ├── static-scaffold
│ │ └── stories
│ │ │ └── index.stories.ts
│ │ └── static
│ │ └── .storybook
│ │ └── main.js
│ ├── demoing-storybook
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ ├── static-scaffold
│ │ └── stories
│ │ │ └── index.stories.js
│ │ └── static
│ │ └── .storybook
│ │ └── main.js
│ ├── git-ignore-lock-files-in-diff
│ └── static
│ │ └── .gitattributes
│ ├── linting-commitlint
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── commitlint.config.js
│ ├── linting-eslint-ts
│ ├── index.js
│ └── templates
│ │ └── package.json
│ ├── linting-eslint
│ ├── index.js
│ └── templates
│ │ └── package.json
│ ├── linting-prettier-ts
│ ├── index.js
│ └── templates
│ │ └── package.json
│ ├── linting-prettier
│ ├── index.js
│ └── templates
│ │ └── package.json
│ ├── linting-ts
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── .husky
│ │ └── pre-commit
│ ├── linting-types-js
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── tsconfig.json
│ ├── linting
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── .husky
│ │ └── pre-commit
│ ├── testing-ts
│ ├── index.js
│ └── templates
│ │ ├── my-el.test.ts
│ │ └── package.json
│ ├── testing-wtr-ts
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── web-test-runner.config.js
│ ├── testing-wtr
│ ├── index.js
│ └── templates
│ │ ├── package.json
│ │ └── static
│ │ └── web-test-runner.config.js
│ ├── testing
│ ├── index.js
│ └── templates
│ │ ├── my-el.test.js
│ │ └── package.json
│ ├── wc-lit-element-ts
│ ├── index.js
│ └── templates
│ │ ├── MyEl.ts
│ │ ├── my-el.ts
│ │ ├── package.json
│ │ ├── partials
│ │ ├── README.demoing.md
│ │ ├── README.linting.md
│ │ └── README.testing.md
│ │ ├── static
│ │ ├── README.md
│ │ ├── demo
│ │ │ └── index.html
│ │ ├── src
│ │ │ └── index.ts
│ │ └── web-dev-server.config.js
│ │ └── tsconfig.json
│ └── wc-lit-element
│ ├── index.js
│ └── templates
│ ├── MyEl.js
│ ├── my-el.js
│ ├── package.json
│ ├── partials
│ ├── README.demoing.md
│ ├── README.linting.md
│ └── README.testing.md
│ └── static
│ ├── README.md
│ ├── demo
│ └── index.html
│ ├── index.js
│ └── web-dev-server.config.js
├── test
├── core.test.js
├── generate-command.js
├── integration.test.js
├── snapshots
│ ├── fully-loaded-app.output.txt
│ └── fully-loaded-app
│ │ ├── .editorconfig
│ │ ├── .gitignore
│ │ ├── .husky
│ │ └── pre-commit
│ │ ├── .storybook
│ │ └── main.js
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── assets
│ │ └── open-wc-logo.svg
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── rollup.config.js
│ │ ├── src
│ │ └── scaffold-app.js
│ │ ├── stories
│ │ └── scaffold-app.stories.js
│ │ ├── test
│ │ └── scaffold-app.test.js
│ │ ├── web-dev-server.config.js
│ │ └── web-test-runner.config.js
├── template
│ └── index.js
└── update-snapshots.js
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage/
3 | dist
4 | stats.html
5 | src/generators/*/templates/**/*
6 | test/**/snapshots
7 | CHANGELOG.md
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve('@open-wc/eslint-config'), require.resolve('eslint-config-prettier')],
3 | overrides: [
4 | {
5 | files: ['**/test/**/*.js', '**/*.config.js'],
6 | rules: {
7 | 'no-console': 'off',
8 | 'no-unused-expressions': 'off',
9 | 'class-methods-use-this': 'off',
10 | 'max-classes-per-file': 'off',
11 | 'import/no-extraneous-dependencies': 'off', // we moved all devDependencies to root
12 | },
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What I did
2 |
3 | 1.
4 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | release:
10 | # Prevents changesets action from creating a PR on forks
11 | if: github.repository == 'open-wc/create'
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v4
17 | with:
18 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
19 | fetch-depth: 0
20 |
21 | - name: Setup Node.js 22.x
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 22.x
25 | registry-url: 'https://registry.npmjs.org'
26 |
27 | - name: Get yarn cache directory path
28 | id: yarn-cache-dir-path
29 | run: echo "::set-output name=dir::$(yarn cache dir)"
30 |
31 | - uses: actions/cache@v4
32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
33 | with:
34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-yarn-
38 |
39 | - name: Install Dependencies
40 | run: yarn --frozen-lockfile
41 |
42 | - name: Build package
43 | run: yarn build
44 |
45 | - name: Setup Git User
46 | run: |
47 | git config --global user.email "hello@modern-web.dev"
48 | git config --global user.name "Modern Web"
49 |
50 | - name: Release & Publish
51 | run: npm run release
52 | env:
53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/verify-node.yml:
--------------------------------------------------------------------------------
1 | name: Verify Node
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - 'renovate/*'
8 |
9 | jobs:
10 | verify-linux:
11 | name: Verify linux
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [18.x, 20.x, 22.x]
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Setup Node ${{ matrix.node-version }}
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 | cache: 'yarn'
24 |
25 | - name: Install dependencies
26 | run: yarn --frozen-lockfile
27 |
28 | - name: Build packages
29 | run: yarn build
30 |
31 | - name: Lint
32 | run: yarn lint
33 |
34 | - name: Test
35 | run: yarn test
36 |
--------------------------------------------------------------------------------
/.github/workflows/verify-windows.yml:
--------------------------------------------------------------------------------
1 | name: Verify Windows
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - 'renovate/*'
8 |
9 | jobs:
10 | verify-windows:
11 | name: Verify windows
12 | runs-on: windows-latest
13 | steps:
14 | - name: Set git to use LF
15 | run: |
16 | git config --global core.autocrlf false
17 | git config --global core.eol lf
18 |
19 | - uses: actions/checkout@v4
20 |
21 | - name: Setup Node 22.x
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 22.x
25 | cache: 'yarn'
26 |
27 | - name: Install dependencies
28 | run: yarn --frozen-lockfile
29 |
30 | - name: Build
31 | run: yarn build
32 |
33 | - name: Test
34 | run: yarn test
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## editors
2 | /.idea
3 | /.vscode
4 |
5 | ## system files
6 | .DS_Store
7 |
8 | ## code coverage folders
9 | coverage/
10 |
11 | ## npm
12 | node_modules
13 | npm-debug.log
14 | yarn-error.log
15 |
16 | ## temp folders
17 | /.tmp/
18 |
19 | ## we prefer yarn.lock
20 | package-lock.json
21 |
22 | ## build output
23 | dist
24 | build-stats.json
25 | stats.html
26 | .rpt2_cache
27 |
28 | ## browserstack
29 | local.log
30 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.20.3
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: 'init/index.html'
3 | title: Create Open Web Components
4 | section: guides
5 | tags:
6 | - guides
7 | ---
8 |
9 | # Create Open Web Components
10 |
11 | Web component project scaffolding.
12 |
13 | [//]: # 'AUTO INSERT HEADER PREPUBLISH'
14 |
15 | ## Usage
16 |
17 | ```bash
18 | npm init @open-wc
19 | ```
20 |
21 |
WARNING
npm init
requires node 18 & npm 6 or higher
22 |
23 | This will kickstart a menu guiding you through all available actions.
24 |
25 | $ npm init @open-wc
26 | npx: installed 14 in 4.074s
27 | _.,,,,,,,,,._
28 | .d'' ``b. Open Web Components Recommendations
29 | .p' Open `q.
30 | .d' Web Components `b. Start or upgrade your web component project with
31 | .d' `b. ease. All our recommendations at your fingertips.
32 | :: ................. ::
33 | `p. .q' See more details at https://open-wc.org/docs/development/generator/
34 | `p. open-wc.org .q'
35 | `b. @openWc .d'
36 | `q.. ..,' Note: you can exit any time with Ctrl+C or Esc
37 | '',,,,,,,,,,''
38 |
39 |
40 | ? What would you like to do today? › - Use arrow-keys. Return to submit.
41 | ❯ Scaffold a new project
42 | Upgrade an existing project
43 |
44 | Our generators are very modular you can pick and choose as you see fit.
45 |
46 | ## Options
47 |
48 | You may pass options to skip the CLI wizard in part or in whole.
49 |
50 | | Option | Type | Description | |
51 | | ----------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --- |
52 | | `--destinationPath` | path | The path the generator will write files to | |
53 | | `--type` | `scaffold`\|`upgrade` | Choose scaffold to create a new project or upgrade to add features to an existing project | |
54 | | `--scaffoldType` | `wc`\|`app` | The type of project to scaffold. wc for a single published component, app for an application | |
55 | | `--features` | `linting`\|`testing`\|`demoing`\|`building` | Which features to include. linting, testing, demoing, or building | |
56 | | `--typescript` | `true`\|`false` | Whether to use TypeScript in your project | |
57 | | `--tagName` | string | The tag name for the web component or app shell element | |
58 | | `--installDependencies` | `yarn`\|`npm`\|`false` | Whether to install dependencies. Choose npm or yarn to install with those package managers, or false to skip installation | |
59 | | `--writeToDisk` | `true`\|`false` | Whether or not to actually write the files to disk | |
60 | | `--help` | | This help message | |
61 |
62 | ## Scaffold generators
63 |
64 | These generators help you kickstart a new app or web component.
65 | They will create a new folder and set up everything you need to get started immediately.
66 |
67 | Example usage:
68 |
69 | ```bash
70 | npm init @open-wc
71 | # Select "Scaffold a new project"
72 | ```
73 |
74 | ### Available scaffold generators:
75 |
76 | - `Web Component`
77 | This generator scaffolds a starting point for a web component. We recommend using this generator when you want to develop and publish a single web component.
78 |
79 |
80 | - `Application`
81 | This generator scaffolds a new starter application. We recommend using this generator at the start of your web component project.
82 |
83 |
84 | ## Features
85 |
86 | The above generators are the perfect playgrounds to prototype.
87 | Add linting, testing, demoing and building whenever the need arises.
88 |
89 | Example usage:
90 |
91 | ```bash
92 | cd existing-web-component
93 | npm init @open-wc
94 | # select "Upgrade an existing project" or add features while scaffolding
95 | ```
96 |
97 | ### Available Upgrade features
98 |
99 | - `Linting`
100 | This generator adds a complete linting setup with ESLint, Prettier, Husky and commitlint.
101 |
102 |
103 | - `Testing`
104 | This generator adds a complete testing setup with Web Test Runner.
105 |
106 |
107 | - `Demoing`
108 | This generator adds a complete demoing setup with Storybook.
109 |
110 |
111 | - `Building`
112 | This generator adds a complete building setup with Rollup.
113 |
114 |
115 | For information on how to extend and customize the generator, see the [docs page](https://open-wc.org/docs/development/generator/#extending)
116 |
117 | ## Commands
118 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['babel-plugin-transform-dynamic-import'],
3 | ignore: ['./src/generators/*/templates/**/*'],
4 | presets: [
5 | [
6 | '@babel/env',
7 | {
8 | targets: {
9 | node: '10',
10 | },
11 | corejs: 2,
12 | useBuiltIns: 'usage',
13 | },
14 | ],
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-wc/create",
3 | "version": "0.38.151",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "description": "Easily setup all the tools of Open Web Components.",
8 | "engines": {
9 | "node": ">=18.20.3"
10 | },
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/open-wc/create.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/open-wc/create/issues"
18 | },
19 | "author": "open-wc",
20 | "homepage": "https://github.com/open-wc/create",
21 | "bin": {
22 | "create-open-wc": "./dist/create.js"
23 | },
24 | "scripts": {
25 | "build": "rm -rf dist && babel src --out-dir dist --copy-files --include-dotfiles",
26 | "lint": "npm run lint:eslint && npm run lint:prettier",
27 | "lint:eslint": "eslint --ext .ts,.js,.mjs,.cjs .",
28 | "lint:prettier": "prettier \"**/*.{ts,js,mjs,cjs,md}\" --check --ignore-path .eslintignore",
29 | "format": "eslint --ext .ts,.js,.mjs,.cjs --fix && prettier \"**/*.{ts,js,mjs,cjs,md}\" --check --ignore-path .eslintignore --write",
30 | "release": "commit-and-tag-version && git push --follow-tags origin master && npm publish",
31 | "start": "npm run build && node ./dist/create.js",
32 | "test": "npm run test:node",
33 | "test:node": "mocha --require @babel/register",
34 | "test:update-snapshots": "node -r @babel/register ./test/update-snapshots.js",
35 | "test:watch": "onchange 'src/**/*.js' 'test/**/*.js' -- npm run test --silent"
36 | },
37 | "files": [
38 | "dist"
39 | ],
40 | "keywords": [
41 | "open-wc",
42 | "owc",
43 | "generator",
44 | "starter-app"
45 | ],
46 | "dependencies": {
47 | "chalk": "^4.1.2",
48 | "command-line-args": "^5.2.1",
49 | "command-line-usage": "^7.0.2",
50 | "dedent": "^1.5.3",
51 | "deepmerge": "^4.3.1",
52 | "ejs": "^3.1.10",
53 | "glob": "^8.1.0",
54 | "prompts": "^2.4.2",
55 | "semver": "^7.6.2"
56 | },
57 | "devDependencies": {
58 | "@babel/cli": "^7.24.7",
59 | "@babel/core": "^7.24.7",
60 | "@babel/register": "^7.24.6",
61 | "@custom-elements-manifest/analyzer": "^0.10.3",
62 | "@open-wc/eslint-config": "^12.0.3",
63 | "@open-wc/testing": "^4.0.0",
64 | "@rollup/plugin-babel": "^6.0.4",
65 | "@rollup/plugin-node-resolve": "^15.2.3",
66 | "@web/rollup-plugin-html": "^2.3.0",
67 | "@web/rollup-plugin-import-meta-assets": "^2.2.1",
68 | "babel-plugin-transform-dynamic-import": "^2.1.0",
69 | "chai": "^4.4.1",
70 | "chai-fs": "^2.0.0",
71 | "eslint": "^8.57.0",
72 | "eslint-config-prettier": "^9.1.0",
73 | "eslint-plugin-import": "^2.29.1",
74 | "eslint-plugin-lit": "^1.14.0",
75 | "eslint-plugin-lit-a11y": "^4.1.3",
76 | "eslint-plugin-wc": "^2.1.0",
77 | "lit": "^3.1.4",
78 | "lit-element": "^4.0.6",
79 | "mocha": "^10.6.0",
80 | "onchange": "^7.1.0",
81 | "prettier": "^3.3.2",
82 | "rollup-plugin-esbuild": "^6.1.1",
83 | "rollup-plugin-workbox": "^8.1.0",
84 | "commit-and-tag-version": "^12.4.1"
85 | },
86 | "prettier": {
87 | "singleQuote": true,
88 | "arrowParens": "avoid",
89 | "printWidth": 100,
90 | "trailingComma": "all"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":semanticPrefixFix",
5 | ":separateMultipleMajorReleases",
6 | ":separatePatchReleases",
7 | ":maintainLockFilesWeekly",
8 | ":widenPeerDependencies"
9 | ],
10 |
11 | "packageRules": [
12 | {
13 | "updateTypes": ["patch"],
14 |
15 | "automerge": true,
16 | "automergeType": "branch"
17 | },
18 | {
19 | "updateTypes": ["minor"],
20 | "matchCurrentVersion": "!/^[~^]?0/",
21 |
22 | "automerge": true,
23 | "automergeType": "branch"
24 | }
25 | ],
26 |
27 | "rangeStrategy": "bump"
28 | }
29 |
--------------------------------------------------------------------------------
/src/Generator.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, import/no-cycle */
2 | import prompts from 'prompts';
3 | import path from 'path';
4 | import { spawn } from 'child_process';
5 |
6 | import {
7 | copyTemplates,
8 | copyTemplate,
9 | copyTemplateJsonInto,
10 | installNpm,
11 | writeFilesToDisk,
12 | optionsToCommand,
13 | } from './core.js';
14 |
15 | /**
16 | * Options for the generator
17 | * @typedef {object} GeneratorOptions
18 | * @property {string} [tagName] the dash-case tag name
19 | * @property {string} [destinationPath='auto'] path to output to. default value 'auto' will output to current working directory
20 | * @property {'scaffold'} [type='scaffold'] path to output to. default value 'auto' will output to current working directory
21 | * @property {'true'|'false'} [writeToDisk] whether to write to disk
22 | * @property {'yarn'|'npm'|'false'} [installDependencies] whether and with which tool to install dependencies
23 | */
24 |
25 | /**
26 | * dash-case to PascalCase
27 | * @param {string} tagName dash-case tag name
28 | * @return {string} PascalCase class name
29 | */
30 | function getClassName(tagName) {
31 | return tagName
32 | .split('-')
33 | .reduce((previous, part) => previous + part.charAt(0).toUpperCase() + part.slice(1), '');
34 | }
35 |
36 | class Generator {
37 | constructor() {
38 | /**
39 | * @type {GeneratorOptions}
40 | */
41 | this.options = {
42 | destinationPath: 'auto',
43 | };
44 | this.templateData = {};
45 | this.wantsNpmInstall = true;
46 | this.wantsWriteToDisk = true;
47 | this.wantsRecreateInfo = true;
48 | this.generatorName = '@open-wc';
49 | }
50 |
51 | execute() {
52 | if (this.options.tagName) {
53 | const { tagName } = this.options;
54 | const className = getClassName(tagName);
55 | this.templateData = { ...this.templateData, tagName, className };
56 |
57 | if (this.options.destinationPath === 'auto') {
58 | this.options.destinationPath = process.cwd();
59 | if (this.options.type === 'scaffold') {
60 | this.options.destinationPath = path.join(process.cwd(), tagName);
61 | }
62 | }
63 | }
64 | }
65 |
66 | destinationPath(destination = '') {
67 | return path.join(this.options.destinationPath, destination);
68 | }
69 |
70 | copyTemplate(from, to, ejsOptions = {}) {
71 | copyTemplate(from, to, this.templateData, ejsOptions);
72 | }
73 |
74 | copyTemplateJsonInto(from, to, options = { mode: 'merge' }, ejsOptions = {}) {
75 | copyTemplateJsonInto(from, to, this.templateData, options, ejsOptions);
76 | }
77 |
78 | async copyTemplates(from, to = this.destinationPath(), ejsOptions = {}) {
79 | return copyTemplates(from, to, this.templateData, ejsOptions);
80 | }
81 |
82 | async end() {
83 | if (this.wantsWriteToDisk) {
84 | this.options.writeToDisk = await writeFilesToDisk();
85 | }
86 |
87 | if (this.wantsNpmInstall) {
88 | const answers = await prompts(
89 | [
90 | {
91 | type: 'select',
92 | name: 'installDependencies',
93 | message: 'Do you want to install dependencies?',
94 | choices: [
95 | { title: 'No', value: 'false' },
96 | { title: 'Yes, with yarn', value: 'yarn' },
97 | { title: 'Yes, with npm', value: 'npm' },
98 | ],
99 | },
100 | ],
101 | {
102 | onCancel: () => {
103 | process.exit();
104 | },
105 | },
106 | );
107 | this.options.installDependencies = answers.installDependencies;
108 | const { installDependencies } = this.options;
109 | if (installDependencies === 'yarn' || installDependencies === 'npm') {
110 | await installNpm(this.options.destinationPath, installDependencies);
111 | await new Promise(resolve => {
112 | const install = spawn(installDependencies, ['run', 'analyze'], {
113 | cwd: this.options.destinationPath,
114 | shell: true,
115 | });
116 | install.stdout.on('data', data => {
117 | console.log(`${data}`.trim());
118 | });
119 |
120 | install.stderr.on('data', data => {
121 | console.log(`analyze: ${data}`);
122 | });
123 |
124 | install.on('close', () => {
125 | resolve();
126 | });
127 | });
128 | }
129 | }
130 |
131 | if (this.wantsRecreateInfo) {
132 | console.log('');
133 | console.log('If you want to rerun this exact same generator you can do so by executing:');
134 | console.log(optionsToCommand(this.options, this.generatorName));
135 | }
136 | }
137 | }
138 |
139 | export default Generator;
140 |
--------------------------------------------------------------------------------
/src/core.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, import/no-cycle */
2 | import { render } from 'ejs';
3 | import { spawn } from 'child_process';
4 | import deepmerge from 'deepmerge';
5 | import fs from 'fs';
6 | import glob from 'glob';
7 | import path from 'path';
8 | import prompts from 'prompts';
9 | import Generator from './Generator.js';
10 |
11 | // order taken from prettier-package-json
12 | const pkgJsonOrder = [
13 | '$schema',
14 | 'private',
15 | 'name',
16 | 'description',
17 | 'license',
18 | 'author',
19 | 'maintainers',
20 | 'contributors',
21 | 'homepage',
22 | 'repository',
23 | 'bugs',
24 | 'version',
25 | 'type',
26 | 'workspaces',
27 | 'main',
28 | 'module',
29 | 'browser',
30 | 'exports',
31 | 'man',
32 | 'preferGlobal',
33 | 'bin',
34 | 'files',
35 | 'directories',
36 | 'scripts',
37 | 'config',
38 | 'sideEffects',
39 | 'types',
40 | 'typings',
41 | 'optionalDependencies',
42 | 'dependencies',
43 | 'bundleDependencies',
44 | 'bundledDependencies',
45 | 'peerDependencies',
46 | 'devDependencies',
47 | 'keywords',
48 | 'engines',
49 | 'engine-strict',
50 | 'engineStrict',
51 | 'os',
52 | 'cpu',
53 | 'publishConfig',
54 | ];
55 |
56 | const sortedValues = ['dependencies', 'devDependencies'];
57 |
58 | /**
59 | *
60 | * @param {Function[]} mixins
61 | * @param {typeof Generator} Base
62 | */
63 | export async function executeMixinGenerator(mixins, options = {}, Base = Generator) {
64 | class Start extends Base {}
65 | mixins.forEach(mixin => {
66 | // @ts-ignore
67 | // eslint-disable-next-line no-class-assign
68 | Start = mixin(Start);
69 | });
70 |
71 | // class Do extends mixins(Base) {}
72 | const inst = new Start();
73 | inst.options = { ...inst.options, ...options };
74 |
75 | await inst.execute();
76 | if (!options.noEnd) {
77 | await inst.end();
78 | }
79 | }
80 |
81 | export const virtualFiles = [];
82 |
83 | export function resetVirtualFiles() {
84 | virtualFiles.length = 0;
85 | }
86 |
87 | /**
88 | * Minimal template system.
89 | * Replaces <%= name %> if provides as template
90 | *
91 | * @example
92 | * processTemplate('prefix <%= name %> suffix', { name: 'foo' })
93 | * // prefix foo suffix
94 | *
95 | * It's also possible to pass custom options to EJS render like changing the delimiter of tags.
96 | *
97 | * @param {string} _fileContent Template as a string
98 | * @param {object} data Object of all the variables to repalce
99 | * @param {ejs.Options} ejsOptions
100 | * @returns {string} Template with all replacements
101 | */
102 | export function processTemplate(_fileContent, data = {}, ejsOptions = {}) {
103 | let fileContent = _fileContent;
104 | fileContent = render(fileContent, data, { debug: false, filename: 'template', ...ejsOptions });
105 | return fileContent;
106 | }
107 |
108 | /**
109 | * Minimal virtual file system
110 | * Stores files to write in an array
111 | *
112 | * @param {string} filePath
113 | * @param {string} content
114 | */
115 | export function writeFileToPath(filePath, content) {
116 | let addNewFile = true;
117 | virtualFiles.forEach((fileMeta, index) => {
118 | if (fileMeta.path === filePath) {
119 | virtualFiles[index].content = content;
120 | addNewFile = false;
121 | }
122 | });
123 | if (addNewFile === true) {
124 | virtualFiles.push({ path: filePath, content });
125 | }
126 | }
127 |
128 | /**
129 | *
130 | * @param {string} filePath
131 | */
132 | export function readFileFromPath(filePath) {
133 | let content = false;
134 | virtualFiles.forEach((fileMeta, index) => {
135 | if (fileMeta.path === filePath) {
136 | // eslint-disable-next-line prefer-destructuring
137 | content = virtualFiles[index].content;
138 | }
139 | });
140 | if (content) {
141 | return content;
142 | }
143 | if (fs.existsSync(filePath)) {
144 | return fs.readFileSync(filePath, 'utf-8');
145 | }
146 | return false;
147 | }
148 |
149 | /**
150 | *
151 | * @param {string} filePath
152 | */
153 | export function deleteVirtualFile(filePath) {
154 | const index = virtualFiles.findIndex(fileMeta => fileMeta.path === filePath);
155 | if (index !== -1) {
156 | virtualFiles.splice(index, 1);
157 | }
158 | }
159 |
160 | let overwriteAllFiles = false;
161 |
162 | /**
163 | *
164 | * @param {boolean} value
165 | */
166 | export function setOverrideAllFiles(value) {
167 | overwriteAllFiles = value;
168 | }
169 |
170 | /**
171 | *
172 | * @param {string} toPath
173 | * @param {string} fileContent
174 | * @param {object} obj Options
175 | */
176 | export async function writeFileToPathOnDisk(
177 | toPath,
178 | fileContent,
179 | { override = false, ask = true } = {},
180 | ) {
181 | const toPathDir = path.dirname(toPath);
182 | if (!fs.existsSync(toPathDir)) {
183 | fs.mkdirSync(toPathDir, { recursive: true });
184 | }
185 | if (fs.existsSync(toPath)) {
186 | if (override || overwriteAllFiles) {
187 | fs.writeFileSync(toPath, fileContent);
188 | } else if (ask) {
189 | let wantOverride = overwriteAllFiles;
190 | if (!wantOverride) {
191 | const answers = await prompts(
192 | [
193 | {
194 | type: 'select',
195 | name: 'overwriteFile',
196 | message: `Do you want to overwrite ${toPath}?`,
197 | choices: [
198 | { title: 'Yes', value: 'true' },
199 | {
200 | title: 'Yes for all files',
201 | value: 'always',
202 | },
203 | { title: 'No', value: 'false' },
204 | ],
205 | },
206 | ],
207 | {
208 | onCancel: () => {
209 | process.exit();
210 | },
211 | },
212 | );
213 | if (answers.overwriteFile === 'always') {
214 | setOverrideAllFiles(true);
215 | wantOverride = true;
216 | }
217 | if (answers.overwriteFile === 'true') {
218 | wantOverride = true;
219 | }
220 | }
221 | if (wantOverride) {
222 | fs.writeFileSync(toPath, fileContent);
223 | }
224 | }
225 | } else {
226 | fs.writeFileSync(toPath, fileContent);
227 | }
228 | }
229 |
230 | /**
231 | * @param {String[]} allFiles pathes to files
232 | * @param {Number} [level] internal to track nesting level
233 | */
234 | export function filesToTree(allFiles, level = 0) {
235 | const files = allFiles.filter(file => !file.includes('/'));
236 | const dirFiles = allFiles.filter(file => file.includes('/'));
237 |
238 | let indent = '';
239 | for (let i = 1; i < level; i += 1) {
240 | indent += '│ ';
241 | }
242 |
243 | let output = '';
244 | const processed = [];
245 |
246 | if (dirFiles.length > 0) {
247 | dirFiles.forEach(dirFile => {
248 | if (!processed.includes(dirFile)) {
249 | const dir = `${dirFile.split('/').shift()}/`;
250 | const subFiles = [];
251 | allFiles.forEach(file => {
252 | if (file.startsWith(dir)) {
253 | subFiles.push(file.substr(dir.length));
254 | processed.push(file);
255 | }
256 | });
257 | output += level === 0 ? `${dir}\n` : `${indent}├── ${dir}\n`;
258 | output += filesToTree(subFiles, level + 1);
259 | }
260 | });
261 | }
262 |
263 | if (files.length === 1) {
264 | output += `${indent}└── ${files[0]}\n`;
265 | }
266 | if (files.length > 1) {
267 | const last = files.pop();
268 | output += `${indent}├── `;
269 | output += files.join(`\n${indent}├── `);
270 | output += `\n${indent}└── ${last}\n`;
271 | }
272 | return output;
273 | }
274 |
275 | /**
276 | *
277 | */
278 | export async function writeFilesToDisk() {
279 | const treeFiles = [];
280 | const root = process.cwd().replace(/\\/g, '/');
281 |
282 | virtualFiles.forEach((vFile, i) => {
283 | virtualFiles[i].path = vFile.path.replace(/\\/g, '/');
284 | });
285 |
286 | virtualFiles.sort((a, b) => {
287 | const pathA = a.path.toLowerCase();
288 | const pathB = b.path.toLowerCase();
289 | if (pathA < pathB) return -1;
290 | if (pathA > pathB) return 1;
291 | return 0;
292 | });
293 |
294 | virtualFiles.forEach(vFile => {
295 | if (vFile.path.startsWith(root)) {
296 | let vFilePath = './';
297 | vFilePath += vFile.path.substr(root.length + 1);
298 | treeFiles.push(vFilePath);
299 | }
300 | });
301 |
302 | console.log('');
303 | console.log(filesToTree(treeFiles));
304 |
305 | const answers = await prompts(
306 | [
307 | {
308 | type: 'select',
309 | name: 'writeToDisk',
310 | message: 'Do you want to write this file structure to disk?',
311 | choices: [
312 | { title: 'Yes', value: 'true' },
313 | { title: 'No', value: 'false' },
314 | ],
315 | },
316 | ],
317 | {
318 | onCancel: () => {
319 | process.exit();
320 | },
321 | },
322 | );
323 |
324 | if (answers.writeToDisk === 'true') {
325 | // eslint-disable-next-line no-restricted-syntax
326 | for (const fileMeta of virtualFiles) {
327 | // eslint-disable-next-line no-await-in-loop
328 | await writeFileToPathOnDisk(fileMeta.path, fileMeta.content);
329 | }
330 | console.log('Writing..... done');
331 | }
332 |
333 | return answers.writeToDisk;
334 | }
335 |
336 | export function optionsToCommand(options, generatorName = '@open-wc') {
337 | let command = `npm init ${generatorName} `;
338 | Object.keys(options).forEach(key => {
339 | if (key !== '_scaffoldFilesFor') {
340 | const value = options[key];
341 | if (typeof value === 'string' || typeof value === 'number') {
342 | command += `--${key} ${value} `;
343 | } else if (typeof value === 'boolean' && value === true) {
344 | command += `--${key} `;
345 | } else if (Array.isArray(value)) {
346 | command += `--${key} ${value.join(' ')} `;
347 | }
348 | }
349 | });
350 | return command;
351 | }
352 |
353 | /**
354 | *
355 | * @param {string} fromPath
356 | * @param {string} toPath
357 | * @param {object} data
358 | * @param {ejs.Options} ejsOptions
359 | */
360 | export function copyTemplate(fromPath, toPath, data, ejsOptions = {}) {
361 | const fileContent = readFileFromPath(fromPath);
362 | if (fileContent) {
363 | const processed = processTemplate(fileContent, data, ejsOptions);
364 | writeFileToPath(toPath, processed);
365 | }
366 | }
367 |
368 | /**
369 | *
370 | * @param {string} fromGlob
371 | * @param {string} [toDir] Directory to copy into
372 | * @param {object} data Replace parameters in files
373 | * @param {ejs.Options} ejsOptions
374 | */
375 | export function copyTemplates(fromGlob, toDir = process.cwd(), data = {}, ejsOptions = {}) {
376 | return new Promise(resolve => {
377 | glob(fromGlob, { dot: true, windowsPathsNoEscape: true }, (er, files) => {
378 | const copiedFiles = [];
379 | files.forEach(filePath => {
380 | if (!fs.lstatSync(filePath).isDirectory()) {
381 | const fileContent = readFileFromPath(filePath);
382 | if (fileContent !== false) {
383 | const processed = processTemplate(fileContent, data, ejsOptions);
384 |
385 | // find path write to (force / also on windows)
386 | const replace = path.join(fromGlob.replace(/\*/g, '')).replace(/\\(?! )/g, '/');
387 | const toPath = filePath.replace(replace, `${toDir}/`);
388 |
389 | copiedFiles.push({ toPath, processed });
390 | writeFileToPath(toPath, processed);
391 | }
392 | }
393 | });
394 | resolve(copiedFiles);
395 | });
396 | });
397 | }
398 |
399 | /**
400 | *
401 | * @param {string} fromPath
402 | * @param {string} toPath
403 | * @param {object} data
404 | * @param {ejs.Options} ejsOptions
405 | */
406 | export function copyTemplateJsonInto(
407 | fromPath,
408 | toPath,
409 | data = {},
410 | { mode = 'merge' } = { mode: 'merge' },
411 | ejsOptions = {},
412 | ) {
413 | const content = readFileFromPath(fromPath);
414 | if (content === false) {
415 | return;
416 | }
417 | const processed = processTemplate(content, data, ejsOptions);
418 | const mergeMeObj = JSON.parse(processed);
419 |
420 | const overwriteMerge = (destinationArray, sourceArray) => sourceArray;
421 |
422 | const emptyTarget = value => (Array.isArray(value) ? [] : {});
423 | const clone = (value, options) => deepmerge(emptyTarget(value), value, options);
424 |
425 | const combineMerge = (target, source, options) => {
426 | const destination = target.slice();
427 |
428 | source.forEach((item, index) => {
429 | if (typeof destination[index] === 'undefined') {
430 | const cloneRequested = options.clone !== false;
431 | const shouldClone = cloneRequested && options.isMergeableObject(item);
432 | destination[index] = shouldClone ? clone(item, options) : item;
433 | } else if (options.isMergeableObject(item)) {
434 | destination[index] = deepmerge(target[index], item, options);
435 | } else if (target.indexOf(item) === -1) {
436 | destination.push(item);
437 | }
438 | });
439 | return destination;
440 | };
441 |
442 | const mergeOptions = { arrayMerge: combineMerge };
443 | if (mode === 'override') {
444 | mergeOptions.arrayMerge = overwriteMerge;
445 | }
446 |
447 | let finalObj = mergeMeObj;
448 | const sourceContent = readFileFromPath(toPath);
449 | if (sourceContent) {
450 | finalObj = deepmerge(JSON.parse(sourceContent), finalObj, mergeOptions);
451 | }
452 |
453 | // sort package.json keys
454 | if (toPath.endsWith('package.json')) {
455 | const temp = {};
456 | const indexOf = k => {
457 | const i = pkgJsonOrder.indexOf(k);
458 | return i === -1 ? Number.MAX_SAFE_INTEGER : i;
459 | };
460 | const entries = Object.entries(finalObj).sort(([a], [b]) => indexOf(a) - indexOf(b));
461 | for (const [k, v] of entries) {
462 | let finalV = v;
463 | if (sortedValues.includes(k)) {
464 | const newV = {};
465 | const vEntries = Object.entries(v).sort();
466 | for (const [k2, v2] of vEntries) {
467 | newV[k2] = v2;
468 | }
469 | finalV = newV;
470 | }
471 | temp[k] = finalV;
472 | }
473 | finalObj = temp;
474 | }
475 |
476 | writeFileToPath(toPath, JSON.stringify(finalObj, null, 2));
477 | }
478 |
479 | /**
480 | * @param {string} command
481 | * @param {object} options
482 | */
483 | // eslint-disable-next-line default-param-last
484 | function _install(command = 'npm', options) {
485 | return new Promise(resolve => {
486 | const install = spawn(command, ['install'], options);
487 | install.stdout.on('data', data => {
488 | console.log(`${data}`.trim());
489 | });
490 |
491 | install.stderr.on('data', data => {
492 | console.log(`${command}: ${data}`);
493 | });
494 |
495 | install.on('close', () => {
496 | resolve();
497 | });
498 | });
499 | }
500 |
501 | /**
502 | *
503 | * @param {string} where
504 | * @param {string} command
505 | */
506 | export async function installNpm(where, command) {
507 | console.log('');
508 | console.log('Installing dependencies...');
509 | console.log('This might take some time...');
510 | console.log(`Using ${command} to install...`);
511 | await _install(command, { cwd: where, shell: true });
512 | console.log('');
513 | }
514 |
--------------------------------------------------------------------------------
/src/create.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* eslint-disable no-console */
4 |
5 | import semver from 'semver';
6 | import chalk from 'chalk';
7 | import { executeMixinGenerator } from './core.js';
8 | import { AppMixin } from './generators/app/index.js';
9 |
10 | (async () => {
11 | try {
12 | if (!semver.gte(process.version, '14.0.0')) {
13 | console.log(chalk.bgRed('\nUh oh! Looks like you dont have Node v14 installed!\n'));
14 | console.log(`You can do this by going to ${chalk.underline.blue(`https://nodejs.org/`)}
15 |
16 | Or if you use nvm:
17 | $ nvm install node ${chalk.gray(`# "node" is an alias for the latest version`)}
18 | $ nvm use node
19 | `);
20 | } else {
21 | await executeMixinGenerator([AppMixin]);
22 | }
23 | } catch (err) {
24 | console.log(err);
25 | }
26 | })();
27 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/index.js:
--------------------------------------------------------------------------------
1 | import { CommonRepoMixin } from '../common-repo/index.js';
2 |
3 | /* eslint-disable no-console */
4 | export const TsAppLitElementMixin = subclass =>
5 | class extends CommonRepoMixin(subclass) {
6 | async execute() {
7 | await super.execute();
8 |
9 | const { tagName, className } = this.templateData;
10 |
11 | // write & rename el class template
12 | this.copyTemplate(
13 | `${__dirname}/templates/my-app.ts`,
14 | this.destinationPath(`src//${tagName}.ts`),
15 | );
16 |
17 | this.copyTemplate(
18 | `${__dirname}/templates/MyApp.ts`,
19 | this.destinationPath(`src/${className}.ts`),
20 | );
21 |
22 | this.copyTemplate(
23 | `${__dirname}/templates/open-wc-logo.svg`,
24 | this.destinationPath(`assets/open-wc-logo.svg`),
25 | );
26 |
27 | this.copyTemplateJsonInto(
28 | `${__dirname}/templates/package.json`,
29 | this.destinationPath('package.json'),
30 | );
31 |
32 | this.copyTemplate(
33 | `${__dirname}/templates/tsconfig.json`,
34 | this.destinationPath('tsconfig.json'),
35 | );
36 |
37 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
38 |
39 | this.copyTemplate(
40 | `${__dirname}/templates/custom-elements.json`,
41 | this.destinationPath('custom-elements.json'),
42 | );
43 |
44 | if (this.options.features && this.options.features.includes('testing')) {
45 | await this.copyTemplates(`${__dirname}/templates/static-testing/**/*`);
46 | }
47 |
48 | if (this.options.features && this.options.features.includes('demoing')) {
49 | await this.copyTemplates(`${__dirname}/templates/static-demoing/**/*`);
50 | }
51 |
52 | if (this.options._scaffoldFilesFor && this.options._scaffoldFilesFor.includes('demoing')) {
53 | this.copyTemplate(
54 | `${__dirname}/templates/my-app.stories.ts`,
55 | this.destinationPath(`./stories/${tagName}.stories.ts`),
56 | );
57 |
58 | await this.copyTemplates(`${__dirname}/templates/static-scaffold-demoing/**/*`);
59 | }
60 |
61 | if (this.options._scaffoldFilesFor && this.options._scaffoldFilesFor.includes('testing')) {
62 | this.copyTemplate(
63 | `${__dirname}/templates/my-app.test.ts`,
64 | this.destinationPath(`./test/${tagName}.test.ts`),
65 | );
66 |
67 | await this.copyTemplates(`${__dirname}/templates/static-scaffold-testing/**/*`);
68 | }
69 | }
70 |
71 | async end() {
72 | await super.end();
73 | console.log('');
74 | console.log('You are all set up now!');
75 | console.log('');
76 | console.log('All you need to do is run:');
77 | console.log(` cd ${this.templateData.tagName}`);
78 | console.log(' npm run start');
79 | console.log('');
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/custom-elements.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0",
3 | "readme": "",
4 | "modules": [
5 | {
6 | "kind": "javascript-module",
7 | "path": "src/scaffold-app.js",
8 | "declarations": [
9 | {
10 | "kind": "class",
11 | "description": "",
12 | "name": "ScaffoldApp",
13 | "members": [
14 | {
15 | "kind": "field",
16 | "name": "header",
17 | "type": {
18 | "text": "string"
19 | },
20 | "default": "'My app'",
21 | "privacy": "public",
22 | "attribute": "header"
23 | }
24 | ],
25 | "attributes": [
26 | {
27 | "name": "header",
28 | "fieldName": "header"
29 | }
30 | ],
31 | "superclass": {
32 | "name": "LitElement",
33 | "package": "lit"
34 | },
35 | "tagName": "scaffold-app",
36 | "customElement": true
37 | }
38 | ],
39 | "exports": [
40 | {
41 | "kind": "custom-element-definition",
42 | "name": "scaffold-app",
43 | "declaration": {
44 | "name": "ScaffoldApp",
45 | "module": "src/scaffold-app.js"
46 | }
47 | }
48 | ]
49 | },
50 | {
51 | "kind": "javascript-module",
52 | "path": "stories/scaffold-app.stories.js",
53 | "declarations": [
54 | {
55 | "kind": "variable",
56 | "name": "App"
57 | }
58 | ],
59 | "exports": [
60 | {
61 | "kind": "js",
62 | "name": "default",
63 | "declaration": {
64 | "module": "stories/scaffold-app.stories.js"
65 | }
66 | },
67 | {
68 | "kind": "js",
69 | "name": "App",
70 | "declaration": {
71 | "name": "App",
72 | "module": "stories/scaffold-app.stories.js"
73 | }
74 | }
75 | ]
76 | }
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/my-app.stories.ts:
--------------------------------------------------------------------------------
1 | import { html, TemplateResult } from 'lit';
2 | import '../src/<%= tagName %>.js';
3 |
4 | export default {
5 | title: '<%= className %>',
6 | component: '<%= tagName %>',
7 | argTypes: {
8 | backgroundColor: { control: 'color' },
9 | },
10 | };
11 |
12 | interface Story {
13 | (args: T): TemplateResult;
14 | args?: Partial;
15 | argTypes?: Record;
16 | }
17 |
18 | interface ArgTypes {
19 | header?: string;
20 | backgroundColor?: string;
21 | }
22 |
23 | const Template: Story = ({ header, backgroundColor = 'white' }: ArgTypes) => html`
24 | <<%= tagName %> style="--<%= tagName %>-background-color: ${backgroundColor}" .header=${header}><%= tagName %>>
25 | `;
26 |
27 | export const App = Template.bind({});
28 | App.args = {
29 | header: 'My app',
30 | };
31 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/my-app.test.ts:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import { fixture, expect } from '@open-wc/testing';
3 |
4 | import type { <%= className %> } from '../src/<%= tagName %>.js';
5 | import '../src/<%= tagName %>.js';
6 |
7 | describe('<%= className %>', () => {
8 | let element: <%= className %>;
9 | beforeEach(async () => {
10 | element = await fixture(html`<<%= tagName %>><%= tagName %>>`);
11 | });
12 |
13 | it('renders a h1', () => {
14 | const h1 = element.shadowRoot!.querySelector('h1')!;
15 | expect(h1).to.exist;
16 | expect(h1.textContent).to.equal('My app');
17 | });
18 |
19 | it('passes the a11y audit', async () => {
20 | await expect(element).shadowDom.to.be.accessible();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/my-app.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit';
2 | import { property, customElement } from 'lit/decorators.js';
3 |
4 | const logo = new URL('../../assets/open-wc-logo.svg', import.meta.url).href;
5 |
6 | @customElement('<%= tagName %>')
7 | export class <%= className %> extends LitElement {
8 | @property({ type: String }) header = 'My app';
9 |
10 | static styles = css`
11 | :host {
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: flex-start;
17 | font-size: calc(10px + 2vmin);
18 | color: #1a2b42;
19 | max-width: 960px;
20 | margin: 0 auto;
21 | text-align: center;
22 | background-color: var(--<%= tagName %>-background-color);
23 | }
24 |
25 | main {
26 | flex-grow: 1;
27 | }
28 |
29 | .logo {
30 | margin-top: 36px;
31 | animation: app-logo-spin infinite 20s linear;
32 | }
33 |
34 | @keyframes app-logo-spin {
35 | from {
36 | transform: rotate(0deg);
37 | }
38 | to {
39 | transform: rotate(360deg);
40 | }
41 | }
42 |
43 | .app-footer {
44 | font-size: calc(12px + 0.5vmin);
45 | align-items: center;
46 | }
47 |
48 | .app-footer a {
49 | margin-left: 5px;
50 | }
51 | `;
52 |
53 | render() {
54 | return html`
55 |
56 |
57 | ${this.header}
58 |
59 | Edit src/<%= className %>.ts
and save to reload.
60 |
66 | Code examples
67 |
68 |
69 |
70 |
79 | `;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/open-wc-logo.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= tagName %>",
3 | "license": "MIT",
4 | "type": "module",
5 | "scripts": {
6 | "start": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"web-dev-server\""
7 | },
8 | "dependencies": {
9 | "lit": "^3.1.4"
10 | },
11 | "devDependencies": {
12 | "@web/dev-server": "^0.4.6",
13 | "concurrently": "^8.2.2",
14 | "typescript": "^5.5.3",
15 | "tslib": "^2.6.3"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/static-demoing/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | stories: ['../out-tsc/stories/**/*.stories.{js,md,mdx}'],
3 | framework: {
4 | name: '@web/storybook-framework-web-components',
5 | },
6 | };
7 |
8 | export default config;
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/static-testing/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // import { playwrightLauncher } from '@web/test-runner-playwright';
2 |
3 | const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
4 |
5 | export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6 | /** Test files to run */
7 | files: 'out-tsc/test/**/*.test.js',
8 |
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Filter out lit dev mode logs */
15 | filterBrowserLogs(log) {
16 | for (const arg of log.args) {
17 | if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | },
23 |
24 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25 | // esbuildTarget: 'auto',
26 |
27 | /** Amount of browsers to run concurrently */
28 | // concurrentBrowsers: 2,
29 |
30 | /** Amount of test files per browser to test concurrently */
31 | // concurrency: 1,
32 |
33 | /** Browsers to run tests on */
34 | // browsers: [
35 | // playwrightLauncher({ product: 'chromium' }),
36 | // playwrightLauncher({ product: 'firefox' }),
37 | // playwrightLauncher({ product: 'webkit' }),
38 | // ],
39 |
40 | // See documentation for all available options
41 | });
42 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/static/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Open-wc Starter App
6 |
7 | [](https://github.com/open-wc)
8 |
9 | ## Quickstart
10 |
11 | To get started:
12 |
13 | ```sh
14 | npm init @open-wc
15 | # requires node 10 & npm 6 or higher
16 | ```
17 |
18 | ## Scripts
19 |
20 | - `start` runs your app for development, reloading on file changes
21 | - `start:build` runs your app after it has been built using the build command
22 | - `build` builds your app and outputs it in your `dist` directory
23 | - `test` runs your test suite with Web Test Runner
24 | - `lint` runs the linter for your project
25 |
26 | ## Tooling configs
27 |
28 | For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
29 |
30 | If you customize the configuration a lot, you can consider moving them to individual files.
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 | <%= tagName %>
20 |
21 |
22 |
23 | <<%= tagName %>><%= tagName %>>
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/static/web-dev-server.config.js:
--------------------------------------------------------------------------------
1 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2 |
3 | /** Use Hot Module replacement by adding --hmr to the start command */
4 | const hmr = process.argv.includes('--hmr');
5 |
6 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7 | open: '/',
8 | watch: !hmr,
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
15 | // esbuildTarget: 'auto'
16 |
17 | /** Set appIndex to enable SPA routing */
18 | appIndex: './index.html',
19 |
20 | plugins: [
21 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
22 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
23 | ],
24 |
25 | // See documentation for all available options
26 | });
27 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element-ts/templates/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "noEmitOnError": true,
7 | "lib": ["es2021", "dom", "DOM.Iterable"],
8 | "strict": true,
9 | "esModuleInterop": false,
10 | "allowSyntheticDefaultImports": true,
11 | "experimentalDecorators": true,
12 | "importHelpers": true,
13 | "outDir": "out-tsc",
14 | "sourceMap": true,
15 | "inlineSources": true,
16 | "rootDir": "./",
17 | "incremental": true,
18 | "skipLibCheck": true
19 | },
20 | "include": ["**/*.ts"]
21 | }
22 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/index.js:
--------------------------------------------------------------------------------
1 | import { CommonRepoMixin } from '../common-repo/index.js';
2 |
3 | /* eslint-disable no-console */
4 | export const AppLitElementMixin = subclass =>
5 | class extends CommonRepoMixin(subclass) {
6 | async execute() {
7 | await super.execute();
8 |
9 | const { tagName, className } = this.templateData;
10 |
11 | // write & rename el class template
12 | this.copyTemplate(
13 | `${__dirname}/templates/my-app.js`,
14 | this.destinationPath(`src//${tagName}.js`),
15 | );
16 |
17 | this.copyTemplate(
18 | `${__dirname}/templates/MyApp.js`,
19 | this.destinationPath(`src/${className}.js`),
20 | );
21 |
22 | this.copyTemplate(
23 | `${__dirname}/templates/open-wc-logo.svg`,
24 | this.destinationPath(`assets/open-wc-logo.svg`),
25 | );
26 |
27 | this.copyTemplateJsonInto(
28 | `${__dirname}/templates/package.json`,
29 | this.destinationPath('package.json'),
30 | );
31 |
32 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
33 |
34 | this.copyTemplate(
35 | `${__dirname}/templates/custom-elements.json`,
36 | this.destinationPath('custom-elements.json'),
37 | );
38 |
39 | if (this.options.features && this.options.features.includes('testing')) {
40 | await this.copyTemplates(`${__dirname}/templates/static-testing/**/*`);
41 | }
42 |
43 | if (this.options.features && this.options.features.includes('demoing')) {
44 | await this.copyTemplates(`${__dirname}/templates/static-demoing/**/*`);
45 | }
46 |
47 | if (this.options._scaffoldFilesFor && this.options._scaffoldFilesFor.includes('demoing')) {
48 | this.copyTemplate(
49 | `${__dirname}/templates/my-app.stories.js`,
50 | this.destinationPath(`./stories/${tagName}.stories.js`),
51 | );
52 |
53 | await this.copyTemplates(`${__dirname}/templates/static-scaffold-demoing/**/*`);
54 | }
55 |
56 | if (this.options._scaffoldFilesFor && this.options._scaffoldFilesFor.includes('testing')) {
57 | this.copyTemplate(
58 | `${__dirname}/templates/my-app.test.js`,
59 | this.destinationPath(`./test/${tagName}.test.js`),
60 | );
61 |
62 | await this.copyTemplates(`${__dirname}/templates/static-scaffold-testing/**/*`);
63 | }
64 | }
65 |
66 | async end() {
67 | await super.end();
68 | console.log('');
69 | console.log('You are all set up now!');
70 | console.log('');
71 | console.log('All you need to do is run:');
72 | console.log(` cd ${this.templateData.tagName}`);
73 | console.log(' npm run start');
74 | console.log('');
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/custom-elements.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0",
3 | "readme": "",
4 | "modules": [
5 | {
6 | "kind": "javascript-module",
7 | "path": "src/scaffold-app.js",
8 | "declarations": [
9 | {
10 | "kind": "class",
11 | "description": "",
12 | "name": "ScaffoldApp",
13 | "members": [
14 | {
15 | "kind": "field",
16 | "name": "header",
17 | "type": {
18 | "text": "string"
19 | },
20 | "default": "'My app'",
21 | "privacy": "public",
22 | "attribute": "header"
23 | }
24 | ],
25 | "attributes": [
26 | {
27 | "name": "header",
28 | "fieldName": "header"
29 | }
30 | ],
31 | "superclass": {
32 | "name": "LitElement",
33 | "package": "lit"
34 | },
35 | "tagName": "scaffold-app",
36 | "customElement": true
37 | }
38 | ],
39 | "exports": [
40 | {
41 | "kind": "custom-element-definition",
42 | "name": "scaffold-app",
43 | "declaration": {
44 | "name": "ScaffoldApp",
45 | "module": "src/scaffold-app.js"
46 | }
47 | }
48 | ]
49 | },
50 | {
51 | "kind": "javascript-module",
52 | "path": "stories/scaffold-app.stories.js",
53 | "declarations": [
54 | {
55 | "kind": "variable",
56 | "name": "App"
57 | }
58 | ],
59 | "exports": [
60 | {
61 | "kind": "js",
62 | "name": "default",
63 | "declaration": {
64 | "module": "stories/scaffold-app.stories.js"
65 | }
66 | },
67 | {
68 | "kind": "js",
69 | "name": "App",
70 | "declaration": {
71 | "name": "App",
72 | "module": "stories/scaffold-app.stories.js"
73 | }
74 | }
75 | ]
76 | }
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/my-app.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit';
2 |
3 | const logo = new URL('../assets/open-wc-logo.svg', import.meta.url).href;
4 |
5 | class <%= className %> extends LitElement {
6 | static properties = {
7 | header: { type: String },
8 | }
9 |
10 | static styles = css`
11 | :host {
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: flex-start;
17 | font-size: calc(10px + 2vmin);
18 | color: #1a2b42;
19 | max-width: 960px;
20 | margin: 0 auto;
21 | text-align: center;
22 | background-color: var(--<%= tagName %>-background-color);
23 | }
24 |
25 | main {
26 | flex-grow: 1;
27 | }
28 |
29 | .logo {
30 | margin-top: 36px;
31 | animation: app-logo-spin infinite 20s linear;
32 | }
33 |
34 | @keyframes app-logo-spin {
35 | from {
36 | transform: rotate(0deg);
37 | }
38 | to {
39 | transform: rotate(360deg);
40 | }
41 | }
42 |
43 | .app-footer {
44 | font-size: calc(12px + 0.5vmin);
45 | align-items: center;
46 | }
47 |
48 | .app-footer a {
49 | margin-left: 5px;
50 | }
51 | `;
52 |
53 | constructor() {
54 | super();
55 | this.header = 'My app';
56 | }
57 |
58 | render() {
59 | return html`
60 |
61 |
62 | ${this.header}
63 |
64 | Edit src/<%= className %>.js
and save to reload.
65 |
71 | Code examples
72 |
73 |
74 |
75 |
84 | `;
85 | }
86 | }
87 |
88 | customElements.define('<%= tagName %>', <%= className %>);
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/my-app.stories.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import '../src/<%= tagName %>.js';
3 |
4 | export default {
5 | title: '<%= className %>',
6 | component: '<%= tagName %>',
7 | argTypes: {
8 | backgroundColor: { control: 'color' },
9 | },
10 | };
11 |
12 | function Template({ header, backgroundColor }) {
13 | return html`
14 | <<%= tagName %>
15 | style="--<%= tagName %>-background-color: ${backgroundColor || 'white'}"
16 | .header=${header}
17 | >
18 | <%= tagName %>>
19 | `;
20 | }
21 |
22 | export const App = Template.bind({});
23 | App.args = {
24 | header: 'My app',
25 | };
26 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/my-app.test.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import { fixture, expect } from '@open-wc/testing';
3 |
4 | import '../src/<%= tagName %>.js';
5 |
6 | describe('<%= className %>', () => {
7 | let element;
8 | beforeEach(async () => {
9 | element = await fixture(html`<<%= tagName %>><%= tagName %>>`);
10 | });
11 |
12 | it('renders a h1', () => {
13 | const h1 = element.shadowRoot.querySelector('h1');
14 | expect(h1).to.exist;
15 | expect(h1.textContent).to.equal('My app');
16 | });
17 |
18 | it('passes the a11y audit', async () => {
19 | await expect(element).shadowDom.to.be.accessible();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/open-wc-logo.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= tagName %>",
3 | "license": "MIT",
4 | "type": "module",
5 | "scripts": {
6 | "start": "web-dev-server"
7 | },
8 | "dependencies": {
9 | "lit": "^3.1.4"
10 | },
11 | "devDependencies": {
12 | "@web/dev-server": "^0.4.6"
13 | }
14 | }
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/static-demoing/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | stories: ['../stories/*.stories.{js,md,mdx}'],
3 | framework: {
4 | name: '@web/storybook-framework-web-components',
5 | },
6 | };
7 |
8 | export default config;
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/static-testing/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // import { playwrightLauncher } from '@web/test-runner-playwright';
2 |
3 | const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
4 |
5 | export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6 | /** Test files to run */
7 | files: 'test/**/*.test.js',
8 |
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Filter out lit dev mode logs */
15 | filterBrowserLogs(log) {
16 | for (const arg of log.args) {
17 | if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | },
23 |
24 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25 | // esbuildTarget: 'auto',
26 |
27 | /** Amount of browsers to run concurrently */
28 | // concurrentBrowsers: 2,
29 |
30 | /** Amount of test files per browser to test concurrently */
31 | // concurrency: 1,
32 |
33 | /** Browsers to run tests on */
34 | // browsers: [
35 | // playwrightLauncher({ product: 'chromium' }),
36 | // playwrightLauncher({ product: 'firefox' }),
37 | // playwrightLauncher({ product: 'webkit' }),
38 | // ],
39 |
40 | // See documentation for all available options
41 | });
42 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/static/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Open-wc Starter App
6 |
7 | [](https://github.com/open-wc)
8 |
9 | ## Quickstart
10 |
11 | To get started:
12 |
13 | ```bash
14 | npm init @open-wc
15 | # requires node 10 & npm 6 or higher
16 | ```
17 |
18 | ## Scripts
19 |
20 | - `start` runs your app for development, reloading on file changes
21 | - `start:build` runs your app after it has been built using the build command
22 | - `build` builds your app and outputs it in your `dist` directory
23 | - `test` runs your test suite with Web Test Runner
24 | - `lint` runs the linter for your project
25 | - `format` fixes linting and formatting errors
26 |
27 | ## Tooling configs
28 |
29 | For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
30 |
31 | If you customize the configuration a lot, you can consider moving them to individual files.
32 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 | <%= tagName %>
20 |
21 |
22 |
23 | <<%= tagName %>><%= tagName %>>
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/generators/app-lit-element/templates/static/web-dev-server.config.js:
--------------------------------------------------------------------------------
1 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2 |
3 | /** Use Hot Module replacement by adding --hmr to the start command */
4 | const hmr = process.argv.includes('--hmr');
5 |
6 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7 | open: '/',
8 | watch: !hmr,
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
15 | // esbuildTarget: 'auto'
16 |
17 | /** Set appIndex to enable SPA routing */
18 | appIndex: './index.html',
19 |
20 | plugins: [
21 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
22 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
23 | ],
24 |
25 | // See documentation for all available options
26 | });
27 |
--------------------------------------------------------------------------------
/src/generators/app/executeViaOptions.js:
--------------------------------------------------------------------------------
1 | import { executeMixinGenerator } from '../../core.js';
2 | import { gatherMixins } from './gatherMixins.js';
3 |
4 | export async function executeViaOptions(options) {
5 | const mixins = gatherMixins(options);
6 |
7 | await executeMixinGenerator(mixins, options);
8 | }
9 |
--------------------------------------------------------------------------------
/src/generators/app/gatherMixins.js:
--------------------------------------------------------------------------------
1 | import { WcLitElementMixin, WcLitElementPackageMixin } from '../wc-lit-element/index.js';
2 | import { LintingMixin } from '../linting/index.js';
3 | import { TestingMixin, TestingScaffoldMixin } from '../testing/index.js';
4 | import {
5 | DemoingStorybookMixin,
6 | DemoingStorybookScaffoldMixin,
7 | } from '../demoing-storybook/index.js';
8 | import { BuildingRollupMixin } from '../building-rollup/index.js';
9 | // ts
10 | import { TsWcLitElementMixin, TsWcLitElementPackageMixin } from '../wc-lit-element-ts/index.js';
11 | import { TsLintingMixin } from '../linting-ts/index.js';
12 | import { TsTestingMixin, TsTestingScaffoldMixin } from '../testing-ts/index.js';
13 | import {
14 | TsDemoingStorybookMixin,
15 | TsDemoingStorybookScaffoldMixin,
16 | } from '../demoing-storybook-ts/index.js';
17 | import { TsBuildingRollupMixin } from '../building-rollup-ts/index.js';
18 |
19 | export function gatherMixins(options) {
20 | let considerScaffoldFilesFor = false;
21 | const mixins = [];
22 |
23 | if (options.type === 'scaffold') {
24 | if (options.typescript === 'true') {
25 | switch (options.scaffoldType) {
26 | case 'wc':
27 | mixins.push(TsWcLitElementPackageMixin);
28 | considerScaffoldFilesFor = true;
29 | break;
30 | case 'wc-lit-element':
31 | mixins.push(TsWcLitElementMixin);
32 | considerScaffoldFilesFor = true;
33 | break;
34 | // no default
35 | }
36 | } else {
37 | switch (options.scaffoldType) {
38 | case 'wc':
39 | mixins.push(WcLitElementPackageMixin);
40 | considerScaffoldFilesFor = true;
41 | break;
42 | case 'wc-lit-element':
43 | mixins.push(WcLitElementMixin);
44 | considerScaffoldFilesFor = true;
45 | break;
46 | // no default
47 | }
48 | }
49 | }
50 |
51 | if (options.features && options.features.length > 0) {
52 | if (options.typescript === 'true') {
53 | options.features.forEach(feature => {
54 | if (feature === 'linting') {
55 | mixins.push(TsLintingMixin);
56 | }
57 | if (feature === 'testing') {
58 | mixins.push(TsTestingMixin);
59 | }
60 | if (feature === 'demoing') {
61 | mixins.push(TsDemoingStorybookMixin);
62 | }
63 | if (feature === 'building') {
64 | mixins.push(TsBuildingRollupMixin);
65 | }
66 | });
67 | } else {
68 | options.features.forEach(feature => {
69 | if (feature === 'linting') {
70 | mixins.push(LintingMixin);
71 | }
72 | if (feature === 'testing') {
73 | mixins.push(TestingMixin);
74 | }
75 | if (feature === 'demoing') {
76 | mixins.push(DemoingStorybookMixin);
77 | }
78 | if (feature === 'building') {
79 | mixins.push(BuildingRollupMixin);
80 | }
81 | });
82 | }
83 | }
84 |
85 | if (
86 | considerScaffoldFilesFor &&
87 | options._scaffoldFilesFor &&
88 | options._scaffoldFilesFor.length > 0
89 | ) {
90 | options._scaffoldFilesFor.forEach(feature => {
91 | if (options.typescript === 'true') {
92 | switch (feature) {
93 | case 'testing':
94 | mixins.push(TsTestingScaffoldMixin);
95 | break;
96 | case 'demoing':
97 | mixins.push(TsDemoingStorybookScaffoldMixin);
98 | break;
99 | // no default
100 | }
101 | } else {
102 | switch (feature) {
103 | case 'testing':
104 | mixins.push(TestingScaffoldMixin);
105 | break;
106 | case 'demoing':
107 | mixins.push(DemoingStorybookScaffoldMixin);
108 | break;
109 | // no default
110 | }
111 | }
112 | });
113 | }
114 |
115 | return mixins;
116 | }
117 |
--------------------------------------------------------------------------------
/src/generators/app/header.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export default `
4 | _.,,,,,,,,,._
5 | .d'' \`\`b. ${chalk.underline('Open Web Components Recommendations')}
6 | .p' Open \`q.
7 | .d' Web Components \`b. Start or upgrade your web component project with
8 | .d' \`b. ease. All our recommendations at your fingertips.
9 | :: ................. ::
10 | \`p. .q'
11 | \`p. open-wc.org .q'
12 | \`b. @openWc .d'
13 | \`q.. ..,' See more details at https://open-wc.org/docs/development/generator/
14 | '',,,,,,,,,,''
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/src/generators/app/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import prompts from 'prompts';
3 | import commandLineArgs from 'command-line-args';
4 | import commandLineUsage from 'command-line-usage';
5 | import { executeMixinGenerator } from '../../core.js';
6 | import { AppLitElementMixin } from '../app-lit-element/index.js';
7 | import { TsAppLitElementMixin } from '../app-lit-element-ts/index.js';
8 |
9 | import header from './header.js';
10 | import { gatherMixins } from './gatherMixins.js';
11 |
12 | /**
13 | * Allows to control the data via command line
14 | *
15 | * example:
16 | * npm init @open-wc --type scaffold --scaffoldType app --tagName foo-bar --installDependencies false
17 | * npm init @open-wc --type upgrade --features linting demoing --tagName foo-bar --installDependencies false
18 | */
19 | const optionDefinitions = [
20 | {
21 | name: 'destinationPath',
22 | description: 'The path the generator will write files to',
23 | type: String,
24 | typeLabel: '{underline path}',
25 | },
26 | {
27 | name: 'type',
28 | description:
29 | 'Choose {bold scaffold} to create a new project or {bold upgrade} to add features to an existing project',
30 | typeLabel: '{underline scaffold|upgrade}',
31 | type: String,
32 | },
33 | {
34 | name: 'scaffoldType',
35 | description:
36 | 'The type of project to scaffold. {bold wc} for a single published component, {bold app} for an application',
37 | type: String,
38 | typeLabel: '{underline wc|app}',
39 | },
40 | {
41 | name: 'features',
42 | description:
43 | 'Which features to include. {bold linting}, {bold testing}, {bold demoing}, or {bold building}',
44 | type: String,
45 | typeLabel: '{underline linting|testing|demoing|building}',
46 | multiple: true,
47 | },
48 | {
49 | name: 'typescript',
50 | description: 'Whether to use TypeScript in your project',
51 | type: String,
52 | typeLabel: '{underline true|false}',
53 | },
54 | {
55 | name: 'tagName',
56 | description: 'The tag name for the web component or app shell element',
57 | type: String,
58 | typeLabel: '{underline string}',
59 | },
60 | {
61 | name: 'installDependencies',
62 | description:
63 | 'Whether to install dependencies. Choose {bold npm} or {bold yarn} to install with those package managers, or {bold false} to skip installation',
64 | type: String,
65 | typeLabel: '{underline yarn|npm|false}',
66 | },
67 | {
68 | name: 'writeToDisk',
69 | description: 'Whether or not to actually write the files to disk',
70 | type: String,
71 | typeLabel: '{underline true|false}',
72 | },
73 | {
74 | name: 'help',
75 | description: 'This help message',
76 | type: Boolean,
77 | },
78 | ];
79 |
80 | const overrides = commandLineArgs(optionDefinitions);
81 |
82 | if (overrides.help) {
83 | const sections = [
84 | {
85 | content: header,
86 | raw: true,
87 | },
88 | {
89 | header: 'Usage',
90 | content: '$ npm init @open-wc []',
91 | },
92 | {
93 | header: 'Options',
94 | optionList: optionDefinitions,
95 | },
96 | ];
97 |
98 | const usage = commandLineUsage(sections);
99 | console.log(usage);
100 | process.exit(0);
101 | }
102 |
103 | prompts.override(overrides);
104 |
105 | export const AppMixin = subclass =>
106 | // eslint-disable-next-line no-shadow
107 | class AppMixin extends subclass {
108 | constructor() {
109 | super();
110 | this.wantsNpmInstall = false;
111 | this.wantsWriteToDisk = false;
112 | this.wantsRecreateInfo = false;
113 | }
114 |
115 | async execute() {
116 | console.log(header);
117 | console.log('Note: you can exit any time with Ctrl+C or Esc');
118 | const questions = [
119 | {
120 | type: 'select',
121 | name: 'type',
122 | message: 'What would you like to do today?',
123 | choices: [
124 | { title: 'Scaffold a new project', value: 'scaffold' },
125 | { title: 'Upgrade an existing project', value: 'upgrade' },
126 | ],
127 | },
128 | {
129 | type: (prev, all) => (all.type === 'scaffold' ? 'select' : null),
130 | name: 'scaffoldType',
131 | message: 'What would you like to scaffold?',
132 | choices: [
133 | { title: 'Web Component', value: 'wc' },
134 | { title: 'Application', value: 'app' },
135 | ],
136 | },
137 | {
138 | type: (prev, all) =>
139 | all.scaffoldType === 'wc' || all.scaffoldType === 'app' || all.type === 'upgrade'
140 | ? 'multiselect'
141 | : null,
142 | name: 'features',
143 | message: 'What would you like to add?',
144 | choices: (prev, all) =>
145 | [
146 | { title: 'Linting (eslint & prettier)', value: 'linting' },
147 | { title: 'Testing (web-test-runner)', value: 'testing' },
148 | { title: 'Demoing (storybook)', value: 'demoing' },
149 | all.scaffoldType !== 'wc' && {
150 | title: 'Building (rollup)',
151 | value: 'building',
152 | },
153 | ].filter(_ => !!_),
154 | },
155 | {
156 | type: 'select',
157 | name: 'typescript',
158 | message: 'Would you like to use typescript?',
159 | choices: [
160 | { title: 'No', value: 'false' },
161 | { title: 'Yes', value: 'true' },
162 | ],
163 | },
164 | {
165 | type: (prev, all) => (all.tagName ? null : 'text'),
166 | name: 'tagName',
167 | message: (prev, all) =>
168 | `What is the tag name of your ${
169 | all.scaffoldType === 'app' ? 'app shell element' : 'web component'
170 | }?`,
171 | validate: tagName =>
172 | !/^([a-z])(?!.*[<>])(?=.*-).+$/.test(tagName)
173 | ? 'You need a minimum of two lowercase words separated by dashes (e.g. foo-bar)'
174 | : true,
175 | },
176 | ];
177 |
178 | /**
179 | * {
180 | * type: 'scaffold',
181 | * scaffoldType: 'wc',
182 | * features: [ 'testing', 'building' ],
183 | * tagName: 'foo-bar',
184 | * installDependencies: 'false'
185 | * }
186 | */
187 | this.options = await prompts(questions, {
188 | onCancel: () => {
189 | process.exit();
190 | },
191 | });
192 |
193 | if (this.options.type === 'scaffold') {
194 | // when using the new project scaffold, infer _scaffoldFilesFor from selected features
195 | this.options._scaffoldFilesFor = [...this.options.features];
196 | }
197 |
198 | const mixins = gatherMixins(this.options);
199 | // app is separate to prevent circular imports
200 | if (this.options.type === 'scaffold' && this.options.scaffoldType === 'app') {
201 | if (this.options.typescript === 'true') {
202 | mixins.push(TsAppLitElementMixin);
203 | } else {
204 | mixins.push(AppLitElementMixin);
205 | }
206 | }
207 | await executeMixinGenerator(mixins, this.options);
208 | }
209 | };
210 |
211 | export default AppMixin;
212 |
--------------------------------------------------------------------------------
/src/generators/building-rollup-ts/index.js:
--------------------------------------------------------------------------------
1 | export const TsBuildingRollupMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/building-rollup-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "build": "rimraf dist && tsc && rollup -c rollup.config.js && <%= scriptRunCommand %> analyze -- --exclude dist",
4 | "start:build": "web-dev-server --root-dir dist --app-index index.html --open"
5 | },
6 | "devDependencies": {
7 | "@rollup/plugin-babel": "^6.0.4",
8 | "@rollup/plugin-node-resolve": "^15.2.3",
9 | "@web/rollup-plugin-html": "^2.3.0",
10 | "@web/rollup-plugin-import-meta-assets": "^2.2.1",
11 | "babel-plugin-template-html-minifier": "^4.1.0",
12 | "deepmerge": "^4.3.1",
13 | "rimraf": "^5.0.9",
14 | "rollup-plugin-esbuild": "^6.1.1",
15 | "rollup-plugin-workbox": "^8.1.0",
16 | "rollup": "^4.18.1"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/generators/building-rollup-ts/templates/static/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from '@rollup/plugin-node-resolve';
2 | import babel from '@rollup/plugin-babel';
3 | import { rollupPluginHTML as html } from '@web/rollup-plugin-html';
4 | import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
5 | import esbuild from 'rollup-plugin-esbuild';
6 | import { generateSW } from 'rollup-plugin-workbox';
7 | import path from 'path';
8 |
9 | export default {
10 | input: 'index.html',
11 | output: {
12 | entryFileNames: '[hash].js',
13 | chunkFileNames: '[hash].js',
14 | assetFileNames: '[hash][extname]',
15 | format: 'es',
16 | dir: 'dist',
17 | },
18 | preserveEntrySignatures: false,
19 |
20 | plugins: [
21 | /** Enable using HTML as rollup entrypoint */
22 | html({
23 | minify: true,
24 | injectServiceWorker: true,
25 | serviceWorkerPath: 'dist/sw.js',
26 | }),
27 | /** Resolve bare module imports */
28 | nodeResolve(),
29 | /** Minify JS, compile JS to a lower language target */
30 | esbuild({
31 | minify: true,
32 | target: ['chrome64', 'firefox67', 'safari11.1'],
33 | }),
34 | /** Bundle assets references via import.meta.url */
35 | importMetaAssets(),
36 | /** Minify html and css tagged template literals */
37 | babel({
38 | plugins: [
39 | [
40 | 'babel-plugin-template-html-minifier',
41 | {
42 | modules: { lit: ['html', { name: 'css', encapsulation: 'style' }] },
43 | failOnError: false,
44 | strictCSS: true,
45 | htmlMinifier: {
46 | collapseWhitespace: true,
47 | conservativeCollapse: true,
48 | removeComments: true,
49 | caseSensitive: true,
50 | minifyCSS: true,
51 | },
52 | },
53 | ],
54 | ],
55 | }),
56 | /** Create and inject a service worker */
57 | generateSW({
58 | globIgnores: ['polyfills/*.js', 'nomodule-*.js'],
59 | navigateFallback: '/index.html',
60 | // where to output the generated sw
61 | swDest: path.join('dist', 'sw.js'),
62 | // directory to match patterns against to be precached
63 | globDirectory: path.join('dist'),
64 | // cache any html js and css by default
65 | globPatterns: ['**/*.{html,js,css,webmanifest}'],
66 | skipWaiting: true,
67 | clientsClaim: true,
68 | runtimeCaching: [{ urlPattern: 'polyfills/*.js', handler: 'CacheFirst' }],
69 | }),
70 | ],
71 | };
72 |
--------------------------------------------------------------------------------
/src/generators/building-rollup/index.js:
--------------------------------------------------------------------------------
1 | export const BuildingRollupMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/building-rollup/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "build": "rimraf dist && rollup -c rollup.config.js && <%= scriptRunCommand %> analyze -- --exclude dist",
4 | "start:build": "web-dev-server --root-dir dist --app-index index.html --open"
5 | },
6 | "devDependencies": {
7 | "@rollup/plugin-babel": "^6.0.4",
8 | "@rollup/plugin-node-resolve": "^15.2.3",
9 | "@web/rollup-plugin-html": "^2.3.0",
10 | "@web/rollup-plugin-import-meta-assets": "^2.2.1",
11 | "babel-plugin-template-html-minifier": "^4.1.0",
12 | "deepmerge": "^4.3.1",
13 | "rimraf": "^5.0.9",
14 | "rollup-plugin-esbuild": "^6.1.1",
15 | "rollup-plugin-workbox": "^8.1.0",
16 | "rollup": "^4.18.1"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/generators/building-rollup/templates/static/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from '@rollup/plugin-node-resolve';
2 | import babel from '@rollup/plugin-babel';
3 | import { rollupPluginHTML as html } from '@web/rollup-plugin-html';
4 | import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
5 | import esbuild from 'rollup-plugin-esbuild';
6 | import { generateSW } from 'rollup-plugin-workbox';
7 | import path from 'path';
8 |
9 | export default {
10 | input: 'index.html',
11 | output: {
12 | entryFileNames: '[hash].js',
13 | chunkFileNames: '[hash].js',
14 | assetFileNames: '[hash][extname]',
15 | format: 'es',
16 | dir: 'dist',
17 | },
18 | preserveEntrySignatures: false,
19 |
20 | plugins: [
21 | /** Enable using HTML as rollup entrypoint */
22 | html({
23 | minify: true,
24 | injectServiceWorker: true,
25 | serviceWorkerPath: 'dist/sw.js',
26 | }),
27 | /** Resolve bare module imports */
28 | nodeResolve(),
29 | /** Minify JS, compile JS to a lower language target */
30 | esbuild({
31 | minify: true,
32 | target: ['chrome64', 'firefox67', 'safari11.1'],
33 | }),
34 | /** Bundle assets references via import.meta.url */
35 | importMetaAssets(),
36 | /** Minify html and css tagged template literals */
37 | babel({
38 | plugins: [
39 | [
40 | 'babel-plugin-template-html-minifier',
41 | {
42 | modules: { lit: ['html', { name: 'css', encapsulation: 'style' }] },
43 | failOnError: false,
44 | strictCSS: true,
45 | htmlMinifier: {
46 | collapseWhitespace: true,
47 | conservativeCollapse: true,
48 | removeComments: true,
49 | caseSensitive: true,
50 | minifyCSS: true,
51 | },
52 | },
53 | ],
54 | ],
55 | }),
56 | /** Create and inject a service worker */
57 | generateSW({
58 | globIgnores: ['polyfills/*.js', 'nomodule-*.js'],
59 | navigateFallback: '/index.html',
60 | // where to output the generated sw
61 | swDest: path.join('dist', 'sw.js'),
62 | // directory to match patterns against to be precached
63 | globDirectory: path.join('dist'),
64 | // cache any html js and css by default
65 | globPatterns: ['**/*.{html,js,css,webmanifest}'],
66 | skipWaiting: true,
67 | clientsClaim: true,
68 | runtimeCaching: [{ urlPattern: 'polyfills/*.js', handler: 'CacheFirst' }],
69 | }),
70 | ],
71 | };
72 |
--------------------------------------------------------------------------------
/src/generators/common-repo/index.js:
--------------------------------------------------------------------------------
1 | export const CommonRepoMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | this.templateData = {
5 | ...this.templateData,
6 | scriptRunCommand: this.options.installDependencies === 'yarn' ? 'yarn' : 'npm run',
7 | year: new Date().getFullYear(),
8 | };
9 |
10 | await super.execute();
11 |
12 | this.copyTemplateJsonInto(
13 | `${__dirname}/templates/package.json`,
14 | this.destinationPath('package.json'),
15 | );
16 |
17 | // write and rename .gitignore
18 | this.copyTemplate(`${__dirname}/templates/gitignore`, this.destinationPath(`.gitignore`));
19 |
20 | // copy all other files
21 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/generators/common-repo/templates/gitignore:
--------------------------------------------------------------------------------
1 | ## editors
2 | /.idea
3 | /.vscode
4 |
5 | ## system files
6 | .DS_Store
7 |
8 | ## npm
9 | /node_modules/
10 | /npm-debug.log
11 |
12 | ## testing
13 | /coverage/
14 |
15 | ## temp folders
16 | /.tmp/
17 |
18 | # build
19 | /_site/
20 | /dist/
21 | /out-tsc/
22 |
23 | storybook-static
24 | custom-elements.json
25 |
--------------------------------------------------------------------------------
/src/generators/common-repo/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= tagName %>",
3 | "version": "0.0.0",
4 | "description": "Webcomponent <%= tagName %> following open-wc recommendations",
5 | "author": "<%= tagName %>",
6 | "license": "MIT",
7 | "customElements": "custom-elements.json",
8 | "scripts": {
9 | "analyze": "cem analyze --litelement"
10 | },
11 | "devDependencies": {
12 | "@custom-elements-manifest/analyzer": "^0.10.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/generators/common-repo/templates/static/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
23 | [*.json]
24 | indent_size = 2
25 |
26 | [*.{html,js,md}]
27 | block_comment_start = /**
28 | block_comment = *
29 | block_comment_end = */
30 |
--------------------------------------------------------------------------------
/src/generators/common-repo/templates/static/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "esbenp.prettier-vscode",
4 | "runem.lit-plugin",
5 | "dbaeumer.vscode-eslint"
6 | ],
7 | "unwantedRecommendations": [
8 | ]
9 | }
--------------------------------------------------------------------------------
/src/generators/common-repo/templates/static/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) <%= year %> <%= tagName %>
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/generators/demoing-storybook-ts/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | export const TsDemoingStorybookMixin = subclass =>
3 | class extends subclass {
4 | async execute() {
5 | await super.execute();
6 |
7 | this.copyTemplateJsonInto(
8 | `${__dirname}/templates/package.json`,
9 | this.destinationPath('package.json'),
10 | );
11 |
12 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
13 | }
14 | };
15 |
16 | export const TsDemoingStorybookScaffoldMixin = subclass =>
17 | class extends subclass {
18 | async execute() {
19 | await super.execute();
20 | await this.copyTemplates(`${__dirname}/templates/static-scaffold/**/*`);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "storybook": "tsc && <%= scriptRunCommand %> analyze -- --exclude dist && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"storybook dev -p 8080\"",
4 | "storybook:build": "tsc && <%= scriptRunCommand %> analyze -- --exclude dist && storybook build"
5 | },
6 | "devDependencies": {
7 | "@storybook/addon-a11y": "^7.6.20",
8 | "@storybook/addon-essentials": "^7.6.20",
9 | "@storybook/addon-links": "^7.6.20",
10 | "@storybook/web-components": "^7.6.20",
11 | "@web/storybook-builder": "^0.1.16",
12 | "@web/storybook-framework-web-components": "^0.1.2",
13 | "storybook": "^7.6.20"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook-ts/templates/static-scaffold/stories/index.stories.ts:
--------------------------------------------------------------------------------
1 | import { html, TemplateResult } from 'lit';
2 | import '../src/<%= tagName %>.js';
3 |
4 | export default {
5 | title: '<%= className %>',
6 | component: '<%= tagName %>',
7 | argTypes: {
8 | header: { control: 'text' },
9 | counter: { control: 'number' },
10 | textColor: { control: 'color' },
11 | },
12 | };
13 |
14 | interface Story {
15 | (args: T): TemplateResult;
16 | args?: Partial;
17 | argTypes?: Record;
18 | }
19 |
20 | interface ArgTypes {
21 | header?: string;
22 | counter?: number;
23 | textColor?: string;
24 | slot?: TemplateResult;
25 | }
26 |
27 | const Template: Story = ({
28 | header = 'Hello world',
29 | counter = 5,
30 | textColor,
31 | slot,
32 | }: ArgTypes) => html`
33 | <<%= tagName %>
34 | style="--<%= tagName %>-text-color: ${textColor || 'black'}"
35 | .header=${header}
36 | .counter=${counter}
37 | >
38 | ${slot}
39 | <%= tagName %>>
40 | `;
41 |
42 | export const Regular = Template.bind({});
43 |
44 | export const CustomHeader = Template.bind({});
45 | CustomHeader.args = {
46 | header: 'My header',
47 | };
48 |
49 | export const CustomCounter = Template.bind({});
50 | CustomCounter.args = {
51 | counter: 123456,
52 | };
53 |
54 | export const SlottedContent = Template.bind({});
55 | SlottedContent.args = {
56 | slot: html`Slotted content
`,
57 | };
58 | SlottedContent.argTypes = {
59 | slot: { table: { disable: true } },
60 | };
61 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook-ts/templates/static/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | stories: ['../**/dist/stories/*.stories.{js,md,mdx}'],
3 | framework: {
4 | name: '@web/storybook-framework-web-components',
5 | },
6 | };
7 |
8 | export default config;
--------------------------------------------------------------------------------
/src/generators/demoing-storybook/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | export const DemoingStorybookMixin = subclass =>
3 | class extends subclass {
4 | async execute() {
5 | await super.execute();
6 |
7 | this.copyTemplateJsonInto(
8 | `${__dirname}/templates/package.json`,
9 | this.destinationPath('package.json'),
10 | );
11 |
12 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
13 | }
14 | };
15 |
16 | export const DemoingStorybookScaffoldMixin = subclass =>
17 | class extends subclass {
18 | async execute() {
19 | await super.execute();
20 | await this.copyTemplates(`${__dirname}/templates/static-scaffold/**/*`);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "storybook": "<%= scriptRunCommand %> analyze -- --exclude dist && storybook dev -p 8080",
4 | "storybook:build": "<%= scriptRunCommand %> analyze -- --exclude dist && storybook build"
5 | },
6 | "devDependencies": {
7 | "@storybook/addon-a11y": "^7.6.20",
8 | "@storybook/addon-essentials": "^7.6.20",
9 | "@storybook/addon-links": "^7.6.20",
10 | "@storybook/web-components": "^7.6.20",
11 | "@web/storybook-builder": "^0.1.16",
12 | "@web/storybook-framework-web-components": "^0.1.2",
13 | "storybook": "^7.6.20"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook/templates/static-scaffold/stories/index.stories.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import '../<%= tagName %>.js';
3 |
4 | export default {
5 | title: '<%= className %>',
6 | component: '<%= tagName %>',
7 | argTypes: {
8 | header: { control: 'text' },
9 | counter: { control: 'number' },
10 | textColor: { control: 'color' },
11 | },
12 | };
13 |
14 | function Template({ header = 'Hello world', counter = 5, textColor, slot }) {
15 | return html`
16 | <<%= tagName %>
17 | style="--<%= tagName %>-text-color: ${textColor || 'black'}"
18 | .header=${header}
19 | .counter=${counter}
20 | >
21 | ${slot}
22 | <%= tagName %>>
23 | `;
24 | }
25 |
26 | export const Regular = Template.bind({});
27 |
28 | export const CustomHeader = Template.bind({});
29 | CustomHeader.args = {
30 | header: 'My header',
31 | };
32 |
33 | export const CustomCounter = Template.bind({});
34 | CustomCounter.args = {
35 | counter: 123456,
36 | };
37 |
38 | export const SlottedContent = Template.bind({});
39 | SlottedContent.args = {
40 | slot: html`Slotted content
`,
41 | };
42 | SlottedContent.argTypes = {
43 | slot: { table: { disable: true } },
44 | };
45 |
--------------------------------------------------------------------------------
/src/generators/demoing-storybook/templates/static/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | stories: ['../stories/*.stories.{js,md,mdx}'],
3 | framework: {
4 | name: '@web/storybook-framework-web-components',
5 | },
6 | };
7 |
8 | export default config;
--------------------------------------------------------------------------------
/src/generators/git-ignore-lock-files-in-diff/static/.gitattributes:
--------------------------------------------------------------------------------
1 | # do not show lock files while doing git diff
2 | package-lock.json -diff
3 | yarn.lock -diff
4 |
--------------------------------------------------------------------------------
/src/generators/linting-commitlint/index.js:
--------------------------------------------------------------------------------
1 | export const LintingCommitlintMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/linting-commitlint/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@commitlint/cli": "^19.3.0",
4 | "@commitlint/config-conventional": "^19.2.2"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/generators/linting-commitlint/templates/static/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | };
4 |
--------------------------------------------------------------------------------
/src/generators/linting-eslint-ts/index.js:
--------------------------------------------------------------------------------
1 | export const TsLintingEsLintMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/generators/linting-eslint-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "eslint": "^8.57.0",
4 | "@open-wc/eslint-config": "^12.0.3",
5 | "@typescript-eslint/parser": "^7.16.0",
6 | "@typescript-eslint/eslint-plugin": "^7.16.0"
7 | },
8 | "eslintConfig": {
9 | "parser": "@typescript-eslint/parser",
10 | "extends": ["@open-wc"],
11 | "plugins": ["@typescript-eslint"],
12 | "rules": {
13 | "no-unused-vars": "off",
14 | "@typescript-eslint/no-unused-vars": ["error"],
15 | "import/no-unresolved": "off",
16 | "import/extensions": ["error", "always", { "ignorePackages": true }]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/generators/linting-eslint/index.js:
--------------------------------------------------------------------------------
1 | export const LintingEsLintMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/generators/linting-eslint/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "eslint": "^8.57.0",
4 | "@open-wc/eslint-config": "^12.0.3"
5 | },
6 | "eslintConfig": {
7 | "extends": [
8 | "@open-wc"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/generators/linting-prettier-ts/index.js:
--------------------------------------------------------------------------------
1 | export const TsLintingPrettierMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/generators/linting-prettier-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "prettier": "^3.3.2",
4 | "eslint-config-prettier": "^9.1.0"
5 | },
6 | "eslintConfig": {
7 | "extends": [
8 | "prettier"
9 | ]
10 | },
11 | "prettier": {
12 | "singleQuote": true,
13 | "arrowParens": "avoid"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/generators/linting-prettier/index.js:
--------------------------------------------------------------------------------
1 | export const LintingPrettierMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/generators/linting-prettier/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "prettier": "^3.3.2",
4 | "eslint-config-prettier": "^9.1.0"
5 | },
6 | "eslintConfig": {
7 | "extends": [
8 | "prettier"
9 | ]
10 | },
11 | "prettier": {
12 | "singleQuote": true,
13 | "arrowParens": "avoid"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/generators/linting-ts/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { TsLintingEsLintMixin } from '../linting-eslint-ts/index.js';
3 | import { TsLintingPrettierMixin } from '../linting-prettier-ts/index.js';
4 |
5 | export const TsLintingMixin = subclass =>
6 | class extends TsLintingPrettierMixin(TsLintingEsLintMixin(subclass)) {
7 | async execute() {
8 | await super.execute();
9 |
10 | // extend package.json
11 | this.copyTemplateJsonInto(
12 | `${__dirname}/templates/package.json`,
13 | this.destinationPath('package.json'),
14 | );
15 |
16 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/generators/linting-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "lint-staged": {
3 | "*.ts": [
4 | "eslint --fix",
5 | "prettier --write"
6 | ]
7 | },
8 | "scripts": {
9 | "lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore",
10 | "format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore",
11 | "prepare": "husky"
12 | },
13 | "devDependencies": {
14 | "husky": "^9.0.11",
15 | "lint-staged": "^15.2.7"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/generators/linting-ts/templates/static/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | ./node_modules/.bin/lint-staged
--------------------------------------------------------------------------------
/src/generators/linting-types-js/index.js:
--------------------------------------------------------------------------------
1 | export const LintingTypesJsMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/linting-types-js/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "lint:types": "tsc"
4 | },
5 | "devDependencies": {
6 | "typescript": "^5.5.3"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/generators/linting-types-js/templates/static/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "lib": ["es2021", "dom", "DOM.Iterable"],
7 | "allowJs": true,
8 | "checkJs": true,
9 | "noEmit": true,
10 | "strict": false,
11 | "noImplicitThis": true,
12 | "alwaysStrict": true,
13 | "esModuleInterop": true
14 | },
15 | "include": [
16 | "**/*.js",
17 | "node_modules/@open-wc/**/*.js"
18 | ],
19 | "exclude": [
20 | "node_modules/!(@open-wc)"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/generators/linting/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { LintingEsLintMixin } from '../linting-eslint/index.js';
3 | import { LintingPrettierMixin } from '../linting-prettier/index.js';
4 |
5 | export const LintingMixin = subclass =>
6 | class extends LintingPrettierMixin(LintingEsLintMixin(subclass)) {
7 | async execute() {
8 | await super.execute();
9 |
10 | // extend package.json
11 | this.copyTemplateJsonInto(
12 | `${__dirname}/templates/package.json`,
13 | this.destinationPath('package.json'),
14 | );
15 |
16 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/generators/linting/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "lint-staged": {
3 | "*.js": [
4 | "eslint --fix",
5 | "prettier --write"
6 | ]
7 | },
8 | "scripts": {
9 | "lint": "eslint --ext .js,.html . --ignore-path .gitignore && prettier \"**/*.js\" --check --ignore-path .gitignore",
10 | "format": "eslint --ext .js,.html . --fix --ignore-path .gitignore && prettier \"**/*.js\" --write --ignore-path .gitignore",
11 | "prepare": "husky"
12 | },
13 | "devDependencies": {
14 | "husky": "^9.0.11",
15 | "lint-staged": "^15.2.7"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/generators/linting/templates/static/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | ./node_modules/.bin/lint-staged
--------------------------------------------------------------------------------
/src/generators/testing-ts/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import { TsTestingWebTestRunnerMixin } from '../testing-wtr-ts/index.js';
3 |
4 | export const TsTestingMixin = subclass =>
5 | class extends TsTestingWebTestRunnerMixin(subclass) {
6 | async execute() {
7 | await super.execute();
8 |
9 | this.copyTemplateJsonInto(
10 | `${__dirname}/templates/package.json`,
11 | this.destinationPath('package.json'),
12 | );
13 | }
14 | };
15 |
16 | export const TsTestingScaffoldMixin = subclass =>
17 | class extends subclass {
18 | async execute() {
19 | await super.execute();
20 |
21 | const { tagName } = this.templateData;
22 | this.copyTemplate(
23 | `${__dirname}/templates/my-el.test.ts`,
24 | this.destinationPath(`test/${tagName}.test.ts`),
25 | );
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/generators/testing-ts/templates/my-el.test.ts:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import { fixture, expect } from '@open-wc/testing';
3 | import { <%= className %> } from '../src/<%= className %>.js';
4 | import '../src/<%= tagName %>.js';
5 |
6 | describe('<%= className %>', () => {
7 | it('has a default header "Hey there" and counter 5', async () => {
8 | const el = await fixture<<%= className %>>(html`<<%= tagName %>><%= tagName %>>`);
9 |
10 | expect(el.header).to.equal('Hey there');
11 | expect(el.counter).to.equal(5);
12 | });
13 |
14 | it('increases the counter on button click', async () => {
15 | const el = await fixture<<%= className %>>(html`<<%= tagName %>><%= tagName %>>`);
16 | el.shadowRoot!.querySelector('button')!.click();
17 |
18 | expect(el.counter).to.equal(6);
19 | });
20 |
21 | it('can override the header via attribute', async () => {
22 | const el = await fixture<<%= className %>>(html`<<%= tagName %> header="attribute header"><%= tagName %>>`);
23 |
24 | expect(el.header).to.equal('attribute header');
25 | });
26 |
27 | it('passes the a11y audit', async () => {
28 | const el = await fixture<<%= className %>>(html`<<%= tagName %>><%= tagName %>>`);
29 |
30 | await expect(el).shadowDom.to.be.accessible();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/generators/testing-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@open-wc/testing": "^4.0.0",
4 | "@types/mocha": "^10.0.7"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/generators/testing-wtr-ts/index.js:
--------------------------------------------------------------------------------
1 | export const TsTestingWebTestRunnerMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/testing-wtr-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "test": "tsc && wtr --coverage",
4 | "test:watch": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch\""
5 | },
6 | "devDependencies": {
7 | "@web/test-runner": "^0.18.2"
8 | }
9 | }
--------------------------------------------------------------------------------
/src/generators/testing-wtr-ts/templates/static/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // import { playwrightLauncher } from '@web/test-runner-playwright';
2 |
3 | const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
4 |
5 | export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6 | /** Test files to run */
7 | files: 'dist/test/**/*.test.js',
8 |
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Filter out lit dev mode logs */
15 | filterBrowserLogs(log) {
16 | for (const arg of log.args) {
17 | if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | },
23 |
24 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25 | // esbuildTarget: 'auto',
26 |
27 | /** Amount of browsers to run concurrently */
28 | // concurrentBrowsers: 2,
29 |
30 | /** Amount of test files per browser to test concurrently */
31 | // concurrency: 1,
32 |
33 | /** Browsers to run tests on */
34 | // browsers: [
35 | // playwrightLauncher({ product: 'chromium' }),
36 | // playwrightLauncher({ product: 'firefox' }),
37 | // playwrightLauncher({ product: 'webkit' }),
38 | // ],
39 |
40 | // See documentation for all available options
41 | });
42 |
--------------------------------------------------------------------------------
/src/generators/testing-wtr/index.js:
--------------------------------------------------------------------------------
1 | export const TestingWebTestRunnerMixin = subclass =>
2 | class extends subclass {
3 | async execute() {
4 | await super.execute();
5 |
6 | this.copyTemplateJsonInto(
7 | `${__dirname}/templates/package.json`,
8 | this.destinationPath('package.json'),
9 | );
10 |
11 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/generators/testing-wtr/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "test": "web-test-runner --coverage",
4 | "test:watch": "web-test-runner --watch"
5 | },
6 | "devDependencies": {
7 | "@web/test-runner": "^0.18.2"
8 | }
9 | }
--------------------------------------------------------------------------------
/src/generators/testing-wtr/templates/static/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // import { playwrightLauncher } from '@web/test-runner-playwright';
2 |
3 | const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
4 |
5 | export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6 | /** Test files to run */
7 | files: 'test/**/*.test.js',
8 |
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Filter out lit dev mode logs */
15 | filterBrowserLogs(log) {
16 | for (const arg of log.args) {
17 | if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | },
23 |
24 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25 | // esbuildTarget: 'auto',
26 |
27 | /** Amount of browsers to run concurrently */
28 | // concurrentBrowsers: 2,
29 |
30 | /** Amount of test files per browser to test concurrently */
31 | // concurrency: 1,
32 |
33 | /** Browsers to run tests on */
34 | // browsers: [
35 | // playwrightLauncher({ product: 'chromium' }),
36 | // playwrightLauncher({ product: 'firefox' }),
37 | // playwrightLauncher({ product: 'webkit' }),
38 | // ],
39 |
40 | // See documentation for all available options
41 | });
42 |
--------------------------------------------------------------------------------
/src/generators/testing/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import { TestingWebTestRunnerMixin } from '../testing-wtr/index.js';
3 |
4 | export const TestingMixin = subclass =>
5 | class extends TestingWebTestRunnerMixin(subclass) {
6 | async execute() {
7 | await super.execute();
8 |
9 | this.copyTemplateJsonInto(
10 | `${__dirname}/templates/package.json`,
11 | this.destinationPath('package.json'),
12 | );
13 | }
14 | };
15 |
16 | export const TestingScaffoldMixin = subclass =>
17 | class extends subclass {
18 | async execute() {
19 | await super.execute();
20 |
21 | const { tagName } = this.templateData;
22 | this.copyTemplate(
23 | `${__dirname}/templates/my-el.test.js`,
24 | this.destinationPath(`test/${tagName}.test.js`),
25 | );
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/generators/testing/templates/my-el.test.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import { fixture, expect } from '@open-wc/testing';
3 |
4 | import '../<%= tagName %>.js';
5 |
6 | describe('<%= className %>', () => {
7 | it('has a default header "Hey there" and counter 5', async () => {
8 | const el = await fixture(html`<<%= tagName %>><%= tagName %>>`);
9 |
10 | expect(el.header).to.equal('Hey there');
11 | expect(el.counter).to.equal(5);
12 | });
13 |
14 | it('increases the counter on button click', async () => {
15 | const el = await fixture(html`<<%= tagName %>><%= tagName %>>`);
16 | el.shadowRoot.querySelector('button').click();
17 |
18 | expect(el.counter).to.equal(6);
19 | });
20 |
21 | it('can override the header via attribute', async () => {
22 | const el = await fixture(html`<<%= tagName %> header="attribute header"><%= tagName %>>`);
23 |
24 | expect(el.header).to.equal('attribute header');
25 | });
26 |
27 | it('passes the a11y audit', async () => {
28 | const el = await fixture(html`<<%= tagName %>><%= tagName %>>`);
29 |
30 | await expect(el).shadowDom.to.be.accessible();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/generators/testing/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@open-wc/testing": "^4.0.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import { join } from 'path';
3 | import { CommonRepoMixin } from '../common-repo/index.js';
4 | import { processTemplate, readFileFromPath } from '../../core.js';
5 |
6 | const compose = (...fns) =>
7 | fns.reduce(
8 | (f, g) =>
9 | (...args) =>
10 | f(g(...args)),
11 | );
12 | const safeReduce = (f, initial) => xs => (Array.isArray(xs) ? xs.reduce(f, initial) : xs);
13 |
14 | const getTemplatePart = compose(processTemplate, readFileFromPath);
15 |
16 | function featureReadmeBlurb(feature) {
17 | const path = join(__dirname, `./templates/partials/README.${feature}.md`);
18 | return getTemplatePart(path);
19 | }
20 |
21 | function featureReadme(acc, feature, i, a) {
22 | return `${acc + featureReadmeBlurb(feature)}${i === a.length - 1 ? '' : '\n'}`;
23 | }
24 |
25 | const safeFeatureReadme = safeReduce(featureReadme, '');
26 |
27 | /* eslint-disable no-console */
28 | export const TsWcLitElementMixin = subclass =>
29 | class extends subclass {
30 | async execute() {
31 | this.templateData.featureReadmes = safeFeatureReadme(this.options.features);
32 |
33 | await super.execute();
34 | const { tagName, className } = this.templateData;
35 |
36 | // write & rename el class template
37 | this.copyTemplate(
38 | `${__dirname}/templates/MyEl.ts`,
39 | this.destinationPath(`src/${className}.ts`),
40 | );
41 |
42 | // write & rename el registration template
43 | this.copyTemplate(
44 | `${__dirname}/templates/my-el.ts`,
45 | this.destinationPath(`src/${tagName}.ts`),
46 | );
47 |
48 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
49 | }
50 | };
51 |
52 | export const TsWcLitElementPackageMixin = subclass =>
53 | class extends CommonRepoMixin(TsWcLitElementMixin(subclass)) {
54 | async execute() {
55 | await super.execute();
56 | // write & rename package.json
57 | this.copyTemplateJsonInto(
58 | `${__dirname}/templates/package.json`,
59 | this.destinationPath('package.json'),
60 | );
61 | this.copyTemplate(
62 | `${__dirname}/templates/custom-elements.json`,
63 | this.destinationPath('custom-elements.json'),
64 | );
65 | this.copyTemplate(
66 | `${__dirname}/templates/tsconfig.json`,
67 | this.destinationPath('tsconfig.json'),
68 | );
69 | }
70 |
71 | async end() {
72 | await super.end();
73 | console.log('');
74 | console.log('You are all set up now!');
75 | console.log('');
76 | console.log('All you need to do is run:');
77 | console.log(` cd ${this.templateData.tagName}`);
78 | console.log(' npm run start');
79 | console.log('');
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/MyEl.ts:
--------------------------------------------------------------------------------
1 | import { html, css, LitElement } from 'lit';
2 | import { property } from 'lit/decorators.js';
3 |
4 | export class <%= className %> extends LitElement {
5 | static styles = css`
6 | :host {
7 | display: block;
8 | padding: 25px;
9 | color: var(--<%= tagName %>-text-color, #000);
10 | }
11 | `;
12 |
13 | @property({ type: String }) header = 'Hey there';
14 |
15 | @property({ type: Number }) counter = 5;
16 |
17 | __increment() {
18 | this.counter += 1;
19 | }
20 |
21 | render() {
22 | return html`
23 | ${this.header} Nr. ${this.counter}!
24 |
25 | `;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/my-el.ts:
--------------------------------------------------------------------------------
1 | import { <%= className %> } from './<%= className %>.js';
2 |
3 | window.customElements.define('<%= tagName %>', <%= className %>);
4 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "dist/src/index.js",
3 | "module": "dist/src/index.js",
4 | "type": "module",
5 | "exports": {
6 | ".": "./dist/src/index.js",
7 | "./<%= tagName %>.js": "./dist/src/<%= tagName %>.js"
8 | },
9 | "scripts": {
10 | "start": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"web-dev-server\"",
11 | "build": "tsc && <%= scriptRunCommand %> analyze -- --exclude dist",
12 | "prepublish": "tsc && <%= scriptRunCommand %> analyze -- --exclude dist"
13 | },
14 | "dependencies": {
15 | "lit": "^3.1.4"
16 | },
17 | "devDependencies": {
18 | "@web/dev-server": "^0.4.6",
19 | "concurrently": "^8.2.2",
20 | "typescript": "^5.5.3",
21 | "tslib": "^2.6.3"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/partials/README.demoing.md:
--------------------------------------------------------------------------------
1 | ## Demoing with Storybook
2 |
3 | To run a local instance of Storybook for your component, run
4 |
5 | ```bash
6 | npm run storybook
7 | ```
8 |
9 | To build a production version of Storybook, run
10 |
11 | ```bash
12 | npm run storybook:build
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/partials/README.linting.md:
--------------------------------------------------------------------------------
1 | ## Linting and formatting
2 |
3 | To scan the project for linting and formatting errors, run
4 |
5 | ```bash
6 | npm run lint
7 | ```
8 |
9 | To automatically fix linting and formatting errors, run
10 |
11 | ```bash
12 | npm run format
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/partials/README.testing.md:
--------------------------------------------------------------------------------
1 | ## Testing with Web Test Runner
2 |
3 | To execute a single test run:
4 |
5 | ```bash
6 | npm run test
7 | ```
8 |
9 | To run the tests in interactive watch mode run:
10 |
11 | ```bash
12 | npm run test:watch
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/static/README.md:
--------------------------------------------------------------------------------
1 | # \<<%= tagName %>>
2 |
3 | This webcomponent follows the [open-wc](https://github.com/open-wc/open-wc) recommendation.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i <%= tagName %>
9 | ```
10 |
11 | ## Usage
12 |
13 | ```html
14 |
17 |
18 | <<%= tagName %>><%= tagName %>>
19 | ```
20 |
21 | <%= featureReadmes %>
22 |
23 | ## Tooling configs
24 |
25 | For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
26 |
27 | If you customize the configuration a lot, you can consider moving them to individual files.
28 |
29 | ## Local Demo with `web-dev-server`
30 |
31 | ```bash
32 | npm start
33 | ```
34 |
35 | To run a local development server that serves the basic demo located in `demo/index.html`
36 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/static/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/static/src/index.ts:
--------------------------------------------------------------------------------
1 | export { <%= className %> } from './<%= className %>.js';
2 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/static/web-dev-server.config.js:
--------------------------------------------------------------------------------
1 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2 |
3 | /** Use Hot Module replacement by adding --hmr to the start command */
4 | const hmr = process.argv.includes('--hmr');
5 |
6 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7 | open: '/demo/',
8 | /** Use regular watch mode if HMR is not enabled. */
9 | watch: !hmr,
10 | /** Resolve bare module imports */
11 | nodeResolve: {
12 | exportConditions: ['browser', 'development'],
13 | },
14 |
15 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
16 | // esbuildTarget: 'auto'
17 |
18 | /** Set appIndex to enable SPA routing */
19 | // appIndex: 'demo/index.html',
20 |
21 | plugins: [
22 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
23 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.lit] }),
24 | ],
25 |
26 | // See documentation for all available options
27 | });
28 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element-ts/templates/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "noEmitOnError": true,
7 | "lib": ["es2021", "dom", "DOM.Iterable"],
8 | "strict": true,
9 | "esModuleInterop": false,
10 | "allowSyntheticDefaultImports": true,
11 | "experimentalDecorators": true,
12 | "importHelpers": true,
13 | "outDir": "dist",
14 | "sourceMap": true,
15 | "inlineSources": true,
16 | "rootDir": "./",
17 | "declaration": true,
18 | "incremental": true,
19 | "skipLibCheck": true
20 | },
21 | "include": ["**/*.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-classes-per-file */
2 | import { join } from 'path';
3 | import { CommonRepoMixin } from '../common-repo/index.js';
4 | import { processTemplate, readFileFromPath } from '../../core.js';
5 |
6 | const compose = (...fns) =>
7 | fns.reduce(
8 | (f, g) =>
9 | (...args) =>
10 | f(g(...args)),
11 | );
12 | const safeReduce = (f, initial) => xs => (Array.isArray(xs) ? xs.reduce(f, initial) : xs);
13 |
14 | const getTemplatePart = compose(processTemplate, readFileFromPath);
15 |
16 | function featureReadmeBlurb(feature) {
17 | const path = join(__dirname, `./templates/partials/README.${feature}.md`);
18 | return getTemplatePart(path);
19 | }
20 |
21 | function featureReadme(acc, feature, i, a) {
22 | return `${acc + featureReadmeBlurb(feature)}${i === a.length - 1 ? '' : '\n'}`;
23 | }
24 |
25 | const safeFeatureReadme = safeReduce(featureReadme, '');
26 |
27 | /* eslint-disable no-console */
28 | export const WcLitElementMixin = subclass =>
29 | class extends subclass {
30 | async execute() {
31 | this.templateData.featureReadmes = safeFeatureReadme(this.options.features);
32 |
33 | await super.execute();
34 | const { tagName, className } = this.templateData;
35 |
36 | // write & rename el class template
37 | this.copyTemplate(
38 | `${__dirname}/templates/MyEl.js`,
39 | this.destinationPath(`src/${className}.js`),
40 | );
41 |
42 | // write & rename el registration template
43 | this.copyTemplate(`${__dirname}/templates/my-el.js`, this.destinationPath(`${tagName}.js`));
44 |
45 | await this.copyTemplates(`${__dirname}/templates/static/**/*`);
46 | }
47 | };
48 |
49 | export const WcLitElementPackageMixin = subclass =>
50 | class extends CommonRepoMixin(WcLitElementMixin(subclass)) {
51 | async execute() {
52 | await super.execute();
53 | // write & rename package.json
54 | this.copyTemplateJsonInto(
55 | `${__dirname}/templates/package.json`,
56 | this.destinationPath('package.json'),
57 | );
58 | this.copyTemplate(
59 | `${__dirname}/templates/custom-elements.json`,
60 | this.destinationPath('custom-elements.json'),
61 | );
62 | }
63 |
64 | async end() {
65 | await super.end();
66 | console.log('');
67 | console.log('You are all set up now!');
68 | console.log('');
69 | console.log('All you need to do is run:');
70 | console.log(` cd ${this.templateData.tagName}`);
71 | console.log(' npm run start');
72 | console.log('');
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/MyEl.js:
--------------------------------------------------------------------------------
1 | import { html, css, LitElement } from 'lit';
2 |
3 | export class <%= className %> extends LitElement {
4 | static styles = css`
5 | :host {
6 | display: block;
7 | padding: 25px;
8 | color: var(--<%= tagName %>-text-color, #000);
9 | }
10 | `;
11 |
12 | static properties = {
13 | header: { type: String },
14 | counter: { type: Number },
15 | };
16 |
17 | constructor() {
18 | super();
19 | this.header = 'Hey there';
20 | this.counter = 5;
21 | }
22 |
23 | __increment() {
24 | this.counter += 1;
25 | }
26 |
27 | render() {
28 | return html`
29 | ${this.header} Nr. ${this.counter}!
30 |
31 | `;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/my-el.js:
--------------------------------------------------------------------------------
1 | import { <%= className %> } from './src/<%= className %>.js';
2 |
3 | window.customElements.define('<%= tagName %>', <%= className %>);
4 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "index.js",
3 | "module": "index.js",
4 | "type": "module",
5 | "scripts": {
6 | "start": "web-dev-server"
7 | },
8 | "exports": {
9 | ".": "./index.js",
10 | "./<%= tagName %>.js": "./<%= tagName %>.js"
11 | },
12 | "dependencies": {
13 | "lit": "^3.1.4"
14 | },
15 | "devDependencies": {
16 | "@web/dev-server": "^0.4.6"
17 | }
18 | }
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/partials/README.demoing.md:
--------------------------------------------------------------------------------
1 | ## Demoing with Storybook
2 |
3 | To run a local instance of Storybook for your component, run
4 |
5 | ```bash
6 | npm run storybook
7 | ```
8 |
9 | To build a production version of Storybook, run
10 |
11 | ```bash
12 | npm run storybook:build
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/partials/README.linting.md:
--------------------------------------------------------------------------------
1 | ## Linting and formatting
2 |
3 | To scan the project for linting and formatting errors, run
4 |
5 | ```bash
6 | npm run lint
7 | ```
8 |
9 | To automatically fix linting and formatting errors, run
10 |
11 | ```bash
12 | npm run format
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/partials/README.testing.md:
--------------------------------------------------------------------------------
1 | ## Testing with Web Test Runner
2 |
3 | To execute a single test run:
4 |
5 | ```bash
6 | npm run test
7 | ```
8 |
9 | To run the tests in interactive watch mode run:
10 |
11 | ```bash
12 | npm run test:watch
13 | ```
14 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/static/README.md:
--------------------------------------------------------------------------------
1 | # \<<%= tagName %>>
2 |
3 | This webcomponent follows the [open-wc](https://github.com/open-wc/open-wc) recommendation.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i <%= tagName %>
9 | ```
10 |
11 | ## Usage
12 |
13 | ```html
14 |
17 |
18 | <<%= tagName %>><%= tagName %>>
19 | ```
20 |
21 | <%= featureReadmes %>
22 |
23 | ## Tooling configs
24 |
25 | For most of the tools, the configuration is in the `package.json` to minimize the amount of files in your project.
26 |
27 | If you customize the configuration a lot, you can consider moving them to individual files.
28 |
29 | ## Local Demo with `web-dev-server`
30 |
31 | ```bash
32 | npm start
33 | ```
34 |
35 | To run a local development server that serves the basic demo located in `demo/index.html`
36 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/static/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/static/index.js:
--------------------------------------------------------------------------------
1 | export { <%= className %> } from './src/<%= className %>.js';
2 |
--------------------------------------------------------------------------------
/src/generators/wc-lit-element/templates/static/web-dev-server.config.js:
--------------------------------------------------------------------------------
1 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2 |
3 | /** Use Hot Module replacement by adding --hmr to the start command */
4 | const hmr = process.argv.includes('--hmr');
5 |
6 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7 | open: '/demo/',
8 | /** Use regular watch mode if HMR is not enabled. */
9 | watch: !hmr,
10 | /** Resolve bare module imports */
11 | nodeResolve: {
12 | exportConditions: ['browser', 'development'],
13 | },
14 |
15 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
16 | // esbuildTarget: 'auto'
17 |
18 | /** Set appIndex to enable SPA routing */
19 | // appIndex: 'demo/index.html',
20 |
21 | plugins: [
22 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
23 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.lit] }),
24 | ],
25 |
26 | // See documentation for all available options
27 | });
28 |
--------------------------------------------------------------------------------
/test/core.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-template-curly-in-string */
2 |
3 | import chai from 'chai';
4 | import fs from 'fs';
5 | import {
6 | copyTemplateJsonInto,
7 | copyTemplates,
8 | deleteVirtualFile,
9 | executeMixinGenerator,
10 | filesToTree,
11 | optionsToCommand,
12 | processTemplate,
13 | readFileFromPath,
14 | resetVirtualFiles,
15 | setOverrideAllFiles,
16 | virtualFiles,
17 | writeFileToPath,
18 | writeFileToPathOnDisk,
19 | } from '../src/core.js';
20 |
21 | const { expect } = chai;
22 |
23 | describe('processTemplate', () => {
24 | it('replaces <%= keyName %> in source if provided as data', async () => {
25 | expect(processTemplate('prefix <%= name %> suffix', { name: 'foo' })).to.equal(
26 | 'prefix foo suffix',
27 | );
28 | });
29 |
30 | it('replaces multiple instances <%= keyName %> in source if provided as data', async () => {
31 | expect(processTemplate('prefix <%= name %> suffix <%= name %>', { name: 'foo' })).to.equal(
32 | 'prefix foo suffix foo',
33 | );
34 | });
35 |
36 | it('should throw an error if variable is not defined as data for source <%= keyName %> ', async () => {
37 | try {
38 | processTemplate('prefix <%= name %> suffix', { foo: 'foo' });
39 | } catch (e) {
40 | expect(e).to.be.an.instanceof(ReferenceError);
41 | }
42 | });
43 |
44 | it('allows passing custom EJS options like changing the delimiter', async () => {
45 | expect(
46 | processTemplate('prefix = name ?> suffix', { name: 'foo' }, { delimiter: '?' }),
47 | ).to.equal('prefix foo suffix');
48 | });
49 | });
50 |
51 | describe('writeFileToPath', () => {
52 | beforeEach(() => {
53 | resetVirtualFiles();
54 | });
55 |
56 | it('stores file to write in an array', async () => {
57 | writeFileToPath('foo/bar.js', 'barfile');
58 | expect(virtualFiles).to.deep.equal([{ path: 'foo/bar.js', content: 'barfile' }]);
59 | writeFileToPath('foo/baz.js', 'bazfile');
60 | expect(virtualFiles).to.deep.equal([
61 | { path: 'foo/bar.js', content: 'barfile' },
62 | { path: 'foo/baz.js', content: 'bazfile' },
63 | ]);
64 | });
65 |
66 | it('will override content for the same path', async () => {
67 | writeFileToPath('foo/bar.js', 'barfile');
68 | writeFileToPath('foo/bar.js', 'updated barfile');
69 | expect(virtualFiles).to.deep.equal([{ path: 'foo/bar.js', content: 'updated barfile' }]);
70 | });
71 | });
72 |
73 | describe('readFileFromPath', () => {
74 | beforeEach(() => {
75 | resetVirtualFiles();
76 | fs.writeFileSync(`./__tmpfoo.txt`, 'content of foo');
77 | });
78 | afterEach(() => {
79 | if (fs.existsSync(`./__tmpfoo.txt`)) {
80 | fs.unlinkSync(`./__tmpfoo.txt`);
81 | }
82 | });
83 |
84 | it('return false for non existing files', async () => {
85 | expect(readFileFromPath('non/existing/path.txt')).to.be.false;
86 | });
87 |
88 | it('reads file from disk', async () => {
89 | expect(readFileFromPath(`./__tmpfoo.txt`)).to.equal('content of foo');
90 | });
91 |
92 | it('reads file from virtual then from disk', async () => {
93 | writeFileToPath(`./__tmpfoo.txt`, 'virtual foo');
94 | expect(readFileFromPath(`./__tmpfoo.txt`)).to.equal('virtual foo');
95 | });
96 | });
97 |
98 | describe('deleteVirtualFile', () => {
99 | beforeEach(() => {
100 | resetVirtualFiles();
101 | });
102 |
103 | it('removes an entry from the array of virtual files', async () => {
104 | writeFileToPath('foo/bar.js', 'barfile');
105 | expect(virtualFiles).to.deep.equal([{ path: 'foo/bar.js', content: 'barfile' }]);
106 |
107 | deleteVirtualFile('foo/bar.js');
108 | expect(virtualFiles).to.deep.equal([]);
109 | });
110 | });
111 |
112 | describe('writeFileToPathOnDisk', () => {
113 | beforeEach(() => {
114 | fs.writeFileSync(`./__tmpfoo.txt`, 'content of foo');
115 | });
116 | afterEach(() => {
117 | if (fs.existsSync(`./__tmpfoo.txt`)) {
118 | fs.unlinkSync(`./__tmpfoo.txt`);
119 | }
120 | });
121 |
122 | it('will not override by default', async () => {
123 | await writeFileToPathOnDisk(`./__tmpfoo.txt`, 'updatedfoofile', { ask: false });
124 | expect(fs.readFileSync(`./__tmpfoo.txt`, 'utf-8')).to.equal('content of foo');
125 | });
126 |
127 | it('will override if set', async () => {
128 | await writeFileToPathOnDisk(`./__tmpfoo.txt`, 'updatedfoofile', {
129 | override: true,
130 | ask: false,
131 | });
132 | expect(fs.readFileSync(`./__tmpfoo.txt`, 'utf-8')).to.equal('updatedfoofile');
133 | });
134 |
135 | it('will override if setOverrideAllFiles(true) is used', async () => {
136 | setOverrideAllFiles(true);
137 | await writeFileToPathOnDisk(`./__tmpfoo.txt`, 'updatedfoofile', { ask: false });
138 | expect(fs.readFileSync(`./__tmpfoo.txt`, 'utf-8')).to.equal('updatedfoofile');
139 | setOverrideAllFiles(false);
140 | });
141 | });
142 |
143 | describe('copyTemplates', () => {
144 | it('returns a promise which resolves with the copied and processed files', async () => {
145 | const copiedFiles = await copyTemplates(`./test/template/**/*`, `source`, {
146 | name: 'hello-world',
147 | });
148 | expect(copiedFiles).to.deep.equal([
149 | {
150 | processed: "console.log('name: hello-world');\n",
151 | toPath: './source/index.js',
152 | },
153 | ]);
154 | });
155 | });
156 |
157 | describe('copyTemplateJsonInto', () => {
158 | beforeEach(() => {
159 | resetVirtualFiles();
160 | });
161 |
162 | it('merges objects', async () => {
163 | writeFileToPath(`source/package.json`, '{ "source": "data" }');
164 | writeFileToPath(`generator/package.json`, '{ "from": "generator" }');
165 | copyTemplateJsonInto(`generator/package.json`, 'source/package.json');
166 | deleteVirtualFile('generator/package.json'); // just used to make test easier
167 |
168 | expect(virtualFiles).to.deep.equal([
169 | {
170 | path: 'source/package.json',
171 | content: '{\n "source": "data",\n "from": "generator"\n}',
172 | },
173 | ]);
174 | });
175 |
176 | it('merges arrays', async () => {
177 | writeFileToPath(`source/package.json`, '{ "array": [1, 2] }');
178 | writeFileToPath(`generator/package.json`, '{ "array": [3] }');
179 | copyTemplateJsonInto(`generator/package.json`, 'source/package.json');
180 | deleteVirtualFile('generator/package.json'); // just used to make test easier
181 |
182 | expect(virtualFiles).to.deep.equal([
183 | {
184 | path: 'source/package.json',
185 | content: '{\n "array": [\n 1,\n 2,\n 3\n ]\n}',
186 | },
187 | ]);
188 | });
189 |
190 | it('can override arrays by setting { mode: "override" } ', async () => {
191 | writeFileToPath(`source/package.json`, '{ "array": [1, 2] }');
192 | writeFileToPath(`generator/package.json`, '{ "array": [3] }');
193 | copyTemplateJsonInto(
194 | `generator/package.json`,
195 | 'source/package.json',
196 | {},
197 | {
198 | mode: 'override',
199 | },
200 | );
201 | deleteVirtualFile('generator/package.json'); // just used to make test easier
202 |
203 | expect(virtualFiles).to.deep.equal([
204 | {
205 | path: 'source/package.json',
206 | content: '{\n "array": [\n 3\n ]\n}',
207 | },
208 | ]);
209 | });
210 | });
211 |
212 | describe('executeMixinGenerator', () => {
213 | it('combines multiple mixins and executes them', async () => {
214 | const FooMixin = subclass =>
215 | class extends subclass {
216 | constructor() {
217 | super();
218 | this.foo = true;
219 | }
220 | };
221 |
222 | const BarMixin = subclass =>
223 | class extends subclass {
224 | constructor() {
225 | super();
226 | this.bar = true;
227 | }
228 | };
229 |
230 | const data = {};
231 | class Base {
232 | execute() {
233 | // @ts-ignore
234 | data.foo = this.foo;
235 | // @ts-ignore
236 | data.bar = this.bar;
237 | data.gotExecuted = true;
238 | }
239 |
240 | // eslint-disable-next-line class-methods-use-this
241 | end() {
242 | data.gotEnded = true;
243 | }
244 | }
245 |
246 | // @ts-ignore
247 | await executeMixinGenerator([FooMixin, BarMixin], {}, Base);
248 |
249 | expect(data).to.deep.equal({
250 | foo: true,
251 | bar: true,
252 | gotExecuted: true,
253 | gotEnded: true,
254 | });
255 | });
256 | });
257 |
258 | describe('optionsToCommand', () => {
259 | it('supports strings', async () => {
260 | const options = {
261 | type: 'scaffold',
262 | };
263 | expect(optionsToCommand(options)).to.equal('npm init @open-wc --type scaffold ');
264 | });
265 |
266 | it('supports numbers', async () => {
267 | const options = {
268 | version: 2,
269 | };
270 | expect(optionsToCommand(options)).to.equal('npm init @open-wc --version 2 ');
271 | });
272 |
273 | it('supports boolean', async () => {
274 | const options = {
275 | writeToDisk: true,
276 | };
277 | expect(optionsToCommand(options)).to.equal('npm init @open-wc --writeToDisk ');
278 | const options2 = {
279 | writeToDisk: false,
280 | };
281 | expect(optionsToCommand(options2)).to.equal('npm init @open-wc ');
282 | });
283 |
284 | it('supports arrays', async () => {
285 | const options = {
286 | features: ['testing', 'demoing'],
287 | };
288 | expect(optionsToCommand(options)).to.equal('npm init @open-wc --features testing demoing ');
289 | });
290 |
291 | it('converts real example', async () => {
292 | const options = {
293 | type: 'scaffold',
294 | scaffoldType: 'wc',
295 | features: ['testing', 'demoing'],
296 | tagName: 'foo-bar',
297 | installDependencies: 'false',
298 | };
299 | expect(optionsToCommand(options)).to.equal(
300 | 'npm init @open-wc --type scaffold --scaffoldType wc --features testing demoing --tagName foo-bar --installDependencies false ',
301 | );
302 | });
303 | });
304 |
305 | describe('filesToTree', () => {
306 | it('renders a single file', async () => {
307 | expect(filesToTree(['./foo.js'])).to.equal(['./', '└── foo.js\n'].join('\n'));
308 | });
309 |
310 | it('renders two files', async () => {
311 | // prettier-ignore
312 | expect(filesToTree(['./foo.js', './bar.js'])).to.equal([
313 | './',
314 | '├── foo.js',
315 | '└── bar.js\n',
316 | ].join('\n'));
317 | });
318 |
319 | it('renders multiple files', async () => {
320 | // prettier-ignore
321 | expect(filesToTree(['./foo.js', './bar.js', './baz.js'])).to.equal([
322 | './',
323 | '├── foo.js',
324 | '├── bar.js',
325 | '└── baz.js\n',
326 | ].join('\n'));
327 | });
328 |
329 | it('renders a directory and file', async () => {
330 | // prettier-ignore
331 | expect(filesToTree(['./foo/foo.js'])).to.equal([
332 | './',
333 | '├── foo/',
334 | '│ └── foo.js\n',
335 | ].join('\n'));
336 | });
337 |
338 | it('renders a directory and file and root file', async () => {
339 | // prettier-ignore
340 | expect(filesToTree(['./foo/foo.js', './bar.js'])).to.equal([
341 | './',
342 | '├── foo/',
343 | '│ └── foo.js',
344 | '└── bar.js\n',
345 | ].join('\n'));
346 | });
347 |
348 | it('renders a nested directory and file', async () => {
349 | // prettier-ignore
350 | expect(filesToTree(['./foo/bar/baz/bong.js'])).to.equal([
351 | './',
352 | '├── foo/',
353 | '│ ├── bar/',
354 | '│ │ ├── baz/',
355 | '│ │ │ └── bong.js\n',
356 | ].join('\n'));
357 | });
358 |
359 | it('renders a nested directory and file', async () => {
360 | // prettier-ignore
361 | expect(filesToTree(['./foo/bar.js', './foo/foo.js'])).to.equal([
362 | './',
363 | '├── foo/',
364 | '│ ├── bar.js',
365 | '│ └── foo.js\n',
366 | ].join('\n'));
367 | });
368 |
369 | it('renders a nested directory and file', async () => {
370 | // prettier-ignore
371 | expect(filesToTree(['./foo/bar/baz.js', './foo/foo.js'])).to.equal([
372 | './',
373 | '├── foo/',
374 | '│ ├── bar/',
375 | '│ │ └── baz.js',
376 | '│ └── foo.js\n',
377 | ].join('\n'));
378 | });
379 |
380 | it('renders an common usecase', async () => {
381 | expect(
382 | filesToTree([
383 | './foo-bar/src/FooBar.js',
384 | './foo-bar/src/foo-bar.js',
385 | './foo-bar/LICENSE',
386 | './foo-bar/README.md',
387 | ]),
388 | ).to.equal(
389 | [
390 | './',
391 | '├── foo-bar/',
392 | '│ ├── src/',
393 | '│ │ ├── FooBar.js',
394 | '│ │ └── foo-bar.js',
395 | '│ ├── LICENSE',
396 | '│ └── README.md\n',
397 | ].join('\n'),
398 | );
399 | });
400 | });
401 |
--------------------------------------------------------------------------------
/test/generate-command.js:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | const COMMAND_PATH = join(__dirname, '../src/create.js');
4 |
5 | export function generateCommand({ destinationPath = '.' } = {}) {
6 | return `node -r @babel/register ${COMMAND_PATH} \
7 | --destinationPath ${destinationPath} \
8 | --type scaffold \
9 | --scaffoldType app \
10 | --features linting testing demoing building \
11 | --typescript false \
12 | --tagName scaffold-app \
13 | --writeToDisk true \
14 | --installDependencies false
15 | `;
16 | }
17 |
--------------------------------------------------------------------------------
/test/integration.test.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import _rimraf from 'rimraf';
3 | import chai from 'chai';
4 | import chaiFs from 'chai-fs';
5 | import { exec as _exec } from 'child_process';
6 | import { promisify } from 'util';
7 | import { lstatSync, readdirSync, readFileSync } from 'fs';
8 | import { join } from 'path';
9 | import { ESLint } from 'eslint';
10 | import { generateCommand } from './generate-command.js';
11 |
12 | const exec = promisify(_exec);
13 |
14 | const rimraf = promisify(_rimraf);
15 |
16 | const { expect } = chai;
17 |
18 | chai.use(chaiFs);
19 |
20 | /** @param {ESLint.LintResult} result */
21 | const getFileMessages = ({ messages, filePath }) =>
22 | !messages.length
23 | ? ''
24 | : messages
25 | .map(
26 | ({ ruleId, line, column, message }) =>
27 | `${filePath} ${line}:${column}\n${message} (${ruleId})`,
28 | )
29 | .join('\n\n')
30 | .trimEnd();
31 |
32 | const ACTUAL_PATH = join(process.cwd(), './scaffold-app');
33 |
34 | /**
35 | * Deletes the test files
36 | */
37 | async function deleteGenerated() {
38 | await rimraf(ACTUAL_PATH);
39 | }
40 |
41 | /**
42 | * Removes text from the cli output which is specific to the local environment, i.e. the full path to the output dir.
43 | * @param {string} output raw output
44 | * @return {string} cleaned output
45 | */
46 | function stripUserDir(output) {
47 | return output.replace(/\b(.*)\/scaffold-app/, '/scaffold-app');
48 | }
49 |
50 | /**
51 | * Asserts that the contents of a file at a path equal the contents of a file at another path
52 | * @param {string} expectedPath path to expected output
53 | * @param {string} actualPath path to actual output
54 | */
55 | function assertFile(expectedPath, actualPath) {
56 | expect(actualPath).to.be.a.file().and.equal(expectedPath);
57 | }
58 |
59 | /**
60 | * Recursively checks a directory's contents, asserting each file's contents
61 | * matches it's counterpart in a snapshot directory
62 | * @param {string} expectedPath snapshot directory path
63 | * @param {string} actualPath output directory path
64 | */
65 | function checkSnapshotContents(expectedPath, actualPath) {
66 | readdirSync(actualPath).forEach(filename => {
67 | const actualFilePath = join(actualPath, filename);
68 | const expectedFilePath = join(expectedPath, filename);
69 | return lstatSync(actualFilePath).isDirectory()
70 | ? checkSnapshotContents(expectedFilePath, actualFilePath)
71 | : assertFile(expectedFilePath, actualFilePath);
72 | });
73 | }
74 |
75 | let stdout;
76 | let stderr;
77 | let EXPECTED_OUTPUT;
78 |
79 | const generate = ({ command, expectedPath }) =>
80 | async function generateTestProject() {
81 | ({ stdout, stderr } = await exec(command));
82 | const EXPECTED_PATH = join(expectedPath, '../fully-loaded-app.output.txt');
83 | EXPECTED_OUTPUT = readFileSync(EXPECTED_PATH, 'utf-8');
84 | };
85 |
86 | describe('create', function create() {
87 | this.timeout(10000);
88 |
89 | // For some reason, this doesn't do anything
90 | const destinationPath = join(__dirname, './output');
91 |
92 | const expectedPath = join(__dirname, './snapshots/fully-loaded-app');
93 |
94 | const command = generateCommand({ destinationPath });
95 |
96 | before(generate({ command, expectedPath }));
97 |
98 | after(deleteGenerated);
99 |
100 | it('scaffolds a fully loaded app project', async () => {
101 | // Check that all files exist, without checking their contents
102 | expect(ACTUAL_PATH).to.be.a.directory().and.deep.equal(expectedPath);
103 | });
104 |
105 | it('generates expected file contents', () => {
106 | // Check recursively all file contents
107 | checkSnapshotContents(expectedPath, ACTUAL_PATH);
108 | });
109 |
110 | it.skip('outputs expected message', () => {
111 | expect(stripUserDir(stdout)).to.equal(stripUserDir(EXPECTED_OUTPUT));
112 | });
113 |
114 | it('does not exit with an error', () => {
115 | expect(stderr).to.not.be.ok;
116 | });
117 |
118 | it('generates a project which passes linting', async () => {
119 | const linter = new ESLint({ useEslintrc: true });
120 | const results = await linter.lintFiles([ACTUAL_PATH]);
121 | const errorCountTotal = results.reduce((sum, r) => sum + r.errorCount, 0);
122 | const warningCountTotal = results.reduce((sum, r) => sum + r.warningCount, 0);
123 | const prettyOutput = `\n\n${results.map(getFileMessages).join('\n')}\n\n`;
124 | expect(errorCountTotal, 'error count').to.equal(0, prettyOutput);
125 | expect(warningCountTotal, 'warning count').to.equal(0, prettyOutput);
126 | });
127 |
128 | it('generates a project with a custom-elements manifest', async () => {
129 | const { customElements } = JSON.parse(readFileSync(join(ACTUAL_PATH, 'package.json'), 'utf8'));
130 | expect(customElements).to.equal('custom-elements.json');
131 | const e = await exec('npm run analyze', { cwd: ACTUAL_PATH });
132 | expect(e.stderr, stderr).to.not.be.ok;
133 | const manifest = JSON.parse(readFileSync(join(ACTUAL_PATH, 'custom-elements.json'), 'utf8'));
134 | expect(manifest.modules.length).to.equal(2);
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app.output.txt:
--------------------------------------------------------------------------------
1 |
2 | _.,,,,,,,,,._
3 | .d'' ``b. Open Web Components Recommendations
4 | .p' Open `q.
5 | .d' Web Components `b. Start or upgrade your web component project with
6 | .d' `b. ease. All our recommendations at your fingertips.
7 | :: ................. ::
8 | `p. .q' See more details at https://open-wc.org/docs/development/generator/
9 | `p. open-wc.org .q'
10 | `b. @openWc .d'
11 | `q.. ..,' Note: you can exit any time with Ctrl+C or Esc
12 | '',,,,,,,,,,''
13 |
14 |
15 |
16 | ./
17 | ├── scaffold-app/
18 | │ ├── .storybook/
19 | │ │ ├── main.js
20 | │ │ └── preview.js
21 | │ ├── components/
22 | │ │ ├── page-main/
23 | │ │ │ ├── /
24 | │ │ │ │ ├── demo/
25 | │ │ │ │ │ └── index.html
26 | │ │ │ │ ├── index.js
27 | │ │ │ │ └── README.md
28 | │ │ │ ├── src/
29 | │ │ │ │ └── PageMain.js
30 | │ │ │ └── page-main.js
31 | │ │ ├── page-one/
32 | │ │ │ ├── /
33 | │ │ │ │ ├── demo/
34 | │ │ │ │ │ └── index.html
35 | │ │ │ │ ├── index.js
36 | │ │ │ │ └── README.md
37 | │ │ │ ├── src/
38 | │ │ │ │ └── PageOne.js
39 | │ │ │ └── page-one.js
40 | │ │ ├── scaffold-app/
41 | │ │ │ ├── demo/
42 | │ │ │ │ └── index.html
43 | │ │ │ ├── src/
44 | │ │ │ │ ├── open-wc-logo.js
45 | │ │ │ │ ├── ScaffoldApp.js
46 | │ │ │ │ └── templateAbout.js
47 | │ │ │ ├── test/
48 | │ │ │ │ └── scaffold-app.test.js
49 | │ │ │ ├── index.js
50 | │ │ │ ├── README.md
51 | │ │ │ └── scaffold-app.js
52 | │ ├── .editorconfig
53 | │ ├── .gitignore
54 | │ ├── custom-elements.json
55 | │ ├── index.html
56 | │ ├── web-test-runner.config.js
57 | │ ├── LICENSE
58 | │ ├── package.json
59 | │ ├── README.md
60 | │ └── rollup.config.js
61 |
62 | Writing..... done
63 |
64 | If you want to rerun this exact same generator you can do so by executing:
65 | npm init @open-wc --destinationPath /path/to/open-wc/scaffold-app --type scaffold --scaffoldType app --features linting testing demoing building --buildingType rollup --tagName scaffold-app --writeToDisk true --installDependencies false
66 |
67 | You are all set up now!
68 |
69 | All you need to do is run:
70 | cd scaffold-app
71 | npm run start
72 |
73 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
23 | [*.json]
24 | indent_size = 2
25 |
26 | [*.{html,js,md}]
27 | block_comment_start = /**
28 | block_comment = *
29 | block_comment_end = */
30 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/.gitignore:
--------------------------------------------------------------------------------
1 | ## editors
2 | /.idea
3 | /.vscode
4 |
5 | ## system files
6 | .DS_Store
7 |
8 | ## npm
9 | /node_modules/
10 | /npm-debug.log
11 |
12 | ## testing
13 | /coverage/
14 |
15 | ## temp folders
16 | /.tmp/
17 |
18 | # build
19 | /_site/
20 | /dist/
21 | /out-tsc/
22 |
23 | storybook-static
24 | custom-elements.json
25 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | ./node_modules/.bin/lint-staged
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../**/stories/*.stories.{js,md,mdx}'],
3 | };
4 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 scaffold-app
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Open-wc Starter App
6 |
7 | [](https://github.com/open-wc)
8 |
9 | ## Quickstart
10 |
11 | To get started:
12 |
13 | ```bash
14 | npm init @open-wc
15 | # requires node 10 & npm 6 or higher
16 | ```
17 |
18 | ## Scripts
19 |
20 | - `start` runs your app for development, reloading on file changes
21 | - `start:build` runs your app after it has been built using the build command
22 | - `build` builds your app and outputs it in your `dist` directory
23 | - `test` runs your test suite with Web Test Runner
24 | - `lint` runs the linter for your project
25 | - `format` fixes linting and formatting errors
26 |
27 | ## Tooling configs
28 |
29 | For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
30 |
31 | If you customize the configuration a lot, you can consider moving them to individual files.
32 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/assets/open-wc-logo.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 | scaffold-app
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scaffold-app",
3 | "description": "Webcomponent scaffold-app following open-wc recommendations",
4 | "license": "MIT",
5 | "author": "scaffold-app",
6 | "version": "0.0.0",
7 | "type": "module",
8 | "scripts": {
9 | "lint": "eslint --ext .js,.html . --ignore-path .gitignore && prettier \"**/*.js\" --check --ignore-path .gitignore",
10 | "format": "eslint --ext .js,.html . --fix --ignore-path .gitignore && prettier \"**/*.js\" --write --ignore-path .gitignore",
11 | "test": "web-test-runner --coverage",
12 | "test:watch": "web-test-runner --watch",
13 | "storybook": "npm run analyze -- --exclude dist && web-dev-server -c .storybook/server.mjs",
14 | "storybook:build": "npm run analyze -- --exclude dist && build-storybook",
15 | "build": "rimraf dist && rollup -c rollup.config.js && npm run analyze -- --exclude dist",
16 | "start:build": "web-dev-server --root-dir dist --app-index index.html --open",
17 | "analyze": "cem analyze --litelement",
18 | "start": "web-dev-server",
19 | "prepare": "husky"
20 | },
21 | "dependencies": {
22 | "lit": "^3.0.0"
23 | },
24 | "devDependencies": {
25 | "@custom-elements-manifest/analyzer": "^0.10.2",
26 | "@open-wc/eslint-config": "^12.0.3",
27 | "@open-wc/testing": "^4.0.0",
28 | "@rollup/plugin-babel": "^6.0.4",
29 | "@rollup/plugin-node-resolve": "^15.2.3",
30 | "@web/dev-server": "^0.4.5",
31 | "@web/dev-server-storybook": "^2.0.3",
32 | "@web/rollup-plugin-html": "^2.3.0",
33 | "@web/rollup-plugin-import-meta-assets": "^2.2.1",
34 | "@web/test-runner": "^0.18.2",
35 | "babel-plugin-template-html-minifier": "^4.1.0",
36 | "deepmerge": "^4.3.1",
37 | "eslint": "^8.31.0",
38 | "eslint-config-prettier": "^9.1.0",
39 | "husky": "^9.0.11",
40 | "lint-staged": "^15.2.5",
41 | "prettier": "^3.2.5",
42 | "rimraf": "^5.0.7",
43 | "rollup": "^4.18.0",
44 | "rollup-plugin-esbuild": "^6.1.1",
45 | "rollup-plugin-workbox": "^8.1.0"
46 | },
47 | "eslintConfig": {
48 | "extends": [
49 | "@open-wc",
50 | "prettier"
51 | ]
52 | },
53 | "prettier": {
54 | "singleQuote": true,
55 | "arrowParens": "avoid"
56 | },
57 | "lint-staged": {
58 | "*.js": [
59 | "eslint --fix",
60 | "prettier --write"
61 | ]
62 | },
63 | "customElements": "custom-elements.json"
64 | }
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from '@rollup/plugin-node-resolve';
2 | import babel from '@rollup/plugin-babel';
3 | import { rollupPluginHTML as html } from '@web/rollup-plugin-html';
4 | import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
5 | import esbuild from 'rollup-plugin-esbuild';
6 | import { generateSW } from 'rollup-plugin-workbox';
7 | import path from 'path';
8 |
9 | export default {
10 | input: 'index.html',
11 | output: {
12 | entryFileNames: '[hash].js',
13 | chunkFileNames: '[hash].js',
14 | assetFileNames: '[hash][extname]',
15 | format: 'es',
16 | dir: 'dist',
17 | },
18 | preserveEntrySignatures: false,
19 |
20 | plugins: [
21 | /** Enable using HTML as rollup entrypoint */
22 | html({
23 | minify: true,
24 | injectServiceWorker: true,
25 | serviceWorkerPath: 'dist/sw.js',
26 | }),
27 | /** Resolve bare module imports */
28 | nodeResolve(),
29 | /** Minify JS, compile JS to a lower language target */
30 | esbuild({
31 | minify: true,
32 | target: ['chrome64', 'firefox67', 'safari11.1'],
33 | }),
34 | /** Bundle assets references via import.meta.url */
35 | importMetaAssets(),
36 | /** Minify html and css tagged template literals */
37 | babel({
38 | plugins: [
39 | [
40 | 'babel-plugin-template-html-minifier',
41 | {
42 | modules: { lit: ['html', { name: 'css', encapsulation: 'style' }] },
43 | failOnError: false,
44 | strictCSS: true,
45 | htmlMinifier: {
46 | collapseWhitespace: true,
47 | conservativeCollapse: true,
48 | removeComments: true,
49 | caseSensitive: true,
50 | minifyCSS: true,
51 | },
52 | },
53 | ],
54 | ],
55 | }),
56 | /** Create and inject a service worker */
57 | generateSW({
58 | globIgnores: ['polyfills/*.js', 'nomodule-*.js'],
59 | navigateFallback: '/index.html',
60 | // where to output the generated sw
61 | swDest: path.join('dist', 'sw.js'),
62 | // directory to match patterns against to be precached
63 | globDirectory: path.join('dist'),
64 | // cache any html js and css by default
65 | globPatterns: ['**/*.{html,js,css,webmanifest}'],
66 | skipWaiting: true,
67 | clientsClaim: true,
68 | runtimeCaching: [{ urlPattern: 'polyfills/*.js', handler: 'CacheFirst' }],
69 | }),
70 | ],
71 | };
72 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/src/scaffold-app.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit';
2 |
3 | const logo = new URL('../assets/open-wc-logo.svg', import.meta.url).href;
4 |
5 | class ScaffoldApp extends LitElement {
6 | static properties = {
7 | header: { type: String },
8 | }
9 |
10 | static styles = css`
11 | :host {
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: flex-start;
17 | font-size: calc(10px + 2vmin);
18 | color: #1a2b42;
19 | max-width: 960px;
20 | margin: 0 auto;
21 | text-align: center;
22 | background-color: var(--scaffold-app-background-color);
23 | }
24 |
25 | main {
26 | flex-grow: 1;
27 | }
28 |
29 | .logo {
30 | margin-top: 36px;
31 | animation: app-logo-spin infinite 20s linear;
32 | }
33 |
34 | @keyframes app-logo-spin {
35 | from {
36 | transform: rotate(0deg);
37 | }
38 | to {
39 | transform: rotate(360deg);
40 | }
41 | }
42 |
43 | .app-footer {
44 | font-size: calc(12px + 0.5vmin);
45 | align-items: center;
46 | }
47 |
48 | .app-footer a {
49 | margin-left: 5px;
50 | }
51 | `;
52 |
53 | constructor() {
54 | super();
55 | this.header = 'My app';
56 | }
57 |
58 | render() {
59 | return html`
60 |
61 |
62 | ${this.header}
63 |
64 | Edit src/ScaffoldApp.js
and save to reload.
65 |
71 | Code examples
72 |
73 |
74 |
75 |
84 | `;
85 | }
86 | }
87 |
88 | customElements.define('scaffold-app', ScaffoldApp);
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/stories/scaffold-app.stories.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import '../src/scaffold-app.js';
3 |
4 | export default {
5 | title: 'ScaffoldApp',
6 | component: 'scaffold-app',
7 | argTypes: {
8 | backgroundColor: { control: 'color' },
9 | },
10 | };
11 |
12 | function Template({ header, backgroundColor }) {
13 | return html`
14 |
18 |
19 | `;
20 | }
21 |
22 | export const App = Template.bind({});
23 | App.args = {
24 | header: 'My app',
25 | };
26 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/test/scaffold-app.test.js:
--------------------------------------------------------------------------------
1 | import { html } from 'lit';
2 | import { fixture, expect } from '@open-wc/testing';
3 |
4 | import '../src/scaffold-app.js';
5 |
6 | describe('ScaffoldApp', () => {
7 | let element;
8 | beforeEach(async () => {
9 | element = await fixture(html``);
10 | });
11 |
12 | it('renders a h1', () => {
13 | const h1 = element.shadowRoot.querySelector('h1');
14 | expect(h1).to.exist;
15 | expect(h1.textContent).to.equal('My app');
16 | });
17 |
18 | it('passes the a11y audit', async () => {
19 | await expect(element).shadowDom.to.be.accessible();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/web-dev-server.config.js:
--------------------------------------------------------------------------------
1 | // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2 |
3 | /** Use Hot Module replacement by adding --hmr to the start command */
4 | const hmr = process.argv.includes('--hmr');
5 |
6 | export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7 | open: '/',
8 | watch: !hmr,
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
15 | // esbuildTarget: 'auto'
16 |
17 | /** Set appIndex to enable SPA routing */
18 | appIndex: './index.html',
19 |
20 | plugins: [
21 | /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
22 | // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
23 | ],
24 |
25 | // See documentation for all available options
26 | });
27 |
--------------------------------------------------------------------------------
/test/snapshots/fully-loaded-app/web-test-runner.config.js:
--------------------------------------------------------------------------------
1 | // import { playwrightLauncher } from '@web/test-runner-playwright';
2 |
3 | const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
4 |
5 | export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6 | /** Test files to run */
7 | files: 'test/**/*.test.js',
8 |
9 | /** Resolve bare module imports */
10 | nodeResolve: {
11 | exportConditions: ['browser', 'development'],
12 | },
13 |
14 | /** Filter out lit dev mode logs */
15 | filterBrowserLogs(log) {
16 | for (const arg of log.args) {
17 | if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18 | return false;
19 | }
20 | }
21 | return true;
22 | },
23 |
24 | /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25 | // esbuildTarget: 'auto',
26 |
27 | /** Amount of browsers to run concurrently */
28 | // concurrentBrowsers: 2,
29 |
30 | /** Amount of test files per browser to test concurrently */
31 | // concurrency: 1,
32 |
33 | /** Browsers to run tests on */
34 | // browsers: [
35 | // playwrightLauncher({ product: 'chromium' }),
36 | // playwrightLauncher({ product: 'firefox' }),
37 | // playwrightLauncher({ product: 'webkit' }),
38 | // ],
39 |
40 | // See documentation for all available options
41 | });
42 |
--------------------------------------------------------------------------------
/test/template/index.js:
--------------------------------------------------------------------------------
1 | console.log('name: <%= name %>');
2 |
--------------------------------------------------------------------------------
/test/update-snapshots.js:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 |
3 | import { existsSync } from 'fs';
4 |
5 | import { execSync } from 'child_process';
6 |
7 | import { generateCommand } from './generate-command.js';
8 |
9 | const destinationPath = join(__dirname, './snapshots');
10 |
11 | const UPDATE_SNAPSHOTS_COMMAND = generateCommand({ destinationPath });
12 |
13 | execSync(UPDATE_SNAPSHOTS_COMMAND);
14 |
15 | // HACK(bennyp): destinationPath doesn't work.
16 | // see https://github.com/open-wc/open-wc/issues/1040
17 | const OOPS_I_WROTE_TO_PACKAGE_ROOT = join(process.cwd(), './scaffold-app');
18 |
19 | const DESTINATION_PATH = join(__dirname, './snapshots/fully-loaded-app');
20 |
21 | if (existsSync(OOPS_I_WROTE_TO_PACKAGE_ROOT)) {
22 | execSync(`rm -rf ${DESTINATION_PATH}`);
23 | execSync(`mv -f ${OOPS_I_WROTE_TO_PACKAGE_ROOT} ${DESTINATION_PATH}`);
24 | }
25 |
--------------------------------------------------------------------------------