diff --git a/.gitignore b/.gitignore index 1e9ffad7c..c21190acc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,11 @@ packages/lit-dev-content/site/docs/unversioned packages/lit-dev-api/api-data/*/repo/ packages/lit-dev-api/api-data/*/INSTALLED +packages/lit-dev-tutorial-plugin/out +packages/lit-dev-tutorial-plugin/lit-dev-tutorial-editor*.vsix +.vscode-test +.test-bin + # We only want to keep '-linux' screenshots which are tested by Github Actions. packages/lit-dev-tests/src/playwright/**/*.png !packages/lit-dev-tests/src/playwright/**/*-linux.png diff --git a/.prettierignore b/.prettierignore index 41c915fed..e260f6508 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,9 @@ package-lock.json /packages/lit-dev-content/_dev/ /packages/lit-dev-content/_site/ /packages/lit-dev-content/rollupout/ +/packages/lit-dev-tutorial-plugin/.vscode-test/ +/packages/lit-dev-tutorial-plugin/.test-bin/ +/packages/lit-dev-tutorial-plugin/out/ # TODO(aomarks) Would be nice to format samples, but Prettier doesn't always do # a great job compared to our manual formatting. /packages/lit-dev-content/samples/ diff --git a/package-lock.json b/package-lock.json index fb1ce78fc..e9e06079b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -184,19 +184,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/@11ty/eleventy/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@11ty/eleventy/node_modules/entities": { "version": "3.0.1", "dev": true, @@ -241,14 +228,6 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/@11ty/eleventy/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@11ty/eleventy/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -264,39 +243,6 @@ "node": ">=10" } }, - "node_modules/@11ty/eleventy/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@11ty/eleventy/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@11ty/eleventy/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@11ty/lodash-custom": { "version": "4.17.21", "dev": true, @@ -513,6 +459,102 @@ "dev": true, "license": "ISC" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.1", "dev": true, @@ -1649,6 +1691,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "license": "MIT", @@ -2025,6 +2077,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, "node_modules/@types/node": { "version": "16.11.41", "license": "MIT" @@ -2126,6 +2184,12 @@ "node": ">=0.10.0" } }, + "node_modules/@types/vscode": { + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.87.0.tgz", + "integrity": "sha512-y3yYJV2esWr8LNjp3VNbSMWG7Y43jC8pCldG8YwiHGAQbsymkkMMt0aDT1xZIOFM2eFcNiUc+dJMx1+Z0UT8fg==", + "dev": true + }, "node_modules/@types/ws": { "version": "7.4.7", "license": "MIT", @@ -2133,6 +2197,36 @@ "@types/node": "*" } }, + "node_modules/@vscode/test-electron": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/test-electron/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@web/config-loader": { "version": "0.1.3", "license": "MIT", @@ -2352,6 +2446,15 @@ "@algolia/transporter": "4.14.2" } }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escape-sequences": { "version": "6.2.1", "license": "MIT", @@ -2791,6 +2894,12 @@ "version": "1.0.0", "license": "BSD-2-Clause" }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/btoa-lite": { "version": "1.0.0", "license": "MIT" @@ -3049,6 +3158,17 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/clone": { "version": "2.1.2", "license": "MIT", @@ -3263,6 +3383,20 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-select": { "version": "5.1.0", "dev": true, @@ -3356,6 +3490,18 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decimal.js": { "version": "10.3.1", "license": "MIT" @@ -3594,6 +3740,12 @@ "node_modules/duplexer": { "version": "0.1.1" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "license": "Apache-2.0", @@ -3688,6 +3840,15 @@ "version": "0.9.3", "license": "MIT" }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "license": "MIT" @@ -3954,6 +4115,31 @@ "node": ">=6" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/for-each": { "version": "0.3.3", "license": "MIT", @@ -3962,6 +4148,34 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "license": "MIT", @@ -4175,6 +4389,15 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "license": "MIT", @@ -4763,6 +4986,12 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/indent-string": { "version": "4.0.0", "license": "MIT", @@ -4998,6 +5227,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "license": "MIT", @@ -5070,6 +5308,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "license": "MIT", @@ -5103,8 +5353,9 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/iso-639-1": { "version": "2.1.15", @@ -5114,6 +5365,24 @@ "node": ">=6.0" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.8.5", "dev": true, @@ -5420,10 +5689,22 @@ "promise": "^7.0.1" } }, - "node_modules/junk": { - "version": "1.0.3", + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, - "license": "MIT", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/junk": { + "version": "1.0.3", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5576,6 +5857,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/limited-request-queue": { "version": "2.0.0", "license": "MIT", @@ -5676,6 +5966,10 @@ "resolved": "packages/lit-dev-tools-esm", "link": true }, + "node_modules/lit-dev-tutorial-editor": { + "resolved": "packages/lit-dev-tutorial-plugin", + "link": true + }, "node_modules/lit-element": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", @@ -5694,6 +5988,21 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "license": "MIT" @@ -5727,6 +6036,92 @@ "version": "4.5.0", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lower-case": { "version": "1.1.4", "dev": true, @@ -6001,6 +6396,159 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mocha": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", + "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "8.1.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/moo": { "version": "0.5.2", "dev": true, @@ -6380,6 +6928,42 @@ "version": "0.8.0", "license": "MIT" }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/param-case": { "version": "2.1.1", "dev": true, @@ -6440,6 +7024,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "license": "MIT", @@ -6447,10 +7040,53 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "6.2.1", "dev": true, @@ -7045,6 +7681,15 @@ "node": ">= 0.10" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7334,10 +7979,37 @@ "randombytes": "^2.1.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/shiki": { "version": "0.14.2", "license": "MIT", @@ -7515,6 +8187,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -7526,6 +8213,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "dev": true, @@ -7541,6 +8241,18 @@ "node": ">=10" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/striptags": { "version": "3.2.0", "license": "MIT" @@ -8186,6 +8898,21 @@ "node": ">=12" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-typed-array": { "version": "1.1.9", "license": "MIT", @@ -8285,6 +9012,113 @@ "node": ">=8" } }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -8343,10 +9177,61 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "license": "ISC" }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ylru": { "version": "1.3.2", "license": "MIT", @@ -8354,6 +9239,18 @@ "node": ">= 4.0.0" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/lit-dev-api": { "version": "0.0.0", "license": "BSD-3-Clause", @@ -8589,6 +9486,76 @@ "lit-dev-tools-cjs": "^0.0.0", "prettier": "^2.3.2" } + }, + "packages/lit-dev-tutorial-plugin": { + "version": "0.0.0", + "license": "BSD-3-Clause", + "devDependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^16.0.0", + "@types/vscode": "^1.87.0", + "@vscode/test-electron": "^2.3.9", + "glob": "^10.3.0", + "mocha": "^10.0.6" + }, + "engines": { + "vscode": "^1.65.0" + } + }, + "packages/lit-dev-tutorial-plugin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/lit-dev-tutorial-plugin/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/lit-dev-tutorial-plugin/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/lit-dev-tutorial-plugin/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } } } } diff --git a/package.json b/package.json index 862d93795..357aec7b1 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "./packages/lit-dev-server:build", "./packages/lit-dev-tests:build", "./packages/lit-dev-tools-cjs:build", - "./packages/lit-dev-tools-esm:build" + "./packages/lit-dev-tools-esm:build", + "./packages/lit-dev-tutorial-plugin:build" ] }, "start": { diff --git a/packages/lit-dev-content/site/learn/learn.11tydata.js b/packages/lit-dev-content/site/learn/learn.11tydata.js index 6e3104857..242be2083 100644 --- a/packages/lit-dev-content/site/learn/learn.11tydata.js +++ b/packages/lit-dev-content/site/learn/learn.11tydata.js @@ -67,7 +67,7 @@ module.exports = {eleventyComputed: {learn: async (data) => { url, date } = content; - if (!title || !summary || !['article', 'tutorial', 'video'].includes(kind) || !url || !date) { + if (!title || summary === undefined || !['article', 'tutorial', 'video'].includes(kind) || !url || !date) { throw new Error(`Invalid content shape for: ${JSON.stringify(content)}`); } if (kind === "tutorial") { diff --git a/packages/lit-dev-tutorial-plugin/.vscode/launch.json b/packages/lit-dev-tutorial-plugin/.vscode/launch.json new file mode 100644 index 000000000..1210201da --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/.vscode/launch.json @@ -0,0 +1,28 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/packages/lit-dev-tutorial-plugin/.vscode/settings.json b/packages/lit-dev-tutorial-plugin/.vscode/settings.json new file mode 100644 index 000000000..ffeaf91cb --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/packages/lit-dev-tutorial-plugin/.vscode/tasks.json b/packages/lit-dev-tutorial-plugin/.vscode/tasks.json new file mode 100644 index 000000000..078ff7e01 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/packages/lit-dev-tutorial-plugin/.vscodeignore b/packages/lit-dev-tutorial-plugin/.vscodeignore new file mode 100644 index 000000000..95280df30 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/*.map +**/*.ts diff --git a/packages/lit-dev-tutorial-plugin/README.md b/packages/lit-dev-tutorial-plugin/README.md new file mode 100644 index 000000000..dfe501449 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/README.md @@ -0,0 +1,29 @@ +⚠️⚠️ BEWARE This package is in Alpha ⚠️⚠️ + +# lit.dev Tutorial Editor README + +This extension provides an editor for tutorials on lit.dev. + +## Features + +- File centralization +- Tutorial creation +- Tutorial deletion +- Step creation +- Step deletion +- Playground file creation +- Playground file deletion +- Step re-arrangement +- Reveal File in Sidebar +- "After" step creation +- "After" step deletion +- Automatic tutorial.json management +- Automatic project.json management + +## Requirements + +You must be in the `lit.dev` root workspace or `lit.dev/packages/lit-dev-content` directory. + +## Known Issues + +It does not handle empty directories or malformed tutorial projects well. diff --git a/packages/lit-dev-tutorial-plugin/package.json b/packages/lit-dev-tutorial-plugin/package.json new file mode 100644 index 000000000..fc6aeef24 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/package.json @@ -0,0 +1,221 @@ +{ + "name": "lit-dev-tutorial-editor", + "private": true, + "displayName": "lit.dev Tutorial Editor", + "description": "Helps you create tutorials for lit.dev", + "version": "0.0.0", + "author": "Google LLC", + "license": "BSD-3-Clause", + "main": "./out/extension.js", + "repository": { + "type": "git", + "url": "https://github.com/lit/lit.dev/", + "directory": "packages/lit-dev-tutorial-extension" + }, + "engines": { + "vscode": "^1.65.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onView:litDevTutorial" + ], + "contributes": { + "views": { + "litDevTutorial": [ + { + "id": "litDevTutorial", + "name": "lit.dev Tutorial Editor" + } + ] + }, + "viewsContainers": { + "activitybar": [ + { + "id": "litDevTutorial", + "title": "lit.dev Tutorial Editor", + "icon": "$(flame)" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "litDevTutorial.refreshEntry", + "when": "view == litDevTutorial", + "group": "navigation" + }, + { + "command": "litDevTutorial.createTutorial", + "when": "view == litDevTutorial", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "litDevTutorial.refreshEntry", + "when": "view == litDevTutorial", + "group": "navigation" + }, + { + "command": "litDevTutorial.createTutorial", + "when": "view == litDevTutorial", + "group": "tutorials" + }, + { + "command": "litDevTutorial.delete", + "when": "view == litDevTutorial && viewItem == tutorial || viewItem =~ /^tutorial-step/ || viewItem =~ /^generic-file.*-deletable/ || viewItem == playground-file || viewItem == afterDir", + "group": "files" + }, + { + "command": "litDevTutorial.addStep", + "when": "view == litDevTutorial && viewItem == tutorial", + "group": "inline" + }, + { + "command": "litDevTutorial.addAfterStep", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step.*-add/", + "group": "inline" + }, + { + "command": "litDevTutorial.moveStepUp", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step.*-up/", + "group": "inline" + }, + { + "command": "litDevTutorial.moveStepDown", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step.*-down/", + "group": "inline" + }, + { + "command": "litDevTutorial.createPlaygroundFile", + "when": "view == litDevTutorial && viewItem == beforeDir || viewItem == afterDir", + "group": "inline" + }, + { + "command": "litDevTutorial.revealInSidebar", + "when": "view == litDevTutorial && viewItem =~ /^generic-file/ || viewItem == playground-file", + "group": "navigation" + }, + { + "command": "litDevTutorial.renameStep", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step/", + "group": "files" + }, + { + "command": "litDevTutorial.makeSolvable", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step.*-unsolvable/", + "group": "files" + }, + { + "command": "litDevTutorial.makeUnsolvable", + "when": "view == litDevTutorial && viewItem =~ /^tutorial-step.*-solvable/", + "group": "files" + } + ] + }, + "commands": [ + { + "command": "litDevTutorial.createTutorial", + "title": "Create Tutorial", + "icon": "$(add)" + }, + { + "command": "litDevTutorial.refreshEntry", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "litDevTutorial.delete", + "title": "Delete", + "icon": "$(trash)" + }, + { + "command": "litDevTutorial.addStep", + "title": "Add Step", + "icon": "$(add)" + }, + { + "command": "litDevTutorial.addAfterStep", + "title": "Add Step", + "icon": "$(plus)" + }, + { + "command": "litDevTutorial.moveStepUp", + "title": "Move Step Up", + "icon": "$(arrow-up)" + }, + { + "command": "litDevTutorial.moveStepDown", + "title": "Move Step Down", + "icon": "$(arrow-down)" + }, + { + "command": "litDevTutorial.createPlaygroundFile", + "title": "Create File", + "icon": "$(plus)" + }, + { + "command": "litDevTutorial.revealInSidebar", + "title": "Reveal in Sidebar", + "icon": "$(go-to-file)" + }, + { + "command": "litDevTutorial.renameStep", + "title": "Rename", + "icon": "$(edit)" + }, + { + "command": "litDevTutorial.makeSolvable", + "title": "Show Solve Button", + "icon": "$(verified)" + }, + { + "command": "litDevTutorial.makeUnsolvable", + "title": "Hide Solve Button", + "icon": "$(unverified)" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run build", + "build": "wireit", + "test": "wireit" + }, + "devDependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^16.0.0", + "@types/vscode": "^1.87.0", + "@vscode/test-electron": "^2.3.9", + "glob": "^10.3.0", + "mocha": "^10.0.6" + }, + "wireit": { + "build": { + "command": "tsc --build --pretty --incremental", + "files": [ + "./src/**/*.ts", + "tsconfig.json" + ], + "output": [ + "./out/", + "./tsconfig.tsbuildinfo" + ], + "clean": "if-file-deleted" + }, + "test": { + "command": "node ./out/test/runTest.js", + "dependencies": [ + "build" + ], + "files": [ + "./out/test/" + ], + "output": [ + "./.vscode-test", + "./.test-bin" + ] + } + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/extension.ts b/packages/lit-dev-tutorial-plugin/src/extension.ts new file mode 100644 index 000000000..82b293b21 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/extension.ts @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import {BeforeAfterDir} from './tree-items/before-after-dir.js'; +import {GenericFile} from './tree-items/generic-file.js'; +import {PlaygroundFile} from './tree-items/playground-file.js'; +import {TutorialStep} from './tree-items/tutorial-step.js'; +import {Tutorial} from './tree-items/tutorial.js'; +import {LitDevTutorialTreeProvider, TutorialTreeItem} from './tree-provider.js'; + +export async function activate() { + const rootPath = + vscode.workspace.workspaceFolders && + vscode.workspace.workspaceFolders.length > 0 + ? vscode.workspace.workspaceFolders[0].uri.fsPath + : undefined; + + if (!rootPath) { + return; + } + + const provider = new LitDevTutorialTreeProvider(); + vscode.window.createTreeView('litDevTutorial', { + canSelectMany: true, + showCollapseAll: true, + treeDataProvider: provider, + }); + + vscode.window.registerTreeDataProvider('litDevTutorial', provider); + vscode.commands.registerCommand('litDevTutorial.refreshEntry', () => { + provider.refresh(); + }); + vscode.commands.registerCommand('litDevTutorial.createTutorial', () => { + provider.createTutorial(); + }); + vscode.commands.registerCommand( + 'litDevTutorial.addStep', + (tutorial: Tutorial) => { + TutorialStep.create(tutorial); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.moveStepUp', + (step: TutorialStep) => { + step.moveUp(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.moveStepDown', + (step: TutorialStep) => { + step.moveDown(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.addAfterStep', + (step: TutorialStep) => { + BeforeAfterDir.create(step, 'after'); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.delete', + (item: TutorialTreeItem) => { + item.delete(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.makeSolvable', + (item: TutorialStep) => { + item.solvable = true; + item.provider.refresh(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.makeUnsolvable', + (item: TutorialStep) => { + item.solvable = false; + item.provider.refresh(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.createPlaygroundFile', + (dir: BeforeAfterDir) => { + PlaygroundFile.create(dir); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.revealInSidebar', + (item: GenericFile | PlaygroundFile) => { + item.revealInSidebar(); + } + ); + vscode.commands.registerCommand( + 'litDevTutorial.renameStep', + (item: TutorialStep) => { + item.rename(); + } + ); +} + +// this method is called when your extension is deactivated +export function deactivate() {} diff --git a/packages/lit-dev-tutorial-plugin/src/fs-helpers.ts b/packages/lit-dev-tutorial-plugin/src/fs-helpers.ts new file mode 100644 index 000000000..678cc693a --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/fs-helpers.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import {TutorialCategory} from './types'; + +const pathExists = (p: string): boolean => { + try { + fs.accessSync(p); + } catch (err) { + return false; + } + return true; +}; + +export const getJson = (packageJsonPath: string): T | null => { + if (pathExists(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + return packageJson; + } else { + return null; + } +}; + +export const addTutorialTo11tyData = ( + dirname: string, + category: TutorialCategory, + filepath: string +) => { + const lines = fs.readFileSync(filepath, 'utf8').split('\n'); + let arrayStartLine = -1; + let arrayEndLine = -1; + let categoryLines: {cat: TutorialCategory; line: number}[] = []; + + for (const [i, line] of lines.entries()) { + if (line.includes(`return await Promise.all([`)) { + arrayStartLine = i; + } else if (line.includes(` ]);`)) { + if (arrayStartLine === -1) { + continue; + } + arrayEndLine = i; + break; + } else if ( + line.includes('// ') && + arrayStartLine !== -1 && + i > arrayStartLine && + arrayEndLine === -1 + ) { + const cat = line.split('// ')[1].trim() as TutorialCategory; + categoryLines.push({cat, line: i}); + } + } + + if (arrayStartLine === -1 || arrayEndLine === -1) { + vscode.window.showErrorMessage('Malformed _data/tutorials.js'); + return; + } + + let line = -1; + let categoryIndex = -1; + + for (const [i, {cat, line: lineNum}] of categoryLines.entries()) { + if (cat === category) { + line = lineNum; + categoryIndex = i; + break; + } + } + + if (line === -1) { + vscode.window.showErrorMessage( + 'Cannot find category ${category} in _data/tutorials.js' + ); + return; + } + + let insertionLine = -1; + const isLastCategory = categoryIndex === categoryLines.length - 1; + + if (isLastCategory) { + insertionLine = arrayEndLine; + } else { + insertionLine = categoryLines[categoryIndex + 1].line - 1; + } + + const newLine = ` loadTutorialData('${dirname}'),`; + + lines.splice(insertionLine, 0, newLine); + + fs.writeFileSync(filepath, lines.join('\n')); +}; + +export const removeTutorialFrom11tyData = ( + dirname: string, + filepath: string +) => { + const lines = fs.readFileSync(filepath, 'utf8').split('\n'); + const index = lines.findIndex((line) => + line.includes(`loadTutorialData('${dirname}')`) + ); + lines.splice(index, 1); + if (index === -1) { + return; + } + fs.writeFileSync(filepath, lines.join('\n')); +}; diff --git a/packages/lit-dev-tutorial-plugin/src/test/runTest.ts b/packages/lit-dev-tutorial-plugin/src/test/runTest.ts new file mode 100644 index 000000000..d3bb4d710 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/test/runTest.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as path from 'path'; + +import {runTests} from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({extensionDevelopmentPath, extensionTestsPath}); + } catch (err) { + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); diff --git a/packages/lit-dev-tutorial-plugin/src/test/suite/extension.test.ts b/packages/lit-dev-tutorial-plugin/src/test/suite/extension.test.ts new file mode 100644 index 000000000..bbe9b6043 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/test/suite/extension.test.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import {createEmptyLitDevRepo, deleteLitDevRepo} from '../util.js'; +import {beforeEach, afterEach, before} from 'mocha'; +// import * as myExtension from '../../extension'; + +suite('Tutorial Extension', () => { + vscode.window.showInformationMessage('Start all tests.'); + + let litDevPath: string; + let litDevContentPath!: string; + before(async () => { + await deleteLitDevRepo(); + }); + + beforeEach(async () => { + const paths = await createEmptyLitDevRepo(); + litDevPath = paths.litDevPath; + litDevContentPath = paths.litDevContentPath; + }); + + suite('startup', () => { + test('recognizes lit.dev repo', async () => { + await vscode.commands.executeCommand( + 'vscode.openFolder', + vscode.Uri.file(litDevPath) + ); + console.log('opened!'); + }); + }); + + afterEach(async () => { + await deleteLitDevRepo(); + }); +}); diff --git a/packages/lit-dev-tutorial-plugin/src/test/suite/index.ts b/packages/lit-dev-tutorial-plugin/src/test/suite/index.ts new file mode 100644 index 000000000..e7eb86905 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/test/suite/index.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as path from 'path'; +import * as Mocha from 'mocha'; +import {glob} from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true, + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise(async (c, e) => { + const files = await glob('**/**.test.js', {cwd: testsRoot}); + console.log(files); + + console.log(path.resolve(testsRoot, files[0])); + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); +} diff --git a/packages/lit-dev-tutorial-plugin/src/test/util.ts b/packages/lit-dev-tutorial-plugin/src/test/util.ts new file mode 100644 index 000000000..ed1614cfd --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/test/util.ts @@ -0,0 +1,308 @@ +import * as fs from 'fs/promises'; +import * as fss from 'fs'; +import * as path from 'path'; +import {TutorialJson} from '../types.js'; + +const findTestBinRoot = () => { + const root = path.join(__dirname, '..', '..'); + return path.join(root, '.test-bin'); +}; + +export const createEmptyLitDevRepo = async () => { + const testBinPath = findTestBinRoot(); + const litDevPath = path.join(testBinPath, 'lit.dev'); + const litDevContentPath = path.join( + litDevPath, + 'packages', + 'lit-dev-content' + ); + const litDevSampleTutorialPath = path.join( + litDevContentPath, + 'samples', + 'tutorials' + ); + const litDevSiteTutorialPath = path.join( + litDevContentPath, + 'site', + 'tutorials' + ); + const litDevSite11tydataPath = path.join( + litDevContentPath, + 'site', + '_data', + 'tutorials.js' + ); + const litDevSiteTutorialContentPath = path.join( + litDevSiteTutorialPath, + 'content' + ); + + await Promise.all([ + fs.mkdir(litDevSampleTutorialPath, {recursive: true}), + fs.mkdir(litDevSiteTutorialContentPath, {recursive: true}), + ]); + + const litDevPackageJson = { + name: 'lit.dev', + }; + + const litDevContentPackageJson = { + name: 'lit-dev-content', + }; + + const baseJson = { + files: { + 'package.json': { + content: '', + hidden: true, + }, + }, + }; + + const tutorials11tyData = ` + const tutorials = await Promise.all([ + // Learn + + // Build + + // Draft + ]); +`; + + await Promise.all([ + fs.writeFile( + path.join(litDevPath, 'package.json'), + JSON.stringify(litDevPackageJson, null, 2) + ), + fs.writeFile( + path.join(litDevContentPath, 'package.json'), + JSON.stringify(litDevContentPackageJson, null, 2) + ), + fs.writeFile( + path.join(litDevSampleTutorialPath, 'base.json'), + JSON.stringify(baseJson, null, 2) + ), + fs.writeFile(litDevSite11tydataPath, tutorials11tyData), + ]); + + return {litDevPath, litDevContentPath}; +}; + +export const deleteLitDevRepo = async () => { + const testBinPath = findTestBinRoot(); + const litDevPath = path.join(testBinPath, 'lit.dev'); + + if (fss.existsSync(litDevPath)) { + await fs.rm(litDevPath, {recursive: true}); + } +}; + +export const prepopulateTutorial = async ( + litDevContentPath: string, + options: { + tutorials: (TutorialJson & {dirName: string})[]; + } = {tutorials: []} +) => { + const litDevSampleTutorialPath = path.join( + litDevContentPath, + 'samples', + 'tutorials' + ); + const litDevSiteTutorialPath = path.join( + litDevContentPath, + 'site', + 'tutorials' + ); + const litDevSite11tydataPath = path.join( + litDevContentPath, + 'site', + '_data', + 'tutorials.js' + ); + const litDevSiteTutorialContentPath = path.join( + litDevSiteTutorialPath, + 'content' + ); + + const promises: Promise[] = []; + const dataFile = await (await fs.readFile(litDevSite11tydataPath)).toString(); + const dataFileLines = dataFile.split('\n'); + + for (const tutorial of options.tutorials) { + const tutorialPath = path.join(litDevSampleTutorialPath, tutorial.dirName); + const tutorialContentPath = path.join( + litDevSiteTutorialContentPath, + tutorial.dirName + ); + promises.push( + fs.mkdir(tutorialPath, {recursive: true}).then(() => { + return Promise.all([ + fs.writeFile(path.join(tutorialPath, 'description.md'), ''), + fs.writeFile( + path.join(tutorialPath, 'tutorial.json'), + JSON.stringify(tutorial, null, 2) + ), + ]); + }) + ); + + for (const [index, step] of tutorial.steps.entries()) { + const stepDirName = `${index}`.padStart(2, '0'); + const stepDirPath = path.join(tutorialPath, stepDirName); + const stepBeforePath = path.join(stepDirPath, 'before'); + const stepAfterPath = path.join(stepDirPath, 'after'); + + const projectJson = { + extends: '/samples/base.json', + files: { + 'index.html': {}, + }, + }; + + promises.push( + fs.mkdir(stepBeforePath, {recursive: true}).then(() => { + return Promise.all([ + fs.writeFile(path.join(stepBeforePath, 'index.html'), ''), + fs.writeFile( + path.join(stepBeforePath, 'project.json'), + JSON.stringify(projectJson, null, 2) + ), + ]); + }) + ); + + if (step.hasAfter) { + promises.push( + fs.mkdir(stepAfterPath, {recursive: true}).then(() => { + return Promise.all([ + fs.writeFile(path.join(stepAfterPath, 'index.html'), ''), + fs.writeFile( + path.join(stepAfterPath, 'project.json'), + JSON.stringify(projectJson, null, 2) + ), + ]); + }) + ); + } + + promises.push( + fs.mkdir(tutorialContentPath, {recursive: true}).then(() => { + return fs.writeFile( + path.join(tutorialContentPath, `${stepDirName}.md`), + '' + ); + }) + ); + } + } + + for (let i = options.tutorials.length - 1; i >= 0; i--) { + const tutorial = options.tutorials[i]; + const tutorialSection = + (tutorial as typeof tutorial & {category?: 'Learn' | 'Build'}).category ?? + 'Learn'; + const tutorialIndex = dataFileLines.findIndex((line) => + line.includes(`// ${tutorialSection}`) + ); + + const tutorialLine = ` loadTutorialData('${tutorial.dirName}'),`; + dataFileLines.splice(tutorialIndex + 1, 0, tutorialLine); + } + + promises.push(fs.writeFile(litDevSite11tydataPath, dataFileLines.join('\n'))); + await Promise.all(promises); +}; + +// (async () => { +// const litdevPath = await creteEmptyLitDevRepo(); +// await prepopulateTutorial(litdevPath.litDevContentPath, { +// tutorials: [{ +// dirName: 'learn-tutorial', +// category: 'Learn', +// difficulty: 'Beginner', +// duration: 65, +// header: 'Learn Tutorial', +// size: 'small', +// steps: [ +// { +// title: 'Step 1', +// hasAfter: false, +// }, +// { +// title: 'Step 2', +// hasAfter: false, +// }, +// { +// title: 'Step 3', +// hasAfter: true, +// }, +// ], +// }, +// { +// dirName: 'learn-2-tutorial', +// category: 'Learn', +// difficulty: 'Beginner', +// duration: 65, +// header: 'Learn Tutorial 2', +// size: 'medium', +// steps: [ +// { +// title: 'First', +// hasAfter: false, +// }, +// { +// title: 'Second', +// hasAfter: false, +// }, +// { +// title: 'Third', +// hasAfter: true, +// }, +// ], +// }, +// { +// dirName: 'build-tutorial', +// category: 'Build', +// difficulty: 'Beginner', +// duration: 65, +// header: 'Build Tutorial', +// size: 'medium', +// steps: [ +// { +// title: 'b1', +// hasAfter: false, +// }, +// { +// title: 'b2', +// hasAfter: false, +// }, +// { +// title: 'b3', +// hasAfter: true, +// }, +// ], +// }, +// { +// dirName: 'draft-tutorial', +// category: 'Draft', +// difficulty: 'Beginner', +// duration: 65, +// header: 'Draft Tutorial', +// size: 'medium', +// steps: [ +// { +// title: 'd1', +// hasAfter: false, +// }, +// { +// title: 'd2', +// hasAfter: false, +// }, +// { +// title: 'd3', +// hasAfter: true, +// }, +// ], +// }], +// }); +// })(); diff --git a/packages/lit-dev-tutorial-plugin/src/tree-items/before-after-dir.ts b/packages/lit-dev-tutorial-plugin/src/tree-items/before-after-dir.ts new file mode 100644 index 000000000..9538c1eea --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-items/before-after-dir.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import {TutorialStep} from './tutorial-step'; +import {LitDevTutorialTreeProvider} from '../tree-provider'; +import {getJson} from '../fs-helpers'; +import {TutorialJson} from '../types'; +import {GenericFile} from './generic-file'; +import {PlaygroundFile} from './playground-file'; + +export class BeforeAfterDir extends vscode.TreeItem { + get path() { + return path.join(this.tutorialStep.path, this.name); + } + + constructor( + public provider: LitDevTutorialTreeProvider, + public tutorialStep: TutorialStep, + public readonly name: 'before' | 'after' + ) { + super( + vscode.Uri.file(path.join(tutorialStep.path, name)), + vscode.TreeItemCollapsibleState.Collapsed + ); + + this.contextValue = `${name}Dir`; + } + + delete() { + if (this.name === 'before') { + return; + } + + fs.rmSync(this.path, {recursive: true}); + + this.tutorialStep.afterDir = undefined; + this.tutorialStep.hasAfter = false; + + const tutorialJsonPath = path.join( + this.tutorialStep.tutorial.path, + 'tutorial.json' + ); + const tutorialJson = getJson(tutorialJsonPath); + + if (tutorialJson) { + const steps = tutorialJson.steps; + const step = steps[this.tutorialStep.step]; + + if (step && this.name === 'after') { + delete step.hasAfter; + } + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + this.provider.refresh(); + } + } + + static create( + tutorialStep: TutorialStep, + name: 'before' | 'after', + checkable?: boolean + ) { + const provider = tutorialStep.provider; + const newDir = new BeforeAfterDir(provider, tutorialStep, name); + newDir.contextValue = `${name}Dir`; + fs.mkdirSync(newDir.path); + + if (name === 'after') { + const tutorialJsonPath = path.join( + tutorialStep.tutorial.path, + 'tutorial.json' + ); + + const tutorialJson = getJson(tutorialJsonPath); + + if (tutorialJson) { + tutorialJson.steps[tutorialStep.step].hasAfter = true; + delete tutorialJson.steps[tutorialStep.step].noSolve; + } + + tutorialStep.hasAfter = true; + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + } + + const projectJson = { + extends: '/samples/base.json', + files: {}, + }; + if (checkable) { + projectJson.extends = '/samples/checkable-tutorial-base.json'; + } + GenericFile.create( + provider, + path.join(newDir.path, 'project.json'), + {}, + JSON.stringify(projectJson, null, 2) + ); + PlaygroundFile.create( + newDir, + 'index.html', + {}, + ` + + + +${ + checkable + ? ' \n' + : '' +} + + +` + ); + if (checkable) { + PlaygroundFile.create( + newDir, + '_check-code.js', + {hidden: true}, + `import {installCodeChecker} from './_check-code-helpers.js'; + +installCodeChecker(async () => { + let passed = true; + let message = ''; + + window.location.reload(); + return {passed, message}; +});` + ); + } + + provider.refresh(); + + return newDir; + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/tree-items/generic-file.ts b/packages/lit-dev-tutorial-plugin/src/tree-items/generic-file.ts new file mode 100644 index 000000000..354af39e7 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-items/generic-file.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import {LitDevTutorialTreeProvider} from '../tree-provider'; + +interface GenericFileConfig { + label?: string; + tooltip?: string; + deletable: boolean; +} + +export class GenericFile extends vscode.TreeItem { + deletable: boolean; + + constructor( + public readonly provider: LitDevTutorialTreeProvider, + public readonly path: string, + config: Partial = {} + ) { + super(vscode.Uri.file(path)); + + const {label, tooltip, deletable} = config; + + if (label) { + this.label = label; + } + + if (tooltip) { + this.tooltip = tooltip; + } + + this.deletable = !!deletable; + + this.command = { + title: '', + command: 'vscode.open', + arguments: [this.resourceUri], + }; + + if (deletable) { + this.contextValue = 'generic-file-deletable'; + } else { + this.contextValue = 'generic-file-nodelete'; + } + } + + delete() { + if (!this.deletable) { + return; + } + fs.rmSync(this.path); + this.provider.refresh(); + } + + static create( + provider: LitDevTutorialTreeProvider, + path: string, + config: Partial = {}, + contents = '' + ) { + const genericFile = new GenericFile(provider, path, config); + fs.writeFileSync(path, contents); + + provider.refresh(); + return genericFile; + } + + revealInSidebar() { + vscode.commands.executeCommand('vscode.open', vscode.Uri.file(this.path)); + vscode.commands.executeCommand( + 'workbench.files.action.showActiveFileInExplorer' + ); + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/tree-items/playground-file.ts b/packages/lit-dev-tutorial-plugin/src/tree-items/playground-file.ts new file mode 100644 index 000000000..49a200d67 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-items/playground-file.ts @@ -0,0 +1,134 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import {LitDevTutorialTreeProvider} from '../tree-provider'; +import {BeforeAfterDir} from './before-after-dir'; +import {PlaygroundProjectManifest} from '../types'; +import {getJson} from '../fs-helpers'; + +export interface PlaygroundFileCreateConfig { + hidden: boolean; +} + +const defaultCreateConfig: PlaygroundFileCreateConfig = { + hidden: false, +}; + +export class PlaygroundFile extends vscode.TreeItem { + projectJsonPath; + + get path() { + return path.join(this.dirItem.path, this.filename); + } + + get isHidden() { + const manifest = getJson(this.projectJsonPath); + return !!manifest?.files?.[this.filename]?.hidden; + } + + constructor( + public readonly provider: LitDevTutorialTreeProvider, + public readonly dirItem: BeforeAfterDir, + public readonly filename: string + ) { + super(vscode.Uri.file(path.join(dirItem.path, filename))); + + this.command = { + title: '', + command: 'vscode.open', + arguments: [this.resourceUri], + }; + + this.contextValue = 'playground-file'; + this.projectJsonPath = path.join(this.dirItem.path, 'project.json'); + } + + delete() { + fs.rmSync(this.path); + const manifest = getJson(this.projectJsonPath); + if (manifest) { + delete manifest.files?.[this.filename]; + fs.writeFileSync(this.projectJsonPath, JSON.stringify(manifest, null, 2)); + } + + this.provider.refresh(); + } + + static async create( + dirItem: BeforeAfterDir, + filename = '', + config: Partial = {}, + contents = '' + ) { + if (!filename) { + const filenameRaw = await vscode.window.showInputBox({ + prompt: 'Enter a filename', + placeHolder: 'my-element.ts', + validateInput: (input) => { + if (!input) { + return 'Filename is required'; + } + if (fs.readdirSync(dirItem.path).includes(input)) { + return 'Filename already exists'; + } + if (input.includes(' ')) { + return 'Filename cannot contain spaces'; + } + if (input.includes('/')) { + return 'Filename cannot contain slashes'; + } + if (input.includes('\\')) { + return 'Filename cannot contain backslashes'; + } + return null; + }, + }); + + if (!filenameRaw) { + return; + } + + filename = filenameRaw; + } + const provider = dirItem.provider; + const {hidden} = {...defaultCreateConfig, ...config}; + const newFile = new PlaygroundFile(provider, dirItem, filename); + fs.writeFileSync(newFile.path, contents); + + const manifest = getJson( + newFile.projectJsonPath + ); + if (manifest) { + if (!manifest.files) { + manifest.files = {}; + } + + manifest.files[newFile.filename] = {}; + + if (hidden) { + manifest.files[newFile.filename].hidden = true; + } + + fs.writeFileSync( + newFile.projectJsonPath, + JSON.stringify(manifest, null, 2) + ); + + provider.refresh(); + return newFile; + } + } + + revealInSidebar() { + vscode.commands.executeCommand('vscode.open', vscode.Uri.file(this.path)); + vscode.commands.executeCommand( + 'workbench.files.action.showActiveFileInExplorer' + ); + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial-step.ts b/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial-step.ts new file mode 100644 index 000000000..50f912cac --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial-step.ts @@ -0,0 +1,320 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import {Tutorial} from './tutorial'; +import {LitDevTutorialTreeProvider} from '../tree-provider'; +import {getJson} from '../fs-helpers'; +import {TutorialJson, TutorialJsonStep} from '../types'; +import {BeforeAfterDir} from './before-after-dir'; + +export class TutorialStep extends vscode.TreeItem { + private _step = -1; + private _hasAfter = false; + checkable = false; + beforeDir!: BeforeAfterDir; + afterDir: BeforeAfterDir | undefined = undefined; + + get dirName() { + return `${this.step}`.padStart(2, '0'); + } + + get path() { + return path.join(this.tutorial.path, this.dirName); + } + + get step() { + return this._step; + } + + set step(value: number) { + const oldPath = this.path; + const oldInstructionsPath = path.join( + this.provider.siteTutorialsContentPath, + this.tutorial.dirName, + `${this.dirName}.md` + ); + const previousStep = this._step; + + this._step = value; + const newInstructionsPath = path.join( + this.provider.siteTutorialsContentPath, + this.tutorial.dirName, + `${this.dirName}.md` + ); + + if (previousStep !== -1) { + fs.renameSync(oldPath, this.path); + fs.renameSync(oldInstructionsPath, newInstructionsPath); + } + + this.contextValue = this.generateContextValue(); + this.fetchMetadata(); + } + + get hasAfter() { + return this._hasAfter; + } + + set hasAfter(value: boolean) { + if (this._hasAfter === value) { + return; + } + + this._hasAfter = value; + + const tutorialJsonPath = path.join(this.tutorial.path, 'tutorial.json'); + let tutorialJson = getJson(tutorialJsonPath)!; + + if (value) { + this.solvable = true; + tutorialJson = getJson(tutorialJsonPath)!; + tutorialJson.steps[this.step].hasAfter = true; + } else { + if (this.afterDir) { + this.afterDir.delete(); + } + delete tutorialJson.steps[this.step].hasAfter; + } + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + this.updateContextValue(); + } + + private _solvable = false; + + set solvable(solvable: boolean) { + if (this._solvable === solvable) { + return; + } + + this._solvable = solvable; + const tutorialJsonPath = path.join(this.tutorial.path, 'tutorial.json'); + let tutorialJson = getJson(tutorialJsonPath)!; + + if (solvable) { + delete tutorialJson.steps[this.step].noSolve; + } else { + this.hasAfter = false; + tutorialJson = getJson(tutorialJsonPath)!; + + tutorialJson.steps[this.step].noSolve = true; + } + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + this.updateContextValue(); + } + + get solvable() { + return this._solvable; + } + + constructor( + public provider: LitDevTutorialTreeProvider, + public tutorial: Tutorial, + step: string, + solvable: boolean + ) { + super( + vscode.Uri.file(path.join(tutorial.path, step)), + vscode.TreeItemCollapsibleState.Collapsed + ); + + this._solvable = solvable; + this.tutorial.pushStep(this); + this.step = Number(step); + this.tooltip = this.dirName; + this.fetchMetadata(); + this.contextValue = this.generateContextValue(); + } + + updateContextValue() { + this.contextValue = this.generateContextValue(); + } + + private fetchMetadata() { + if (this.step === -1 || this.step === -2) { + return; + } + const tutorialJson = getJson( + path.join(this.tutorial.path, 'tutorial.json') + ); + + if (tutorialJson) { + this.label = tutorialJson.steps[this.step]?.title; + this.description = `[${this.dirName}]`; + this.hasAfter = !!tutorialJson.steps[this.step]?.hasAfter; + this.checkable = !!tutorialJson.steps[this.step]?.checkable; + } else { + this.label = this.dirName; + } + } + + private generateContextValue() { + const first = this.step === 0 ? '' : '-up'; + const last = this.step === this.tutorial.steps.length - 1 ? '' : '-down'; + const afterVal = this.hasAfter ? '' : '-add'; + const solvable = this.solvable ? '-solvable' : '-unsolvable'; + + return `tutorial-step${first}${last}${afterVal}${solvable}`; + } + + deleteFiles() { + const tutorialJsonPath = path.join(this.tutorial.path, 'tutorial.json'); + const tutorialJson = getJson(tutorialJsonPath); + tutorialJson?.steps.splice(this.dirName as unknown as number, 1); + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + + fs.rmSync(this.path, {recursive: true}); + fs.rmSync( + path.join( + this.provider.siteTutorialsContentPath, + this.tutorial.dirName, + `${this.dirName}.md` + ) + ); + } + + delete() { + this.tutorial.removeStep(this); + } + + moveUp() { + this.tutorial.moveStepUp(this); + } + + moveDown() { + this.tutorial.moveStepDown(this); + } + + static async create(tutorial: Tutorial) { + const provider = tutorial.provider; + const dirName = `${tutorial.steps.length}`.padStart(2, '0'); + + const title = await vscode.window.showInputBox({ + prompt: 'Step title', + placeHolder: 'Styling the component', + validateInput: (value: string) => { + if (value.length === 0) { + return 'Title is required.'; + } + return null; + }, + }); + + if (!title) { + return; + } + + const isSolvableStr = await vscode.window.showQuickPick(['Yes', 'No'], { + title: 'Should this step have a "solve" button?', + }); + + if (!isSolvableStr) { + return; + } + + const hideSolve = isSolvableStr !== 'Yes'; + let hasAfter = false; + + if (!hideSolve) { + const hasAfterStr = await vscode.window.showQuickPick(['Yes', 'No'], { + title: 'Is "after" step the "before" of the next step?', + }); + + if (!hasAfterStr) { + return; + } + + hasAfter = hasAfterStr === 'Yes'; + } + + const checkableRaw = await vscode.window.showQuickPick(['Yes', 'No'], { + placeHolder: 'Is this step checkable?', + }); + + if (!checkableRaw) { + return; + } + + const checkable = checkableRaw === 'Yes'; + + fs.mkdirSync(path.join(tutorial.path, dirName)); + fs.writeFileSync( + path.join( + provider.siteTutorialsContentPath, + tutorial.dirName, + `${dirName}.md` + ), + '' + ); + + const tutorialJsonPath = path.join(tutorial.path, 'tutorial.json'); + const tutorialJson = getJson(tutorialJsonPath)!; + const jsonStep: TutorialJsonStep = { + title, + hasAfter: true, + }; + + if (checkable) { + jsonStep.checkable = true; + } + + if (hideSolve) { + jsonStep.noSolve = true; + } + + if (hasAfter) { + jsonStep.hasAfter = true; + } + + tutorialJson.steps.push(jsonStep); + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + + const step = new TutorialStep(provider, tutorial, dirName, hideSolve); + tutorial.pushStep(step); + + const beforeStep = BeforeAfterDir.create(step, 'before', checkable); + step.beforeDir = beforeStep; + + if (hasAfter && !hideSolve) { + const afterStep = BeforeAfterDir.create(step, 'after'); + step.afterDir = afterStep; + } + + provider.refresh(); + return step; + } + + async rename() { + let newName = await vscode.window.showInputBox({ + prompt: 'New step name', + value: this.label as string, + validateInput: (value: string) => { + if (value.length === 0) { + return 'Name is required.'; + } + return null; + }, + }); + + if (!newName) { + return; + } + + const tutorialJsonPath = path.join(this.tutorial.path, 'tutorial.json'); + const tutorialJson = getJson(tutorialJsonPath)!; + tutorialJson.steps[this.step].title = newName; + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + this.label = newName; + this.provider.refresh(); + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial.ts b/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial.ts new file mode 100644 index 000000000..c8292ee35 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-items/tutorial.ts @@ -0,0 +1,284 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import {LitDevTutorialTreeProvider} from '../tree-provider'; +import { + TutorialCardSize, + TutorialCategory, + TutorialDifficulty, + TutorialJson, +} from '../types'; +import {TutorialStep} from './tutorial-step'; +import { + addTutorialTo11tyData, + getJson, + removeTutorialFrom11tyData, +} from '../fs-helpers'; + +export class Tutorial extends vscode.TreeItem { + steps: TutorialStep[] = []; + + get path() { + return path.join(this.provider.samplesTutorialsPath, this.dirName); + } + + constructor( + public provider: LitDevTutorialTreeProvider, + readonly dirName: string + ) { + super( + vscode.Uri.file(path.join(provider.samplesTutorialsPath, dirName)), + vscode.TreeItemCollapsibleState.Collapsed + ); + this.tooltip = dirName; + this.init(); + this.contextValue = 'tutorial'; + } + + async init() { + const tutorialJson = getJson( + path.join( + this.provider.samplesTutorialsPath, + this.dirName, + 'tutorial.json' + ) + ); + + if (tutorialJson) { + this.label = tutorialJson.header; + } else { + this.label = `tutorial.json not found for ${this.dirName}`; + } + } + + pushStep(step: TutorialStep) { + this.steps.push(step); + this.steps[this.steps.length - 2]?.updateContextValue(); + } + + static async create(provider: LitDevTutorialTreeProvider) { + let dirName = await vscode.window.showInputBox({ + prompt: 'Directory name', + placeHolder: 'my-tutorial', + validateInput: (value) => { + if (!value) { + return 'Please enter a directory name'; + } + + const actualName = value.split(' ').join('-'); + + if ( + fs.existsSync(path.join(provider.samplesTutorialsPath, actualName)) + ) { + return 'Directory already exists'; + } + + return null; + }, + }); + + if (dirName === undefined) { + return; + } + + dirName = dirName.split(' ').join('-'); + + const name = await vscode.window.showInputBox({ + prompt: 'Tutorial name', + placeHolder: 'My Tutorial', + validateInput: (value) => { + if (!value) { + return 'Please enter a tutorial name'; + } + + return null; + }, + }); + + if (name === undefined) { + return; + } + + const difficulty = (await vscode.window.showQuickPick( + ['Beginner', 'Intermediate', 'Advanced', ''], + { + placeHolder: 'Difficulty', + } + )) as TutorialDifficulty | undefined; + + if (difficulty === undefined) { + return; + } + + const duration = (await vscode.window.showInputBox({ + prompt: 'Duration (mins)', + value: '0', + validateInput: (value) => { + if (!value) { + return 'Please enter a duration'; + } + + const duration = parseInt(value); + + if (isNaN(duration)) { + return 'Please enter a valid duration'; + } + + return null; + }, + })) as unknown as number | undefined; + + if (duration === undefined) { + return; + } + + const imgSrc = await vscode.window.showInputBox({ + prompt: 'Card Image source (optional)', + placeHolder: '/images/tutorials/my-tutorial/card-image.png', + }); + + let imgAlt = ''; + + if (imgSrc) { + imgAlt = (await vscode.window.showInputBox({ + prompt: 'Image alt text', + validateInput: (value) => { + if (!value) { + return 'Please enter an alt text'; + } + + return null; + }, + })) as string; + } + + const tutorialPath = path.join(provider.samplesTutorialsPath, dirName); + + const tutorialContentPath = path.join( + provider.siteTutorialsContentPath, + dirName + ); + + try { + fs.mkdirSync(tutorialPath); + fs.mkdirSync(tutorialContentPath); + } catch (_e) {} + + const dateObj = new Date(); + const date = `${dateObj.getFullYear()}-${ + dateObj.getMonth() + 1 + }-${dateObj.getDate()}`; + + const tutorialJson: TutorialJson = { + header: name, + difficulty: difficulty, + duration: duration, + imgSrc, + imgAlt, + date, + steps: [], + }; + + fs.writeFileSync( + path.join(tutorialPath, 'tutorial.json'), + JSON.stringify(tutorialJson, null, 2) + ); + + fs.writeFileSync(path.join(tutorialPath, 'description.md'), ''); + + await addTutorialTo11tyData(dirName, 'Learn', provider.site11tyDataPath); + + provider.refresh(); + + return new Tutorial(provider, dirName); + } + + delete() { + fs.rmSync(this.path, {recursive: true}); + fs.rmSync(path.join(this.provider.siteTutorialsContentPath, this.dirName), { + recursive: true, + }); + removeTutorialFrom11tyData(this.dirName, this.provider.site11tyDataPath); + this.provider.refresh(); + } + + removeStep(stepItem: TutorialStep) { + this.steps = this.steps.sort((a, b) => a.step - b.step); + stepItem.deleteFiles(); + + this.steps.splice(stepItem.step, 1); + + for (let i = stepItem.step; i < this.steps.length; i++) { + const step = this.steps[i]; + step.step -= 1; + } + + this.provider.refresh(); + } + + moveStepUp(stepItem: TutorialStep) { + this.steps = this.steps.sort((a, b) => a.step - b.step); + + if (stepItem.step === 0) { + return; + } + + const prevStepItem = this.steps[stepItem.step - 1]; + const index = stepItem.step; + const previousIndex = prevStepItem.step; + + // prevent fs.renameSync from throwing an error + stepItem.step = -2; + prevStepItem.step = index; + stepItem.step = previousIndex; + + this.steps[stepItem.step] = stepItem; + this.steps[prevStepItem.step] = prevStepItem; + + const tutorialJsonPath = path.join(this.path, 'tutorial.json'); + const tutorialJson = getJson(tutorialJsonPath)!; + const jsonStep = tutorialJson?.steps[index]; + const jsonPrevStep = tutorialJson?.steps[previousIndex]; + tutorialJson.steps[index] = jsonPrevStep; + tutorialJson.steps[previousIndex] = jsonStep; + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + + this.provider.refresh(); + } + + moveStepDown(stepItem: TutorialStep) { + this.steps = this.steps.sort((a, b) => a.step - b.step); + + if (stepItem.step === this.steps.length - 1) { + return; + } + + const nextStepItem = this.steps[stepItem.step + 1]; + const index = stepItem.step; + const nextIndex = nextStepItem.step; + + stepItem.step = -2; + nextStepItem.step = index; + stepItem.step = nextIndex; + + this.steps[stepItem.step] = stepItem; + this.steps[nextStepItem.step] = nextStepItem; + + const tutorialJsonPath = path.join(this.path, 'tutorial.json'); + const tutorialJson = getJson(tutorialJsonPath)!; + const jsonStep = tutorialJson?.steps[index]; + const jsonNextStep = tutorialJson?.steps[nextIndex]; + tutorialJson.steps[index] = jsonNextStep; + tutorialJson.steps[nextIndex] = jsonStep; + + fs.writeFileSync(tutorialJsonPath, JSON.stringify(tutorialJson, null, 2)); + + this.provider.refresh(); + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/tree-provider.ts b/packages/lit-dev-tutorial-plugin/src/tree-provider.ts new file mode 100644 index 000000000..f20ab719b --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/tree-provider.ts @@ -0,0 +1,266 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import {Tutorial} from './tree-items/tutorial'; +import {GenericFile} from './tree-items/generic-file'; +import {TutorialStep} from './tree-items/tutorial-step'; +import {getJson} from './fs-helpers'; +import {BeforeAfterDir} from './tree-items/before-after-dir'; +import {PlaygroundFile} from './tree-items/playground-file'; +import {TutorialJson} from './types'; + +interface PackageJson { + name?: string; +} + +export type TutorialTreeItem = + | Tutorial + | GenericFile + | TutorialStep + | BeforeAfterDir + | PlaygroundFile; + +export class LitDevTutorialTreeProvider + implements vscode.TreeDataProvider +{ + get workspaceRoot() { + return vscode.workspace.workspaceFolders?.[0].uri.fsPath!; + } + + private _contentPath = ''; + + get contentPath() { + return path.join(this.workspaceRoot, this._contentPath); + } + + get samplesTutorialsPath() { + return path.join(this.contentPath, 'samples', 'tutorials'); + } + + get siteTutorialsPath() { + return path.join(this.contentPath, 'site', 'tutorials'); + } + + get site11tyDataPath() { + return path.join(this.contentPath, 'site', '_data', 'tutorials.js'); + } + + get siteTutorialsContentPath() { + return path.join(this.contentPath, 'site', 'tutorials', 'content'); + } + + private _onDidChangeTreeData = new vscode.EventEmitter< + TutorialTreeItem | undefined | null | void + >(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + getTreeItem(element: TutorialTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: TutorialTreeItem): TutorialTreeItem[] { + if (!this.workspaceRoot) { + vscode.window.showInformationMessage('No dependency in empty workspace'); + return []; + } + + if (element) { + return this.onChild(element); + } else { + // initial load + return this.onInitChildren(); + } + } + + private onInitChildren() { + let contentPath = ''; + const possContentPath = path.join( + this.workspaceRoot, + 'packages', + 'lit-dev-content' + ); + + const rootPackageJsonPath = path.join(this.workspaceRoot, 'package.json'); + const contentPackageJsonPath = path.join(possContentPath, 'package.json'); + + const [rootPackage, contentPackage] = [ + getJson(rootPackageJsonPath), + getJson(contentPackageJsonPath), + ]; + + // determine whether we are in a lit.dev repo or the lit-dev-content + // subpackage + if (rootPackage?.name === 'lit-dev-content') { + contentPath = '.'; + } else if (contentPackage?.name === 'lit-dev-content') { + contentPath = path.relative(this.workspaceRoot, possContentPath); + } + + this._contentPath = contentPath; + + let items: TutorialTreeItem[] = []; + + if (contentPath) { + items = [...items, ...this.getInitialContent()]; + + return items; + } else { + vscode.window.showInformationMessage('This is not a lit.dev workspace.'); + return []; + } + } + + private onChild(element: TutorialTreeItem): TutorialTreeItem[] { + if (element instanceof Tutorial) { + return this.onTutorial(element); + } else if (element instanceof TutorialStep) { + return this.onTutorialStep(element); + } else if (element instanceof BeforeAfterDir) { + return this.onBeforeAfterDir(element); + } + + return []; + } + + private onTutorial(element: Tutorial) { + const tutorialPath = path.join(this.samplesTutorialsPath, element.dirName); + const tutorialDirEntries = fs.readdirSync(tutorialPath, { + withFileTypes: true, + }); + const tutorialStepEntries = tutorialDirEntries.filter((file) => + file.isDirectory() + ); + const tutorialFileEntries = tutorialDirEntries.filter((file) => + file.isFile() + ); + + const tutorialJson = getJson( + path.join(element.path, 'tutorial.json') + )!; + const jsonSteps = tutorialJson.steps; + + const steps = tutorialStepEntries.map((step, index) => { + const jsonStep = jsonSteps[index]; + return new TutorialStep(this, element, step.name, !jsonStep.noSolve); + }); + const files = tutorialFileEntries.map( + (file) => new GenericFile(this, path.join(tutorialPath, file.name)) + ); + return [...steps, ...files]; + } + + private onTutorialStep(element: TutorialStep) { + const tutorialPath = element.path; + const tutorialEntries = fs.readdirSync(tutorialPath, {withFileTypes: true}); + const tutorialDirEntries = tutorialEntries + .filter( + (file) => + (file.isDirectory() && file.name === 'before') || + file.name === 'after' + ) + .reverse(); + + const beforeAfterDirs = tutorialDirEntries.map((entry) => { + const dir = new BeforeAfterDir( + this, + element, + entry.name as 'before' | 'after' + ); + if (entry.name === 'before') { + element.beforeDir = dir; + } else if (entry.name === 'after') { + element.afterDir = dir; + } + + return dir; + }); + + const stepInstructionsMdFile = new GenericFile( + this, + path.join( + this.siteTutorialsContentPath, + element.tutorial.dirName, + `${element.dirName}.md` + ), + { + label: 'Instructions', + tooltip: `${element.dirName}.md`, + } + ); + + return [...beforeAfterDirs, stepInstructionsMdFile]; + } + + private onBeforeAfterDir(element: BeforeAfterDir) { + const tutorialPath = element.path; + const tutorialEntries = fs.readdirSync(tutorialPath, {withFileTypes: true}); + const tutorialFileEntries = tutorialEntries.filter((file) => file.isFile()); + + const files = tutorialFileEntries.map((file) => { + if (file.name === 'project.json') { + return new GenericFile(this, path.join(tutorialPath, file.name)); + } else { + return new PlaygroundFile(this, element, file.name); + } + }); + + const projectJsonFile = files.find((file) => file instanceof GenericFile)!; + const playgroundFiles = files.filter( + (file) => !(file instanceof GenericFile) + ); + + return [...playgroundFiles, projectJsonFile]; + } + + private getInitialContent() { + const samplesDir = fs.readdirSync(this.samplesTutorialsPath, { + withFileTypes: true, + }); + + const samplesPath = path.join(this.contentPath, 'samples'); + const configEntries = fs.readdirSync(samplesPath, { + withFileTypes: true, + }); + const tutorialDirs = samplesDir.filter((dir) => dir.isDirectory()); + const configs = configEntries.filter( + (dir) => + dir.name.endsWith('.json') && + dir.isFile() && + dir.name !== 'tsconfig.json' + ); + + const tutorialDataFilePath = this.site11tyDataPath; + + const tutorials: Tutorial[] = []; + + tutorialDirs.forEach((dir) => tutorials.push(new Tutorial(this, dir.name))); + + const configsFiles: GenericFile[] = []; + + configs.forEach((config) => + configsFiles.push( + new GenericFile(this, path.join(samplesPath, config.name)) + ) + ); + + return [ + ...tutorials, + ...configsFiles, + new GenericFile(this, tutorialDataFilePath), + ]; + } + + refresh() { + this._onDidChangeTreeData.fire(); + } + + createTutorial() { + Tutorial.create(this); + } +} diff --git a/packages/lit-dev-tutorial-plugin/src/types.ts b/packages/lit-dev-tutorial-plugin/src/types.ts new file mode 100644 index 000000000..572a89a99 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/src/types.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +export type {ProjectManifest as PlaygroundProjectManifest} from 'playground-elements/shared/worker-api'; + +export type TutorialDifficulty = '' | 'Beginner' | 'Intermediate' | 'Advanced'; +export type TutorialCardSize = 'tiny' | 'small' | 'medium' | 'large'; +export type TutorialCategory = 'Learn' | 'Build' | 'Draft'; +export interface TutorialJsonStep { + title: string; + hasAfter?: boolean; + checkable?: boolean; + noSolve?: boolean; +} +export interface TutorialJson { + header: string; + difficulty: TutorialDifficulty; + duration: number; + imgSrc?: string; + imgAlt?: string; + steps: TutorialJsonStep[]; + date: string; +} diff --git a/packages/lit-dev-tutorial-plugin/tsconfig.json b/packages/lit-dev-tutorial-plugin/tsconfig.json new file mode 100644 index 000000000..fb305d5e8 --- /dev/null +++ b/packages/lit-dev-tutorial-plugin/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */, + "skipLibCheck": true + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + } +}