├── .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}>> 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 %>>>`); 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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 28 | 29 | 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 | [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](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 %>>> 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 | > 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 %>>>`); 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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 28 | 29 | 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 | [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](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 %>>> 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 | > 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 | > 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 %>>>`); 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 %>>>`); 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">>`); 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 %>>>`); 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 %>>>`); 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 %>>>`); 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">>`); 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 %>>>`); 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 %>>> 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 %>>> 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 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 | [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 28 | 29 | 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 | --------------------------------------------------------------------------------