diff --git a/.changeset/eight-frogs-roll.md b/.changeset/blue-feet-roll.md
similarity index 100%
rename from .changeset/eight-frogs-roll.md
rename to .changeset/blue-feet-roll.md
diff --git a/.changeset/cyan-moose-know.md b/.changeset/cyan-moose-know.md
new file mode 100644
index 0000000000..2573b29ffd
--- /dev/null
+++ b/.changeset/cyan-moose-know.md
@@ -0,0 +1,5 @@
+---
+'@lit-labs/context': patch
+---
+
+Make @consume decorator work with optional fields
diff --git a/.changeset/thirty-years-camp.md b/.changeset/dirty-mugs-rhyme.md
similarity index 100%
rename from .changeset/thirty-years-camp.md
rename to .changeset/dirty-mugs-rhyme.md
diff --git a/.changeset/dull-months-prove.md b/.changeset/dull-months-prove.md
new file mode 100644
index 0000000000..dbcde03150
--- /dev/null
+++ b/.changeset/dull-months-prove.md
@@ -0,0 +1,6 @@
+---
+'@lit-labs/analyzer': minor
+'@lit-labs/gen-manifest': minor
+---
+
+Added support for export, slot, cssPart, and cssProperty to analyzer and manifest generator. Also improved JS project analysis performance.
diff --git a/.changeset/five-falcons-suffer.md b/.changeset/five-falcons-suffer.md
new file mode 100644
index 0000000000..f158aa283d
--- /dev/null
+++ b/.changeset/five-falcons-suffer.md
@@ -0,0 +1,6 @@
+---
+'@lit-labs/analyzer': minor
+'@lit-labs/cli': minor
+---
+
+Added superclass analysis to ClassDeclaration, along with the ability to query exports of a Module (via `getExport()` and `getResolvedExport()`) and the ability to dereference `Reference`s to the `Declaration` they point to (via `dereference()`). A ClassDeclaration's superClass may be interrogated via `classDeclaration.heritage.superClass.dereference()` (`heritage.superClass` returns a `Reference`, which can be dereferenced to access its superclass's `ClassDeclaration` model.
diff --git a/.changeset/fresh-oranges-search.md b/.changeset/fresh-oranges-search.md
new file mode 100644
index 0000000000..a845151cc8
--- /dev/null
+++ b/.changeset/fresh-oranges-search.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/.changeset/lemon-llamas-rule.md b/.changeset/lemon-llamas-rule.md
new file mode 100644
index 0000000000..16b2e14b1a
--- /dev/null
+++ b/.changeset/lemon-llamas-rule.md
@@ -0,0 +1,5 @@
+---
+'@lit-labs/context': minor
+---
+
+Rename ContextKey to Context
diff --git a/.changeset/moody-colts-trade.md b/.changeset/moody-colts-trade.md
deleted file mode 100644
index ec1a0edd26..0000000000
--- a/.changeset/moody-colts-trade.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-'@lit-labs/analyzer': minor
-'@lit-labs/gen-wrapper-angular': minor
-'@lit-labs/gen-wrapper-react': minor
-'@lit-labs/gen-wrapper-vue': minor
----
-
-Fixes bug where global install of CLI resulted in incompatible use of analyzer between CLI packages. Fixes #3234.
diff --git a/.changeset/six-buttons-cover.md b/.changeset/six-buttons-cover.md
new file mode 100644
index 0000000000..a845151cc8
--- /dev/null
+++ b/.changeset/six-buttons-cover.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/.changeset/tidy-flowers-mate.md b/.changeset/tidy-flowers-mate.md
new file mode 100644
index 0000000000..8389a645c4
--- /dev/null
+++ b/.changeset/tidy-flowers-mate.md
@@ -0,0 +1,5 @@
+---
+'@lit-labs/react': patch
+---
+
+Filter \_\_forwaredRef from build.
diff --git a/.changeset/unlucky-lamps-sing.md b/.changeset/unlucky-lamps-sing.md
new file mode 100644
index 0000000000..d9a3128b3a
--- /dev/null
+++ b/.changeset/unlucky-lamps-sing.md
@@ -0,0 +1,5 @@
+---
+'@lit-labs/context': patch
+---
+
+Allow ContextProvider to be added lazily and still work with ContextRoot
diff --git a/.changeset/unlucky-parents-melt.md b/.changeset/unlucky-parents-melt.md
new file mode 100644
index 0000000000..a987f2ccc5
--- /dev/null
+++ b/.changeset/unlucky-parents-melt.md
@@ -0,0 +1,5 @@
+---
+'@lit-labs/context': patch
+---
+
+Rename @contextProvided and @contextProvider to @consume and @provide
diff --git a/.changeset/wild-pillows-carry.md b/.changeset/wild-pillows-carry.md
new file mode 100644
index 0000000000..c7f92b3f90
--- /dev/null
+++ b/.changeset/wild-pillows-carry.md
@@ -0,0 +1,6 @@
+---
+'@lit-labs/cli': minor
+'@lit-labs/gen-utils': minor
+---
+
+Implemented lit init element command
diff --git a/.eslintignore b/.eslintignore
index 7f656a9644..f627f7c58a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -75,6 +75,7 @@ packages/lit-html/async-directive.*
packages/lit-html/polyfill-support.*
packages/lit-html/private-ssr-support.*
packages/lit-html/static.*
+packages/lit-html/is-server.*
packages/lit-starter-js/node_modules/*
packages/lit-starter-js/docs/*
@@ -159,6 +160,9 @@ packages/labs/cli/index.d.ts
packages/labs/cli/index.d.ts.map
packages/labs/cli/test-gen/
+packages/labs/cli-localize/lib/
+packages/labs/cli-localize/node_modules/
+
packages/labs/context/development/
packages/labs/context/test/
packages/labs/context/node_modules/
@@ -174,6 +178,13 @@ packages/labs/eleventy-plugin-lit/test/
# Switches Node into module mode for tests
!packages/labs/eleventy-plugin-lit/test/package.json
+packages/labs/gen-manifest/index.js
+packages/labs/gen-manifest/index.js.map
+packages/labs/gen-manifest/index.d.ts
+packages/labs/gen-manifest/index.d.ts.map
+packages/labs/gen-manifest/test/
+packages/labs/gen-manifest/gen-output/
+
packages/labs/gen-utils/lib/
packages/labs/gen-utils/test/
packages/labs/gen-utils/index.js
@@ -306,6 +317,8 @@ packages/labs/virtualizer/test/**/*.d.ts.map
packages/labs/virtualizer/test/**/*.js
packages/labs/virtualizer/test/**/*.js.map
packages/labs/virtualizer/test/screenshot/cases/*/actual.*.png
+packages/labs/virtualizer/events.d.ts*
+packages/labs/virtualizer/events.js*
packages/labs/vue-utils/development/
packages/labs/vue-utils/test/
diff --git a/.github/workflows/add-issues-to-project.yml b/.github/workflows/add-issues-to-project.yml
index f7fcb83079..882eefa6f6 100644
--- a/.github/workflows/add-issues-to-project.yml
+++ b/.github/workflows/add-issues-to-project.yml
@@ -24,13 +24,13 @@ jobs:
gh api graphql -f query='
query($organization: String!, $project_number: Int!) {
organization(login: $organization){
- projectNext(number: $project_number) {
+ projectV2(number: $project_number) {
id
}
}
}' -f organization=$ORGANIZATION -F project_number=$PROJECT_NUMBER > project_data.json
- echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
+ echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
@@ -39,8 +39,8 @@ jobs:
run: |
gh api graphql -f query='
mutation($project_id:ID!, $issue_id:ID!) {
- addProjectNextItem(input: {projectId: $project_id, contentId: $issue_id}) {
- projectNextItem {
+ addProjectV2ItemById(input: {projectId: $project_id, contentId: $issue_id}) {
+ item {
id
}
}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1b2caf19eb..0f6baad1b9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -70,9 +70,7 @@ jobs:
- uses: actions/setup-node@v2
with:
- # Pin to avoid version with problematic npm. See https://github.com/npm/cli/issues/4980
- # TODO(augustinekim) Unpin when latest node installed by action includes fixed npm
- node-version: 16.15.0
+ node-version: 16
cache: 'npm'
cache-dependency-path: package-lock.json
diff --git a/.prettierignore b/.prettierignore
index d3f48c00af..17589d7975 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -75,6 +75,7 @@ packages/lit-html/async-directive.*
packages/lit-html/polyfill-support.*
packages/lit-html/private-ssr-support.*
packages/lit-html/static.*
+packages/lit-html/is-server.*
packages/lit-starter-js/node_modules/
packages/lit-starter-js/**/custom-elements.json
@@ -145,6 +146,10 @@ packages/labs/cli/index.d.ts
packages/labs/cli/index.d.ts.map
packages/labs/cli/test-gen/
+packages/labs/cli/test-goldens/
+packages/labs/cli-localize/lib/
+packages/labs/cli-localize/node_modules/
+
packages/labs/context/development/
packages/labs/context/test/
packages/labs/context/node_modules/
@@ -160,6 +165,13 @@ packages/labs/eleventy-plugin-lit/test/
# Switches Node into module mode for tests
!packages/labs/eleventy-plugin-lit/test/package.json
+packages/labs/gen-manifest/index.js
+packages/labs/gen-manifest/index.js.map
+packages/labs/gen-manifest/index.d.ts
+packages/labs/gen-manifest/index.d.ts.map
+packages/labs/gen-manifest/test/
+packages/labs/gen-manifest/gen-output/
+
packages/labs/gen-utils/lib/
packages/labs/gen-utils/test/
packages/labs/gen-utils/index.js
@@ -289,6 +301,8 @@ packages/labs/virtualizer/test/**/*.d.ts.map
packages/labs/virtualizer/test/**/*.js
packages/labs/virtualizer/test/**/*.js.map
packages/labs/virtualizer/test/screenshot/cases/*/actual.*.png
+packages/labs/virtualizer/events.d.ts*
+packages/labs/virtualizer/events.js*
packages/labs/vue-utils/development/
packages/labs/vue-utils/test/
diff --git a/README.md b/README.md
index 81aaace752..a4afa0ac3c 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,17 @@
-
+
### Simple. Fast. Web Components.
[![Build Status](https://github.com/lit/lit/actions/workflows/tests.yml/badge.svg)](https://github.com/lit/lit/actions/workflows/tests.yml)
[![Published on npm](https://img.shields.io/npm/v/lit.svg?logo=npm)](https://www.npmjs.com/package/lit)
-[![Join our Slack](https://img.shields.io/badge/slack-join%20chat-4a154b.svg?logo=slack)](https://lit.dev/slack-invite/)
+[![Join our Discord](https://img.shields.io/badge/discord-join%20chat-5865F2.svg?logo=discord&logoColor=fff)](https://lit.dev/discord/)
[![Mentioned in Awesome Lit](https://awesome.re/mentioned-badge.svg)](https://github.com/web-padawan/awesome-lit)
diff --git a/lit-next.code-workspace b/lit-next.code-workspace
index 92c91e42f4..7cd051ab40 100644
--- a/lit-next.code-workspace
+++ b/lit-next.code-workspace
@@ -56,6 +56,26 @@
"name": "cli",
"path": "packages/labs/cli"
},
+ {
+ "name": "gen-manifest",
+ "path": "packages/labs/gen-manifest"
+ },
+ {
+ "name": "gen-react",
+ "path": "packages/labs/gen-manifest"
+ },
+ {
+ "name": "gen-angular",
+ "path": "packages/labs/gen-manifest"
+ },
+ {
+ "name": "gen-vue",
+ "path": "packages/labs/gen-manifest"
+ },
+ {
+ "name": "gen-utils",
+ "path": "packages/labs/gen-manifest"
+ },
{
"name": "lit-monorepo",
"path": "."
diff --git a/package-lock.json b/package-lock.json
index 6809531d52..4c79cf1a5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"@web/test-runner-mocha": "^0.7.5",
"@web/test-runner-playwright": "^0.8.9",
"@web/test-runner-saucelabs": "^0.8.0",
+ "cross-env": "^7.0.3",
"eslint": "^8.13.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-no-only-tests": "^2.4.0",
@@ -2563,6 +2564,10 @@
"resolved": "packages/labs/cli",
"link": true
},
+ "node_modules/@lit-labs/cli-localize": {
+ "resolved": "packages/labs/cli-localize",
+ "link": true
+ },
"node_modules/@lit-labs/context": {
"resolved": "packages/labs/context",
"link": true
@@ -2571,6 +2576,10 @@
"resolved": "packages/labs/eleventy-plugin-lit",
"link": true
},
+ "node_modules/@lit-labs/gen-manifest": {
+ "resolved": "packages/labs/gen-manifest",
+ "link": true
+ },
"node_modules/@lit-labs/gen-utils": {
"resolved": "packages/labs/gen-utils",
"link": true
@@ -3590,9 +3599,9 @@
}
},
"node_modules/@testim/chrome-version": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.2.tgz",
- "integrity": "sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz",
+ "integrity": "sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==",
"dev": true
},
"node_modules/@tsconfig/node10": {
@@ -7395,17 +7404,17 @@
}
},
"node_modules/chromedriver": {
- "version": "105.0.0",
- "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-105.0.0.tgz",
- "integrity": "sha512-BX3GOUW5m6eiW9cVVF8hw+EFxvrGqYCxbwOqnpk8PjbNFqL5xjy7yel+e6ilJPjckAYFutMKs8XJvOs/W85vvg==",
+ "version": "107.0.3",
+ "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz",
+ "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
- "@testim/chrome-version": "^1.1.2",
- "axios": "^0.27.2",
- "del": "^6.0.0",
+ "@testim/chrome-version": "^1.1.3",
+ "axios": "^1.1.3",
+ "compare-versions": "^5.0.1",
"extract-zip": "^2.0.1",
- "https-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
"proxy-from-env": "^1.1.0",
"tcp-port-used": "^1.0.1"
},
@@ -7413,17 +7422,18 @@
"chromedriver": "bin/chromedriver"
},
"engines": {
- "node": ">=10"
+ "node": ">=14"
}
},
"node_modules/chromedriver/node_modules/axios": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
- "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
+ "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
"dev": true,
"dependencies": {
- "follow-redirects": "^1.14.9",
- "form-data": "^4.0.0"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
}
},
"node_modules/chromedriver/node_modules/form-data": {
@@ -7963,6 +7973,12 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
+ "node_modules/compare-versions": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.1.tgz",
+ "integrity": "sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ==",
+ "dev": true
+ },
"node_modules/compatfactory": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/compatfactory/-/compatfactory-1.0.1.tgz",
@@ -8333,6 +8349,24 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
@@ -8442,9 +8476,9 @@
},
"node_modules/custom-elements-manifest": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
- "integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==",
- "dev": true
+ "resolved": "git+ssh://git@github.com/webcomponents/custom-elements-manifest.git#f893b205ec661f485772ace6eb7d0e15efa958d4",
+ "dev": true,
+ "license": "BSD-3-Clause"
},
"node_modules/custom-event": {
"version": "1.0.1",
@@ -8857,28 +8891,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/del": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
- "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
- "dev": true,
- "dependencies": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -12989,24 +13001,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -14661,10 +14655,6 @@
"lit-analyzer": "cli.js"
}
},
- "node_modules/lit-analyzer-test-files": {
- "resolved": "packages/labs/analyzer/test-files/basic-elements",
- "link": true
- },
"node_modules/lit-analyzer/node_modules/@nodelib/fs.stat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
@@ -23803,7 +23793,7 @@
"tachometer": "^0.7.0"
},
"devDependencies": {
- "chromedriver": "^105.0.0"
+ "chromedriver": "^107.0.3"
}
},
"packages/internal-scripts": {
@@ -23833,29 +23823,26 @@
},
"packages/labs/analyzer": {
"name": "@lit-labs/analyzer",
- "version": "0.2.2",
+ "version": "0.4.0",
"license": "BSD-3-Clause",
"dependencies": {
"package-json-type": "^1.0.3",
"typescript": "~4.7.4"
- },
- "devDependencies": {
- "lit-analyzer-test-files": "./test-files/basic-elements/"
}
},
"packages/labs/analyzer/test-files/basic-elements": {
"name": "@lit-internal/test-basic-elements",
- "dev": true,
+ "extraneous": true,
"dependencies": {
"lit": "^2.0.0"
}
},
"packages/labs/cli": {
"name": "@lit-labs/cli",
- "version": "0.1.0",
+ "version": "0.2.1",
"license": "BSD-3-Clause",
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit/localize-tools": "^0.6.1",
"chalk": "^5.0.1",
@@ -23877,6 +23864,33 @@
"node": ">=14.8.0"
}
},
+ "packages/labs/cli-localize": {
+ "name": "@lit-labs/cli-localize",
+ "version": "0.1.0",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/localize-tools": "^0.6.3"
+ },
+ "devDependencies": {
+ "typescript": "~4.6.2"
+ },
+ "engines": {
+ "node": ">=14.8.0"
+ }
+ },
+ "packages/labs/cli-localize/node_modules/typescript": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
+ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
"packages/labs/cli/node_modules/chalk": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
@@ -23918,12 +23932,41 @@
"node": ">=12.16.0"
}
},
+ "packages/labs/gen-manifest": {
+ "name": "@lit-labs/gen-manifest",
+ "version": "0.0.2",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/analyzer": "^0.4.0",
+ "@lit-labs/gen-utils": "^0.1.0",
+ "custom-elements-manifest": "^2.0.0"
+ },
+ "devDependencies": {
+ "@lit-internal/tests": "^0.0.0",
+ "@types/node": "^17.0.31",
+ "uvu": "^0.5.3"
+ },
+ "engines": {
+ "node": ">=14.8.0"
+ }
+ },
+ "packages/labs/gen-manifest/node_modules/@types/node": {
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
+ "dev": true
+ },
+ "packages/labs/gen-manifest/node_modules/custom-elements-manifest": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-2.0.0.tgz",
+ "integrity": "sha512-1MmhBRszwnNYqn56nkMeHXn/Zlh998+6Yal3wedbXI7NzKPG02GDgjspdN1NiuDtt2yb5n94JvFwPOF7Prnocg=="
+ },
"packages/labs/gen-utils": {
"name": "@lit-labs/gen-utils",
- "version": "0.1.0",
+ "version": "0.1.2",
"license": "BSD-3-Clause",
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0"
+ "@lit-labs/analyzer": "^0.4.0"
},
"devDependencies": {
"@lit-internal/tests": "^0.0.0"
@@ -23934,10 +23977,10 @@
},
"packages/labs/gen-wrapper-angular": {
"name": "@lit-labs/gen-wrapper-angular",
- "version": "0.0.1",
+ "version": "0.0.3",
"license": "BSD-3-Clause",
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0"
},
"devDependencies": {
@@ -23952,10 +23995,10 @@
},
"packages/labs/gen-wrapper-react": {
"name": "@lit-labs/gen-wrapper-react",
- "version": "0.1.0",
+ "version": "0.2.1",
"license": "BSD-3-Clause",
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0"
},
"devDependencies": {
@@ -23967,10 +24010,10 @@
},
"packages/labs/gen-wrapper-vue": {
"name": "@lit-labs/gen-wrapper-vue",
- "version": "0.1.1",
+ "version": "0.2.1",
"license": "BSD-3-Clause",
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit-labs/vue-utils": "^0.1.0"
},
@@ -23995,7 +24038,7 @@
},
"packages/labs/observers": {
"name": "@lit-labs/observers",
- "version": "1.0.2",
+ "version": "1.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.1.0"
@@ -24007,7 +24050,7 @@
},
"packages/labs/react": {
"name": "@lit-labs/react",
- "version": "1.0.8",
+ "version": "1.1.0",
"license": "BSD-3-Clause",
"devDependencies": {
"@lit-internal/scripts": "^1.0.0",
@@ -24116,7 +24159,7 @@
},
"packages/labs/task": {
"name": "@lit-labs/task",
- "version": "1.1.3",
+ "version": "2.0.0",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.1.0"
@@ -24178,12 +24221,12 @@
}
},
"packages/lit": {
- "version": "2.3.1",
+ "version": "2.4.1",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.4.0",
"lit-element": "^3.2.0",
- "lit-html": "^2.3.0"
+ "lit-html": "^2.4.0"
},
"devDependencies": {
"@lit-internal/scripts": "^1.0.0",
@@ -24209,7 +24252,7 @@
}
},
"packages/lit-html": {
- "version": "2.3.1",
+ "version": "2.4.0",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
@@ -24417,7 +24460,7 @@
},
"packages/reactive-element": {
"name": "@lit/reactive-element",
- "version": "1.4.1",
+ "version": "1.4.2",
"license": "BSD-3-Clause",
"devDependencies": {
"@babel/cli": "^7.14.6",
@@ -26297,7 +26340,7 @@
"version": "file:packages/benchmarks",
"requires": {
"@lit/reactive-element": "^1.1.0",
- "chromedriver": "^105.0.0",
+ "chromedriver": "^107.0.3",
"lit-element": "^3.1.0",
"lit-html": "^2.1.0",
"tachometer": "^0.7.0"
@@ -26391,7 +26434,6 @@
"@lit-labs/analyzer": {
"version": "file:packages/labs/analyzer",
"requires": {
- "lit-analyzer-test-files": "./test-files/basic-elements/",
"package-json-type": "^1.0.3",
"typescript": "~4.7.4"
}
@@ -26400,7 +26442,7 @@
"version": "file:packages/labs/cli",
"requires": {
"@lit-internal/tests": "^0.0.0",
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit/localize-tools": "^0.6.1",
"@types/command-line-args": "^5.2.0",
@@ -26420,6 +26462,21 @@
}
}
},
+ "@lit-labs/cli-localize": {
+ "version": "file:packages/labs/cli-localize",
+ "requires": {
+ "@lit/localize-tools": "^0.6.3",
+ "typescript": "~4.6.2"
+ },
+ "dependencies": {
+ "typescript": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
+ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
+ "dev": true
+ }
+ }
+ },
"@lit-labs/context": {
"version": "file:packages/labs/context",
"requires": {
@@ -26439,11 +26496,35 @@
"rimraf": "^3.0.2"
}
},
+ "@lit-labs/gen-manifest": {
+ "version": "file:packages/labs/gen-manifest",
+ "requires": {
+ "@lit-internal/tests": "^0.0.0",
+ "@lit-labs/analyzer": "^0.4.0",
+ "@lit-labs/gen-utils": "^0.1.0",
+ "@types/node": "^17.0.31",
+ "custom-elements-manifest": "^2.0.0",
+ "uvu": "^0.5.3"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
+ "dev": true
+ },
+ "custom-elements-manifest": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-2.0.0.tgz",
+ "integrity": "sha512-1MmhBRszwnNYqn56nkMeHXn/Zlh998+6Yal3wedbXI7NzKPG02GDgjspdN1NiuDtt2yb5n94JvFwPOF7Prnocg=="
+ }
+ }
+ },
"@lit-labs/gen-utils": {
"version": "file:packages/labs/gen-utils",
"requires": {
"@lit-internal/tests": "^0.0.0",
- "@lit-labs/analyzer": "^0.2.0"
+ "@lit-labs/analyzer": "^0.4.0"
}
},
"@lit-labs/gen-wrapper-angular": {
@@ -26451,7 +26532,7 @@
"requires": {
"@esm-bundle/chai": "^4.1.5",
"@lit-internal/tests": "0.0.0",
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@types/chai": "^4.0.1",
"@types/mocha": "^9.0.0"
@@ -26461,7 +26542,7 @@
"version": "file:packages/labs/gen-wrapper-react",
"requires": {
"@lit-internal/tests": "^0.0.0",
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0"
}
},
@@ -26469,7 +26550,7 @@
"version": "file:packages/labs/gen-wrapper-vue",
"requires": {
"@lit-internal/tests": "^0.0.0",
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit-labs/vue-utils": "^0.1.0"
}
@@ -27527,9 +27608,9 @@
}
},
"@testim/chrome-version": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.2.tgz",
- "integrity": "sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz",
+ "integrity": "sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==",
"dev": true
},
"@tsconfig/node10": {
@@ -30638,28 +30719,29 @@
}
},
"chromedriver": {
- "version": "105.0.0",
- "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-105.0.0.tgz",
- "integrity": "sha512-BX3GOUW5m6eiW9cVVF8hw+EFxvrGqYCxbwOqnpk8PjbNFqL5xjy7yel+e6ilJPjckAYFutMKs8XJvOs/W85vvg==",
+ "version": "107.0.3",
+ "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz",
+ "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==",
"dev": true,
"requires": {
- "@testim/chrome-version": "^1.1.2",
- "axios": "^0.27.2",
- "del": "^6.0.0",
+ "@testim/chrome-version": "^1.1.3",
+ "axios": "^1.1.3",
+ "compare-versions": "^5.0.1",
"extract-zip": "^2.0.1",
- "https-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
"proxy-from-env": "^1.1.0",
"tcp-port-used": "^1.0.1"
},
"dependencies": {
"axios": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
- "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
+ "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
"dev": true,
"requires": {
- "follow-redirects": "^1.14.9",
- "form-data": "^4.0.0"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
}
},
"form-data": {
@@ -31084,6 +31166,12 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
+ "compare-versions": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.1.tgz",
+ "integrity": "sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ==",
+ "dev": true
+ },
"compatfactory": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/compatfactory/-/compatfactory-1.0.1.tgz",
@@ -31380,6 +31468,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.1"
+ }
+ },
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
@@ -31473,10 +31570,9 @@
"dev": true
},
"custom-elements-manifest": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
- "integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==",
- "dev": true
+ "version": "git+ssh://git@github.com/webcomponents/custom-elements-manifest.git#f893b205ec661f485772ace6eb7d0e15efa958d4",
+ "dev": true,
+ "from": "custom-elements-manifest@1.0.0"
},
"custom-event": {
"version": "1.0.1",
@@ -31791,22 +31887,6 @@
"isobject": "^3.0.1"
}
},
- "del": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
- "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
- "dev": true,
- "requires": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
- }
- },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -34858,18 +34938,6 @@
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
"dev": true
},
- "is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "dev": true
- },
- "is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true
- },
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
@@ -36183,7 +36251,7 @@
"@webcomponents/template": "^1.4.4",
"@webcomponents/webcomponentsjs": "^2.6.0",
"lit-element": "^3.2.0",
- "lit-html": "^2.3.0",
+ "lit-html": "^2.4.0",
"tslib": "^2.0.3"
}
},
@@ -36363,12 +36431,6 @@
}
}
},
- "lit-analyzer-test-files": {
- "version": "file:packages/labs/analyzer/test-files/basic-elements",
- "requires": {
- "lit": "^2.0.0"
- }
- },
"lit-element": {
"version": "file:packages/lit-element",
"requires": {
diff --git a/package.json b/package.json
index db0082bb0c..65cf1e01b3 100644
--- a/package.json
+++ b/package.json
@@ -195,6 +195,7 @@
"@web/test-runner-mocha": "^0.7.5",
"@web/test-runner-playwright": "^0.8.9",
"@web/test-runner-saucelabs": "^0.8.0",
+ "cross-env": "^7.0.3",
"eslint": "^8.13.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-no-only-tests": "^2.4.0",
diff --git a/packages/benchmarks/package.json b/packages/benchmarks/package.json
index 1c4ddda3a9..dfaffc4d67 100644
--- a/packages/benchmarks/package.json
+++ b/packages/benchmarks/package.json
@@ -68,6 +68,6 @@
"tachometer": "^0.7.0"
},
"devDependencies": {
- "chromedriver": "^105.0.0"
+ "chromedriver": "^107.0.3"
}
}
diff --git a/packages/labs/analyzer/.vscode/launch.json b/packages/labs/analyzer/.vscode/launch.json
index 225b6c951e..f5e40122fb 100644
--- a/packages/labs/analyzer/.vscode/launch.json
+++ b/packages/labs/analyzer/.vscode/launch.json
@@ -9,7 +9,7 @@
"request": "launch",
"name": "Test",
"skipFiles": ["/**"],
- "program": "${workspaceFolder}/node_modules/.bin/uvu",
+ "program": "${workspaceFolder}/../../../node_modules/uvu/bin.js",
"args": ["test", "\\_test\\.js$"],
"outFiles": ["${workspaceFolder}/**/*.js"],
"console": "integratedTerminal"
diff --git a/packages/labs/analyzer/CHANGELOG.md b/packages/labs/analyzer/CHANGELOG.md
index d6d405fc46..7dd7c301d2 100644
--- a/packages/labs/analyzer/CHANGELOG.md
+++ b/packages/labs/analyzer/CHANGELOG.md
@@ -1,5 +1,25 @@
# @lit-labs/analyzer
+## 0.4.0
+
+### Minor Changes
+
+- [#3333](https://github.com/lit/lit/pull/3333) [`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e) - Cache Module models based on dependencies.
+
+### Patch Changes
+
+- [#2990](https://github.com/lit/lit/pull/2990) [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771) - Added initial implementation of custom elements manifest generator (WIP).
+
+## 0.3.0
+
+### Minor Changes
+
+- [#3304](https://github.com/lit/lit/pull/3304) [`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a) - Added support for analyzing JavaScript files.
+
+- [#3288](https://github.com/lit/lit/pull/3288) [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e) - Refactored Analyzer into better fit for use in plugins. Analyzer class now takes a ts.Program, and PackageAnalyzer takes a package path and creates a program to analyze a package on the filesystem.
+
+- [#3254](https://github.com/lit/lit/pull/3254) [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9) - Fixes bug where global install of CLI resulted in incompatible use of analyzer between CLI packages. Fixes #3234.
+
## 0.2.2
### Patch Changes
diff --git a/packages/labs/analyzer/package.json b/packages/labs/analyzer/package.json
index 524757cea0..f9ab4cc6a9 100644
--- a/packages/labs/analyzer/package.json
+++ b/packages/labs/analyzer/package.json
@@ -1,6 +1,6 @@
{
"name": "@lit-labs/analyzer",
- "version": "0.2.2",
+ "version": "0.4.0",
"publishConfig": {
"access": "public"
},
@@ -36,7 +36,7 @@
},
"test": {
"#comment": "The quotes around the file regex must be double quotes on windows!",
- "command": "uvu test \"_test\\.js$\"",
+ "command": "cross-env NODE_OPTIONS=--enable-source-maps uvu test \"_test\\.js$\"",
"dependencies": [
"build",
"../../lit:build"
@@ -58,8 +58,5 @@
"dependencies": {
"package-json-type": "^1.0.3",
"typescript": "~4.7.4"
- },
- "devDependencies": {
- "lit-analyzer-test-files": "./test-files/basic-elements/"
}
}
diff --git a/packages/labs/analyzer/src/index.ts b/packages/labs/analyzer/src/index.ts
index cd3edd0d8b..3aabd2b437 100644
--- a/packages/labs/analyzer/src/index.ts
+++ b/packages/labs/analyzer/src/index.ts
@@ -5,16 +5,19 @@
*/
export {Analyzer} from './lib/analyzer.js';
+export {createPackageAnalyzer} from './lib/analyze-package.js';
export type {
Package,
Module,
Reference,
Type,
+ Event,
Declaration,
VariableDeclaration,
ClassDeclaration,
LitElementDeclaration,
+ LitElementExport,
PackageJson,
ModuleWithLitElementDeclarations,
} from './lib/model.js';
diff --git a/packages/labs/analyzer/src/lib/analyze-package.ts b/packages/labs/analyzer/src/lib/analyze-package.ts
new file mode 100644
index 0000000000..959b3a5c86
--- /dev/null
+++ b/packages/labs/analyzer/src/lib/analyze-package.ts
@@ -0,0 +1,93 @@
+import ts from 'typescript';
+import {AbsolutePath} from './paths.js';
+import * as path from 'path';
+import {DiagnosticsError} from './errors.js';
+import {Analyzer} from './analyzer.js';
+
+/**
+ * Returns an analyzer for a Lit npm package based on a filesystem path.
+ *
+ * The path may specify a package root folder, or a specific tsconfig file. When
+ * specifying a folder, if no tsconfig.json file is found directly in the root
+ * folder, the project will be analyzed as JavaScript.
+ */
+export const createPackageAnalyzer = (packagePath: AbsolutePath) => {
+ // This logic accepts either a path to folder containing a tsconfig.json
+ // directly inside it or a path to a specific tsconfig file. If no tsconfig
+ // file is found, we fallback to creating a Javascript program.
+ const isDirectory = ts.sys.directoryExists(packagePath);
+ const configFileName = isDirectory
+ ? path.join(packagePath, 'tsconfig.json')
+ : packagePath;
+ let commandLine: ts.ParsedCommandLine;
+ if (ts.sys.fileExists(configFileName)) {
+ const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
+ commandLine = ts.parseJsonConfigFileContent(
+ configFile.config /* json */,
+ ts.sys /* host */,
+ packagePath /* basePath */,
+ undefined /* existingOptions */,
+ path.relative(packagePath, configFileName) /* configFileName */
+ );
+ } else if (isDirectory) {
+ console.info(`No tsconfig.json found; assuming package is JavaScript.`);
+ commandLine = ts.parseJsonConfigFileContent(
+ {
+ compilerOptions: {
+ // TODO(kschaaf): probably want to make this configurable
+ module: 'ES2020',
+ lib: ['es2020', 'DOM'],
+ allowJs: true,
+ skipLibCheck: true,
+ skipDefaultLibCheck: true,
+ // With `allowJs: true`, the program will automatically include every
+ // .d.ts file under node_modules/@types regardless of whether the
+ // program imported modules associated with those types, which can
+ // dramatically slow down the program analysis (this does not
+ // automatically happen when allowJs is false). For now, eliminating
+ // `typeRoots` fixes the automatic over-inclusion of .d.ts files as
+ // long as nodeResolution is properly set (it will still import .d.ts
+ // files into the project as expected based on imports). It may
+ // however cause a failure to find definitely-typed .d.ts files for
+ // imports in a JS project, but it seems unlikely these would be
+ // installed anyway.
+ typeRoots: [],
+ moduleResolution: 'node',
+ },
+ include: ['**/*.js'],
+ },
+ ts.sys /* host */,
+ packagePath /* basePath */
+ );
+ } else {
+ throw new Error(
+ `The specified path '${packagePath}' was not a folder or a tsconfig file.`
+ );
+ }
+
+ // Ensure that `parent` nodes are set in the AST by creating a compiler
+ // host with this configuration; without these, `getText()` and other
+ // API's that require crawling up the AST tree to find the source file
+ // text may fail
+ const compilerHost = ts.createCompilerHost(
+ commandLine.options,
+ /* setParentNodes */ true
+ );
+ const program = ts.createProgram(
+ commandLine.fileNames,
+ commandLine.options,
+ compilerHost
+ );
+
+ const analyzer = new Analyzer({getProgram: () => program, fs: ts.sys, path});
+
+ const diagnostics = program.getSemanticDiagnostics();
+ if (diagnostics.length > 0) {
+ throw new DiagnosticsError(
+ diagnostics,
+ `Error analyzing package '${packagePath}': Please fix errors first`
+ );
+ }
+
+ return analyzer;
+};
diff --git a/packages/labs/analyzer/src/lib/analyzer.ts b/packages/labs/analyzer/src/lib/analyzer.ts
index 203427683f..02119cafb7 100644
--- a/packages/labs/analyzer/src/lib/analyzer.ts
+++ b/packages/labs/analyzer/src/lib/analyzer.ts
@@ -5,91 +5,117 @@
*/
import ts from 'typescript';
-import {Package, PackageJson} from './model.js';
-import {ProgramContext} from './program-context.js';
+import {Package, PackageJson, AnalyzerInterface, Module} from './model.js';
import {AbsolutePath} from './paths.js';
-import * as fs from 'fs';
-import * as path from 'path';
import {getModule} from './javascript/modules.js';
export {PackageJson};
+import {
+ getPackageInfo,
+ getPackageRootForModulePath,
+} from './javascript/packages.js';
+
+export interface AnalyzerInit {
+ getProgram: () => ts.Program;
+ fs: AnalyzerInterface['fs'];
+ path: AnalyzerInterface['path'];
+ basePath?: AbsolutePath;
+}
/**
- * An analyzer for Lit npm packages
+ * An analyzer for Lit typescript modules.
*/
-export class Analyzer {
- readonly packageRoot: AbsolutePath;
- readonly programContext: ProgramContext;
+export class Analyzer implements AnalyzerInterface {
+ // Cache of Module models by path; invalidated when the sourceFile
+ // or any of its dependencies change
+ readonly moduleCache = new Map();
+ private readonly _getProgram: () => ts.Program;
+ readonly fs: AnalyzerInterface['fs'];
+ readonly path: AnalyzerInterface['path'];
+ private _commandLine: ts.ParsedCommandLine | undefined = undefined;
- /**
- * @param packageRoot The root directory of the package to analyze. Currently
- * this directory must have a tsconfig.json and package.json.
- */
- constructor(packageRoot: AbsolutePath) {
- this.packageRoot = packageRoot;
+ constructor(init: AnalyzerInit) {
+ this._getProgram = init.getProgram;
+ this.fs = init.fs;
+ this.path = init.path;
+ }
- // TODO(kschaaf): Consider moving the package.json and tsconfig.json
- // to analyzePackage() or move it to an async factory function that
- // passes these to the constructor as arguments.
- const packageJsonFilename = path.join(packageRoot, 'package.json');
- let packageJsonText;
- try {
- packageJsonText = fs.readFileSync(packageJsonFilename, 'utf8');
- } catch (e) {
- throw new Error(`package.json not found at ${packageJsonFilename}`);
- }
- let packageJson;
- try {
- packageJson = JSON.parse(packageJsonText);
- } catch (e) {
- throw new Error(`Malformed package.json found at ${packageJsonFilename}`);
- }
- if (packageJson.name === undefined) {
- throw new Error(
- `package.json in ${packageJsonFilename} did not have a name.`
- );
- }
+ get program() {
+ return this._getProgram();
+ }
- const configFileName = ts.findConfigFile(
- packageRoot,
- ts.sys.fileExists,
- 'tsconfig.json'
- );
- if (configFileName === undefined) {
- // TODO: use a hard-coded tsconfig for JS projects.
- throw new Error(`tsconfig.json not found in ${packageRoot}`);
- }
- const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
- // Note `configFileName` is optional but must be set for
- // `getOutputFileNames` to work correctly; however, it must be relative to
- // `packageRoot`
- const commandLine = ts.parseJsonConfigFileContent(
- configFile.config /* json */,
- ts.sys /* host */,
- packageRoot /* basePath */,
- undefined /* existingOptions */,
- path.relative(packageRoot, configFileName) /* configFileName */
- );
+ get commandLine() {
+ return (this._commandLine ??= getCommandLineFromProgram(this));
+ }
- this.programContext = new ProgramContext(
- packageRoot,
- commandLine,
- packageJson
- );
+ getModule(modulePath: AbsolutePath) {
+ return getModule(modulePath, this);
}
- analyzePackage() {
- const rootFileNames = this.programContext.program.getRootFileNames();
+ getPackage() {
+ const rootFileNames = this.program.getRootFileNames();
+
+ // Find the package.json for this package based on the first root filename
+ // in the program (we assume all root files in a program belong to the same
+ // package)
+ if (rootFileNames.length === 0) {
+ throw new Error('No source files found in package.');
+ }
+ const packageInfo = getPackageInfo(rootFileNames[0] as AbsolutePath, this);
return new Package({
- rootDir: this.packageRoot,
+ ...packageInfo,
modules: rootFileNames.map((fileName) =>
getModule(
- this.programContext.program.getSourceFile(path.normalize(fileName))!,
- this.programContext
+ this.path.normalize(fileName) as AbsolutePath,
+ this,
+ packageInfo
)
),
- tsConfig: this.programContext.commandLine,
- packageJson: this.programContext.packageJson,
});
}
}
+
+/**
+ * Extracts a `ts.ParsedCommandLine` (essentially, the key bits of a
+ * `tsconfig.json`) from the analyzer's `ts.Program`.
+ *
+ * The `ts.getOutputFileNames()` function must be passed a
+ * `ts.ParsedCommandLine`; since not all usages of the analyzer create the
+ * program directly from a tsconfig (plugins get passed the program only),
+ * this allows backing the `ParsedCommandLine` out of an existing program.
+ */
+export const getCommandLineFromProgram = (
+ analyzer: Analyzer
+): ts.ParsedCommandLine => {
+ const compilerOptions = analyzer.program.getCompilerOptions();
+ const files = analyzer.program.getRootFileNames();
+ const json = {
+ files,
+ compilerOptions,
+ };
+ if (compilerOptions.configFilePath !== undefined) {
+ // For a TS project, derive the package root from the config file path
+ const packageRoot = analyzer.path.basename(
+ compilerOptions.configFilePath as string
+ );
+ return ts.parseJsonConfigFileContent(
+ json,
+ ts.sys,
+ packageRoot,
+ undefined,
+ compilerOptions.configFilePath as string
+ );
+ } else {
+ // Otherwise, this is a JS project; we can determine the package root
+ // based on the package.json location; we can look that up based on
+ // the first root file
+ const packageRoot = getPackageRootForModulePath(
+ files[0] as AbsolutePath,
+ analyzer
+ // Note we don't pass a configFilePath since we don't have one; This just
+ // means we can't use ts.getOutputFileNames(), which we isn't needed in
+ // JS program
+ );
+ return ts.parseJsonConfigFileContent(json, ts.sys, packageRoot);
+ }
+};
diff --git a/packages/labs/analyzer/src/lib/javascript/classes.ts b/packages/labs/analyzer/src/lib/javascript/classes.ts
index 4d45028791..50aabacfcc 100644
--- a/packages/labs/analyzer/src/lib/javascript/classes.ts
+++ b/packages/labs/analyzer/src/lib/javascript/classes.ts
@@ -7,20 +7,130 @@
/**
* @fileoverview
*
- * Utilities for working with classes
+ * Utilities for analyzing class declarations
*/
import ts from 'typescript';
-import {ClassDeclaration} from '../model.js';
-import {ProgramContext} from '../program-context.js';
+import {DiagnosticsError} from '../errors.js';
+import {
+ ClassDeclaration,
+ AnalyzerInterface,
+ DeclarationInfo,
+ ClassHeritage,
+ Reference,
+} from '../model.js';
+import {
+ isLitElementSubclass,
+ getLitElementDeclaration,
+} from '../lit-element/lit-element.js';
+import {hasExportKeyword, getReferenceForIdentifier} from '../references.js';
-export const getClassDeclaration = (
+/**
+ * Returns an analyzer `ClassDeclaration` model for the given
+ * ts.ClassDeclaration.
+ */
+const getClassDeclaration = (
declaration: ts.ClassDeclaration,
- _programContext: ProgramContext
-): ClassDeclaration => {
+ analyzer: AnalyzerInterface
+) => {
+ if (isLitElementSubclass(declaration, analyzer)) {
+ return getLitElementDeclaration(declaration, analyzer);
+ }
return new ClassDeclaration({
// TODO(kschaaf): support anonymous class expressions when assigned to a const
name: declaration.name?.text ?? '',
node: declaration,
+ getHeritage: () => getHeritage(declaration, analyzer),
});
};
+
+/**
+ * Returns the name of a class declaration.
+ */
+const getClassDeclarationName = (declaration: ts.ClassDeclaration) => {
+ const name =
+ declaration.name?.text ??
+ // The only time a class declaration will not have a name is when it is
+ // a default export, aka `export default class { }`
+ (declaration.modifiers?.some((s) => s.kind === ts.SyntaxKind.DefaultKeyword)
+ ? 'default'
+ : undefined);
+ if (name === undefined) {
+ throw new DiagnosticsError(
+ declaration,
+ 'Unexpected class declaration without a name'
+ );
+ }
+ return name;
+};
+
+/**
+ * Returns name and model factory for a class declaration.
+ */
+export const getClassDeclarationInfo = (
+ declaration: ts.ClassDeclaration,
+ analyzer: AnalyzerInterface
+): DeclarationInfo => {
+ return {
+ name: getClassDeclarationName(declaration),
+ factory: () => getClassDeclaration(declaration, analyzer),
+ isExport: hasExportKeyword(declaration),
+ };
+};
+
+/**
+ * Returns the superClass and any applied mixins for a given class declaration.
+ */
+export const getHeritage = (
+ declaration: ts.ClassLikeDeclarationBase,
+ analyzer: AnalyzerInterface
+): ClassHeritage => {
+ const extendsClause = declaration.heritageClauses?.find(
+ (c) => c.token === ts.SyntaxKind.ExtendsKeyword
+ );
+ if (extendsClause !== undefined) {
+ if (extendsClause.types.length !== 1) {
+ throw new DiagnosticsError(
+ extendsClause,
+ 'Internal error: did not expect extends clause to have multiple types'
+ );
+ }
+ return getHeritageFromExpression(
+ extendsClause.types[0].expression,
+ analyzer
+ );
+ }
+ // No extends clause; return empty heritage
+ return {
+ mixins: [],
+ superClass: undefined,
+ };
+};
+
+export const getHeritageFromExpression = (
+ expression: ts.Expression,
+ analyzer: AnalyzerInterface
+): ClassHeritage => {
+ // TODO(kschaaf): Support for extracting mixing applications from the heritage
+ // expression https://github.com/lit/lit/issues/2998
+ const mixins: Reference[] = [];
+ const superClass = getSuperClass(expression, analyzer);
+ return {
+ superClass,
+ mixins,
+ };
+};
+
+export const getSuperClass = (
+ expression: ts.Expression,
+ analyzer: AnalyzerInterface
+): Reference => {
+ // TODO(kschaaf) Could add support for inline class expressions here as well
+ if (ts.isIdentifier(expression)) {
+ return getReferenceForIdentifier(expression, analyzer);
+ }
+ throw new DiagnosticsError(
+ expression,
+ `Expected expression to be a concrete superclass. Mixins are not yet supported.`
+ );
+};
diff --git a/packages/labs/analyzer/src/lib/javascript/jsdoc.ts b/packages/labs/analyzer/src/lib/javascript/jsdoc.ts
new file mode 100644
index 0000000000..5b448618cc
--- /dev/null
+++ b/packages/labs/analyzer/src/lib/javascript/jsdoc.ts
@@ -0,0 +1,134 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import ts from 'typescript';
+import {DiagnosticsError} from '../errors.js';
+import {AnalyzerInterface, NamedJSDocInfo, NodeJSDocInfo} from '../model.js';
+
+/**
+ * @fileoverview
+ *
+ * Utilities for parsing JSDoc comments.
+ */
+
+/**
+ * Remove line feeds from JSDoc summaries, so they are normalized to
+ * unix `\n` line endings.
+ */
+const normalizeLineEndings = (s: string) => s.replace(/\r/g, '');
+
+// Regex for parsing name, summary, and descriptions from JSDoc comments
+const parseNameDescSummaryRE =
+ /^\s*(?[^\s:]+)([\s-:]+)?(?[^\n\r]+)?([\n\r]+(?[\s\S]*))?$/m;
+
+// Regex for parsing summary and description from JSDoc comments
+const parseDescSummaryRE =
+ /^\s*(?[^\n\r]+)\r?\n\r?\n(?[\s\S]*)$/m;
+
+/**
+ * Parses name, summary, and description from JSDoc tag for things like @slot,
+ * @cssPart, and @cssProp.
+ *
+ * Supports the following patterns following the tag (TS parses the tag for us):
+ * * @slot name
+ * * @slot name summary
+ * * @slot name: summary
+ * * @slot name - summary
+ * * @slot name - summary...
+ * *
+ * * description (multiline)
+ */
+export const parseNameDescSummary = (
+ tag: ts.JSDocTag
+): NamedJSDocInfo | undefined => {
+ const {comment} = tag;
+ if (comment == undefined) {
+ return undefined;
+ }
+ if (typeof comment !== 'string') {
+ throw new DiagnosticsError(tag, `Internal error: unsupported node type`);
+ }
+ const nameDescSummary = comment.match(parseNameDescSummaryRE);
+ if (nameDescSummary === null) {
+ throw new DiagnosticsError(tag, 'Unexpected JSDoc format');
+ }
+ const {name, description, summary} = nameDescSummary.groups!;
+ const v: NamedJSDocInfo = {name};
+ if (summary !== undefined) {
+ v.summary = summary;
+ }
+ if (description !== undefined) {
+ v.description = normalizeLineEndings(description);
+ }
+ return v;
+};
+
+/**
+ * Parse summary, description, and deprecated information from JSDoc comments on
+ * a given node.
+ */
+export const parseNodeJSDocInfo = (
+ node: ts.Node,
+ analyzer: AnalyzerInterface
+): NodeJSDocInfo => {
+ const v: NodeJSDocInfo = {};
+ const jsDocTags = ts.getJSDocTags(node);
+ if (jsDocTags !== undefined) {
+ for (const tag of jsDocTags) {
+ const {comment} = tag;
+ if (comment !== undefined && typeof comment !== 'string') {
+ throw new DiagnosticsError(
+ tag,
+ `Internal error: unsupported node type`
+ );
+ }
+ switch (tag.tagName.text) {
+ case 'description':
+ if (comment !== undefined) {
+ v.description = normalizeLineEndings(comment);
+ }
+ break;
+ case 'summary':
+ if (comment !== undefined) {
+ v.summary = comment;
+ }
+ break;
+ case 'deprecated':
+ v.deprecated = comment !== undefined ? comment : true;
+ break;
+ }
+ }
+ }
+ // If we didn't have a tagged @description, we'll use any untagged text as
+ // the description. If we also didn't have a @summary and the untagged text
+ // has a line break, we'll use the first chunk as the summary, and the
+ // remainder as a description.
+ if (v.description === undefined) {
+ // Strangely, it only seems possible to get the untagged jsdoc comments
+ // via the typechecker/symbol API
+ const checker = analyzer.program.getTypeChecker();
+ const symbol =
+ checker.getSymbolAtLocation(node) ??
+ checker.getTypeAtLocation(node).getSymbol();
+ const comments = symbol?.getDocumentationComment(checker);
+ if (comments !== undefined) {
+ const comment = comments.map((c) => c.text).join('\n');
+ if (v.summary !== undefined) {
+ v.description = normalizeLineEndings(comment);
+ } else {
+ const info = comment.match(parseDescSummaryRE);
+ if (info === null) {
+ v.description = normalizeLineEndings(comment);
+ } else {
+ const {summary, description} = info.groups!;
+ v.summary = summary;
+ v.description = normalizeLineEndings(description);
+ }
+ }
+ }
+ }
+ return v;
+};
diff --git a/packages/labs/analyzer/src/lib/javascript/modules.ts b/packages/labs/analyzer/src/lib/javascript/modules.ts
index f4bac3057c..c3a207ce03 100644
--- a/packages/labs/analyzer/src/lib/javascript/modules.ts
+++ b/packages/labs/analyzer/src/lib/javascript/modules.ts
@@ -4,65 +4,323 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
+/**
+ * @fileoverview
+ *
+ * Utilities for analyzing ES modules
+ */
+
import ts from 'typescript';
-import {Module} from '../model.js';
import {
- isLitElement,
- getLitElementDeclaration,
-} from '../lit-element/lit-element.js';
-import * as path from 'path';
-import {getClassDeclaration} from './classes.js';
-import {getVariableDeclarations} from './variables.js';
-import {ProgramContext} from '../program-context.js';
-import {AbsolutePath, absoluteToPackage} from '../paths.js';
+ Module,
+ AnalyzerInterface,
+ PackageInfo,
+ Declaration,
+ DeclarationInfo,
+ ExportMap,
+ DeclarationMap,
+ ModuleInfo,
+ LocalNameOrReference,
+} from '../model.js';
+import {getClassDeclarationInfo} from './classes.js';
+import {
+ getExportAssignmentVariableDeclarationInfo,
+ getVariableDeclarationInfo,
+} from './variables.js';
+import {AbsolutePath, PackagePath, absoluteToPackage} from '../paths.js';
+import {getPackageInfo} from './packages.js';
+import {DiagnosticsError} from '../errors.js';
+import {
+ getExportReferences,
+ getImportReferenceForSpecifierExpression,
+ getSpecifierString,
+} from '../references.js';
+
+/**
+ * Returns the sourcePath, jsPath, and package.json contents of the containing
+ * package for the given module path.
+ *
+ * This is a minimal subset of module information needed for constructing a
+ * Reference object for a module.
+ */
+export const getModuleInfo = (
+ modulePath: AbsolutePath,
+ analyzer: AnalyzerInterface,
+ packageInfo: PackageInfo = getPackageInfo(modulePath, analyzer)
+): ModuleInfo => {
+ // The packageRoot for this module is needed for translating the source file
+ // path to a package relative path, and the packageName is needed for
+ // generating references to any symbols in this module.
+ const {rootDir, packageJson} = packageInfo;
+ const absJsPath = getJSPathFromSourcePath(
+ modulePath as AbsolutePath,
+ analyzer
+ );
+ const jsPath =
+ absJsPath !== undefined
+ ? absoluteToPackage(absJsPath, rootDir)
+ : ('not/implemented' as PackagePath);
+ const sourcePath = absoluteToPackage(
+ analyzer.path.normalize(modulePath) as AbsolutePath,
+ rootDir
+ );
+ return {
+ jsPath,
+ sourcePath,
+ packageJson,
+ };
+};
+/**
+ * Returns an analyzer `Module` model for the given module path.
+ */
export const getModule = (
- sourceFile: ts.SourceFile,
- programContext: ProgramContext
+ modulePath: AbsolutePath,
+ analyzer: AnalyzerInterface,
+ packageInfo: PackageInfo = getPackageInfo(modulePath, analyzer)
) => {
- const sourcePath = absoluteToPackage(
- path.normalize(sourceFile.fileName) as AbsolutePath,
- programContext.packageRoot
+ // Return cached module if we've parsed this sourceFile already and its
+ // dependencies haven't changed
+ const cachedModule = getAndValidateModuleFromCache(modulePath, analyzer);
+ if (cachedModule !== undefined) {
+ return cachedModule;
+ }
+ const sourceFile = analyzer.program.getSourceFile(
+ analyzer.path.normalize(modulePath)
);
- const fullSourcePath = path.join(programContext.packageRoot, sourcePath);
- const jsPath = ts
- .getOutputFileNames(programContext.commandLine, fullSourcePath, false)
- .filter((f) => f.endsWith('.js'))[0];
- // TODO(kschaaf): this could happen if someone imported only a .d.ts file;
- // we might need to handle this differently
- if (jsPath === undefined) {
- throw new Error(`Could not determine output filename for '${sourcePath}'`);
+ if (sourceFile === undefined) {
+ throw new Error(`Program did not contain a source file for ${modulePath}`);
}
- const module = new Module({
- sourcePath,
- // The jsPath appears to come out of the ts API with unix
- // separators; since sourcePath uses OS separators, normalize
- // this so that all our model paths are OS-native
- jsPath: absoluteToPackage(
- path.normalize(jsPath) as AbsolutePath,
- programContext.packageRoot as AbsolutePath
- ),
- sourceFile,
- });
-
- programContext.currentModule = module;
+ const dependencies = new Set();
+ const declarationMap: DeclarationMap = new Map Declaration>();
+ const exportMap: ExportMap = new Map();
+ const reexports: ts.Expression[] = [];
+ const addDeclaration = (info: DeclarationInfo) => {
+ const {name, factory, isExport} = info;
+ declarationMap.set(name, factory);
+ if (isExport) {
+ exportMap.set(name, name);
+ }
+ };
+ // Find and add models for declarations in the module
+ // TODO(kschaaf): Add Function and MixinDeclarations
for (const statement of sourceFile.statements) {
if (ts.isClassDeclaration(statement)) {
- module.declarations.push(
- isLitElement(statement, programContext)
- ? getLitElementDeclaration(statement, programContext)
- : getClassDeclaration(statement, programContext)
- );
+ addDeclaration(getClassDeclarationInfo(statement, analyzer));
} else if (ts.isVariableStatement(statement)) {
- module.declarations.push(
- ...statement.declarationList.declarations
- .map((dec) => getVariableDeclarations(dec, dec.name, programContext))
- .flat()
+ getVariableDeclarationInfo(statement, analyzer).forEach(addDeclaration);
+ } else if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
+ const {exportClause, moduleSpecifier} = statement;
+ if (exportClause === undefined) {
+ // Case: `export * from 'foo';` The `exportClause` is undefined for
+ // wildcard exports. Add the re-exported module specifier to the
+ // `reexports` list, and we will add references to the exportMap lazily
+ // the first time exports are queried
+ if (moduleSpecifier === undefined) {
+ throw new DiagnosticsError(
+ statement,
+ `Expected a wildcard export to have a module specifier.`
+ );
+ }
+ reexports.push(moduleSpecifier);
+ } else {
+ // Case: `export {...}` and `export {...} from '...'`
+ // Add all of the exports in this export statement to the exportMap
+ getExportReferences(exportClause, moduleSpecifier, analyzer).forEach(
+ ({exportName, decNameOrRef}) =>
+ exportMap.set(exportName, decNameOrRef)
+ );
+ }
+ } else if (ts.isExportAssignment(statement)) {
+ addDeclaration(
+ getExportAssignmentVariableDeclarationInfo(statement, analyzer)
+ );
+ } else if (ts.isImportDeclaration(statement)) {
+ dependencies.add(
+ getPathForModuleSpecifierExpression(statement.moduleSpecifier, analyzer)
);
}
}
- programContext.currentModule = undefined;
+ // Construct module and save in cache
+ const module = new Module({
+ ...getModuleInfo(modulePath, analyzer, packageInfo),
+ sourceFile,
+ declarationMap,
+ dependencies,
+ exportMap,
+ finalizeExports: () => finalizeExports(reexports, exportMap, analyzer),
+ });
+ analyzer.moduleCache.set(
+ analyzer.path.normalize(sourceFile.fileName) as AbsolutePath,
+ module
+ );
return module;
};
+
+/**
+ * For any re-exported modules (i.e. `export * from 'foo'`), add all of the
+ * exported names of the reexported module to the given exportMap, with
+ * References into that module.
+ */
+const finalizeExports = (
+ reexportSpecifiers: ts.Expression[],
+ exportMap: ExportMap,
+ analyzer: AnalyzerInterface
+) => {
+ for (const moduleSpecifier of reexportSpecifiers) {
+ const module = getModule(
+ getPathForModuleSpecifierExpression(moduleSpecifier, analyzer),
+ analyzer
+ );
+ for (const name of module.exportNames) {
+ exportMap.set(
+ name,
+ getImportReferenceForSpecifierExpression(
+ moduleSpecifier,
+ name,
+ analyzer
+ )
+ );
+ }
+ }
+};
+
+/**
+ * Returns a cached Module model for the given module path if it and all of its
+ * dependencies' models are still valid since the model was cached. If the
+ * cached module is out-of-date and needs to be re-created, this method returns
+ * undefined.
+ */
+const getAndValidateModuleFromCache = (
+ modulePath: AbsolutePath,
+ analyzer: AnalyzerInterface
+): Module | undefined => {
+ const module = analyzer.moduleCache.get(modulePath);
+ // A cached module is only valid if the source file that was used has not
+ // changed in the current program, and if all of its dependencies are still
+ // valid
+ if (module !== undefined) {
+ if (
+ module.sourceFile === analyzer.program.getSourceFile(modulePath) &&
+ depsAreValid(module, analyzer)
+ ) {
+ return module;
+ }
+ analyzer.moduleCache.delete(modulePath);
+ }
+ return undefined;
+};
+
+/**
+ * Returns true if all dependencies of the module are still valid.
+ */
+const depsAreValid = (module: Module, analyzer: AnalyzerInterface) =>
+ Array.from(module.dependencies).every((path) => depIsValid(path, analyzer));
+
+/**
+ * Returns true if the given dependency is valid, meaning that if it has a
+ * cached model, the model is still valid. Dependencies that don't yet have a
+ * cached model are considered valid.
+ */
+const depIsValid = (modulePath: AbsolutePath, analyzer: AnalyzerInterface) => {
+ if (analyzer.moduleCache.has(modulePath)) {
+ // If a dep has a model, it is valid only if its deps are valid
+ return Boolean(getAndValidateModuleFromCache(modulePath, analyzer));
+ } else {
+ // Deps that don't have a cached model are considered valid
+ return true;
+ }
+};
+
+/**
+ * For a given source file, return its associated JS file.
+ *
+ * For a JS source file, these will be the same thing. For a TS file, we use the
+ * TS API to determine where the associated JS will be output based on tsconfig
+ * settings.
+ */
+const getJSPathFromSourcePath = (
+ sourcePath: AbsolutePath,
+ analyzer: AnalyzerInterface
+) => {
+ sourcePath = analyzer.path.normalize(sourcePath) as AbsolutePath;
+ // If the source file was already JS, just return that
+ if (sourcePath.endsWith('js')) {
+ return sourcePath;
+ }
+ // TODO(kschaaf): If the source file was a declaration file, this means we're
+ // likely getting information about an externally imported package that had
+ // types. In this case, we'll need to update our logic to resolve the import
+ // specifier to the JS path (in addition to the source file path that we do
+ // today). Unfortunately, TS's specifier resolver always prefers a declaration
+ // file, and due to type roots and other tsconfig fancies, it's not
+ // straightforward to go from a declaration file to a source file. In order to
+ // properly implement this we'll probably need to bring our own node module
+ // resolver ala https://www.npmjs.com/package/resolve. That change should be
+ // done along with the custom-elements.json manifest work.
+ if (sourcePath.endsWith('.d.ts')) {
+ return undefined;
+ }
+ // Use the TS API to determine where the associated JS will be output based
+ // on tsconfig settings.
+ const outputPath = ts
+ .getOutputFileNames(analyzer.commandLine, sourcePath, false)
+ .filter((f) => f.endsWith('.js'))[0];
+ // TODO(kschaaf): this could happen if someone imported only a .d.ts file;
+ // we might need to handle this differently
+ if (outputPath === undefined) {
+ throw new Error(`Could not determine output filename for '${sourcePath}'`);
+ }
+ // The filename appears to come out of the ts API with
+ // unix separators; since sourcePath uses OS separators, normalize this so
+ // that all our model paths are OS-native
+ return analyzer.path.normalize(outputPath) as AbsolutePath;
+};
+
+/**
+ * Resolves a module specifier expression node to an absolute path on disk.
+ */
+export const getPathForModuleSpecifierExpression = (
+ specifierExpression: ts.Expression,
+ analyzer: AnalyzerInterface
+): AbsolutePath => {
+ const specifier = getSpecifierString(specifierExpression);
+ return getPathForModuleSpecifier(specifier, specifierExpression, analyzer);
+};
+
+/**
+ * Resolve a module specifier to an absolute path on disk.
+ */
+export const getPathForModuleSpecifier = (
+ specifier: string,
+ location: ts.Node,
+ analyzer: AnalyzerInterface
+): AbsolutePath => {
+ const resolvedPath = ts.resolveModuleName(
+ specifier,
+ location.getSourceFile().fileName,
+ analyzer.commandLine.options,
+ analyzer.fs
+ ).resolvedModule?.resolvedFileName;
+ if (resolvedPath === undefined) {
+ throw new DiagnosticsError(
+ location,
+ `Could not resolve specifier ${specifier} to filesystem path.`
+ );
+ }
+ return analyzer.path.normalize(resolvedPath) as AbsolutePath;
+};
+
+/**
+ * Returns the declaration for the named export of the given module path;
+ * note that if the given module re-exported a declaration from another
+ * module, references are followed to the concrete declaration, which is
+ * returned.
+ */
+export const getResolvedExportFromSourcePath = (
+ modulePath: AbsolutePath,
+ name: string,
+ analyzer: AnalyzerInterface
+) => getModule(modulePath, analyzer)?.getResolvedExport(name);
diff --git a/packages/labs/analyzer/src/lib/javascript/packages.ts b/packages/labs/analyzer/src/lib/javascript/packages.ts
new file mode 100644
index 0000000000..3c4b3c784a
--- /dev/null
+++ b/packages/labs/analyzer/src/lib/javascript/packages.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {AnalyzerInterface, PackageInfo, PackageJson} from '../model.js';
+import {AbsolutePath} from '../paths.js';
+
+/**
+ * Starting from a given module path, searches up until the nearest package.json
+ * is found, returning that folder. If none is found, an error is thrown.
+ */
+export const getPackageRootForModulePath = (
+ modulePath: AbsolutePath,
+ analyzer: AnalyzerInterface
+): AbsolutePath => {
+ // TODO(kschaaf): Add caching & invalidation
+ const {fs, path} = analyzer;
+ let searchDir = modulePath as string;
+ const root = path.parse(searchDir).root;
+ while (!fs.fileExists(path.join(searchDir, 'package.json'))) {
+ if (searchDir === root) {
+ throw new Error(`No package.json found for module path ${modulePath}`);
+ }
+ searchDir = path.dirname(searchDir);
+ }
+ return searchDir as AbsolutePath;
+};
+
+/**
+ * Reads and parses a package.json file contained in the given folder.
+ */
+const getPackageJsonFromPackageRoot = (
+ packageRoot: AbsolutePath,
+ analyzer: AnalyzerInterface
+): PackageJson => {
+ // TODO(kschaaf): Add caching & invalidation
+ const {fs, path} = analyzer;
+ const packageJson = fs.readFile(path.join(packageRoot, 'package.json'));
+ if (packageJson !== undefined) {
+ return JSON.parse(packageJson) as PackageJson;
+ }
+ throw new Error(`No package.json found at ${packageRoot}`);
+};
+
+/**
+ * Returns an analyzer `PackageInfo` model for the nearest package of the given
+ * path.
+ */
+export const getPackageInfo = (
+ path: AbsolutePath,
+ analyzer: AnalyzerInterface
+) => {
+ const rootDir = getPackageRootForModulePath(path, analyzer);
+ const packageJson = getPackageJsonFromPackageRoot(rootDir, analyzer);
+ const {name} = packageJson;
+ if (name === undefined) {
+ throw new Error(
+ `Expected package name in ${analyzer.path.join(rootDir, 'package.json')}`
+ );
+ }
+ return new PackageInfo({
+ name,
+ rootDir: analyzer.path.normalize(rootDir) as AbsolutePath,
+ packageJson,
+ });
+};
diff --git a/packages/labs/analyzer/src/lib/javascript/variables.ts b/packages/labs/analyzer/src/lib/javascript/variables.ts
index aead529a57..31458ec513 100644
--- a/packages/labs/analyzer/src/lib/javascript/variables.ts
+++ b/packages/labs/analyzer/src/lib/javascript/variables.ts
@@ -7,31 +7,72 @@
/**
* @fileoverview
*
- * Utilities for working with classes
+ * Utilities for analyzing variable declarations
*/
import ts from 'typescript';
-import {VariableDeclaration} from '../model.js';
-import {ProgramContext} from '../program-context.js';
+import {
+ VariableDeclaration,
+ AnalyzerInterface,
+ DeclarationInfo,
+} from '../model.js';
+import {hasExportKeyword} from '../references.js';
import {DiagnosticsError} from '../errors.js';
+import {getTypeForNode} from '../types.js';
type VariableName =
| ts.Identifier
| ts.ObjectBindingPattern
| ts.ArrayBindingPattern;
-export const getVariableDeclarations = (
+/**
+ * Returns an analyzer `VariableDeclaration` model for the given
+ * ts.Identifier within a potentially nested ts.VariableDeclaration.
+ */
+const getVariableDeclaration = (
+ dec: ts.VariableDeclaration,
+ name: ts.Identifier,
+ analyzer: AnalyzerInterface
+): VariableDeclaration => {
+ return new VariableDeclaration({
+ name: name.text,
+ node: dec,
+ type: getTypeForNode(name, analyzer),
+ });
+};
+
+/**
+ * Returns name and model factories for all variable
+ * declarations in a variable statement.
+ */
+export const getVariableDeclarationInfo = (
+ statement: ts.VariableStatement,
+ analyzer: AnalyzerInterface
+): DeclarationInfo[] => {
+ const isExport = hasExportKeyword(statement);
+ return statement.declarationList.declarations
+ .map((d) => getVariableDeclarationInfoList(d, d.name, isExport, analyzer))
+ .flat();
+};
+
+/**
+ * For a given `VariableName` (which might be a simple identifier or a
+ * destructuring pattern which contains more identifiers), return an array of
+ * tuples of name and factory for each declaration.
+ */
+const getVariableDeclarationInfoList = (
dec: ts.VariableDeclaration,
name: VariableName,
- programContext: ProgramContext
-): VariableDeclaration[] => {
+ isExport: boolean,
+ analyzer: AnalyzerInterface
+): DeclarationInfo[] => {
if (ts.isIdentifier(name)) {
return [
- new VariableDeclaration({
+ {
name: name.text,
- node: dec,
- type: programContext.getTypeForNode(name),
- }),
+ factory: () => getVariableDeclaration(dec, name, analyzer),
+ isExport,
+ },
];
} else if (
// Recurse into the elements of an array/object destructuring variable
@@ -43,7 +84,9 @@ export const getVariableDeclarations = (
ts.isBindingElement(el)
) as ts.BindingElement[];
return els
- .map((el) => getVariableDeclarations(dec, el.name, programContext))
+ .map((el) =>
+ getVariableDeclarationInfoList(dec, el.name, isExport, analyzer)
+ )
.flat();
} else {
throw new DiagnosticsError(
@@ -52,3 +95,37 @@ export const getVariableDeclarations = (
);
}
};
+
+/**
+ * Returns declaration info & factory for a default export assignment.
+ */
+export const getExportAssignmentVariableDeclarationInfo = (
+ exportAssignment: ts.ExportAssignment,
+ analyzer: AnalyzerInterface
+): DeclarationInfo => {
+ return {
+ name: 'default',
+ factory: () =>
+ getExportAssignmentVariableDeclaration(exportAssignment, analyzer),
+ isExport: true,
+ };
+};
+
+/**
+ * Returns an analyzer `VariableDeclaration` model for the given default
+ * ts.ExportAssignment, handling the case of: `export const 'some expression'`;
+ *
+ * Note that even though this technically isn't a VariableDeclaration in
+ * TS, we model it as one since it could unobservably be implemented as
+ * `const varDec = 'some expression'; export {varDec as default} `
+ */
+const getExportAssignmentVariableDeclaration = (
+ exportAssignment: ts.ExportAssignment,
+ analyzer: AnalyzerInterface
+): VariableDeclaration => {
+ return new VariableDeclaration({
+ name: 'default',
+ node: exportAssignment,
+ type: getTypeForNode(exportAssignment.expression, analyzer),
+ });
+};
diff --git a/packages/labs/analyzer/src/lib/lit-element/decorators.ts b/packages/labs/analyzer/src/lib/lit-element/decorators.ts
index 2cb020715e..6954e64d4f 100644
--- a/packages/labs/analyzer/src/lib/lit-element/decorators.ts
+++ b/packages/labs/analyzer/src/lib/lit-element/decorators.ts
@@ -7,7 +7,7 @@
/**
* @fileoverview
*
- * Utilities for working with ReactiveElement decorators.
+ * Utilities for analyzing ReactiveElement decorators.
*/
import ts from 'typescript';
diff --git a/packages/labs/analyzer/src/lib/lit-element/events.ts b/packages/labs/analyzer/src/lib/lit-element/events.ts
index ca37e15ea9..37cc2794d7 100644
--- a/packages/labs/analyzer/src/lib/lit-element/events.ts
+++ b/packages/labs/analyzer/src/lib/lit-element/events.ts
@@ -7,54 +7,46 @@
/**
* @fileoverview
*
- * Utilities for working with events
+ * Utilities for analyzing with events
*/
import ts from 'typescript';
import {DiagnosticsError} from '../errors.js';
import {Event} from '../model.js';
-import {ProgramContext} from '../program-context.js';
+import {AnalyzerInterface} from '../model.js';
+import {getTypeForJSDocTag} from '../types.js';
-import {LitClassDeclaration} from './lit-element.js';
-
-export const getEvents = (
- node: LitClassDeclaration,
- programContext: ProgramContext
+/**
+ * Returns an array of analyzer `Event` models for the given
+ * ts.ClassDeclaration.
+ */
+export const addEventsToMap = (
+ tag: ts.JSDocTag,
+ events: Map,
+ analyzer: AnalyzerInterface
) => {
- const events = new Map();
- const jsDocTags = ts.getJSDocTags(node);
- if (jsDocTags !== undefined) {
- for (const tag of jsDocTags) {
- if (tag.tagName.text === 'fires') {
- const {comment} = tag;
- if (comment === undefined) {
- continue;
- } else if (typeof comment === 'string') {
- const result = parseFiresTagComment(comment);
- if (result === undefined) {
- throw new DiagnosticsError(
- tag,
- 'The @fires annotation was not in a recognized form. ' +
- 'Use `@fires event-name {Type} - Description`.'
- );
- }
- const {name, type, description} = result;
- events.set(name, {
- name,
- type: type ? programContext.getTypeForJSDocTag(tag) : undefined,
- description,
- });
- } else {
- // TODO: when do we get a ts.NodeArray?
- throw new DiagnosticsError(
- tag,
- `Internal error: unsupported node type`
- );
- }
- }
+ const {comment} = tag;
+ if (comment === undefined) {
+ return;
+ } else if (typeof comment === 'string') {
+ const result = parseFiresTagComment(comment);
+ if (result === undefined) {
+ throw new DiagnosticsError(
+ tag,
+ 'The @fires annotation was not in a recognized form. ' +
+ 'Use `@fires event-name {Type} - Description`.'
+ );
}
+ const {name, type, description} = result;
+ events.set(name, {
+ name,
+ type: type ? getTypeForJSDocTag(tag, analyzer) : undefined,
+ description,
+ });
+ } else {
+ // TODO: when do we get a ts.NodeArray?
+ throw new DiagnosticsError(tag, `Internal error: unsupported node type`);
}
- return events;
};
const parseFiresTagComment = (comment: string) => {
diff --git a/packages/labs/analyzer/src/lib/lit-element/lit-element.ts b/packages/labs/analyzer/src/lib/lit-element/lit-element.ts
index ae43865a9b..7bb26fc219 100644
--- a/packages/labs/analyzer/src/lib/lit-element/lit-element.ts
+++ b/packages/labs/analyzer/src/lib/lit-element/lit-element.ts
@@ -7,14 +7,20 @@
/**
* @fileoverview
*
- * Utilities for working with LitElement (and ReactiveElement) declarations.
+ * Utilities for analyzing LitElement (and ReactiveElement) declarations.
*/
import ts from 'typescript';
-import {LitElementDeclaration} from '../model.js';
-import {ProgramContext} from '../program-context.js';
+import {getHeritage} from '../javascript/classes.js';
+import {parseNodeJSDocInfo, parseNameDescSummary} from '../javascript/jsdoc.js';
+import {
+ LitElementDeclaration,
+ AnalyzerInterface,
+ Event,
+ NamedJSDocInfo,
+} from '../model.js';
import {isCustomElementDecorator} from './decorators.js';
-import {getEvents} from './events.js';
+import {addEventsToMap} from './events.js';
import {getProperties} from './properties.js';
/**
@@ -23,28 +29,99 @@ import {getProperties} from './properties.js';
*/
export const getLitElementDeclaration = (
node: LitClassDeclaration,
- programContext: ProgramContext
+ analyzer: AnalyzerInterface
): LitElementDeclaration => {
return new LitElementDeclaration({
tagname: getTagName(node),
// TODO(kschaaf): support anonymous class expressions when assigned to a const
name: node.name?.text ?? '',
node,
- reactiveProperties: getProperties(node, programContext),
- events: getEvents(node, programContext),
+ reactiveProperties: getProperties(node, analyzer),
+ ...getJSDocData(node, analyzer),
+ getHeritage: () => getHeritage(node, analyzer),
});
};
+/**
+ * Parses element metadata from jsDoc tags from a LitElement declaration into
+ * Maps of .
+ */
+export const getJSDocData = (
+ node: LitClassDeclaration,
+ analyzer: AnalyzerInterface
+) => {
+ const events = new Map();
+ const slots = new Map();
+ const cssProperties = new Map();
+ const cssParts = new Map();
+ const jsDocTags = ts.getJSDocTags(node);
+ if (jsDocTags !== undefined) {
+ for (const tag of jsDocTags) {
+ switch (tag.tagName.text) {
+ case 'fires':
+ addEventsToMap(tag, events, analyzer);
+ break;
+ case 'slot':
+ addNamedJSDocInfoToMap(slots, tag);
+ break;
+ case 'cssProp':
+ addNamedJSDocInfoToMap(cssProperties, tag);
+ break;
+ case 'cssProperty':
+ addNamedJSDocInfoToMap(cssProperties, tag);
+ break;
+ case 'cssPart':
+ addNamedJSDocInfoToMap(cssParts, tag);
+ break;
+ }
+ }
+ }
+ return {
+ ...parseNodeJSDocInfo(node, analyzer),
+ events,
+ slots,
+ cssProperties,
+ cssParts,
+ };
+};
+
+/**
+ * Adds name, description, and summary info for a given jsdoc tag into the
+ * provided map.
+ */
+const addNamedJSDocInfoToMap = (
+ map: Map,
+ tag: ts.JSDocTag
+) => {
+ const info = parseNameDescSummary(tag);
+ if (info !== undefined) {
+ map.set(info.name, info);
+ }
+};
+
/**
* Returns true if this type represents the actual LitElement class.
*/
-const _isLitElementClassDeclaration = (t: ts.BaseType) => {
+const _isLitElementClassDeclaration = (
+ t: ts.BaseType,
+ analyzer: AnalyzerInterface
+) => {
// TODO: should we memoize this for performance?
const declarations = t.getSymbol()?.getDeclarations();
if (declarations?.length !== 1) {
return false;
}
const node = declarations[0];
+ return _isLitElement(node) || isLitElementSubclass(node, analyzer);
+};
+
+/**
+ * Returns true if the given declaration is THE LitElement declaration.
+ *
+ * TODO(kschaaf): consider a less brittle method of detecting canonical
+ * LitElement
+ */
+const _isLitElement = (node: ts.Declaration) => {
return (
_isLitElementModule(node.getSourceFile()) &&
ts.isClassDeclaration(node) &&
@@ -52,6 +129,9 @@ const _isLitElementClassDeclaration = (t: ts.BaseType) => {
);
};
+/**
+ * Returns true if the given source file is THE lit-element source file.
+ */
const _isLitElementModule = (file: ts.SourceFile) => {
return (
file.fileName.endsWith('/node_modules/lit-element/lit-element.d.ts') ||
@@ -74,19 +154,18 @@ export type LitClassDeclaration = ts.ClassDeclaration & {
/**
* Returns true if `node` is a ClassLikeDeclaration that extends LitElement.
*/
-export const isLitElement = (
+export const isLitElementSubclass = (
node: ts.Node,
- programContext: ProgramContext
+ analyzer: AnalyzerInterface
): node is LitClassDeclaration => {
if (!ts.isClassLike(node)) {
return false;
}
- const type = programContext.checker.getTypeAtLocation(
- node
- ) as ts.InterfaceType;
- const baseTypes = programContext.checker.getBaseTypes(type);
+ const checker = analyzer.program.getTypeChecker();
+ const type = checker.getTypeAtLocation(node) as ts.InterfaceType;
+ const baseTypes = checker.getBaseTypes(type);
for (const t of baseTypes) {
- if (_isLitElementClassDeclaration(t)) {
+ if (_isLitElementClassDeclaration(t, analyzer)) {
return true;
}
}
@@ -94,13 +173,12 @@ export const isLitElement = (
};
/**
- * Returns the tagname associated with a
+ * Returns the tagname associated with a LitClassDeclaration
* @param declaration
* @returns
*/
export const getTagName = (declaration: LitClassDeclaration) => {
- // TODO (justinfagnani): support customElements.define()
- let tagname: string | undefined = undefined;
+ let tagName: string | undefined = undefined;
const customElementDecorator = declaration.decorators?.find(
isCustomElementDecorator
);
@@ -109,7 +187,33 @@ export const getTagName = (declaration: LitClassDeclaration) => {
customElementDecorator.expression.arguments.length === 1 &&
ts.isStringLiteral(customElementDecorator.expression.arguments[0])
) {
- tagname = customElementDecorator.expression.arguments[0].text;
+ // Get tag from decorator: `@customElement('x-foo')`
+ tagName = customElementDecorator.expression.arguments[0].text;
+ } else {
+ // Otherwise, look for imperative define in the form of:
+ // `customElements.define('x-foo', XFoo);`
+ declaration.parent.forEachChild((child) => {
+ if (
+ ts.isExpressionStatement(child) &&
+ ts.isCallExpression(child.expression) &&
+ ts.isPropertyAccessExpression(child.expression.expression) &&
+ child.expression.arguments.length >= 2
+ ) {
+ const [tagNameArg, ctorArg] = child.expression.arguments;
+ const {expression, name} = child.expression.expression;
+ if (
+ ts.isIdentifier(expression) &&
+ expression.text === 'customElements' &&
+ ts.isIdentifier(name) &&
+ name.text === 'define' &&
+ ts.isStringLiteralLike(tagNameArg) &&
+ ts.isIdentifier(ctorArg) &&
+ ctorArg.text === declaration.name?.text
+ ) {
+ tagName = tagNameArg.text;
+ }
+ }
+ });
}
- return tagname;
+ return tagName;
};
diff --git a/packages/labs/analyzer/src/lib/lit-element/properties.ts b/packages/labs/analyzer/src/lib/lit-element/properties.ts
index c2026593f7..9ce53cd293 100644
--- a/packages/labs/analyzer/src/lib/lit-element/properties.ts
+++ b/packages/labs/analyzer/src/lib/lit-element/properties.ts
@@ -7,48 +7,210 @@
/**
* @fileoverview
*
- * Utilities for working with reactive property declarations
+ * Utilities for analyzing reactive property declarations
*/
import ts from 'typescript';
import {LitClassDeclaration} from './lit-element.js';
-import {ReactiveProperty} from '../model.js';
-import {ProgramContext} from '../program-context.js';
+import {ReactiveProperty, AnalyzerInterface} from '../model.js';
+import {getTypeForNode} from '../types.js';
import {getPropertyDecorator, getPropertyOptions} from './decorators.js';
+import {DiagnosticsError} from '../errors.js';
+
+const isStatic = (prop: ts.PropertyDeclaration) =>
+ prop.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword);
export const getProperties = (
- node: LitClassDeclaration,
- programContext: ProgramContext
+ classDeclaration: LitClassDeclaration,
+ analyzer: AnalyzerInterface
) => {
const reactiveProperties = new Map();
+ const undecoratedProperties = new Map();
- const propertyDeclarations = node.members.filter((m) =>
- ts.isPropertyDeclaration(m)
+ // Filter down to just the property and getter declarations
+ const propertyDeclarations = classDeclaration.members.filter(
+ (m) => ts.isPropertyDeclaration(m) || ts.isGetAccessorDeclaration(m)
) as unknown as ts.NodeArray;
+
+ let staticProperties;
+
for (const prop of propertyDeclarations) {
if (!ts.isIdentifier(prop.name)) {
- // TODO(justinfagnani): emit error instead
- throw new Error('unsupported property name');
+ throw new DiagnosticsError(prop, 'Unsupported property name');
}
const name = prop.name.text;
const propertyDecorator = getPropertyDecorator(prop);
if (propertyDecorator !== undefined) {
+ // Decorated property; get property options from the decorator and add
+ // them to the reactiveProperties map
const options = getPropertyOptions(propertyDecorator);
reactiveProperties.set(name, {
name,
- type: programContext.getTypeForNode(prop),
- node: prop,
+ type: getTypeForNode(prop, analyzer),
attribute: getPropertyAttribute(options, name),
typeOption: getPropertyType(options),
reflect: getPropertyReflect(options),
converter: getPropertyConverter(options),
});
+ } else if (name === 'properties' && isStatic(prop)) {
+ // This field has the static properties block (initializer or getter).
+ // Note we will process this after the loop so that the
+ // `undecoratedProperties` map is complete before processing the static
+ // properties block.
+ staticProperties = prop;
+ } else if (!isStatic(prop)) {
+ // Store the declaration node for any undecorated properties. In a TS
+ // program that happens to use a static properties block along with
+ // the `declare` keyword to type the field, we can use this node to
+ // get/infer the TS type of the field from
+ undecoratedProperties.set(name, prop);
}
}
+
+ // Handle static properties block (initializer or getter).
+ if (staticProperties !== undefined) {
+ addPropertiesFromStaticBlock(
+ classDeclaration,
+ staticProperties,
+ undecoratedProperties,
+ reactiveProperties,
+ analyzer
+ );
+ }
+
return reactiveProperties;
};
+/**
+ * Given a static properties declaration (field or getter), add property
+ * options to the provided `reactiveProperties` map.
+ */
+const addPropertiesFromStaticBlock = (
+ classDeclaration: LitClassDeclaration,
+ properties: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
+ undecoratedProperties: Map,
+ reactiveProperties: Map,
+ analyzer: AnalyzerInterface
+) => {
+ // Add any constructor initializers to the undecorated properties node map
+ // from which we can infer types from. This is the primary path that JS source
+ // can get their inferred types (in TS, types will come from the undecorated
+ // fields passed in, since you need to declare the field to assign it in the
+ // constructor).
+ addConstructorInitializers(classDeclaration, undecoratedProperties);
+ // Find the object literal from the initializer or getter return value
+ const object = getStaticPropertiesObjectLiteral(properties);
+ // Loop over each key/value in the object and add them to the map
+ for (const prop of object.properties) {
+ if (
+ ts.isPropertyAssignment(prop) &&
+ ts.isIdentifier(prop.name) &&
+ ts.isObjectLiteralExpression(prop.initializer)
+ ) {
+ const name = prop.name.text;
+ const options = prop.initializer;
+ const nodeForType = undecoratedProperties.get(name);
+ reactiveProperties.set(name, {
+ name,
+ type:
+ nodeForType !== undefined
+ ? getTypeForNode(nodeForType, analyzer)
+ : undefined,
+ attribute: getPropertyAttribute(options, name),
+ typeOption: getPropertyType(options),
+ reflect: getPropertyReflect(options),
+ converter: getPropertyConverter(options),
+ });
+ } else {
+ throw new DiagnosticsError(
+ prop,
+ 'Unsupported static properties entry. Expected a string identifier key and object literal value.'
+ );
+ }
+ }
+};
+
+/**
+ * Find the object literal for a static properties block.
+ *
+ * If a ts.PropertyDeclaration, it will look like:
+ *
+ * static properties = { ... };
+ *
+ * If a ts.GetAccessorDeclaration, it will look like:
+ *
+ * static get properties() {
+ * return {... }
+ * }
+ */
+const getStaticPropertiesObjectLiteral = (
+ properties: ts.PropertyDeclaration | ts.GetAccessorDeclaration
+): ts.ObjectLiteralExpression => {
+ let object: ts.ObjectLiteralExpression | undefined = undefined;
+ if (
+ ts.isPropertyDeclaration(properties) &&
+ properties.initializer !== undefined &&
+ ts.isObjectLiteralExpression(properties.initializer)
+ ) {
+ // `properties` has a static initializer; get the object from there
+ object = properties.initializer;
+ } else if (ts.isGetAccessorDeclaration(properties)) {
+ // Object was in a static getter: find the object in the return value
+ const statements = properties.body?.statements;
+ const statement = statements?.[statements.length - 1];
+ if (
+ statement !== undefined &&
+ ts.isReturnStatement(statement) &&
+ statement.expression !== undefined &&
+ ts.isObjectLiteralExpression(statement.expression)
+ ) {
+ object = statement.expression;
+ }
+ }
+ if (object === undefined) {
+ throw new DiagnosticsError(
+ properties,
+ `Unsupported static properties format. Expected an object literal assigned in a static initializer or returned from a static getter.`
+ );
+ }
+ return object;
+};
+
+/**
+ * Adds any field initializers in the given class's constructor to the provided
+ * map. This will be used for inferring the type of fields in JS programs.
+ */
+const addConstructorInitializers = (
+ classDeclaration: ts.ClassDeclaration,
+ undecoratedProperties: Map
+) => {
+ const ctor = classDeclaration.forEachChild((node) =>
+ ts.isConstructorDeclaration(node) ? node : undefined
+ );
+ if (ctor !== undefined) {
+ ctor.body?.statements.forEach((stmt) => {
+ // Look for initializers in the form of `this.foo = xxxx`
+ if (
+ ts.isExpressionStatement(stmt) &&
+ ts.isBinaryExpression(stmt.expression) &&
+ ts.isPropertyAccessExpression(stmt.expression.left) &&
+ stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
+ ts.isIdentifier(stmt.expression.left.name) &&
+ !undecoratedProperties.has(stmt.expression.left.name.text)
+ ) {
+ // Add the initializer expression to the map
+ undecoratedProperties.set(
+ // Property name
+ stmt.expression.left.name.text,
+ // Expression from which we can infer a type
+ stmt.expression.right
+ );
+ }
+ });
+ }
+};
+
/**
* Gets the `attribute` property of a property options object as a string.
*/
diff --git a/packages/labs/analyzer/src/lib/model.ts b/packages/labs/analyzer/src/lib/model.ts
index 001dc92131..cee52cd67c 100644
--- a/packages/labs/analyzer/src/lib/model.ts
+++ b/packages/labs/analyzer/src/lib/model.ts
@@ -10,6 +10,9 @@ import {AbsolutePath, PackagePath} from './paths.js';
import {IPackageJson as PackageJson} from 'package-json-type';
export {PackageJson};
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type Constructor = new (...args: any[]) => T;
+
/**
* Return type of `getLitElementModules`: contains a module and filtered list of
* LitElementDeclarations contained within it.
@@ -19,23 +22,33 @@ export type ModuleWithLitElementDeclarations = {
declarations: LitElementDeclaration[];
};
-export interface PackageInit {
+export interface PackageInfoInit {
+ name: string;
rootDir: AbsolutePath;
packageJson: PackageJson;
- tsConfig: ts.ParsedCommandLine;
- modules: ReadonlyArray;
}
-export class Package {
+export class PackageInfo {
+ readonly name: string;
readonly rootDir: AbsolutePath;
- readonly modules: ReadonlyArray;
- readonly tsConfig: ts.ParsedCommandLine;
readonly packageJson: PackageJson;
- constructor(init: PackageInit) {
+ constructor(init: PackageInfoInit) {
+ this.name = init.name;
this.rootDir = init.rootDir;
this.packageJson = init.packageJson;
- this.tsConfig = init.tsConfig;
+ }
+}
+
+export interface PackageInit extends PackageInfo {
+ modules: ReadonlyArray;
+}
+
+export class Package extends PackageInfo {
+ readonly modules: ReadonlyArray;
+
+ constructor(init: PackageInit) {
+ super(init);
this.modules = init.modules;
}
@@ -61,10 +74,25 @@ export class Package {
}
}
+export type LocalNameOrReference = string | Reference;
+export type ExportMap = Map;
+export type DeclarationMap = Map Declaration)>;
+
export interface ModuleInit {
sourceFile: ts.SourceFile;
sourcePath: PackagePath;
jsPath: PackagePath;
+ packageJson: PackageJson;
+ declarationMap: DeclarationMap;
+ exportMap: ExportMap;
+ dependencies: Set;
+ finalizeExports?: () => void;
+}
+
+export interface ModuleInfo {
+ sourcePath: PackagePath;
+ jsPath: PackagePath;
+ packageJson: PackageJson;
}
export class Module {
@@ -83,23 +111,164 @@ export class Module {
* project this will be the same as `sourcePath`.
*/
readonly jsPath: PackagePath;
- readonly declarations: Array = [];
+ /**
+ * A map of names to models or model factories for all Declarations in this module.
+ */
+ private readonly _declarationMap: DeclarationMap;
+ /**
+ * Private storage for all declarations within this module, memoized
+ * in `get declarations()` getter.
+ */
+ private _declarations: Declaration[] | undefined = undefined;
+ /**
+ * A set of all dependencies of this module, as module absolute paths.
+ */
+ readonly dependencies: Set;
+ /**
+ * The package.json contents for the package containing this module.
+ */
+ readonly packageJson: PackageJson;
+ /**
+ * A map of exported names to local declaration names or References, in
+ * the case of re-exported symbols.
+ */
+ private readonly _exportMap: ExportMap;
+ /**
+ * A list of module paths for all wildcard re-exports
+ */
+ private _finalizeExports: (() => void) | undefined;
constructor(init: ModuleInit) {
this.sourceFile = init.sourceFile;
this.sourcePath = init.sourcePath;
this.jsPath = init.jsPath;
+ this.packageJson = init.packageJson;
+ this._declarationMap = init.declarationMap;
+ this.dependencies = init.dependencies;
+ this._exportMap = init.exportMap;
+ this._finalizeExports = init.finalizeExports;
+ }
+
+ /**
+ * Ensures the list of exports includes the names of all reexports
+ * from other modules.
+ */
+ private _ensureExportsFinalized() {
+ if (this._finalizeExports !== undefined) {
+ this._finalizeExports();
+ this._finalizeExports = undefined;
+ }
+ }
+
+ /**
+ * Returns names of all exported declarations.
+ */
+ get exportNames() {
+ this._ensureExportsFinalized();
+ return Array.from(this._exportMap.keys());
+ }
+
+ /**
+ * Given an exported symbol name, returns a Declaration if it was
+ * defined in this module, or a Reference if it was imported from
+ * another module.
+ */
+ getExport(name: string): Declaration | Reference {
+ this._ensureExportsFinalized();
+ const exp = this._exportMap.get(name);
+ if (exp === undefined) {
+ throw new Error(
+ `Module ${this.sourcePath} did not contain an export named ${name}`
+ );
+ } else if (exp instanceof Reference) {
+ return exp;
+ } else {
+ return this.getDeclaration(exp);
+ }
+ }
+
+ /**
+ * Return Reference for given export name.
+ *
+ * For references to local declarations, the module will be undefined.
+ * For re-exports, the Reference will point to a package & module.
+ */
+ getExportReference(name: string): Reference {
+ const exp = this.getExport(name);
+ if (exp instanceof Declaration) {
+ return new Reference({name: exp.name, dereference: () => exp});
+ } else {
+ return exp;
+ }
+ }
+
+ /**
+ * Given an exported symbol name, returns the concrete Declaration
+ * for that symbol, following it through any re-exports.
+ */
+ getResolvedExport(name: string): Declaration {
+ let exp = this.getExport(name);
+ while (exp instanceof Reference) {
+ exp = exp.dereference();
+ }
+ return exp as Declaration;
+ }
+
+ /**
+ * Returns a `Declaration` model for the given name in top-level module scope.
+ *
+ * Note, the name is local to the module, and the declaration may be exported
+ * from with a different name. The declaration is always concrete, and will
+ * never be a `Reference`.
+ */
+ getDeclaration(name: string): Declaration {
+ let dec = this._declarationMap.get(name);
+ if (dec === undefined) {
+ throw new Error(
+ `Module ${this.sourcePath} did not contain a declaration named ${name}`
+ );
+ }
+ // Overwrite a factory with its output (a `Declaration` model) on first
+ // request
+ if (typeof dec === 'function') {
+ this._declarationMap.set(name, (dec = dec()));
+ }
+ return dec;
+ }
+
+ /**
+ * Returns a list of all Declarations locally defined in this module.
+ */
+ get declarations() {
+ return (this._declarations ??= Array.from(this._declarationMap.keys()).map(
+ (name) => this.getDeclaration(name)
+ ));
+ }
+
+ /**
+ * Returns all custom elements registered in this module.
+ */
+ getCustomElementExports(): LitElementExport[] {
+ return this.declarations.filter(
+ (d) => d.isLitElementDeclaration() && d.tagname !== undefined
+ ) as LitElementExport[];
}
}
-interface DeclarationInit {
+interface DeclarationInit extends NodeJSDocInfo {
name: string;
}
export abstract class Declaration {
- name: string;
+ readonly name: string;
+ readonly description?: string | undefined;
+ readonly summary?: string | undefined;
+ readonly deprecated?: string | boolean | undefined;
constructor(init: DeclarationInit) {
this.name = init.name;
+ this.description = init.description;
+ this.summary = init.summary;
+ this.deprecated = init.deprecated;
}
isVariableDeclaration(): this is VariableDeclaration {
return this instanceof VariableDeclaration;
@@ -113,12 +282,12 @@ export abstract class Declaration {
}
export interface VariableDeclarationInit extends DeclarationInit {
- node: ts.VariableDeclaration;
+ node: ts.VariableDeclaration | ts.ExportAssignment;
type: Type | undefined;
}
export class VariableDeclaration extends Declaration {
- readonly node: ts.VariableDeclaration;
+ readonly node: ts.VariableDeclaration | ts.ExportAssignment;
readonly type: Type | undefined;
constructor(init: VariableDeclarationInit) {
super(init);
@@ -127,23 +296,51 @@ export class VariableDeclaration extends Declaration {
}
}
+export type ClassHeritage = {
+ mixins: Reference[];
+ superClass: Reference | undefined;
+};
+
export interface ClassDeclarationInit extends DeclarationInit {
node: ts.ClassDeclaration;
+ getHeritage: () => ClassHeritage;
}
export class ClassDeclaration extends Declaration {
readonly node: ts.ClassDeclaration;
+ private _getHeritage: () => ClassHeritage;
+ private _heritage: ClassHeritage | undefined = undefined;
constructor(init: ClassDeclarationInit) {
super(init);
this.node = init.node;
+ this._getHeritage = init.getHeritage;
+ }
+
+ get heritage(): ClassHeritage {
+ return (this._heritage ??= this._getHeritage());
}
}
+export interface NamedJSDocInfo {
+ name: string;
+ description?: string | undefined;
+ summary?: string | undefined;
+}
+
+export interface NodeJSDocInfo {
+ description?: string | undefined;
+ summary?: string | undefined;
+ deprecated?: string | boolean | undefined;
+}
+
interface LitElementDeclarationInit extends ClassDeclarationInit {
tagname: string | undefined;
reactiveProperties: Map;
- readonly events: Map;
+ events: Map;
+ slots: Map;
+ cssProperties: Map;
+ cssParts: Map;
}
export class LitElementDeclaration extends ClassDeclaration {
@@ -159,22 +356,33 @@ export class LitElementDeclaration extends ClassDeclaration {
readonly tagname: string | undefined;
readonly reactiveProperties: Map;
-
readonly events: Map;
+ readonly slots: Map;
+ readonly cssProperties: Map;
+ readonly cssParts: Map;
constructor(init: LitElementDeclarationInit) {
super(init);
this.tagname = init.tagname;
this.reactiveProperties = init.reactiveProperties;
this.events = init.events;
+ this.slots = init.slots;
+ this.cssProperties = init.cssProperties;
+ this.cssParts = init.cssParts;
}
}
+/**
+ * A LitElementDeclaration that has been globally registered with a tagname.
+ */
+export interface LitElementExport extends LitElementDeclaration {
+ tagname: string;
+}
+
export interface ReactiveProperty {
name: string;
- node: ts.PropertyDeclaration;
- type: Type;
+ type: Type | undefined;
reflect: boolean;
@@ -213,9 +421,10 @@ export interface LitModule {
export interface ReferenceInit {
name: string;
- package?: string;
- module?: string;
+ package?: string | undefined;
+ module?: string | undefined;
isGlobal?: boolean;
+ dereference?: () => Declaration | undefined;
}
export class Reference {
@@ -223,11 +432,14 @@ export class Reference {
readonly package: string | undefined;
readonly module: string | undefined;
readonly isGlobal: boolean;
+ private readonly _dereference: () => Declaration | undefined;
+ private _model: Declaration | undefined = undefined;
constructor(init: ReferenceInit) {
this.name = init.name;
this.package = init.package;
this.module = init.module;
this.isGlobal = init.isGlobal ?? false;
+ this._dereference = init.dereference ?? (() => undefined);
}
get moduleSpecifier() {
@@ -236,17 +448,43 @@ export class Reference {
? undefined
: (this.package || '') + separator + (this.module || '');
}
+
+ /**
+ * Returns the Declaration model that this reference points to, optionally
+ * validating (and casting) it to be of a given type by passing a model
+ * constructor.
+ */
+ dereference(type?: Constructor | undefined): T {
+ const model = (this._model ??= this._dereference());
+ if (type !== undefined && model !== undefined && !(model instanceof type)) {
+ throw new Error(
+ `Expected reference to ${this.name} in module ${this.moduleSpecifier} to be of type ${type.name}`
+ );
+ }
+ return model as T;
+ }
+}
+
+export interface TypeInit {
+ type: ts.Type;
+ text: string;
+ getReferences: () => Reference[];
}
export class Type {
type: ts.Type;
text: string;
- references: Reference[];
+ private _getReferences: () => Reference[];
+ private _references: Reference[] | undefined = undefined;
+
+ constructor(init: TypeInit) {
+ this.type = init.type;
+ this.text = init.text;
+ this._getReferences = init.getReferences;
+ }
- constructor(type: ts.Type, text: string, references: Reference[]) {
- this.type = type;
- this.text = text;
- this.references = references;
+ get references() {
+ return (this._references ??= this._getReferences());
}
}
@@ -276,3 +514,37 @@ export const getImportsStringForReferences = (references: Reference[]) => {
)
.join('\n');
};
+
+export interface AnalyzerInterface {
+ moduleCache: Map;
+ program: ts.Program;
+ commandLine: ts.ParsedCommandLine;
+ fs: Pick<
+ ts.System,
+ | 'readDirectory'
+ | 'readFile'
+ | 'realpath'
+ | 'fileExists'
+ | 'useCaseSensitiveFileNames'
+ >;
+ path: Pick<
+ typeof import('path'),
+ | 'join'
+ | 'relative'
+ | 'dirname'
+ | 'basename'
+ | 'dirname'
+ | 'parse'
+ | 'normalize'
+ | 'isAbsolute'
+ >;
+}
+
+/**
+ * The name, model factory, and export information about a given declaration.
+ */
+export type DeclarationInfo = {
+ name: string;
+ factory: () => Declaration;
+ isExport?: boolean;
+};
diff --git a/packages/labs/analyzer/src/lib/paths.ts b/packages/labs/analyzer/src/lib/paths.ts
index a31c493637..b89305a379 100644
--- a/packages/labs/analyzer/src/lib/paths.ts
+++ b/packages/labs/analyzer/src/lib/paths.ts
@@ -5,6 +5,7 @@
*/
import * as pathlib from 'path';
+import {AnalyzerInterface} from './model.js';
/**
* An absolute path
@@ -40,3 +41,20 @@ export const absoluteToPackage = (
}
return packagePath as PackagePath;
};
+
+export const resolveExtension = (
+ path: AbsolutePath,
+ analyzer: AnalyzerInterface,
+ extensions = ['js', 'mjs']
+) => {
+ if (analyzer.fs.fileExists(path)) {
+ return path;
+ }
+ for (const ext of extensions) {
+ const fileName = `${path}.${ext}`;
+ if (analyzer.fs.fileExists(fileName)) {
+ return fileName;
+ }
+ }
+ throw new Error(`Could not resolve ${path} to a file on disk.`);
+};
diff --git a/packages/labs/analyzer/src/lib/program-context.ts b/packages/labs/analyzer/src/lib/program-context.ts
deleted file mode 100644
index ac179e424d..0000000000
--- a/packages/labs/analyzer/src/lib/program-context.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-/**
- * @fileoverview Utility class that stores context about the current program
- * under analysis and helpers for generating Type objects.
- *
- * TODO(kschaaf): This code could just be part of the Analyzer base class, but
- * is factored out for now to avoid circular references. We could also just make
- * an Analyzer interface that modules can import to type the Analyzer when
- * passed as an argument.
- */
-
-import ts from 'typescript';
-import {DiagnosticsError, createDiagnostic} from './errors.js';
-import path from 'path';
-import fs from 'fs';
-import {Module, PackageJson, Type, Reference} from './model.js';
-import {AbsolutePath} from './paths.js';
-
-const npmModule = /^(?(@\w+\/\w+)|\w+)\/?(?.*)$/;
-
-type FileCache = Map;
-
-/**
- * Create a language service host that reads from the filesystem initially,
- * and supports updating individual source files in memory by updating its
- * content and version in the FileCache.
- */
-const createServiceHost = (
- commandLine: ts.ParsedCommandLine,
- cache: FileCache
-): ts.LanguageServiceHost => {
- return {
- getScriptFileNames: () => commandLine.fileNames,
- getScriptVersion: (fileName) =>
- cache.get(fileName)?.version.toString() ?? '-1',
- getScriptSnapshot: (fileName) => {
- let file = cache.get(fileName);
- if (file === undefined) {
- if (!fs.existsSync(fileName)) {
- return undefined;
- }
- file = {
- version: 0,
- content: ts.ScriptSnapshot.fromString(
- fs.readFileSync(fileName, 'utf-8')
- ),
- };
- cache.set(fileName, file);
- }
- return file.content;
- },
- getCurrentDirectory: () => process.cwd(),
- getCompilationSettings: () => commandLine.options,
- getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
- fileExists: ts.sys.fileExists,
- readFile: ts.sys.readFile,
- readDirectory: ts.sys.readDirectory,
- directoryExists: ts.sys.directoryExists,
- getDirectories: ts.sys.getDirectories,
- };
-};
-
-/**
- * Utility class that stores context about the current program under
- * analysis and helpers for generating Type objects.
- */
-export class ProgramContext {
- readonly packageRoot: AbsolutePath;
- readonly commandLine: ts.ParsedCommandLine;
- readonly packageJson: PackageJson;
- readonly service: ts.LanguageService;
- program!: ts.Program;
- checker!: ts.TypeChecker;
-
- currentModule: Module | undefined = undefined;
-
- fileCache: FileCache = new Map();
-
- constructor(
- packageRoot: AbsolutePath,
- commandLine: ts.ParsedCommandLine,
- packageJson: PackageJson
- ) {
- this.packageRoot = packageRoot;
- this.commandLine = commandLine;
- this.packageJson = packageJson;
- this.service = ts.createLanguageService(
- createServiceHost(commandLine, this.fileCache),
- ts.createDocumentRegistry()
- );
- this.invalidate();
- const diagnostics = this.program.getSemanticDiagnostics();
- if (diagnostics.length > 0) {
- throw new DiagnosticsError(
- diagnostics,
- `Error analyzing package '${packageJson.name}': Please fix errors first`
- );
- }
- this.extractJsdocTypes();
- }
-
- /**
- * Re-initialize the program and checker (causes the file cache versions to be
- * diffed, and changed files to be re-parsed/checked)
- */
- private invalidate() {
- this.program = this.service.getProgram()!;
- this.checker = this.program.getTypeChecker();
- }
-
- /**
- * Update the file cache with the new text of one or more modified source
- * file, and re-parse/check them
- */
- updateFiles(sourceFiles: ts.SourceFile[]) {
- for (const sourceFile of sourceFiles) {
- const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
- let file = this.fileCache.get(sourceFile.fileName);
- if (file === undefined) {
- file = {version: 0};
- this.fileCache.set(sourceFile.fileName, file);
- }
- file.content = ts.ScriptSnapshot.fromString(
- printer.printFile(sourceFile)
- );
- file.version++;
- }
- this.invalidate();
- }
-
- /**
- * Associates jsdoc tags with any string `{Type}`s in them
- */
- private jsdocTypeMap = new WeakMap();
-
- /**
- * Generates ts.Type nodes for types specified as {Type} strings in the
- * comment text of custom jsdoc tags listed in the `customJSDocTypeTags`
- * allowlist.
- *
- * Achieved by doing a pass over the AST, finding all such jsdoc tags and
- * extracting the type text, generating dummy `let __$$customJsDOCType52:
- * SomeType;` statements, finding those and extracting their type, and then
- * re-associating them with the jsdoc comments via a WeakMap. The original
- * source is restored (and re-parsed/checked) prior to returning.
- *
- * This should be run once prior to analysis. The type for a jsdoc comment can
- * then be looked up via `getTypeForJSDocTag`.
- */
- private extractJsdocTypes() {
- const origSourceFiles = [];
- const modifiedSourceFiles = [];
- const tags: ts.JSDocTag[] = [];
- for (const fileName of this.program.getRootFileNames()) {
- const inferStatements: string[] = [];
- const sourceFile = this.program.getSourceFile(path.normalize(fileName))!;
- // Find JSDoc tags that contain string types and generate dummy variable
- // declarations using their types
- visitCustomJSDocTypeTags(
- sourceFile,
- (tag: ts.JSDocTag, typeString: string) => {
- tags.push(tag);
- inferStatements.push(
- `export let ${customJSDocTypeInferPrefix}${inferStatements.length}: ${typeString};\n`
- );
- }
- );
- // Update the source files with the dummy variable declarations (add to
- // end). We could try to keep these in the same lexical scope as the jsdoc
- // comment, but that's a lot more work, and given any references used also
- // need to be exported, seems unlikely they would use non-module scoped
- // symbols
- if (inferStatements.length > 0) {
- const inferStatementText = inferStatements.join('');
- const newSourceFile = sourceFile.update(
- sourceFile.text + inferStatementText,
- ts.createTextChangeRange(
- ts.createTextSpan(sourceFile.text.length, 0),
- inferStatementText.length
- )
- );
- origSourceFiles.push(sourceFile);
- modifiedSourceFiles.push(newSourceFile);
- }
- }
- if (modifiedSourceFiles.length > 0) {
- // If we had source files with type tags, update the language service
- // with the modified files
- this.updateFiles(modifiedSourceFiles);
- const origErrors = this.program.getSemanticDiagnostics();
- if (origErrors.length) {
- const retargetedErrors: ts.Diagnostic[] = [];
- for (const error of origErrors) {
- const tag = getTagForError(error, tags);
- if (tag) {
- retargetedErrors.push(
- createDiagnostic(tag, error.messageText as string)
- );
- } else {
- throw new Error(
- 'Internal error: Could not associate inferred type error with original jsdoc tag.'
- );
- }
- }
- throw new DiagnosticsError(
- retargetedErrors,
- `Error analyzing package '${this.packageJson.name}': Please fix errors first`
- );
- }
- // Find the inferred types and store them in a list
- const types: ts.Type[] = [];
- for (const modifiedSourceFile of modifiedSourceFiles) {
- const sourceFile = this.program.getSourceFile(
- modifiedSourceFile.fileName
- )!;
- visitCustomJSDocTypeInferredTypes(
- sourceFile,
- (node: ts.VariableDeclaration) => {
- types.push(this.checker.getTypeAtLocation(node));
- }
- );
- }
- // Restore the original source files
- this.updateFiles(origSourceFiles);
- // Find typed JSDoc tags again and associate them with their type
- for (const origSourceFile of origSourceFiles) {
- const sourceFile = this.program.getSourceFile(origSourceFile.fileName)!;
- let tagIndex = 0;
- visitCustomJSDocTypeTags(sourceFile, (tag: ts.JSDocTag) => {
- const type = types[tagIndex++];
- this.jsdocTypeMap.set(tag, type);
- });
- }
- }
- }
-
- /**
- * Returns a ts.Symbol for a name in scope at a given location in the AST.
- * TODO(kschaaf): There are ~1748 symbols in scope of a typical hello world,
- * due to DOM globals. Perf might become an issue here. This is a reason to
- * look for a better Type visitor than typedoc:
- * https://github.com/lit/lit/issues/3001
- */
- getSymbolForName(name: string, location: ts.Node): ts.Symbol | undefined {
- return this.checker
- .getSymbolsInScope(
- location,
- (ts.SymbolFlags as unknown as {All: number}).All
- )
- .filter((s) => s.name === name)[0];
- }
-
- /**
- * Returns an analyzer `Type` object for the given jsDoc tag.
- *
- * Note, the tag type must
- */
- getTypeForJSDocTag(tag: ts.JSDocTag): Type {
- if (!customJSDocTypeTags.has(tag.tagName.text)) {
- throw new DiagnosticsError(
- tag,
- `Internal error: '${tag.tagName.text}' is not included in customJSDocTypeTags.`
- );
- }
- const type = this.jsdocTypeMap.get(tag);
- if (type === undefined) {
- throw new DiagnosticsError(
- tag,
- `Internal error: no type was pre-processed for this JSDoc tag`
- );
- }
- return this.getTypeForType(type, tag);
- }
-
- /**
- * Returns an analyzer `Type` object for the given AST node.
- */
- getTypeForNode(node: ts.Node): Type {
- // Since getTypeAtLocation will return `any` for an untyped node, to support
- // jsdoc @type for JS (TBD), we look at the jsdoc type first.
- const jsdocType = ts.getJSDocType(node);
- return this.getTypeForType(
- jsdocType
- ? this.checker.getTypeFromTypeNode(jsdocType)
- : this.checker.getTypeAtLocation(node),
- node
- );
- }
-
- /**
- * Returns the module specifier for a declaration if it was imported,
- * or `undefined` if the declaration was not imported.
- */
- getImportModuleSpecifier(declaration: ts.Node): string | undefined {
- // TODO(kschaaf) support the various import syntaxes, e.g. `import {foo as bar} from 'baz'`
- if (
- ts.isImportSpecifier(declaration) &&
- ts.isNamedImports(declaration.parent) &&
- ts.isImportClause(declaration.parent.parent) &&
- ts.isImportDeclaration(declaration.parent.parent.parent)
- ) {
- const module = declaration.parent.parent.parent.moduleSpecifier
- .getText()
- // Remove quotes
- .slice(1, -1);
- return module;
- }
- return undefined;
- }
-
- /**
- * Returns an analyzer `Reference` object for the given symbol.
- *
- * If the symbol's declaration was imported, the Reference will be based on
- * the import's module specifier; otherwise the Reference will point to the
- * current module being analyzed.
- */
- getReferenceForSymbol(symbol: ts.Symbol, location: ts.Node): Reference {
- const {name} = symbol;
- if (this.currentModule === undefined) {
- throw new Error(`Internal error: expected currentModule to be set`);
- }
- // TODO(kschaaf): Do we need to check other declarations? The assumption is
- // that even with multiple declarations (e.g. because of class interface +
- // constructor), the reference would point to the same location for all,
- // or else (in the case of e.g. namespace augmentation) it will be global
- // and not need a specific module specifier.
- const declaration = symbol?.declarations?.[0];
- if (declaration === undefined) {
- throw new DiagnosticsError(
- location,
- `Could not find declaration for symbol '${name}'`
- );
- }
- // There are 6 cases to cover:
- // 1. A global symbol that wasn't imported; in this case, its declaration
- // will exist in a different source file than where we got the symbol
- // from. For all other cases, the symbol's declaration node will be in
- // this file, either as an ImportModuleSpecifier or a normal declaration.
- // 2. A symbol imported from a URL. The declaration will be an
- // ImportModuleSpecifier and its module path will be parsable as a URL.
- // 3. A symbol imported from a relative file within this package. The
- // declaration will be an ImportModuleSpecifier and its module path will
- // start with a '.'
- // 4. A symbol imported from an absolute path. The declaration will be an
- // ImportModuleSpecifier and its module path will start with a '/' This
- // is a weird case to cover in the analyzer because it isn't portable.
- // 5. A symbol imported from an external package. The declaration will be an
- // ImportModuleSpecifier and its module path will not start with a '.'
- // 6. A symbol declared in this file. The declaration will be one of many
- // declaration types (just not an ImportModuleSpecifier).
- if (declaration.getSourceFile() !== location.getSourceFile()) {
- // If the reference declaration doesn't exist in this module, it must have
- // been a global (whose declaration is in an ambient .d.ts file)
- // TODO(kschaaf): We might want to further differentiate e.g. DOM globals
- // (that don't have any e.g. source to link to) from other ambient
- // declarations where we could at least point to a declaration file
- return new Reference({
- name,
- isGlobal: true,
- });
- } else {
- const module = this.getImportModuleSpecifier(declaration);
- if (module !== undefined) {
- // The symbol was imported; check whether it is a URL, absolute, package
- // local, or external
- try {
- new URL(module);
- // If this didn't throw, module was a valid URL; no package, just
- // use the URL as the module
- return new Reference({
- name,
- package: '',
- module: module,
- });
- } catch {
- if (module[0] === '.') {
- // Relative import from this package: use the current package and
- // module path relative to this module
- return new Reference({
- name,
- package: this.packageJson.name!,
- module: path.join(
- path.dirname(this.currentModule.jsPath),
- module
- ),
- });
- } else if (module[0] === '/') {
- // Absolute import; no package, just use the entire path as the
- // module
- return new Reference({
- name,
- package: '',
- module: module,
- });
- } else {
- // External import: extract the npm package (taking care to respect
- // npm orgs) and module specifier (if any)
- const info = module.match(npmModule);
- if (!info || !info.groups) {
- throw new DiagnosticsError(
- declaration,
- `External npm package could not be parsed from module specifier '${module}'.`
- );
- }
- return new Reference({
- name,
- package: info.groups.package,
- module: info.groups.module,
- });
- }
- }
- } else {
- // Declared in this file: use the current package and module
- return new Reference({
- name,
- package: this.packageJson.name!,
- module: this.currentModule.jsPath,
- });
- }
- }
- }
-
- /**
- * Converts a ts.Type into an analyzer Type object (which wraps
- * the ts.Type, but also provides analyzer Reference objects).
- */
- getTypeForType(type: ts.Type, location: ts.Node): Type {
- // Ensure we treat inferred `foo = 'hi'` as 'string' not '"hi"'
- type = this.checker.getBaseTypeOfLiteralType(type);
- const text = this.checker.typeToString(type);
- const typeNode = this.checker.typeToTypeNode(
- type,
- location,
- ts.NodeBuilderFlags.IgnoreErrors
- );
- if (typeNode === undefined) {
- throw new DiagnosticsError(
- location,
- `Internal error: could not convert type to type node`
- );
- }
- const references: Reference[] = [];
- const visit = (node: ts.Node) => {
- if (ts.isTypeReferenceNode(node) || ts.isImportTypeNode(node)) {
- const name = getRootName(
- ts.isTypeReferenceNode(node) ? node.typeName : node.qualifier
- );
- // TODO(kschaaf): we'd like to just do
- // `checker.getSymbolAtLocation(node)` to get the symbol, but it appears
- // that nodes created with `checker.typeToTypeNode()` do not have
- // associated symbols, so we need to look up by name via
- // `checker.getSymbolsInScope()`
- const symbol = this.getSymbolForName(name, location);
- if (symbol === undefined) {
- throw new DiagnosticsError(
- location,
- `Could not get symbol for '${name}'.`
- );
- }
- references.push(this.getReferenceForSymbol(symbol, location));
- }
- ts.forEachChild(node, visit);
- };
- visit(typeNode);
-
- return new Type(type, text, references);
- }
-}
-
-const customJSDocTypeTags = new Set(['fires']);
-
-/**
- * Visitor that calls callback for each ts.JSDocUnknownTag containing a `{...}`
- * type string
- */
-const visitCustomJSDocTypeTags = (
- node: ts.Node,
- callback: (tag: ts.JSDocTag, typeString: string) => void
-) => {
- const jsDocTags = ts.getJSDocTags(node);
- if (jsDocTags?.length > 0) {
- for (const tag of jsDocTags) {
- if (
- ts.isJSDocUnknownTag(tag) &&
- customJSDocTypeTags.has(tag.tagName.text) &&
- typeof tag.comment === 'string'
- ) {
- const match = tag.comment.match(/{(?.*)}/);
- if (match) {
- callback(tag, match.groups!.type);
- }
- }
- }
- }
- ts.forEachChild(node, (child: ts.Node) =>
- visitCustomJSDocTypeTags(child, callback)
- );
-};
-
-/**
- * Gets the left-most name of a (possibly qualified) identifier, i.e.
- * 'Foo' returns 'Foo', but 'ts.SyntaxKind' returns 'ts'. This is the
- * symbol that would need to be imported into a given scope.
- */
-const getRootName = (
- name: ts.Identifier | ts.QualifiedName | undefined
-): string => {
- if (name === undefined) {
- return '';
- }
- if (ts.isQualifiedName(name)) {
- return getRootName(name.left);
- } else {
- return name.text;
- }
-};
-
-const customJSDocTypeInferPrefix = '__$$customJsDOCType';
-
-/**
- * Visitor that calls callback for each dummy type declaration created to
- * infer types from jsDoc strings.
- */
-const visitCustomJSDocTypeInferredTypes = (
- node: ts.Node,
- callback: (node: ts.VariableDeclaration) => void
-) => {
- if (
- ts.isVariableDeclaration(node) &&
- ts.isIdentifier(node.name) &&
- node.name.text.startsWith(customJSDocTypeInferPrefix)
- ) {
- callback(node);
- }
- ts.forEachChild(node, (child) =>
- visitCustomJSDocTypeInferredTypes(child, callback)
- );
-};
-
-/**
- * Returns the ts.JSDocTag tag for a given diagnostic error.
- *
- * Each type declaration includes an index in its identifier, which is extracted
- * and used to identify the jsDoc tag.
- */
-const getTagForError = (
- error: ts.Diagnostic,
- tags: ts.JSDocTag[]
-): ts.JSDocTag | undefined => {
- if (error.file === undefined) {
- return;
- }
- const beforeError = error.file.text.slice(0, error.start);
- const symbolStart = beforeError.lastIndexOf(customJSDocTypeInferPrefix);
- const index = parseInt(
- beforeError.slice(symbolStart + customJSDocTypeInferPrefix.length),
- 10
- );
- return tags[index];
-};
diff --git a/packages/labs/analyzer/src/lib/references.ts b/packages/labs/analyzer/src/lib/references.ts
new file mode 100644
index 0000000000..046dbad338
--- /dev/null
+++ b/packages/labs/analyzer/src/lib/references.ts
@@ -0,0 +1,415 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import ts from 'typescript';
+import {DiagnosticsError} from './errors.js';
+import {AnalyzerInterface, LocalNameOrReference, Reference} from './model.js';
+import {
+ getResolvedExportFromSourcePath,
+ getPathForModuleSpecifier,
+ getModuleInfo,
+} from './javascript/modules.js';
+import {AbsolutePath} from './paths.js';
+
+const npmModule = /^(?(@[^/]+\/[^/]+)|[^/]+)\/?(?.*)$/;
+
+/**
+ * Returns a ts.Symbol for a name in scope at a given location in the AST.
+ * TODO(kschaaf): There are ~1748 symbols in scope of a typical hello world,
+ * due to DOM globals. Perf might become an issue here.
+ */
+export const getSymbolForName = (
+ name: string,
+ location: ts.Node,
+ analyzer: AnalyzerInterface
+): ts.Symbol | undefined => {
+ return analyzer.program
+ .getTypeChecker()
+ .getSymbolsInScope(
+ location,
+ (ts.SymbolFlags as unknown as {All: number}).All
+ )
+ .filter((s) => s.name === name)[0];
+};
+
+/**
+ * Returns if the given declaration is exported from the module or not.
+ */
+export const hasExportKeyword = (node: ts.Statement) =>
+ !!node.modifiers?.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
+
+interface ModuleSpecifierInfo {
+ specifier: string;
+ location: ts.Node;
+ name: string;
+}
+
+/**
+ * Returns the module specifier expression for a declaration if it was imported,
+ * or `undefined` if the declaration was not imported.
+ */
+const getImportSpecifierInfo = (
+ declaration: ts.Node
+): ModuleSpecifierInfo | undefined => {
+ // TODO(kschaaf) support the various import syntaxes, e.g. `import {foo as bar} from 'baz'`
+ if (
+ ts.isImportSpecifier(declaration) &&
+ ts.isNamedImports(declaration.parent) &&
+ ts.isImportClause(declaration.parent.parent) &&
+ ts.isImportDeclaration(declaration.parent.parent.parent)
+ ) {
+ const specifierExpression =
+ declaration.parent.parent.parent.moduleSpecifier;
+ const specifier = getSpecifierString(specifierExpression);
+ return {
+ specifier,
+ location: specifierExpression,
+ name: declaration.propertyName?.text ?? declaration.name.text,
+ };
+ }
+ return undefined;
+};
+
+/**
+ * Returns an analyzer `Reference` object for the given identifier.
+ *
+ * If the symbol's declaration was imported, the Reference will be based on
+ * the import's module specifier; otherwise the Reference will point to the
+ * current module being analyzed.
+ */
+export const getReferenceForIdentifier = (
+ identifier: ts.Identifier,
+ analyzer: AnalyzerInterface
+) => {
+ const symbol = analyzer.program
+ .getTypeChecker()
+ .getSymbolAtLocation(identifier);
+ if (symbol === undefined) {
+ throw new DiagnosticsError(
+ identifier,
+ 'Internal error: Could not get symbol for identifier.'
+ );
+ }
+ return getReferenceForSymbol(symbol, identifier, analyzer);
+};
+
+/**
+ * Returns an analyzer `Reference` model for the given ts.Symbol.
+ *
+ * If the symbol's declaration was imported, the Reference will be based on
+ * the import's module specifier; otherwise the Reference will point to the
+ * current module being analyzed.
+ */
+export function getReferenceForSymbol(
+ symbol: ts.Symbol,
+ location: ts.Node,
+ analyzer: AnalyzerInterface
+): Reference {
+ const {name: symbolName} = symbol;
+ // TODO(kschaaf): Do we need to check other declarations? The assumption is
+ // that even with multiple declarations (e.g. because of class interface +
+ // constructor), the reference would point to the same location for all,
+ // or else (in the case of e.g. namespace augmentation) it will be global
+ // and not need a specific module specifier.
+ const declaration = symbol?.declarations?.[0];
+ if (declaration === undefined) {
+ throw new DiagnosticsError(
+ location,
+ `Could not find declaration for symbol '${symbolName}'`
+ );
+ }
+ const declarationSourceFile = declaration.getSourceFile();
+ const locationSourceFile = location.getSourceFile();
+ // There are three top-level cases to cover:
+ // 1. A global symbol that wasn't imported.
+ // 2. An imported symbol
+ // 3. A symbol declared in this file.
+ if (declarationSourceFile !== locationSourceFile) {
+ // If the reference declaration doesn't exist in this module, it must have
+ // been a global (whose declaration is in an ambient .d.ts file)
+ // TODO(kschaaf): We might want to further differentiate e.g. DOM globals
+ // (that don't have any e.g. source to link to) from other ambient
+ // declarations where we could at least point to a declaration file
+ return getGlobalReference(declarationSourceFile, symbolName, analyzer);
+ } else {
+ // For all other cases, the symbol's declaration node will be in this file,
+ // either as an ImportDeclaration or a normal declaration.
+ const importInfo = getImportSpecifierInfo(declaration);
+ if (importInfo !== undefined) {
+ // Declaration was imported
+ return getImportReference(
+ importInfo.specifier,
+ importInfo.location,
+ importInfo.name,
+ analyzer
+ );
+ } else {
+ // Declared in this file: use the current package and module
+ return getLocalReference(location, symbolName, analyzer);
+ }
+ }
+}
+
+/**
+ * Returns a `Reference` for a global symbol that was not imported.
+ */
+const getGlobalReference = (
+ declarationSourceFile: ts.SourceFile,
+ name: string,
+ analyzer: AnalyzerInterface
+) => {
+ return new Reference({
+ name,
+ isGlobal: true,
+ dereference: () =>
+ getResolvedExportFromSourcePath(
+ declarationSourceFile.fileName as AbsolutePath,
+ name,
+ analyzer
+ ),
+ });
+};
+
+/**
+ * Returns a `Reference` for a symbol that was imported.
+ *
+ * There are 4 main categories of imports we cover:
+ *
+ * 1. A symbol imported from a URL. The declaration will be an
+ * ImportModuleSpecifier and its module path will be parsable as a URL.
+ *
+ * 2. A symbol imported from a relative file within this package. The
+ * declaration will be an ImportModuleSpecifier and its module path will
+ * start with a '.'
+ *
+ * 3. A symbol imported from an absolute path. The declaration will be an
+ * ImportModuleSpecifier and its module path will start with a '/' This is a
+ * weird case to cover in the analyzer because it isn't portable.
+ *
+ * 4. A symbol imported from an external package. The declaration will be an
+ * ImportModuleSpecifier and its module path will not start with a '.'
+ */
+export const getImportReference = (
+ specifier: string,
+ location: ts.Node,
+ name: string,
+ analyzer: AnalyzerInterface
+) => {
+ const {path} = analyzer;
+ let refPackage;
+ let refModule;
+ // Check whether it is a URL, absolute, package local, or external
+ try {
+ new URL(specifier);
+ refPackage = '';
+ refModule = specifier;
+ } catch {
+ if (specifier[0] === '.') {
+ // Relative import from this package: use the current package and
+ // module path relative to this module
+ const sourceFilePath = location.getSourceFile().fileName as AbsolutePath;
+ const module = getModuleInfo(sourceFilePath, analyzer);
+ refPackage = module.packageJson.name;
+ refModule = path.join(path.dirname(module.jsPath), specifier);
+ } else if (analyzer.path.isAbsolute(specifier)) {
+ // Absolute import; no package, just use the entire path as the
+ // module
+ refPackage = '';
+ refModule = specifier;
+ } else {
+ // External import: extract the npm package (taking care to respect
+ // npm orgs) and module specifier (if any)
+ const info = specifier.match(npmModule);
+ if (!info || !info.groups) {
+ throw new DiagnosticsError(
+ location,
+ `External npm package could not be parsed from module specifier '${specifier}'.`
+ );
+ }
+ refPackage = info.groups.package;
+ refModule = info.groups.module || undefined;
+ }
+ }
+ return new Reference({
+ name,
+ package: refPackage,
+ module: refModule,
+ dereference: () =>
+ getResolvedExportFromSourcePath(
+ getPathForModuleSpecifier(specifier, location, analyzer),
+ name,
+ analyzer
+ ),
+ });
+};
+
+/**
+ * Returns a `Reference` for a symbol that was imported.
+ */
+export const getImportReferenceForSpecifierExpression = (
+ specifierExpression: ts.Expression,
+ name: string,
+ analyzer: AnalyzerInterface
+) => {
+ const specifier = getSpecifierString(specifierExpression);
+ return getImportReference(specifier, specifierExpression, name, analyzer);
+};
+
+/**
+ * Returns a `Reference` to a symbol declared in the current source file.
+ */
+const getLocalReference = (
+ location: ts.Node,
+ name: string,
+ analyzer: AnalyzerInterface
+) => {
+ const module = getModuleInfo(
+ location.getSourceFile().fileName as AbsolutePath,
+ analyzer
+ );
+ return new Reference({
+ name,
+ package: module.packageJson.name,
+ module: module.jsPath,
+ dereference: () =>
+ getResolvedExportFromSourcePath(
+ location.getSourceFile().fileName as AbsolutePath,
+ name,
+ analyzer
+ ),
+ });
+};
+
+/**
+ * For a given export clause and (optional) specifier from an export statement,
+ * returns an array of objects mapping the export name to a
+ * LocalNameOrReference, which is a string name that can be looked up directly
+ * in `getDeclaration()` of the declaring module for local declarations, or a
+ * `Reference` in the case of re-exported declarations.
+ *
+ * For example:
+ * ```
+ * import {a} from 'foo';
+ * const b = 'b';
+ * const c = 'c';
+ * export {a as x, b as y, c};
+ * ```
+ * This would return (using pseudo-code for Reference objects):
+ * ```
+ * [
+ * {name: 'x', reference: new Reference('a', 'foo')},
+ * {name: 'y', reference: 'b'},
+ * {name: 'c', reference: 'c'},
+ * ]
+ * ```
+ *
+ * This also handles explicit re-export syntax, which all become References:
+ * ```
+ * export {a as x, b as y, c} from 'foo';
+ * ```
+ *
+ * Finally, this handles namespace exports, which become a single Reference:
+ * ```
+ * export * as ns from 'foo';
+ * ```
+ * becomes:
+ * ```
+ * [{name: 'ns', reference: new Reference('*', 'foo')}]
+ * ```
+ */
+export const getExportReferences = (
+ exportClause: ts.NamedExportBindings,
+ moduleSpecifier: ts.Expression | undefined,
+ analyzer: AnalyzerInterface
+): Array<{exportName: string; decNameOrRef: LocalNameOrReference}> => {
+ const refs: Array<{exportName: string; decNameOrRef: string | Reference}> =
+ [];
+ if (ts.isNamedExports(exportClause)) {
+ for (const el of exportClause.elements) {
+ const exportName = el.name.getText();
+ const localNameNode = el.propertyName ?? el.name;
+ const localName = localNameNode.getText();
+ if (moduleSpecifier !== undefined) {
+ // This was an explicit re-export (e.g. `export {a} from 'foo'`), so add
+ // a Reference
+ const specifier = getSpecifierString(moduleSpecifier);
+ refs.push({
+ exportName,
+ decNameOrRef: getImportReference(
+ specifier,
+ moduleSpecifier,
+ localName,
+ analyzer
+ ),
+ });
+ } else {
+ // Get the declaration for this symbol, so we can determine if
+ // it was declared locally or not. Note we use name-based searching
+ // to find the symbol, because `getSymbolAtLocation()` for
+ // `export {Foo}` will annoyingly just point back to the export
+ // line, rather than the location it was actually declared.
+ const symbol = getSymbolForName(localName, localNameNode, analyzer);
+ const decl = symbol?.declarations?.[0];
+ if (symbol === undefined || decl === undefined) {
+ throw new DiagnosticsError(
+ el,
+ `Could not find declaration for symbol`
+ );
+ }
+ if (ts.isImportSpecifier(decl)) {
+ // If the declaration was an import specifier, this means it's being
+ // re-exported, so add a Reference
+ refs.push({
+ exportName,
+ decNameOrRef: getReferenceForSymbol(symbol, decl, analyzer),
+ });
+ } else {
+ // Otherwise, the declaration is local, so just add its name; this
+ // can be looked up directly in `getDeclaration` for the module
+ refs.push({exportName, decNameOrRef: localName});
+ }
+ }
+ }
+ } else if (
+ // e.g. `export * as ns from 'foo'`;
+ ts.isNamespaceExport(exportClause) &&
+ moduleSpecifier !== undefined
+ ) {
+ const specifier = getSpecifierString(moduleSpecifier);
+ refs.push({
+ exportName: exportClause.name.getText(),
+ decNameOrRef: getImportReference(
+ specifier,
+ moduleSpecifier,
+ '*',
+ analyzer
+ ),
+ });
+ } else {
+ throw new DiagnosticsError(
+ exportClause,
+ `Unhandled form of ExportDeclaration`
+ );
+ }
+ return refs;
+};
+
+/**
+ * Returns the specifier string from a specifier expression.
+ *
+ * For a given import statement:
+ * ```
+ * import {foo} from 'foo/bar.js';
+ * ```
+ * The specifierExpression is the string literal 'foo/bar.js' whose getText()
+ * includes the quotes. This function returns the string value without the
+ * quotes.
+ */
+export const getSpecifierString = (specifierExpression: ts.Expression) => {
+ // A specifier expression is always expected to be a quoted string literal.
+ // Slice off the quotes and return the text.
+ return specifierExpression.getText().slice(1, -1);
+};
diff --git a/packages/labs/analyzer/src/lib/types.ts b/packages/labs/analyzer/src/lib/types.ts
new file mode 100644
index 0000000000..29c27bc54c
--- /dev/null
+++ b/packages/labs/analyzer/src/lib/types.ts
@@ -0,0 +1,273 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import ts from 'typescript';
+import {DiagnosticsError} from './errors.js';
+import {getPackageInfo} from './javascript/packages.js';
+import {Type, Reference, AnalyzerInterface} from './model.js';
+import {
+ AbsolutePath,
+ absoluteToPackage,
+ PackagePath,
+ resolveExtension,
+} from './paths.js';
+import {
+ getImportReference,
+ getReferenceForSymbol,
+ getSymbolForName,
+} from './references.js';
+
+/**
+ * Returns an analyzer `Type` object for the given jsDoc tag.
+ *
+ * Note, the tag type must
+ */
+export const getTypeForJSDocTag = (
+ tag: ts.JSDocTag,
+ analyzer: AnalyzerInterface
+): Type | undefined => {
+ const typeString =
+ ts.isJSDocUnknownTag(tag) && typeof tag.comment === 'string'
+ ? tag.comment?.match(/{(?.*)}/)?.groups?.type
+ : undefined;
+ if (typeString !== undefined) {
+ const typeNode = parseType(typeString);
+ if (typeNode == undefined) {
+ throw new DiagnosticsError(
+ tag,
+ `Internal error: failed to parse type from JSDoc comment.`
+ );
+ }
+ const type = analyzer.program
+ .getTypeChecker()
+ .getTypeFromTypeNode(typeNode);
+ return new Type({
+ type,
+ text: typeString,
+ getReferences: () => getReferencesForTypeNode(typeNode, tag, analyzer),
+ });
+ } else {
+ return undefined;
+ }
+};
+
+/**
+ * Returns an analyzer `Type` object for the given AST node.
+ */
+export const getTypeForNode = (
+ node: ts.Node,
+ analyzer: AnalyzerInterface
+): Type => {
+ // Since getTypeAtLocation will return `any` for an untyped node, to support
+ // jsdoc @type for JS (TBD), we look at the jsdoc type first.
+ const jsdocType = ts.getJSDocType(node);
+ return getTypeForType(
+ jsdocType
+ ? analyzer.program.getTypeChecker().getTypeFromTypeNode(jsdocType)
+ : analyzer.program.getTypeChecker().getTypeAtLocation(node),
+ node,
+ analyzer
+ );
+};
+
+/**
+ * Converts a ts.Type into an analyzer Type object (which wraps
+ * the ts.Type, but also provides analyzer Reference objects).
+ */
+const getTypeForType = (
+ type: ts.Type,
+ location: ts.Node,
+ analyzer: AnalyzerInterface
+): Type => {
+ const checker = analyzer.program.getTypeChecker();
+ // Ensure we treat inferred `foo = 'hi'` as 'string' not '"hi"'
+ type = checker.getBaseTypeOfLiteralType(type);
+ const text = checker.typeToString(type);
+ const typeNode = checker.typeToTypeNode(
+ type,
+ location,
+ ts.NodeBuilderFlags.IgnoreErrors
+ );
+ if (typeNode === undefined) {
+ throw new DiagnosticsError(
+ location,
+ `Internal error: could not convert type to type node`
+ );
+ }
+ return new Type({
+ type,
+ text,
+ getReferences: () => getReferencesForTypeNode(typeNode, location, analyzer),
+ });
+};
+
+/**
+ * For a given TypeNode syntax tree, walk the AST and extract Reference
+ * model objects for any TypeReferenceNode or ImportTypeNode's.
+ */
+const getReferencesForTypeNode = (
+ typeNode: ts.TypeNode,
+ location: ts.Node,
+ analyzer: AnalyzerInterface
+): Reference[] => {
+ const references: Reference[] = [];
+ const visit = (node: ts.Node) => {
+ if (ts.isTypeReferenceNode(node)) {
+ const name = getRootName(node.typeName);
+ // TODO(kschaaf): we'd like to just do
+ // `checker.getSymbolAtLocation(node)` to get the symbol, but it appears
+ // that nodes created with `checker.typeToTypeNode()` do not have
+ // associated symbols, so we need to look up by name via
+ // `checker.getSymbolsInScope()`
+ const symbol = getSymbolForName(name, location, analyzer);
+ if (symbol === undefined) {
+ throw new DiagnosticsError(
+ location,
+ `Could not get symbol for '${name}'.`
+ );
+ }
+ references.push(getReferenceForSymbol(symbol, location, analyzer));
+ } else if (ts.isImportTypeNode(node)) {
+ if (!ts.isLiteralTypeNode(node.argument)) {
+ throw new DiagnosticsError(node, 'Expected a string literal.');
+ }
+ const name = getRootName(node.qualifier);
+ if (!ts.isStringLiteral(node.argument.literal)) {
+ throw new DiagnosticsError(
+ location,
+ `Expected import specifier to be a string literal`
+ );
+ }
+ const specifier = getSpecifierFromTypeImport(
+ node.argument.literal.text,
+ analyzer
+ );
+ // TODO(kschaaf): This may have been an inferred type from a transitive
+ // dependency; in this case we should include version information in the
+ // reference model
+ references.push(
+ getImportReference(specifier, node.argument.literal, name, analyzer)
+ );
+ }
+ ts.forEachChild(node, visit);
+ };
+ visit(typeNode);
+ return references;
+};
+
+/**
+ * If the given specifier is an absolute path, turns it into an npm import
+ * specifier by looking for its package.json and using package information found
+ * there.
+ *
+ * If the path was not absolute, it returns the specifier as-is.
+ */
+const getSpecifierFromTypeImport = (
+ specifier: string,
+ analyzer: AnalyzerInterface
+) => {
+ specifier = analyzer.path.normalize(
+ resolveExtension(specifier as AbsolutePath, analyzer)
+ );
+ if (analyzer.path.isAbsolute(specifier)) {
+ const {
+ rootDir,
+ name,
+ packageJson: {main, module},
+ } = getPackageInfo(specifier as AbsolutePath, analyzer);
+ let modulePath = absoluteToPackage(specifier as AbsolutePath, rootDir);
+ const packageMain = module ?? main;
+ if (packageMain !== undefined && modulePath === packageMain) {
+ modulePath = '' as PackagePath;
+ }
+ specifier = name + (modulePath ? `/${modulePath}` : '');
+ }
+ return specifier;
+};
+
+/**
+ * Gets the left-most name of a (possibly qualified) identifier, i.e.
+ * 'Foo' returns 'Foo', but 'ts.SyntaxKind' returns 'ts'. This is the
+ * symbol that would need to be imported into a given scope.
+ */
+const getRootName = (
+ name: ts.Identifier | ts.QualifiedName | undefined
+): string => {
+ if (name === undefined) {
+ return '';
+ }
+ if (ts.isQualifiedName(name)) {
+ return getRootName(name.left);
+ } else {
+ return name.text;
+ }
+};
+
+/**
+ * Below is a minimal LanguageService that allows quickly parsing snippets of TS
+ * syntax into an AST. There's one file `contents.ts` in the program that we
+ * update with snippets to parse.
+ *
+ * This allows extracting a type string from a custom JSDoc comment (like
+ * `@event`), parsing it into syntax, and than walking it to extract references.
+ *
+ * Note, the program may have semantic errors (e.g. it may reference imports
+ * that aren't included in the snippet), but that's ok because we only care
+ * about parsing the syntax. When extracting references, we can use
+ * `getSymbolByName` to re-associate symbols by name from a disassociated syntax
+ * tree and the actual program.
+ *
+ * TODO(kschaaf): Providing diagnostic errors for semantically incorrect custom
+ * JSDoc types would be nice, but that's pretty difficult in analyzers
+ * where we don't control the TS program creation such that we can re-write
+ * source files, etc. (as is the case when using in plugins).
+ */
+
+let contents = '';
+let contentsId = 0;
+
+const service = ts.createLanguageService(
+ {
+ getScriptFileNames: () => ['contents.ts'],
+ getScriptVersion: () => contentsId.toString(),
+ getScriptSnapshot: () => ts.ScriptSnapshot.fromString(contents),
+ getCurrentDirectory: () => '',
+ getCompilationSettings: () => ({include: ['contents.ts']}),
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
+ fileExists: () => true,
+ readFile: () => undefined,
+ readDirectory: () => [],
+ directoryExists: () => true,
+ getDirectories: () => [],
+ },
+ ts.createDocumentRegistry()
+);
+
+/**
+ * Uses the stub LSH above to parse a type string and return a syntax-only
+ * TypeNode (there will be no valid symbol information retrievable directly from
+ * these nodes).
+ */
+const parseType = (typeString: string): ts.TypeNode | undefined => {
+ // Update the file
+ contents = `export type typeToParse = ${typeString}`;
+ contentsId++;
+ // Get a new program & parsed source file
+ const sourceFile = service
+ .getProgram()
+ ?.getSourceFileByPath('contents.ts' as ts.Path);
+ if (sourceFile === undefined) {
+ return undefined;
+ }
+ // Find the type alias node and return it
+ let typeNode: ts.TypeNode | undefined = undefined;
+ ts.forEachChild(sourceFile, (node) => {
+ if (ts.isTypeAliasDeclaration(node)) {
+ typeNode = node.type;
+ }
+ });
+ return typeNode;
+};
diff --git a/packages/labs/analyzer/src/test/analyzer_test.ts b/packages/labs/analyzer/src/test/analyzer_test.ts
index 955e77e616..d5012454cb 100644
--- a/packages/labs/analyzer/src/test/analyzer_test.ts
+++ b/packages/labs/analyzer/src/test/analyzer_test.ts
@@ -9,45 +9,50 @@ import {suite} from 'uvu';
import * as assert from 'uvu/assert';
import * as path from 'path';
import {fileURLToPath} from 'url';
+import {getOutputFilename, getSourceFilename, languages} from './utils.js';
-import {Analyzer, AbsolutePath} from '../index.js';
-
-const test = suite<{analyzer: Analyzer; packagePath: AbsolutePath}>(
- 'Basic Analyzer tests'
-);
-
-test.before((ctx) => {
- try {
- const packagePath = (ctx.packagePath = fileURLToPath(
- new URL('../test-files/basic-elements', import.meta.url).href
- ) as AbsolutePath);
- ctx.analyzer = new Analyzer(packagePath);
- } catch (error) {
- // Uvu has a bug where it silently ignores failures in before and after,
- // see https://github.com/lukeed/uvu/issues/191.
- console.error('uvu before error', error);
- process.exit(1);
- }
-});
-
-test('Reads project files', ({analyzer, packagePath}) => {
- const rootFileNames = analyzer.programContext.program.getRootFileNames();
- assert.equal(rootFileNames.length, 5);
-
- const elementAPath = path.resolve(packagePath, 'src', 'element-a.ts');
- const sourceFile =
- analyzer.programContext.program.getSourceFile(elementAPath);
- assert.ok(sourceFile);
-});
-
-test('Analyzer finds class declarations', ({analyzer}) => {
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/class-a.ts')
+import {createPackageAnalyzer, Analyzer, AbsolutePath} from '../index.js';
+
+for (const lang of languages) {
+ const test = suite<{analyzer: Analyzer; packagePath: AbsolutePath}>(
+ `Basic Analyzer tests (${lang})`
);
- assert.equal(elementAModule?.jsPath, path.normalize('out/class-a.js'));
- assert.equal(elementAModule?.declarations.length, 1);
- assert.equal(elementAModule?.declarations[0].name, 'ClassA');
-});
-test.run();
+ test.before((ctx) => {
+ try {
+ const packagePath = (ctx.packagePath = fileURLToPath(
+ new URL(`../test-files/${lang}/basic-elements`, import.meta.url).href
+ ) as AbsolutePath);
+ ctx.analyzer = createPackageAnalyzer(packagePath);
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ test('Reads project files', ({analyzer, packagePath}) => {
+ const rootFileNames = analyzer.program.getRootFileNames();
+ assert.equal(rootFileNames.length, 6);
+
+ const elementAPath = path.resolve(
+ packagePath,
+ getSourceFilename('element-a', lang)
+ );
+ const sourceFile = analyzer.program.getSourceFile(elementAPath);
+ assert.ok(sourceFile);
+ });
+
+ test('Analyzer finds class declarations', ({analyzer}) => {
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('class-a', lang)
+ );
+ assert.equal(elementAModule?.jsPath, getOutputFilename('class-a', lang));
+ assert.equal(elementAModule?.declarations.length, 1);
+ assert.equal(elementAModule?.declarations[0].name, 'ClassA');
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/javascript/exports_test.ts b/packages/labs/analyzer/src/test/javascript/exports_test.ts
new file mode 100644
index 0000000000..6dee50f01a
--- /dev/null
+++ b/packages/labs/analyzer/src/test/javascript/exports_test.ts
@@ -0,0 +1,390 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {suite} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+import {AbsolutePath} from '../../lib/paths.js';
+import {getSourceFilename, InMemoryAnalyzer, languages} from '../utils.js';
+
+for (const lang of languages) {
+ const test = suite<{
+ analyzer: InMemoryAnalyzer;
+ }>(`Exports tests (${lang})`);
+
+ test.before.each((ctx) => {
+ ctx.analyzer = new InMemoryAnalyzer(lang, {
+ '/package.json': JSON.stringify({name: '@lit-internal/in-memory-test'}),
+ });
+ });
+
+ test('local declaration export via export keyword', ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/a', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, undefined);
+ });
+
+ test('local declaration export via export statement', ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ const a = 'a';
+ export {a};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/a', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, undefined);
+ });
+
+ test('local declaration export via export statement, renamed', ({
+ analyzer,
+ }) => {
+ analyzer.setFile(
+ '/a',
+ `
+ const aInternal = 'a';
+ export {aInternal as a};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/a', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'aInternal');
+ assert.equal(a.module, undefined);
+ });
+
+ test('local declaration export via export statement, multiple', ({
+ analyzer,
+ }) => {
+ analyzer.setFile(
+ '/a',
+ `
+ const aInternal = 'a';
+ const b = 'b';
+ export {aInternal as a, b, aInternal as c};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/a', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a', 'b', 'c']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'aInternal');
+ assert.equal(a.module, undefined);
+ const b = module.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(b.module, undefined);
+ const c = module.getExportReference('c');
+ assert.equal(c.name, 'aInternal');
+ assert.equal(c.module, undefined);
+ });
+
+ test('reexport via export statement with specifier', ({analyzer}) => {
+ analyzer.setFile('/a', `export const a = 'a';`);
+ analyzer.setFile(
+ '/b',
+ `
+ export {a} from './a.js';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test('reexport via export statement with specifier, renamed', ({
+ analyzer,
+ }) => {
+ analyzer.setFile('/a', `export const a = 'a';`);
+ analyzer.setFile(
+ '/b',
+ `
+ export {a as a2} from './a.js';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a2']);
+ const a = module.getExportReference('a2');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test('reexport via export statement with specifier, multiple', ({
+ analyzer,
+ }) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ export const b = 'b';
+ `
+ );
+ analyzer.setFile(
+ '/b',
+ `
+ export {a as a2, b} from './a.js';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a2', 'b']);
+ const a = module.getExportReference('a2');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ const b = module.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(b.module, 'a.js');
+ });
+
+ test('reexport via export statement of imported symbol', ({analyzer}) => {
+ analyzer.setFile('/a', `export const a = 'a';`);
+ analyzer.setFile(
+ '/b',
+ `
+ import {a} from './a.js'
+ export {a};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test('reexport via export statement of renamed imported symbol', ({
+ analyzer,
+ }) => {
+ analyzer.setFile('/a', `export const a = 'a';`);
+ analyzer.setFile(
+ '/b',
+ `
+ import {a as a2} from './a.js'
+ export {a2};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a2']);
+ const a = module.getExportReference('a2');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test('reexport via export statement of renamed imported symbol, renamed', ({
+ analyzer,
+ }) => {
+ analyzer.setFile('/a', `export const a = 'a';`);
+ analyzer.setFile(
+ '/b',
+ `
+ import {a as a2} from './a.js'
+ export {a2 as a3};
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a3']);
+ const a = module.getExportReference('a3');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test('reexport via export statement of imported symbols, multiple', ({
+ analyzer,
+ }) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ export const b = 'b';
+ export const c = 'c';
+ `
+ );
+ analyzer.setFile(
+ '/b',
+ `
+ import {a} from './a.js'
+ import {b, c as c2} from './a.js'
+ export {a, b, c2 as c};
+ export {c2}
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a', 'b', 'c', 'c2']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ const b = module.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(b.module, 'a.js');
+ const c = module.getExportReference('c');
+ assert.equal(c.name, 'c');
+ assert.equal(c.module, 'a.js');
+ const c2 = module.getExportReference('c2');
+ assert.equal(c2.name, 'c');
+ assert.equal(c2.module, 'a.js');
+ });
+
+ test('export {x as default}', ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ const a = 'a';
+ const b = 'b';
+ export {a as default, b}
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/a', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['b', 'default']);
+ const a = module.getExportReference('default');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, undefined);
+ const b = module.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(b.module, undefined);
+ });
+
+ test(`export * from 'module'`, ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ export const b = 'b';
+ `
+ );
+ analyzer.setFile(
+ '/b',
+ `
+ export * from './a.js';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['a', 'b']);
+ const a = module.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'a.js');
+ const b = module.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(a.module, 'a.js');
+ });
+
+ test(`export * from 'module', transitively`, ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ export const b = 'b';
+ `
+ );
+ analyzer.setFile(
+ '/b',
+ `
+ export const c = 'c';
+ export const d = 'd';
+ export * from './a.js';
+ `
+ );
+ analyzer.setFile(
+ '/c',
+ `
+ export * from './b.js';
+ `
+ );
+ const moduleC = analyzer.getModule(
+ getSourceFilename('/c', lang) as AbsolutePath
+ );
+ assert.equal(moduleC.exportNames.sort(), ['a', 'b', 'c', 'd']);
+ const a = moduleC.getExportReference('a');
+ assert.equal(a.name, 'a');
+ assert.equal(a.module, 'b.js');
+ const b = moduleC.getExportReference('b');
+ assert.equal(b.name, 'b');
+ assert.equal(b.module, 'b.js');
+ const c = moduleC.getExportReference('c');
+ assert.equal(c.name, 'c');
+ assert.equal(c.module, 'b.js');
+ const d = moduleC.getExportReference('d');
+ assert.equal(d.name, 'd');
+ assert.equal(d.module, 'b.js');
+
+ const moduleB = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(moduleB.exportNames.sort(), ['a', 'b', 'c', 'd']);
+ const a2 = moduleB.getExportReference('a');
+ assert.equal(a2.name, 'a');
+ assert.equal(a2.module, 'a.js');
+ const b2 = moduleB.getExportReference('b');
+ assert.equal(b2.name, 'b');
+ assert.equal(a2.module, 'a.js');
+ const c2 = moduleB.getExportReference('c');
+ assert.equal(c2.name, 'c');
+ assert.equal(c2.module, undefined);
+ const d2 = moduleB.getExportReference('d');
+ assert.equal(d2.name, 'd');
+ assert.equal(d2.module, undefined);
+ });
+
+ test('export * as ns', ({analyzer}) => {
+ analyzer.setFile(
+ '/a',
+ `
+ export const a = 'a';
+ export const b = 'b';
+ `
+ );
+ analyzer.setFile(
+ '/b',
+ `
+ export * as ns from './a.js';
+ `
+ );
+ const module = analyzer.getModule(
+ getSourceFilename('/b', lang) as AbsolutePath
+ );
+ assert.equal(module.exportNames.sort(), ['ns']);
+ const ns = module.getExportReference('ns');
+ assert.equal(ns.name, '*');
+ assert.equal(ns.module, 'a.js');
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/javascript/modules_test.ts b/packages/labs/analyzer/src/test/javascript/modules_test.ts
new file mode 100644
index 0000000000..aef49451d0
--- /dev/null
+++ b/packages/labs/analyzer/src/test/javascript/modules_test.ts
@@ -0,0 +1,238 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {suite} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+import path from 'path';
+import {fileURLToPath} from 'url';
+import {getSourceFilename, InMemoryAnalyzer, languages} from '../utils.js';
+
+import {
+ createPackageAnalyzer,
+ Analyzer,
+ AbsolutePath,
+ Module,
+} from '../../index.js';
+
+for (const lang of languages) {
+ const test = suite<{
+ analyzer: Analyzer;
+ packagePath: AbsolutePath;
+ module: Module;
+ }>(`Module tests (${lang})`);
+
+ test.before((ctx) => {
+ try {
+ const packagePath = fileURLToPath(
+ new URL(`../../test-files/${lang}/modules`, import.meta.url).href
+ ) as AbsolutePath;
+ const analyzer = createPackageAnalyzer(packagePath);
+
+ const result = analyzer.getPackage();
+ const file = getSourceFilename('module-a', lang);
+ const module = result.modules.find((m) => m.sourcePath === file);
+ if (module === undefined) {
+ throw new Error(`Analyzer did not analyze file '${file}'`);
+ }
+
+ ctx.packagePath = packagePath;
+ ctx.analyzer = analyzer;
+ ctx.module = module;
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ test('Dependencies correctly analyzed', ({module}) => {
+ const getMonorepoSubpath = (f: string) =>
+ f?.slice(f.lastIndexOf('packages' + path.sep));
+ const expectedDeps = new Set([
+ // This import will either be to a .ts file or a .js file depending on
+ // language, since it's in the program
+ getSourceFilename(
+ `packages/labs/analyzer/test-files/${lang}/modules/module-b`,
+ lang
+ ),
+ // The Lit import will always be a .d.ts file regardless of language since
+ // it's outside the program and has declarations
+ path.normalize(`packages/lit/index.d.ts`),
+ ]);
+ assert.equal(expectedDeps.size, module.dependencies.size);
+ for (const d of module.dependencies) {
+ assert.ok(
+ expectedDeps.has(getMonorepoSubpath(d)),
+ `${getMonorepoSubpath(d)} not in\n ${Array.from(expectedDeps).join(
+ '\n'
+ )}`
+ );
+ }
+ });
+
+ test.run();
+
+ const cachingTest = suite<{
+ analyzer: InMemoryAnalyzer;
+ }>(`Module caching tests (${lang})`);
+
+ cachingTest.before.each((ctx) => {
+ ctx.analyzer = new InMemoryAnalyzer(lang, {
+ '/package.json': JSON.stringify({name: '@lit-internal/in-memory-test'}),
+ });
+ });
+
+ cachingTest(
+ 'getModule returns same model when unchanged, different when changed',
+ ({analyzer}) => {
+ analyzer.setFile('/module', `export const foo = 'foo';`);
+ // Read initial model for module
+ const module1 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.equal(module1.declarations.length, 1);
+ // Read again with no change
+ const module2 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module2);
+ // Change dependency and we expect a different model
+ analyzer.setFile('/module', `export class Foo {}; export class Bar {};`);
+ const module3 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module2 !== module3);
+ assert.equal(module3.declarations.length, 2);
+ }
+ );
+
+ cachingTest(
+ 'getModule returns same model when direct dependency unchanged, different when changed',
+ ({analyzer}) => {
+ analyzer.setFile('/dep1', `export class Bar { bar = 1; }`);
+ analyzer.setFile(
+ '/module',
+ `import {Bar} from './dep1.js';
+ export class Foo extends Bar {};`
+ );
+ // Read initial model for module
+ const module1 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ // Read again with no change, we expect the cached model to be returned
+ const module2 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module2);
+ // Change dependency; we still expect the same model because nothing
+ // has caused the dependency model to be created yet
+ analyzer.setFile('/dep1', `export class Bar { bar = 2; }`);
+ const module3 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module3);
+ // Now cause the dependency model to be created and cached; we still
+ // expect the same model since nothing has been invalidated
+ const dep1 = analyzer.getModule(
+ getSourceFilename('/dep1', lang) as AbsolutePath
+ );
+ const module4 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module4);
+ // Now change the dependency; since module depends on dep1, and since the
+ // dep1 model has been created/cached (and is now invalid), the module's
+ // model should be created anew, ensuring it has no stale information
+ // cached from dep1
+ analyzer.setFile('/dep1', `export class Bar { bar = 2; }`);
+ const module5 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module4 !== module5);
+ // We should also see that the dependency model is re-created if we ask
+ // for it
+ const dep2 = analyzer.getModule(
+ getSourceFilename('/dep1', lang) as AbsolutePath
+ );
+ assert.ok(dep1 !== dep2);
+ }
+ );
+
+ cachingTest(
+ 'getModule returns same model when transitive dependency unchanged, different when changed',
+ ({analyzer}) => {
+ analyzer.setFile('/dep2', `export class Baz { baz = 1; }`);
+ analyzer.setFile(
+ '/dep1',
+ `import {Baz} from './dep2.js';
+ export class Bar extends Baz {}`
+ );
+ analyzer.setFile(
+ '/module',
+ `import {Bar} from './dep1.js';
+ export class Foo extends Bar {};`
+ );
+ // Read initial models for module and deps
+ const module1 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ analyzer.getModule(getSourceFilename('/dep1', lang) as AbsolutePath);
+ analyzer.getModule(getSourceFilename('/dep2', lang) as AbsolutePath);
+ // Read again with no change
+ const module2 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module2);
+ // Change transitive dependency and we expect a different model
+ analyzer.setFile('/dep2', `export class Baz { baz = 2; }`);
+ const module3 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module2 !== module3);
+ }
+ );
+
+ cachingTest(
+ 'getModule returns same model when unrelated file changed',
+ ({analyzer}) => {
+ analyzer.setFile('/unrelated', `export class Unrelated { n = 1; }`);
+ analyzer.setFile('/dep2', `export class Baz { }`);
+ analyzer.setFile(
+ '/dep1',
+ `import {Baz} from './dep2.js';
+ export class Bar extends Baz {}`
+ );
+ analyzer.setFile(
+ '/module',
+ `import {Bar} from './dep1.js';
+ export class Foo extends Bar {};`
+ );
+ // Read initial models for module and deps
+ const module1 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ analyzer.getModule(getSourceFilename('/dep1', lang) as AbsolutePath);
+ analyzer.getModule(getSourceFilename('/dep2', lang) as AbsolutePath);
+ analyzer.getModule(getSourceFilename('/unrelated', lang) as AbsolutePath);
+ // Change unrelated module and we expect the same module
+ analyzer.setFile('/unrelated', `export class Unrelated { n = 2; }`);
+ const module2 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module1 === module2);
+ // Change transitive dependency and we expect a different model
+ analyzer.setFile('/dep2', `export class Baz { baz = 2; }`);
+ const module3 = analyzer.getModule(
+ getSourceFilename('/module', lang) as AbsolutePath
+ );
+ assert.ok(module2 !== module3);
+ }
+ );
+
+ cachingTest.run();
+}
diff --git a/packages/labs/analyzer/src/test/lit-element/events_test.ts b/packages/labs/analyzer/src/test/lit-element/events_test.ts
index 4f2139e387..5177b146a6 100644
--- a/packages/labs/analyzer/src/test/lit-element/events_test.ts
+++ b/packages/labs/analyzer/src/test/lit-element/events_test.ts
@@ -7,137 +7,156 @@
import {suite} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
-import * as path from 'path';
import {fileURLToPath} from 'url';
-
-import {Analyzer, AbsolutePath, LitElementDeclaration} from '../../index.js';
-
-const test = suite<{
- analyzer: Analyzer;
- packagePath: AbsolutePath;
- element: LitElementDeclaration;
-}>('LitElement event tests');
-
-test.before((ctx) => {
- try {
- const packagePath = fileURLToPath(
- new URL('../../test-files/events', import.meta.url).href
- ) as AbsolutePath;
- const analyzer = new Analyzer(packagePath);
-
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/element-a.ts')
+import {getSourceFilename, languages} from '../utils.js';
+
+import {
+ createPackageAnalyzer,
+ Analyzer,
+ AbsolutePath,
+ LitElementDeclaration,
+} from '../../index.js';
+
+for (const lang of languages) {
+ const test = suite<{
+ analyzer: Analyzer;
+ packagePath: AbsolutePath;
+ element: LitElementDeclaration;
+ }>(`LitElement event tests (${lang})`);
+
+ test.before((ctx) => {
+ try {
+ const packagePath = fileURLToPath(
+ new URL(`../../test-files/${lang}/events`, import.meta.url).href
+ ) as AbsolutePath;
+ const analyzer = createPackageAnalyzer(packagePath);
+
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-a', lang)
+ );
+ const element = elementAModule!.declarations.filter((d) =>
+ d.isLitElementDeclaration()
+ )[0] as LitElementDeclaration;
+
+ ctx.packagePath = packagePath;
+ ctx.analyzer = analyzer;
+ ctx.element = element;
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ test('Correct number of events found', ({element}) => {
+ assert.equal(element.events.size, 10);
+ });
+
+ test('Just event name', ({element}) => {
+ const event = element.events.get('event');
+ assert.ok(event);
+ assert.equal(event.name, 'event');
+ assert.equal(event.description, undefined);
+ });
+
+ test('Event with description', ({element}) => {
+ const event = element.events.get('event-two');
+ assert.ok(event);
+ assert.equal(event.name, 'event-two');
+ assert.equal(event.description, 'This is an event');
+ });
+
+ test('Event with dash-separated description', ({element}) => {
+ const event = element.events.get('event-three');
+ assert.ok(event);
+ assert.equal(event.name, 'event-three');
+ assert.equal(event.description, 'This is another event');
+ });
+
+ test('Event with type', ({element}) => {
+ const event = element.events.get('typed-event');
+ assert.ok(event);
+ assert.equal(event.name, 'typed-event');
+ assert.equal(event.type?.text, 'MouseEvent');
+ assert.equal(event.description, undefined);
+ assert.equal(event.type?.references[0].name, 'MouseEvent');
+ assert.equal(event.type?.references[0].isGlobal, true);
+ });
+
+ test('Event with type and description', ({element}) => {
+ const event = element.events.get('typed-event-two');
+ assert.ok(event);
+ assert.equal(event.name, 'typed-event-two');
+ assert.equal(event.type?.text, 'MouseEvent');
+ assert.equal(event.description, 'This is a typed event');
+ });
+
+ test('Event with type and dash-separated description', ({element}) => {
+ const event = element.events.get('typed-event-three');
+ assert.ok(event);
+ assert.equal(event.name, 'typed-event-three');
+ assert.equal(event.type?.text, 'MouseEvent');
+ assert.equal(event.description, 'This is another typed event');
+ });
+
+ test('Event with local custom event type', ({element}) => {
+ const event = element.events.get('local-custom-event');
+ assert.ok(event);
+ assert.equal(event.type?.text, 'LocalCustomEvent');
+ assert.equal(
+ event.type?.references[0].package,
+ '@lit-internal/test-events'
+ );
+ assert.equal(event.type?.references[0].module, 'element-a.js');
+ assert.equal(event.type?.references[0].name, 'LocalCustomEvent');
+ });
+
+ test('Event with imported custom event type', ({element}) => {
+ const event = element.events.get('external-custom-event');
+ assert.ok(event);
+ assert.equal(event.type?.text, 'ExternalCustomEvent');
+ assert.equal(
+ event.type?.references[0].package,
+ '@lit-internal/test-events'
);
- const element = elementAModule!.declarations.filter((d) =>
- d.isLitElementDeclaration()
- )[0] as LitElementDeclaration;
-
- ctx.packagePath = packagePath;
- ctx.analyzer = analyzer;
- ctx.element = element;
- } catch (error) {
- // Uvu has a bug where it silently ignores failures in before and after,
- // see https://github.com/lukeed/uvu/issues/191.
- console.error('uvu before error', error);
- process.exit(1);
- }
-});
-
-test('Correct number of events found', ({element}) => {
- assert.equal(element.events.size, 10);
-});
-
-test('Just event name', ({element}) => {
- const event = element.events.get('event');
- assert.ok(event);
- assert.equal(event.name, 'event');
- assert.equal(event.description, undefined);
-});
-
-test('Event with description', ({element}) => {
- const event = element.events.get('event-two');
- assert.ok(event);
- assert.equal(event.name, 'event-two');
- assert.equal(event.description, 'This is an event');
-});
-
-test('Event with dash-separated description', ({element}) => {
- const event = element.events.get('event-three');
- assert.ok(event);
- assert.equal(event.name, 'event-three');
- assert.equal(event.description, 'This is another event');
-});
-
-test('Event with type', ({element}) => {
- const event = element.events.get('typed-event');
- assert.ok(event);
- assert.equal(event.name, 'typed-event');
- assert.equal(event.type?.text, 'MouseEvent');
- assert.equal(event.description, undefined);
- assert.equal(event.type?.references[0].name, 'MouseEvent');
- assert.equal(event.type?.references[0].isGlobal, true);
-});
-
-test('Event with type and description', ({element}) => {
- const event = element.events.get('typed-event-two');
- assert.ok(event);
- assert.equal(event.name, 'typed-event-two');
- assert.equal(event.type?.text, 'MouseEvent');
- assert.equal(event.description, 'This is a typed event');
-});
-
-test('Event with type and dash-separated description', ({element}) => {
- const event = element.events.get('typed-event-three');
- assert.ok(event);
- assert.equal(event.name, 'typed-event-three');
- assert.equal(event.type?.text, 'MouseEvent');
- assert.equal(event.description, 'This is another typed event');
-});
-
-test('Event with local custom event type', ({element}) => {
- const event = element.events.get('local-custom-event');
- assert.ok(event);
- assert.equal(event.type?.text, 'LocalCustomEvent');
- assert.equal(event.type?.references[0].package, '@lit-internal/test-events');
- assert.equal(event.type?.references[0].module, 'element-a.js');
- assert.equal(event.type?.references[0].name, 'LocalCustomEvent');
-});
-
-test('Event with imported custom event type', ({element}) => {
- const event = element.events.get('external-custom-event');
- assert.ok(event);
- assert.equal(event.type?.text, 'ExternalCustomEvent');
- assert.equal(event.type?.references[0].package, '@lit-internal/test-events');
- assert.equal(event.type?.references[0].module, 'custom-event.js');
- assert.equal(event.type?.references[0].name, 'ExternalCustomEvent');
-});
-
-test('Event with generic custom event type', ({element}) => {
- const event = element.events.get('generic-custom-event');
- assert.ok(event);
- assert.equal(event.type?.text, 'CustomEvent');
- assert.equal(event.type?.references[0].name, 'CustomEvent');
- assert.equal(event.type?.references[0].isGlobal, true);
- assert.equal(event.type?.references[1].package, '@lit-internal/test-events');
- assert.equal(event.type?.references[1].module, 'custom-event.js');
- assert.equal(event.type?.references[1].name, 'ExternalClass');
-});
-
-test('Event with custom event type with inline detail', ({element}) => {
- const event = element.events.get('inline-detail-custom-event');
- assert.ok(event);
- assert.equal(
- event.type?.text,
- 'CustomEvent<{ event: MouseEvent; more: { impl: ExternalClass; }; }>'
- );
- assert.equal(event.type?.references[0].name, 'CustomEvent');
- assert.equal(event.type?.references[0].isGlobal, true);
- assert.equal(event.type?.references[1].name, 'MouseEvent');
- assert.equal(event.type?.references[1].isGlobal, true);
- assert.equal(event.type?.references[2].package, '@lit-internal/test-events');
- assert.equal(event.type?.references[2].module, 'custom-event.js');
- assert.equal(event.type?.references[2].name, 'ExternalClass');
-});
-
-test.run();
+ assert.equal(event.type?.references[0].module, 'custom-event.js');
+ assert.equal(event.type?.references[0].name, 'ExternalCustomEvent');
+ });
+
+ test('Event with generic custom event type', ({element}) => {
+ const event = element.events.get('generic-custom-event');
+ assert.ok(event);
+ assert.equal(event.type?.text, 'CustomEvent');
+ assert.equal(event.type?.references[0].name, 'CustomEvent');
+ assert.equal(event.type?.references[0].isGlobal, true);
+ assert.equal(
+ event.type?.references[1].package,
+ '@lit-internal/test-events'
+ );
+ assert.equal(event.type?.references[1].module, 'custom-event.js');
+ assert.equal(event.type?.references[1].name, 'ExternalClass');
+ });
+
+ test('Event with custom event type with inline detail', ({element}) => {
+ const event = element.events.get('inline-detail-custom-event');
+ assert.ok(event);
+ assert.equal(
+ event.type?.text,
+ 'CustomEvent<{ event: MouseEvent; more: { impl: ExternalClass; }; }>'
+ );
+ assert.equal(event.type?.references[0].name, 'CustomEvent');
+ assert.equal(event.type?.references[0].isGlobal, true);
+ assert.equal(event.type?.references[1].name, 'MouseEvent');
+ assert.equal(event.type?.references[1].isGlobal, true);
+ assert.equal(
+ event.type?.references[2].package,
+ '@lit-internal/test-events'
+ );
+ assert.equal(event.type?.references[2].module, 'custom-event.js');
+ assert.equal(event.type?.references[2].name, 'ExternalClass');
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/lit-element/jsdoc_test.ts b/packages/labs/analyzer/src/test/lit-element/jsdoc_test.ts
new file mode 100644
index 0000000000..da5fcc9187
--- /dev/null
+++ b/packages/labs/analyzer/src/test/lit-element/jsdoc_test.ts
@@ -0,0 +1,306 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {suite} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+import {fileURLToPath} from 'url';
+import {getSourceFilename, languages} from '../utils.js';
+
+import {createPackageAnalyzer, Module, AbsolutePath} from '../../index.js';
+
+for (const lang of languages) {
+ const test = suite<{
+ getModule: (name: string) => Module;
+ }>(`LitElement event tests (${lang})`);
+
+ test.before((ctx) => {
+ try {
+ const packagePath = fileURLToPath(
+ new URL(`../../test-files/${lang}/jsdoc`, import.meta.url).href
+ ) as AbsolutePath;
+ const analyzer = createPackageAnalyzer(packagePath);
+ ctx.getModule = (name: string) =>
+ analyzer.getModule(
+ getSourceFilename(
+ analyzer.path.join(packagePath, name),
+ lang
+ ) as AbsolutePath
+ );
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ // slots
+
+ test('slots - Correct number found', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(element.slots.size, 5);
+ });
+
+ test('slots - basic', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const slot = element.slots.get('basic');
+ assert.ok(slot);
+ assert.equal(slot.summary, undefined);
+ assert.equal(slot.description, undefined);
+ });
+
+ test('slots - with-summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const slot = element.slots.get('with-summary');
+ assert.ok(slot);
+ assert.equal(slot.summary, 'Summary for with-summary');
+ assert.equal(slot.description, undefined);
+ });
+
+ test('slots - with-summary-dash', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const slot = element.slots.get('with-summary-dash');
+ assert.ok(slot);
+ assert.equal(slot.summary, 'Summary for with-summary-dash');
+ assert.equal(slot.description, undefined);
+ });
+
+ test('slots - with-summary-colon', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const slot = element.slots.get('with-summary-colon');
+ assert.ok(slot);
+ assert.equal(slot.summary, 'Summary for with-summary-colon');
+ assert.equal(slot.description, undefined);
+ });
+
+ test('slots - with-description', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const slot = element.slots.get('with-description');
+ assert.ok(slot);
+ assert.equal(slot.summary, 'Summary for with-description');
+ assert.equal(
+ slot.description,
+ 'Description for with-description\nMore description for with-description\n\nEven more description for with-description'
+ );
+ });
+
+ // cssParts
+
+ test('cssParts - Correct number found', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(element.cssParts.size, 5);
+ });
+
+ test('cssParts - basic', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const part = element.cssParts.get('basic');
+ assert.ok(part);
+ assert.equal(part.summary, undefined);
+ assert.equal(part.description, undefined);
+ });
+
+ test('cssParts - with-summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const part = element.cssParts.get('with-summary');
+ assert.ok(part);
+ assert.equal(part.summary, 'Summary for :part(with-summary)');
+ assert.equal(part.description, undefined);
+ });
+
+ test('cssParts - with-summary-dash', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const part = element.cssParts.get('with-summary-dash');
+ assert.ok(part);
+ assert.equal(part.summary, 'Summary for :part(with-summary-dash)');
+ assert.equal(part.description, undefined);
+ });
+
+ test('cssParts - with-summary-colon', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const part = element.cssParts.get('with-summary-colon');
+ assert.ok(part);
+ assert.equal(part.summary, 'Summary for :part(with-summary-colon)');
+ assert.equal(part.description, undefined);
+ });
+
+ test('cssParts - with-description', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const part = element.cssParts.get('with-description');
+ assert.ok(part);
+ assert.equal(part.summary, 'Summary for :part(with-description)');
+ assert.equal(
+ part.description,
+ 'Description for :part(with-description)\nMore description for :part(with-description)\n\nEven more description for :part(with-description)'
+ );
+ });
+
+ // cssProperties
+
+ test('cssProperties - Correct number found', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(element.cssProperties.size, 10);
+ });
+
+ test('cssProperties - basic', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--basic');
+ assert.ok(prop);
+ assert.equal(prop.summary, undefined);
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - with-summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--with-summary');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --with-summary');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - with-summary-colon', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--with-summary-colon');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --with-summary-colon');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - with-summary-dash', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--with-summary-dash');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --with-summary-dash');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - with-description', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--with-description');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --with-description');
+ assert.equal(
+ prop.description,
+ 'Description for --with-description\nMore description for --with-description\n\nEven more description for --with-description'
+ );
+ });
+
+ test('cssProperties - short-basic', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--short-basic');
+ assert.ok(prop);
+ assert.equal(prop.summary, undefined);
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - short-with-summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--short-with-summary');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --short-with-summary');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - short-with-summary-colon', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--short-with-summary-colon');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --short-with-summary-colon');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - short-with-summary-dash', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--short-with-summary-dash');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --short-with-summary-dash');
+ assert.equal(prop.description, undefined);
+ });
+
+ test('cssProperties - short-with-description', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('ElementA');
+ assert.ok(element.isLitElementDeclaration());
+ const prop = element.cssProperties.get('--short-with-description');
+ assert.ok(prop);
+ assert.equal(prop.summary, 'Summary for --short-with-description');
+ assert.equal(
+ prop.description,
+ 'Description for --short-with-description\nMore description for --short-with-description\n\nEven more description for --short-with-description'
+ );
+ });
+
+ // description, summary, deprecated
+
+ test('tagged description and summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration('TaggedDescription');
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(
+ element.description,
+ `TaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat.`
+ );
+ assert.equal(element.summary, `TaggedDescription summary.`);
+ assert.equal(element.deprecated, `TaggedDescription deprecated message.`);
+ });
+
+ test('untagged description', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration(
+ 'UntaggedDescription'
+ );
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(
+ element.description,
+ `UntaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat.`
+ );
+ assert.equal(element.summary, `UntaggedDescription summary.`);
+ assert.equal(element.deprecated, `UntaggedDescription deprecated message.`);
+ });
+
+ test('untagged description and summary', ({getModule}) => {
+ const element = getModule('element-a').getDeclaration(
+ 'UntaggedDescSummary'
+ );
+ assert.ok(element.isLitElementDeclaration());
+ assert.equal(
+ element.description,
+ `UntaggedDescSummary description. Lorem ipsum dolor sit amet, consectetur
+adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat.`
+ );
+ assert.equal(element.summary, `UntaggedDescSummary summary.`);
+ assert.equal(element.deprecated, true);
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/lit-element/lit-element_test.ts b/packages/labs/analyzer/src/test/lit-element/lit-element_test.ts
index e7fdc69c8b..cd670188f2 100644
--- a/packages/labs/analyzer/src/test/lit-element/lit-element_test.ts
+++ b/packages/labs/analyzer/src/test/lit-element/lit-element_test.ts
@@ -7,81 +7,152 @@
import {suite} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
-import * as path from 'path';
import {fileURLToPath} from 'url';
+import {getSourceFilename, languages} from '../utils.js';
-import {Analyzer, AbsolutePath, LitElementDeclaration} from '../../index.js';
-
-const test = suite<{analyzer: Analyzer; packagePath: AbsolutePath}>(
- 'LitElement tests'
-);
-
-test.before((ctx) => {
- try {
- const packagePath = (ctx.packagePath = fileURLToPath(
- new URL('../../test-files/basic-elements', import.meta.url).href
- ) as AbsolutePath);
- ctx.analyzer = new Analyzer(packagePath);
- } catch (error) {
- // Uvu has a bug where it silently ignores failures in before and after,
- // see https://github.com/lukeed/uvu/issues/191.
- console.error('uvu before error', error);
- process.exit(1);
- }
-});
-
-test('isLitElementDeclaration returns false for non-LitElement', ({
- analyzer,
-}) => {
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/not-lit.ts')
- );
- const decl = elementAModule!.declarations.find((d) => d.name === 'NotLit')!;
- assert.ok(decl);
- assert.equal(decl.isLitElementDeclaration(), false);
-});
-
-test('Analyzer finds LitElement declarations', ({analyzer}) => {
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/element-a.ts')
- );
- assert.equal(elementAModule?.declarations.length, 1);
- const decl = elementAModule!.declarations[0];
- assert.equal(decl.name, 'ElementA');
- assert.ok(decl.isLitElementDeclaration());
-
- // TODO (justinfagnani): test for customElements.define()
- assert.equal((decl as LitElementDeclaration).tagname, 'element-a');
-});
-
-test('Analyzer finds LitElement properties via decorators', ({analyzer}) => {
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/element-a.ts')
+import {createPackageAnalyzer, Analyzer, AbsolutePath} from '../../index.js';
+
+// Get actual constructor to test internal ability to assert the type
+// of a dereferenced Declaration
+import {ClassDeclaration, LitElementDeclaration} from '../../lib/model.js';
+
+for (const lang of languages) {
+ const test = suite<{analyzer: Analyzer; packagePath: AbsolutePath}>(
+ `LitElement tests (${lang})`
);
- const decl = elementAModule!.declarations[0] as LitElementDeclaration;
-
- // ElementA has `a` and `b` properties
- assert.equal(decl.reactiveProperties.size, 2);
-
- const aProp = decl.reactiveProperties.get('a');
- assert.ok(aProp);
- assert.equal(aProp.name, 'a', 'property name');
- assert.equal(aProp.attribute, 'a', 'attribute name');
- assert.equal(aProp.type.text, 'string');
- // TODO (justinfagnani) better assertion
- assert.ok(aProp.type);
- assert.equal(aProp.reflect, false);
-
- const bProp = decl.reactiveProperties.get('b');
- assert.ok(bProp);
- assert.equal(bProp.name, 'b');
- assert.equal(bProp.attribute, 'bbb');
- // This is inferred
- assert.equal(bProp.type.text, 'number');
- assert.equal(bProp.typeOption, 'Number');
-});
-
-test.run();
+
+ test.before((ctx) => {
+ try {
+ const packagePath = (ctx.packagePath = fileURLToPath(
+ new URL(`../../test-files/${lang}/basic-elements`, import.meta.url).href
+ ) as AbsolutePath);
+ ctx.analyzer = createPackageAnalyzer(packagePath);
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ test('isLitElementDeclaration returns false for non-LitElement', ({
+ analyzer,
+ }) => {
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('not-lit', lang)
+ );
+ const decl = elementAModule?.getDeclaration('NotLit');
+ assert.ok(decl);
+ assert.equal(decl.isLitElementDeclaration(), false);
+ });
+
+ test('Analyzer finds LitElement declarations', ({analyzer}) => {
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-a', lang)
+ );
+ assert.equal(elementAModule?.declarations.length, 1);
+ const decl = elementAModule!.declarations[0];
+ assert.equal(decl.name, 'ElementA');
+ assert.ok(decl.isLitElementDeclaration());
+ assert.equal(decl.tagname, 'element-a');
+ });
+
+ test('Analyzer finds LitElement properties ', ({analyzer}) => {
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-a', lang)
+ );
+ const decl = elementAModule?.getDeclaration('ElementA');
+ assert.ok(decl?.isLitElementDeclaration());
+
+ // ElementA has `a` and `b` properties
+ assert.equal(decl.reactiveProperties.size, 3);
+
+ const aProp = decl.reactiveProperties.get('a');
+ assert.ok(aProp);
+ assert.equal(aProp.name, 'a', 'property name');
+ assert.equal(aProp.attribute, 'a', 'attribute name');
+ assert.equal(aProp.type?.text, 'string');
+ // TODO (justinfagnani) better assertion
+ assert.ok(aProp.type);
+ assert.equal(aProp.reflect, false);
+
+ const bProp = decl.reactiveProperties.get('b');
+ assert.ok(bProp);
+ assert.equal(bProp.name, 'b');
+ assert.equal(bProp.attribute, 'bbb');
+ assert.equal(bProp.type?.text, 'number');
+ assert.equal(bProp.typeOption, 'Number');
+
+ const cProp = decl.reactiveProperties.get('c');
+ assert.ok(cProp);
+ assert.equal(cProp.name, 'c');
+ assert.equal(cProp.attribute, 'c');
+ assert.equal(cProp.type?.text, lang === 'ts' ? 'any' : undefined);
+ });
+
+ test('Analyzer finds LitElement properties from static getter', ({
+ analyzer,
+ }) => {
+ const result = analyzer.getPackage();
+ const elementBModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-b', lang)
+ );
+ const decl = elementBModule?.getDeclaration('ElementB');
+ assert.ok(decl?.isLitElementDeclaration());
+
+ // ElementB has `foo` and `bar` properties defined in a static properties getter
+ assert.equal(decl.reactiveProperties.size, 2);
+
+ const fooProp = decl.reactiveProperties.get('foo');
+ assert.ok(fooProp);
+ assert.equal(fooProp.name, 'foo', 'property name');
+ assert.equal(fooProp.attribute, 'foo', 'attribute name');
+ assert.equal(fooProp.type?.text, 'string');
+ assert.equal(fooProp.reflect, false);
+
+ const bProp = decl.reactiveProperties.get('bar');
+ assert.ok(bProp);
+ assert.equal(bProp.name, 'bar');
+ assert.equal(bProp.attribute, 'bar');
+
+ // This is inferred
+ assert.equal(bProp.type?.text, 'number');
+ assert.equal(bProp.typeOption, 'Number');
+ });
+
+ test('Analyezr finds subclass of LitElement', ({analyzer}) => {
+ const result = analyzer.getPackage();
+ const module = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-c', lang)
+ );
+ const elementC = module?.getDeclaration('ElementC');
+ assert.ok(elementC?.isLitElementDeclaration());
+
+ assert.equal(elementC.reactiveProperties.size, 1);
+ const bazProp = elementC.reactiveProperties.get('baz');
+ assert.ok(bazProp);
+
+ const elementBRef = elementC.heritage.superClass;
+ assert.ok(elementBRef);
+ const elementB = elementBRef.dereference(LitElementDeclaration);
+ assert.ok(elementB.isLitElementDeclaration());
+ assert.ok(elementB.name, 'ElementB');
+ assert.equal(elementB.reactiveProperties.size, 2);
+ const fooProp = elementB.reactiveProperties.get('foo');
+ assert.ok(fooProp);
+ const barProp = elementB.reactiveProperties.get('bar');
+ assert.ok(barProp);
+
+ const litElementRef = elementB.heritage.superClass;
+ assert.ok(litElementRef);
+ const litElement = litElementRef.dereference(ClassDeclaration);
+ assert.ok(litElement.isClassDeclaration());
+ assert.not.ok(litElement.isLitElementDeclaration());
+ assert.ok(litElement.name, 'LitElement');
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/lit-element/properties_test.ts b/packages/labs/analyzer/src/test/lit-element/properties_test.ts
index da14539f1f..29983512aa 100644
--- a/packages/labs/analyzer/src/test/lit-element/properties_test.ts
+++ b/packages/labs/analyzer/src/test/lit-element/properties_test.ts
@@ -7,193 +7,209 @@
import {suite} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
-import * as path from 'path';
import {fileURLToPath} from 'url';
-import ts from 'typescript';
-
-import {Analyzer, AbsolutePath, LitElementDeclaration} from '../../index.js';
-
-const test = suite<{
- analyzer: Analyzer;
- packagePath: AbsolutePath;
- element: LitElementDeclaration;
-}>('LitElement property tests');
-
-test.before((ctx) => {
- try {
- const packagePath = fileURLToPath(
- new URL('../../test-files/decorators-properties', import.meta.url).href
- ) as AbsolutePath;
- const analyzer = new Analyzer(packagePath);
-
- const result = analyzer.analyzePackage();
- const elementAModule = result.modules.find(
- (m) => m.sourcePath === path.normalize('src/element-a.ts')
+import {getSourceFilename, languages} from '../utils.js';
+
+import {
+ createPackageAnalyzer,
+ Analyzer,
+ AbsolutePath,
+ LitElementDeclaration,
+} from '../../index.js';
+
+for (const lang of languages) {
+ const test = suite<{
+ analyzer: Analyzer;
+ packagePath: AbsolutePath;
+ element: LitElementDeclaration;
+ }>(`LitElement property tests (${lang})`);
+
+ test.before((ctx) => {
+ try {
+ const packagePath = fileURLToPath(
+ new URL(`../../test-files/${lang}/properties`, import.meta.url).href
+ ) as AbsolutePath;
+ const analyzer = createPackageAnalyzer(packagePath);
+
+ const result = analyzer.getPackage();
+ const elementAModule = result.modules.find(
+ (m) => m.sourcePath === getSourceFilename('element-a', lang)
+ );
+ const element = elementAModule?.getDeclaration('ElementA');
+ assert.ok(element?.isLitElementDeclaration());
+
+ ctx.packagePath = packagePath;
+ ctx.analyzer = analyzer;
+ ctx.element = element;
+ } catch (error) {
+ // Uvu has a bug where it silently ignores failures in before and after,
+ // see https://github.com/lukeed/uvu/issues/191.
+ console.error('uvu before error', error);
+ process.exit(1);
+ }
+ });
+
+ test('non-decorated fields are not reactive', ({element}) => {
+ // TODO(justinfagnani): we might want to change the representation
+ // so we have a collection of all fields, some of which are reactive.
+ assert.equal(element.reactiveProperties.has('notDecorated'), false);
+ });
+
+ test('string property with no options', ({element}) => {
+ const property = element.reactiveProperties.get('noOptionsString');
+ assert.ok(property);
+ assert.equal(property.name, 'noOptionsString');
+ assert.equal(property.attribute, 'nooptionsstring');
+ assert.equal(property.type?.text, 'string');
+ assert.equal(property.type?.references.length, 0);
+ assert.ok(property.type);
+ assert.equal(property.reflect, false);
+ assert.equal(property.converter, undefined);
+ });
+
+ test('number property with no options', ({element}) => {
+ const property = element.reactiveProperties.get('noOptionsNumber')!;
+ assert.equal(property.name, 'noOptionsNumber');
+ assert.equal(property.attribute, 'nooptionsnumber');
+ assert.equal(property.type?.text, 'number');
+ assert.equal(property.type?.references.length, 0);
+ assert.ok(property.type);
+ });
+
+ test('string property with type', ({element}) => {
+ const property = element.reactiveProperties.get('typeString')!;
+ assert.equal(property.type?.text, 'string');
+ assert.equal(property.type?.references.length, 0);
+ assert.ok(property.type);
+ });
+
+ test('number property with type', ({element}) => {
+ const property = element.reactiveProperties.get('typeNumber')!;
+ assert.equal(property.type?.text, 'number');
+ assert.equal(property.type?.references.length, 0);
+ assert.ok(property.type);
+ });
+
+ test('boolean property with type', ({element}) => {
+ const property = element.reactiveProperties.get('typeBoolean')!;
+ assert.equal(property.type?.text, 'boolean');
+ assert.equal(property.type?.references.length, 0);
+ assert.ok(property.type);
+ });
+
+ test('property typed with local class', ({element}) => {
+ const property = element.reactiveProperties.get('localClass')!;
+ assert.equal(property.type?.text, 'LocalClass');
+ assert.equal(property.type?.references.length, 1);
+ assert.equal(property.type?.references[0].name, 'LocalClass');
+ assert.equal(
+ property.type?.references[0].package,
+ '@lit-internal/test-properties'
);
- const element = elementAModule!.declarations.filter((d) =>
- d.isLitElementDeclaration()
- )[0] as LitElementDeclaration;
-
- ctx.packagePath = packagePath;
- ctx.analyzer = analyzer;
- ctx.element = element;
- } catch (error) {
- // Uvu has a bug where it silently ignores failures in before and after,
- // see https://github.com/lukeed/uvu/issues/191.
- console.error('uvu before error', error);
- process.exit(1);
- }
-});
-
-test('non-decorated fields are not reactive', ({element}) => {
- // TODO(justinfagnani): we might want to change the representation
- // so we have a collection of all fields, some of which are reactive.
- assert.equal(element.reactiveProperties.has('notDecorated'), false);
-});
-
-test('string property with no options', ({element}) => {
- const property = element.reactiveProperties.get('noOptionsString');
- assert.ok(property);
- assert.equal(property.name, 'noOptionsString');
- assert.equal(property.attribute, 'nooptionsstring');
- assert.equal(property.type.text, 'string');
- assert.equal(property.type.references.length, 0);
- assert.ok(property.type);
- assert.equal(property.reflect, false);
- assert.equal(property.converter, undefined);
-});
-
-test('number property with no options', ({element}) => {
- const property = element.reactiveProperties.get('noOptionsNumber')!;
- assert.equal(property.name, 'noOptionsNumber');
- assert.equal(property.attribute, 'nooptionsnumber');
- assert.equal(property.type.text, 'number');
- assert.equal(property.type.references.length, 0);
- assert.ok(property.type);
-});
-
-test('string property with type', ({element}) => {
- const property = element.reactiveProperties.get('typeString')!;
- assert.equal(property.type.text, 'string');
- assert.equal(property.type.references.length, 0);
- assert.ok(property.type);
-});
-
-test('number property with type', ({element}) => {
- const property = element.reactiveProperties.get('typeNumber')!;
- assert.equal(property.type.text, 'number');
- assert.equal(property.type.references.length, 0);
- assert.ok(property.type);
-});
-
-test('boolean property with type', ({element}) => {
- const property = element.reactiveProperties.get('typeBoolean')!;
- assert.equal(property.type.text, 'boolean');
- assert.equal(property.type.references.length, 0);
- assert.ok(property.type);
-});
-
-test('property typed with local class', ({element}) => {
- const property = element.reactiveProperties.get('localClass')!;
- assert.equal(property.type.text, 'LocalClass');
- assert.equal(property.type.references.length, 1);
- assert.equal(property.type.references[0].name, 'LocalClass');
- assert.equal(
- property.type.references[0].package,
- '@lit-internal/test-decorators-properties'
- );
- assert.equal(property.type.references[0].module, 'element-a.js');
-});
-
-test('property typed with imported class', ({element}) => {
- const property = element.reactiveProperties.get('importedClass')!;
- assert.equal(property.type.text, 'ImportedClass');
- assert.equal(property.type.references.length, 1);
- assert.equal(property.type.references[0].name, 'ImportedClass');
- assert.equal(
- property.type.references[0].package,
- '@lit-internal/test-decorators-properties'
- );
- assert.equal(property.type.references[0].module, 'external.js');
-});
-
-test('property typed with global class', ({element}) => {
- const property = element.reactiveProperties.get('globalClass')!;
- assert.equal(property.type.text, 'HTMLElement');
- assert.equal(property.type.references.length, 1);
- assert.equal(property.type.references[0].name, 'HTMLElement');
- assert.equal(property.type.references[0].isGlobal, true);
-});
-
-test('property typed with union', ({element}) => {
- const property = element.reactiveProperties.get('union')!;
- ts.isUnionTypeNode(property.node);
- assert.equal(property.type.references.length, 3);
- // The order is not necessarily reliable. It changed between TypeScript
- // versions once.
-
- const localClass = property.type.references.find(
- (node) => node.name === 'LocalClass'
- );
- assert.ok(localClass);
- assert.equal(localClass.package, '@lit-internal/test-decorators-properties');
- assert.equal(localClass.module, 'element-a.js');
-
- const htmlElement = property.type.references.find(
- (node) => node.name === 'HTMLElement'
- );
- assert.ok(htmlElement);
- assert.equal(htmlElement.isGlobal, true);
-
- const importedClass = property.type.references.find(
- (node) => node.name === 'ImportedClass'
- );
- assert.ok(importedClass);
- assert.equal(
- importedClass.package,
- '@lit-internal/test-decorators-properties'
- );
- assert.equal(importedClass.module, 'external.js');
-});
-
-test('reflect: true', ({element}) => {
- const property = element.reactiveProperties.get('reflectTrue')!;
- assert.equal(property.reflect, true);
-});
-
-test('reflect: false', ({element}) => {
- const property = element.reactiveProperties.get('reflectFalse')!;
- assert.equal(property.reflect, false);
-});
-
-test('reflect: undefined', ({element}) => {
- const property = element.reactiveProperties.get('reflectUndefined')!;
- assert.equal(property.reflect, false);
-});
-
-test('attribute: true', ({element}) => {
- const property = element.reactiveProperties.get('attributeTrue')!;
- assert.equal(property.attribute, 'attributetrue');
-});
-
-test('attribute: false', ({element}) => {
- const property = element.reactiveProperties.get('attributeFalse')!;
- assert.equal(property.attribute, undefined);
-});
-
-test('attribute: undefined', ({element}) => {
- const property = element.reactiveProperties.get('attributeUndefined')!;
- assert.equal(property.attribute, 'attributeundefined');
-});
-
-test('attribute: string', ({element}) => {
- const property = element.reactiveProperties.get('attributeString')!;
- assert.equal(property.attribute, 'abc');
-});
-
-test('custom converter', ({element}) => {
- const property = element.reactiveProperties.get('customConverter')!;
- assert.ok(property.converter);
-});
-
-test.run();
+ assert.equal(property.type?.references[0].module, 'element-a.js');
+ });
+
+ test('property typed with imported class', ({element}) => {
+ const property = element.reactiveProperties.get('importedClass')!;
+ assert.equal(property.type?.text, 'ImportedClass');
+ assert.equal(property.type?.references.length, 1);
+ assert.equal(property.type?.references[0].name, 'ImportedClass');
+ assert.equal(
+ property.type?.references[0].package,
+ '@lit-internal/test-properties'
+ );
+ assert.equal(property.type?.references[0].module, 'external.js');
+ });
+
+ test('property typed with global class', ({element}) => {
+ const property = element.reactiveProperties.get('globalClass')!;
+ assert.equal(property.type?.text, 'HTMLElement');
+ assert.equal(property.type?.references.length, 1);
+ assert.equal(property.type?.references[0].name, 'HTMLElement');
+ assert.equal(property.type?.references[0].isGlobal, true);
+ });
+
+ test('property typed with union', ({element}) => {
+ // TODO(kschaaf): TS seems to have some support for inferring union types
+ // from JS initializers, but if there are n possible types (e.g. `this.foo =
+ // new A() || new B() || new C()`), it seems to only generate a union type
+ // with n-1 types in it (e.g. A | B). For now let's just skip it.
+ if (lang === 'js') {
+ return;
+ }
+ const property = element.reactiveProperties.get('union')!;
+ assert.equal(property.type?.references.length, 3);
+ // The order is not necessarily reliable. It changed between TypeScript
+ // versions once.
+
+ const localClass = property.type?.references.find(
+ (node) => node.name === 'LocalClass'
+ );
+ assert.ok(localClass);
+ assert.equal(localClass.package, '@lit-internal/test-properties');
+ assert.equal(localClass.module, 'element-a.js');
+
+ const htmlElement = property.type?.references.find(
+ (node) => node.name === 'HTMLElement'
+ );
+ assert.ok(htmlElement);
+ assert.equal(htmlElement.isGlobal, true);
+
+ const importedClass = property.type?.references.find(
+ (node) => node.name === 'ImportedClass'
+ );
+ assert.ok(importedClass);
+ assert.equal(importedClass.package, '@lit-internal/test-properties');
+ assert.equal(importedClass.module, 'external.js');
+ });
+
+ test('reflect: true', ({element}) => {
+ const property = element.reactiveProperties.get('reflectTrue')!;
+ assert.equal(property.reflect, true);
+ });
+
+ test('reflect: false', ({element}) => {
+ const property = element.reactiveProperties.get('reflectFalse')!;
+ assert.equal(property.reflect, false);
+ });
+
+ test('reflect: undefined', ({element}) => {
+ const property = element.reactiveProperties.get('reflectUndefined')!;
+ assert.equal(property.reflect, false);
+ });
+
+ test('attribute: true', ({element}) => {
+ const property = element.reactiveProperties.get('attributeTrue')!;
+ assert.equal(property.attribute, 'attributetrue');
+ });
+
+ test('attribute: false', ({element}) => {
+ const property = element.reactiveProperties.get('attributeFalse')!;
+ assert.equal(property.attribute, undefined);
+ });
+
+ test('attribute: undefined', ({element}) => {
+ const property = element.reactiveProperties.get('attributeUndefined')!;
+ assert.equal(property.attribute, 'attributeundefined');
+ });
+
+ test('attribute: string', ({element}) => {
+ const property = element.reactiveProperties.get('attributeString')!;
+ assert.equal(property.attribute, 'abc');
+ });
+
+ test('custom converter', ({element}) => {
+ const property = element.reactiveProperties.get('customConverter')!;
+ assert.ok(property.converter);
+ });
+
+ test('property defined in static properties block', ({element}) => {
+ const property = element.reactiveProperties.get('staticProp')!;
+ assert.equal(property.type?.text, 'number');
+ assert.equal(property.type?.references.length, 0);
+ assert.equal(property.typeOption, 'Number');
+ assert.equal(property.attribute, 'static-prop');
+ });
+
+ test.run();
+}
diff --git a/packages/labs/analyzer/src/test/types_test.ts b/packages/labs/analyzer/src/test/types_test.ts
index 7b25656003..53d3e5388d 100644
--- a/packages/labs/analyzer/src/test/types_test.ts
+++ b/packages/labs/analyzer/src/test/types_test.ts
@@ -4,17 +4,15 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import 'source-map-support/register.js';
import {suite} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
import {fileURLToPath} from 'url';
import {
- Analyzer,
+ createPackageAnalyzer,
AbsolutePath,
Module,
- VariableDeclaration,
getImportsStringForReferences,
} from '../index.js';
@@ -25,10 +23,10 @@ const test = suite<{module: Module; packagePath: AbsolutePath}>('Types tests');
test.before((ctx) => {
try {
const packagePath = (ctx.packagePath = fileURLToPath(
- new URL('../test-files/types', import.meta.url).href
+ new URL('../test-files/ts/types', import.meta.url).href
) as AbsolutePath);
- const analyzer = new Analyzer(packagePath);
- const pkg = analyzer.analyzePackage();
+ const analyzer = createPackageAnalyzer(packagePath);
+ const pkg = analyzer.getPackage();
ctx.module = pkg.modules.filter((m) => m.jsPath === 'module.js')[0];
} catch (e) {
// Uvu has a bug where it silently ignores failures in before and after,
@@ -39,9 +37,10 @@ test.before((ctx) => {
});
const typeForVariable = (module: Module, name: string) => {
- const dec = module.declarations.filter((dec) => dec.name === name)[0];
+ const dec = module.getDeclaration(name);
+ assert.ok(dec.isVariableDeclaration());
assert.ok(dec, `Could not find symbol named ${name}`);
- const type = (dec as VariableDeclaration).type;
+ const type = dec.type;
assert.ok(type);
return type;
};
@@ -214,7 +213,7 @@ test('complexType', ({module}) => {
assert.equal(type.references[1].isGlobal, true);
assert.equal(type.references[2].name, 'LitElement');
assert.equal(type.references[2].package, 'lit');
- assert.equal(type.references[2].module, '');
+ assert.equal(type.references[2].module, undefined);
assert.equal(type.references[2].isGlobal, false);
assert.equal(type.references[3].name, 'ImportedClass');
assert.equal(type.references[3].package, '@lit-internal/test-types');
@@ -238,7 +237,7 @@ test('destructObjNested', ({module}) => {
assert.equal(type.references.length, 1);
assert.equal(type.references[0].name, 'LitElement');
assert.equal(type.references[0].package, 'lit');
- assert.equal(type.references[0].module, '');
+ assert.equal(type.references[0].module, undefined);
assert.equal(type.references[0].isGlobal, false);
});
@@ -268,7 +267,7 @@ test('separatelyExportedDestructObjNested', ({module}) => {
assert.equal(type.references.length, 1);
assert.equal(type.references[0].name, 'LitElement');
assert.equal(type.references[0].package, 'lit');
- assert.equal(type.references[0].module, '');
+ assert.equal(type.references[0].module, undefined);
assert.equal(type.references[0].isGlobal, false);
});
@@ -288,10 +287,24 @@ test('separatelyExportedDestructArrNested', ({module}) => {
assert.equal(type.references.length, 1);
assert.equal(type.references[0].name, 'LitElement');
assert.equal(type.references[0].package, 'lit');
- assert.equal(type.references[0].module, '');
+ assert.equal(type.references[0].module, undefined);
assert.equal(type.references[0].isGlobal, false);
});
+test('importedType', ({module}) => {
+ const type = typeForVariable(module, 'importedType');
+ //assert.equal(type.text, 'TemplateResult<1>');
+ assert.equal(type.references.length, 2);
+ assert.equal(type.references[0].name, 'ImportedClass');
+ assert.equal(type.references[0].package, '@lit-internal/test-types');
+ assert.equal(type.references[0].module, 'external.js');
+ assert.equal(type.references[0].isGlobal, false);
+ assert.equal(type.references[1].name, 'TemplateResult');
+ assert.equal(type.references[1].package, 'lit-html');
+ assert.equal(type.references[1].module, undefined);
+ assert.equal(type.references[1].isGlobal, false);
+});
+
test('getImportsStringForReferences', ({module}) => {
const type = typeForVariable(module, 'complexType');
assert.equal(
diff --git a/packages/labs/analyzer/src/test/utils.ts b/packages/labs/analyzer/src/test/utils.ts
new file mode 100644
index 0000000000..18fbeb0742
--- /dev/null
+++ b/packages/labs/analyzer/src/test/utils.ts
@@ -0,0 +1,202 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import * as path from 'path';
+import * as fs from 'fs';
+import ts from 'typescript';
+import {AbsolutePath, Analyzer} from '../index.js';
+
+type Language = 'ts' | 'js';
+
+export const languages: Language[] = ['ts', 'js'];
+
+// Note these functions assume a specific setup in tsconfig.json for tests of TS
+// projects using these helpers, namely that the `rootDir` is 'src' and the
+// `outDir` is 'out'
+
+export const getSourceFilename = (f: string, lang: Language) =>
+ path.normalize(
+ lang === 'ts'
+ ? path.join(path.dirname(f), 'src', path.basename(f) + '.ts')
+ : f + '.js'
+ );
+
+export const getOutputFilename = (f: string, lang: Language) =>
+ path.normalize(
+ lang === 'ts'
+ ? path.join(path.dirname(f), 'out', path.basename(f) + '.js')
+ : path.normalize(f + '.js')
+ );
+
+// The following code implements an InMemoryAnalyzer that uses a language
+// service host backed by an updatable in-memory file cache, to allow for easy
+// program invalidation in tests.
+
+// Note that because some paths come into the language host filesystem
+// abstractions in posix format and others may come in in OS-native
+// format, we normalize all paths going in/out of the in-memory cache to
+// posix format. There is apparently no path lib method to do this.
+// https://stackoverflow.com/questions/53799385/how-can-i-convert-a-windows-path-to-posix-path-using-node-path
+const normalize = (p: string) => p.split(path.sep).join(path.posix.sep);
+
+/**
+ * Map of filenames -> content
+ */
+export interface Files {
+ [index: string]: string;
+}
+
+/**
+ * Map of filenames -> versioned content
+ */
+interface Cache {
+ [index: string]: {content: string; version: number};
+}
+
+/**
+ * Common compiler options between TS & JS
+ */
+const compilerOptions: ts.CompilerOptions = {
+ target: ts.ScriptTarget.ES2020,
+ lib: ['es2020', 'DOM'],
+ module: ts.ModuleKind.ES2020,
+ outDir: './',
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
+ experimentalDecorators: true,
+ skipDefaultLibCheck: true,
+ skipLibCheck: true,
+ noEmit: true,
+};
+
+/**
+ * JS-specific config
+ */
+const tsconfigJS = {
+ ...compilerOptions,
+ allowJs: true,
+};
+
+/**
+ * TS-specific config
+ */
+const tsconfigTS = {
+ ...compilerOptions,
+ rootDir: '/src',
+ outDir: '/',
+ configFilePath: '/',
+};
+
+/**
+ * Returns true if the file is a standard TS library. These are the only
+ * files we read off of disk; everything else comes from the in-memory cache.
+ */
+const isLib = (fileName: string) =>
+ normalize(fileName).includes('node_modules/typescript/lib');
+
+/**
+ * Simulates "reading" a directory from the in-memory cache. The cache stores a
+ * flat list of filenames as keys, so this filters the list using a regex that
+ * matches the given path, and returns a list of path segments immediately
+ * following the directory (deduped using a Set, since if there are multiple
+ * files in a subdirectory under this directory, the subdirectory name would
+ * show up multiple times)
+ */
+const readDirectory = (cache: Cache, dir: AbsolutePath) => {
+ const sep = dir.endsWith('/') ? '' : '\\/';
+ const matcher = new RegExp(`^${dir}${sep}([^/]+)`);
+ return Array.from(
+ new Set(
+ Object.keys(cache)
+ .map((f) => f.match(matcher)?.[1])
+ .filter((f) => !!f) as string[]
+ )
+ );
+};
+
+/**
+ * Creates a ts.LanguageServiceHost that reads from an in-memory cache for all
+ * files except TS standard libs, which are read off of disk.
+ */
+const createHost = (cache: Cache, lang: Language) => {
+ return {
+ getScriptFileNames: () =>
+ Object.keys(cache).filter((s) => s.endsWith('.ts') || s.endsWith('.js')),
+ getScriptVersion: (fileName: string) =>
+ cache[normalize(fileName)].version.toString(),
+ getScriptSnapshot: (fileName: string) => {
+ if (isLib(fileName)) {
+ return fs.existsSync(fileName)
+ ? ts.ScriptSnapshot.fromString(
+ fs.readFileSync(path.normalize(fileName), 'utf-8')
+ )
+ : undefined;
+ } else {
+ return fileName in cache
+ ? ts.ScriptSnapshot.fromString(cache[normalize(fileName)].content)
+ : undefined;
+ }
+ },
+ getCurrentDirectory: () => '/',
+ getCompilationSettings: () => (lang === 'ts' ? tsconfigTS : tsconfigJS),
+ getDefaultLibFileName: (options: ts.CompilerOptions) =>
+ ts.getDefaultLibFilePath(options),
+ fileExists: (fileName: string) =>
+ isLib(fileName)
+ ? ts.sys.fileExists(path.normalize(fileName))
+ : normalize(fileName) in cache,
+ readFile: (fileName: string) => cache[normalize(fileName)].content,
+ readDirectory: (fileName: string) =>
+ readDirectory(cache, normalize(fileName) as AbsolutePath),
+ directoryExists: (dir: string) =>
+ readDirectory(cache, normalize(dir) as AbsolutePath).length > 0,
+ getDirectories: () => [],
+ };
+};
+
+/**
+ * An Analyzer that analyzes an in-memory set of files passed into the
+ * constructor, which may be updated after the fact using the `setFile()` API.
+ */
+export class InMemoryAnalyzer extends Analyzer {
+ private _cache: Cache;
+ private _dirty = false;
+ private _lang: Language;
+
+ constructor(lang: Language, files: Files = {}) {
+ const cache: Cache = Object.fromEntries(
+ Object.entries(files).map(([name, content]) => [
+ normalize(name),
+ {content, version: 0},
+ ])
+ );
+ const host = createHost(cache, lang);
+ const service = ts.createLanguageService(host, ts.createDocumentRegistry());
+ let program: ts.Program;
+ super({
+ getProgram: () => {
+ if (program === undefined || this._dirty) {
+ this._dirty = false;
+ program = service.getProgram()!;
+ }
+ return program;
+ },
+ fs: {
+ ...host,
+ useCaseSensitiveFileNames: false,
+ },
+ path,
+ });
+ this._cache = cache;
+ this._lang = lang;
+ }
+
+ setFile(name: string, content: string) {
+ const fileName = normalize(getSourceFilename(name, this._lang));
+ const prev = this._cache[fileName] ?? {version: -1};
+ this._cache[fileName] = {content, version: prev.version + 1};
+ this._dirty = true;
+ }
+}
diff --git a/packages/labs/analyzer/test-files/decorators-properties/package.json b/packages/labs/analyzer/test-files/decorators-properties/package.json
deleted file mode 100644
index 2f3cae1fb2..0000000000
--- a/packages/labs/analyzer/test-files/decorators-properties/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "@lit-internal/test-decorators-properties",
- "dependencies": {
- "lit": "^2.0.0"
- }
-}
diff --git a/packages/labs/analyzer/test-files/basic-elements/src/class-a.ts b/packages/labs/analyzer/test-files/js/basic-elements/class-a.js
similarity index 100%
rename from packages/labs/analyzer/test-files/basic-elements/src/class-a.ts
rename to packages/labs/analyzer/test-files/js/basic-elements/class-a.js
diff --git a/packages/labs/analyzer/test-files/basic-elements/src/default-element.ts b/packages/labs/analyzer/test-files/js/basic-elements/default-element.js
similarity index 100%
rename from packages/labs/analyzer/test-files/basic-elements/src/default-element.ts
rename to packages/labs/analyzer/test-files/js/basic-elements/default-element.js
diff --git a/packages/labs/analyzer/test-files/js/basic-elements/element-a.js b/packages/labs/analyzer/test-files/js/basic-elements/element-a.js
new file mode 100644
index 0000000000..7cb4368702
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/basic-elements/element-a.js
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement, html, css} from 'lit';
+
+export class ElementA extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ static properties = {
+ a: {},
+ b: {reflect: true, type: Number, attribute: 'bbb'},
+ c: {},
+ };
+
+ static c = 'should not be inferred as type for c';
+
+ constructor() {
+ super();
+ this.a = '';
+ this.b = 1;
+ }
+
+ render() {
+ return html`${this.a}
`;
+ }
+}
+customElements.define('element-a', ElementA);
diff --git a/packages/labs/analyzer/test-files/basic-elements/src/element-b.ts b/packages/labs/analyzer/test-files/js/basic-elements/element-b.js
similarity index 64%
rename from packages/labs/analyzer/test-files/basic-elements/src/element-b.ts
rename to packages/labs/analyzer/test-files/js/basic-elements/element-b.js
index 51f696037d..b93102d27c 100644
--- a/packages/labs/analyzer/test-files/basic-elements/src/element-b.ts
+++ b/packages/labs/analyzer/test-files/js/basic-elements/element-b.js
@@ -5,9 +5,7 @@
*/
import {LitElement, html, css} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-@customElement('element-b')
export class ElementB extends LitElement {
static styles = css`
:host {
@@ -15,8 +13,18 @@ export class ElementB extends LitElement {
}
`;
- @property()
- foo?: string;
+ static get properties() {
+ return {
+ foo: {},
+ bar: {type: Number},
+ };
+ }
+
+ constructor() {
+ super();
+ this.foo = 'hi';
+ this.bar = 42;
+ }
render() {
return html`${this.foo}
`;
diff --git a/packages/labs/analyzer/test-files/js/basic-elements/element-c.js b/packages/labs/analyzer/test-files/js/basic-elements/element-c.js
new file mode 100644
index 0000000000..7493c7ce43
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/basic-elements/element-c.js
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {html} from 'lit';
+import {ElementB} from './element-b.js';
+
+export class ElementC extends ElementB {
+ static properties = {
+ baz: {},
+ };
+
+ constructor() {
+ super();
+ this.baz = 'baz';
+ }
+
+ render() {
+ return html`${super.render()} ${this.baz}
`;
+ }
+}
+customElements.define('element-c', ElementC);
diff --git a/packages/labs/analyzer/test-files/basic-elements/src/not-lit.ts b/packages/labs/analyzer/test-files/js/basic-elements/not-lit.js
similarity index 100%
rename from packages/labs/analyzer/test-files/basic-elements/src/not-lit.ts
rename to packages/labs/analyzer/test-files/js/basic-elements/not-lit.js
diff --git a/packages/labs/analyzer/test-files/js/basic-elements/package.json b/packages/labs/analyzer/test-files/js/basic-elements/package.json
new file mode 100644
index 0000000000..7af9f4969a
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/basic-elements/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-basic-elements-js",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/js/events/custom-event.js b/packages/labs/analyzer/test-files/js/events/custom-event.js
new file mode 100644
index 0000000000..408a175904
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/events/custom-event.js
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export class ExternalCustomEvent extends Event {
+ constructor(message) {
+ super('external-custom-event');
+ this.message = message;
+ }
+}
+
+export class ExternalClass {
+ constructor() {
+ this.someData = 42;
+ }
+}
diff --git a/packages/labs/analyzer/test-files/js/events/element-a.js b/packages/labs/analyzer/test-files/js/events/element-a.js
new file mode 100644
index 0000000000..7000ae17b3
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/events/element-a.js
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement} from 'lit';
+import {ExternalCustomEvent, ExternalClass} from './custom-event.js';
+
+export class LocalCustomEvent extends Event {
+ constructor(message) {
+ super('local-custom-event');
+ this.message = message;
+ }
+}
+/**
+ * A cool custom element.
+ *
+ * @fires event
+ * @fires event-two This is an event
+ * @fires event-three - This is another event
+ * @fires typed-event {MouseEvent}
+ * @fires typed-event-two {MouseEvent} This is a typed event
+ * @fires typed-event-three {MouseEvent} - This is another typed event
+ * @fires external-custom-event {ExternalCustomEvent} - External custom event
+ * @fires local-custom-event {LocalCustomEvent} - Local custom event
+ * @fires generic-custom-event {CustomEvent} - Local custom event
+ * @fires inline-detail-custom-event {CustomEvent<{ event: MouseEvent; more: { impl: ExternalClass; }; }>}
+ *
+ * @comment malformed fires tag:
+ *
+ * @fires
+ */
+export class ElementA extends LitElement {
+ fireExternalEvent() {
+ this.dispatchEvent(new ExternalCustomEvent('external'));
+ }
+ fireLocalEvent() {
+ this.dispatchEvent(new LocalCustomEvent('local'));
+ }
+ fireGenericEvent() {
+ this.dispatchEvent(
+ new CustomEvent() <
+ ExternalClass >
+ ('generic-custom-event',
+ {
+ detail: new ExternalClass(),
+ })
+ );
+ }
+}
+customElements.define('element-a', ElementA);
diff --git a/packages/labs/analyzer/test-files/events/package.json b/packages/labs/analyzer/test-files/js/events/package.json
similarity index 100%
rename from packages/labs/analyzer/test-files/events/package.json
rename to packages/labs/analyzer/test-files/js/events/package.json
diff --git a/packages/labs/analyzer/test-files/js/jsdoc/element-a.js b/packages/labs/analyzer/test-files/js/jsdoc/element-a.js
new file mode 100644
index 0000000000..20e64dcd08
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/jsdoc/element-a.js
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement} from 'lit';
+
+/**
+ * A cool custom element.
+ *
+ * @slot basic
+ * @slot with-summary Summary for with-summary
+ * @slot with-summary-dash - Summary for with-summary-dash
+ * @slot with-summary-colon: Summary for with-summary-colon
+ * @slot with-description - Summary for with-description
+ * Description for with-description
+ * More description for with-description
+ *
+ * Even more description for with-description
+ *
+ * @cssPart basic
+ * @cssPart with-summary Summary for :part(with-summary)
+ * @cssPart with-summary-dash - Summary for :part(with-summary-dash)
+ * @cssPart with-summary-colon: Summary for :part(with-summary-colon)
+ * @cssPart with-description - Summary for :part(with-description)
+ * Description for :part(with-description)
+ * More description for :part(with-description)
+ *
+ * Even more description for :part(with-description)
+ *
+ * @cssProperty --basic
+ * @cssProperty --with-summary Summary for --with-summary
+ * @cssProperty --with-summary-dash - Summary for --with-summary-dash
+ * @cssProperty --with-summary-colon: Summary for --with-summary-colon
+ * @cssProperty --with-description - Summary for --with-description
+ * Description for --with-description
+ * More description for --with-description
+ *
+ * Even more description for --with-description
+ *
+ * @cssProp --short-basic
+ * @cssProp --short-with-summary Summary for --short-with-summary
+ * @cssProp --short-with-summary-dash - Summary for --short-with-summary-dash
+ * @cssProp --short-with-summary-colon: Summary for --short-with-summary-colon
+ * @cssProp --short-with-description - Summary for --short-with-description
+ * Description for --short-with-description
+ * More description for --short-with-description
+ *
+ * Even more description for --short-with-description
+ */
+export class ElementA extends LitElement {}
+customElements.define('element-a', ElementA);
+
+/**
+ * @description TaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ * @summary TaggedDescription summary.
+ * @deprecated TaggedDescription deprecated message.
+ */
+export class TaggedDescription extends LitElement {}
+
+/**
+ * UntaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ *
+ * @deprecated UntaggedDescription deprecated message.
+ * @summary UntaggedDescription summary.
+ */
+export class UntaggedDescription extends LitElement {}
+
+/**
+ * UntaggedDescSummary summary.
+ *
+ * UntaggedDescSummary description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ *
+ * @deprecated
+ */
+export class UntaggedDescSummary extends LitElement {}
diff --git a/packages/labs/analyzer/test-files/js/jsdoc/package.json b/packages/labs/analyzer/test-files/js/jsdoc/package.json
new file mode 100644
index 0000000000..0942e4bbb1
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/jsdoc/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-jsdoc",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/js/modules/module-a.js b/packages/labs/analyzer/test-files/js/modules/module-a.js
new file mode 100644
index 0000000000..58b7f876b9
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/modules/module-a.js
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {a, b} from './module-b.js';
+import {LitElement} from 'lit';
+import {c} from '../modules/module-b.js';
+
+export {a, b, c, LitElement};
diff --git a/packages/labs/analyzer/test-files/js/modules/module-b.js b/packages/labs/analyzer/test-files/js/modules/module-b.js
new file mode 100644
index 0000000000..b05c1b4297
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/modules/module-b.js
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export const a = 'a';
+export const b = 'b';
+export const c = 'c';
diff --git a/packages/labs/analyzer/test-files/js/modules/package.json b/packages/labs/analyzer/test-files/js/modules/package.json
new file mode 100644
index 0000000000..a900c279aa
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/modules/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-modules",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/js/properties/element-a.js b/packages/labs/analyzer/test-files/js/properties/element-a.js
new file mode 100644
index 0000000000..47823335c1
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/properties/element-a.js
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement} from 'lit';
+import {ImportedClass} from './external.js';
+
+export class LocalClass {
+ constructor() {
+ this.someData = 42;
+ }
+}
+
+export class ElementA extends LitElement {
+ static properties = {
+ noOptionsString: {},
+ noOptionsNumber: {},
+ typeString: {type: String},
+ typeNumber: {type: Number},
+ typeBoolean: {type: Boolean},
+ reflectTrue: {reflect: true},
+ reflectFalse: {reflect: false},
+ reflectUndefined: {reflect: undefined},
+ attributeTrue: {attribute: true},
+ attributeFalse: {attribute: false},
+ attributeUndefined: {attribute: undefined},
+ attributeString: {attribute: 'abc'},
+ customConverter: {converter: {fromAttribute() {}, toAttribute() {}}},
+ localClass: {},
+ importedClass: {},
+ globalClass: {},
+ union: {},
+ staticProp: {attribute: 'static-prop', type: Number},
+ };
+
+ constructor() {
+ super();
+ this.notDecorated = '';
+ this.noOptionsString = '';
+ this.noOptionsNumber = 42;
+ this.typeString = '';
+ this.typeNumber = 42;
+ this.typeBoolean = true;
+ this.reflectTrue = '';
+ this.reflectFalse = '';
+ this.reflectUndefined = '';
+ this.attributeTrue = '';
+ this.attributeFalse = '';
+ this.attributeUndefined = '';
+ this.attributeString = '';
+ this.customConverter = '';
+ this.localClass = new LocalClass();
+ this.importedClass = new ImportedClass();
+ this.globalClass = document.createElement('foo');
+ this.staticProp = 42;
+ }
+}
+customElements.define('element-a', ElementA);
diff --git a/packages/labs/analyzer/test-files/decorators-properties/src/external.ts b/packages/labs/analyzer/test-files/js/properties/external.js
similarity index 100%
rename from packages/labs/analyzer/test-files/decorators-properties/src/external.ts
rename to packages/labs/analyzer/test-files/js/properties/external.js
diff --git a/packages/labs/analyzer/test-files/js/properties/package.json b/packages/labs/analyzer/test-files/js/properties/package.json
new file mode 100644
index 0000000000..92cc1070a3
--- /dev/null
+++ b/packages/labs/analyzer/test-files/js/properties/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-properties",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/basic-elements/package.json b/packages/labs/analyzer/test-files/ts/basic-elements/package.json
similarity index 100%
rename from packages/labs/analyzer/test-files/basic-elements/package.json
rename to packages/labs/analyzer/test-files/ts/basic-elements/package.json
diff --git a/packages/labs/analyzer/test-files/ts/basic-elements/src/class-a.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/class-a.ts
new file mode 100644
index 0000000000..3894a16c0b
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/class-a.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export class ClassA {
+ foo = 1;
+}
diff --git a/packages/labs/analyzer/test-files/ts/basic-elements/src/default-element.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/default-element.ts
new file mode 100644
index 0000000000..2550efb8e8
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/default-element.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement} from 'lit';
+
+export default class extends LitElement {}
diff --git a/packages/labs/analyzer/test-files/basic-elements/src/element-a.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-a.ts
similarity index 86%
rename from packages/labs/analyzer/test-files/basic-elements/src/element-a.ts
rename to packages/labs/analyzer/test-files/ts/basic-elements/src/element-a.ts
index 07cc8cebb6..677b51b542 100644
--- a/packages/labs/analyzer/test-files/basic-elements/src/element-a.ts
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-a.ts
@@ -21,6 +21,11 @@ export class ElementA extends LitElement {
@property({reflect: true, type: Number, attribute: 'bbb'})
b = 1;
+ @property()
+ c;
+
+ static c = 'should not be inferred as type for c';
+
render() {
return html`${this.a}
`;
}
diff --git a/packages/labs/analyzer/test-files/ts/basic-elements/src/element-b.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-b.ts
new file mode 100644
index 0000000000..929d85416f
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-b.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement, html, css} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+@customElement('element-b')
+export class ElementB extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+
+ // Adds a property defined in a static properties block to make sure this
+ // works in TypeScript as expected
+ static get properties() {
+ return {
+ bar: {type: Number},
+ };
+ }
+
+ @property()
+ foo?: string;
+
+ declare bar: number;
+
+ constructor() {
+ super();
+ this.bar = 42;
+ }
+
+ render() {
+ return html`${this.foo}
`;
+ }
+}
diff --git a/packages/labs/analyzer/test-files/ts/basic-elements/src/element-c.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-c.ts
new file mode 100644
index 0000000000..b5e760d9c4
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/element-c.ts
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {html} from 'lit';
+import {ElementB} from './element-b.js';
+import {customElement, property} from 'lit/decorators.js';
+
+@customElement('element-c')
+export class ElementC extends ElementB {
+ @property()
+ baz = 'baz';
+
+ render() {
+ return html`${super.render()} ${this.baz}
`;
+ }
+}
diff --git a/packages/labs/analyzer/test-files/ts/basic-elements/src/not-lit.ts b/packages/labs/analyzer/test-files/ts/basic-elements/src/not-lit.ts
new file mode 100644
index 0000000000..abb8c47575
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/basic-elements/src/not-lit.ts
@@ -0,0 +1,3 @@
+class LitElement {}
+
+export class NotLit extends LitElement {}
diff --git a/packages/labs/analyzer/test-files/basic-elements/tsconfig.json b/packages/labs/analyzer/test-files/ts/basic-elements/tsconfig.json
similarity index 100%
rename from packages/labs/analyzer/test-files/basic-elements/tsconfig.json
rename to packages/labs/analyzer/test-files/ts/basic-elements/tsconfig.json
diff --git a/packages/labs/analyzer/test-files/ts/events/package.json b/packages/labs/analyzer/test-files/ts/events/package.json
new file mode 100644
index 0000000000..fccb63eaa0
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/events/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-events",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/events/src/custom-event.ts b/packages/labs/analyzer/test-files/ts/events/src/custom-event.ts
similarity index 100%
rename from packages/labs/analyzer/test-files/events/src/custom-event.ts
rename to packages/labs/analyzer/test-files/ts/events/src/custom-event.ts
diff --git a/packages/labs/analyzer/test-files/events/src/element-a.ts b/packages/labs/analyzer/test-files/ts/events/src/element-a.ts
similarity index 100%
rename from packages/labs/analyzer/test-files/events/src/element-a.ts
rename to packages/labs/analyzer/test-files/ts/events/src/element-a.ts
diff --git a/packages/labs/analyzer/test-files/decorators-properties/tsconfig.json b/packages/labs/analyzer/test-files/ts/events/tsconfig.json
similarity index 100%
rename from packages/labs/analyzer/test-files/decorators-properties/tsconfig.json
rename to packages/labs/analyzer/test-files/ts/events/tsconfig.json
diff --git a/packages/labs/analyzer/test-files/ts/jsdoc/package.json b/packages/labs/analyzer/test-files/ts/jsdoc/package.json
new file mode 100644
index 0000000000..0942e4bbb1
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/jsdoc/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-jsdoc",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/ts/jsdoc/src/element-a.ts b/packages/labs/analyzer/test-files/ts/jsdoc/src/element-a.ts
new file mode 100644
index 0000000000..5886ebd07a
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/jsdoc/src/element-a.ts
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElement} from 'lit';
+import {customElement} from 'lit/decorators.js';
+
+/**
+ * A cool custom element.
+ *
+ * @slot basic
+ * @slot with-summary Summary for with-summary
+ * @slot with-summary-dash - Summary for with-summary-dash
+ * @slot with-summary-colon: Summary for with-summary-colon
+ * @slot with-description - Summary for with-description
+ * Description for with-description
+ * More description for with-description
+ *
+ * Even more description for with-description
+ *
+ * @cssPart basic
+ * @cssPart with-summary Summary for :part(with-summary)
+ * @cssPart with-summary-dash - Summary for :part(with-summary-dash)
+ * @cssPart with-summary-colon: Summary for :part(with-summary-colon)
+ * @cssPart with-description - Summary for :part(with-description)
+ * Description for :part(with-description)
+ * More description for :part(with-description)
+ *
+ * Even more description for :part(with-description)
+ *
+ * @cssProperty --basic
+ * @cssProperty --with-summary Summary for --with-summary
+ * @cssProperty --with-summary-dash - Summary for --with-summary-dash
+ * @cssProperty --with-summary-colon: Summary for --with-summary-colon
+ * @cssProperty --with-description - Summary for --with-description
+ * Description for --with-description
+ * More description for --with-description
+ *
+ * Even more description for --with-description
+ *
+ * @cssProp --short-basic
+ * @cssProp --short-with-summary Summary for --short-with-summary
+ * @cssProp --short-with-summary-dash - Summary for --short-with-summary-dash
+ * @cssProp --short-with-summary-colon: Summary for --short-with-summary-colon
+ * @cssProp --short-with-description - Summary for --short-with-description
+ * Description for --short-with-description
+ * More description for --short-with-description
+ *
+ * Even more description for --short-with-description
+ */
+@customElement('element-a')
+export class ElementA extends LitElement {}
+
+/**
+ * @description TaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ * @summary TaggedDescription summary.
+ * @deprecated TaggedDescription deprecated message.
+ */
+export class TaggedDescription extends LitElement {}
+
+/**
+ * UntaggedDescription description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ *
+ * @deprecated UntaggedDescription deprecated message.
+ * @summary UntaggedDescription summary.
+ */
+export class UntaggedDescription extends LitElement {}
+
+/**
+ * UntaggedDescSummary summary.
+ *
+ * UntaggedDescSummary description. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ * aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ * nisi ut aliquip ex ea commodo consequat.
+ *
+ * @deprecated
+ */
+export class UntaggedDescSummary extends LitElement {}
diff --git a/packages/labs/analyzer/test-files/events/tsconfig.json b/packages/labs/analyzer/test-files/ts/jsdoc/tsconfig.json
similarity index 100%
rename from packages/labs/analyzer/test-files/events/tsconfig.json
rename to packages/labs/analyzer/test-files/ts/jsdoc/tsconfig.json
diff --git a/packages/labs/analyzer/test-files/ts/modules/package.json b/packages/labs/analyzer/test-files/ts/modules/package.json
new file mode 100644
index 0000000000..a900c279aa
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/modules/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-modules",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/ts/modules/src/module-a.ts b/packages/labs/analyzer/test-files/ts/modules/src/module-a.ts
new file mode 100644
index 0000000000..50be8be97e
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/modules/src/module-a.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {a, b, c} from './module-b.js';
+import {LitElement} from 'lit';
+
+export {a, b, c, LitElement};
diff --git a/packages/labs/analyzer/test-files/ts/modules/src/module-b.ts b/packages/labs/analyzer/test-files/ts/modules/src/module-b.ts
new file mode 100644
index 0000000000..b05c1b4297
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/modules/src/module-b.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export const a = 'a';
+export const b = 'b';
+export const c = 'c';
diff --git a/packages/labs/analyzer/test-files/types/tsconfig.json b/packages/labs/analyzer/test-files/ts/modules/tsconfig.json
similarity index 100%
rename from packages/labs/analyzer/test-files/types/tsconfig.json
rename to packages/labs/analyzer/test-files/ts/modules/tsconfig.json
diff --git a/packages/labs/analyzer/test-files/ts/properties/package.json b/packages/labs/analyzer/test-files/ts/properties/package.json
new file mode 100644
index 0000000000..92cc1070a3
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/properties/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@lit-internal/test-properties",
+ "dependencies": {
+ "lit": "^2.0.0"
+ }
+}
diff --git a/packages/labs/analyzer/test-files/decorators-properties/src/element-a.ts b/packages/labs/analyzer/test-files/ts/properties/src/element-a.ts
similarity index 88%
rename from packages/labs/analyzer/test-files/decorators-properties/src/element-a.ts
rename to packages/labs/analyzer/test-files/ts/properties/src/element-a.ts
index f84480b533..4c5f7dda22 100644
--- a/packages/labs/analyzer/test-files/decorators-properties/src/element-a.ts
+++ b/packages/labs/analyzer/test-files/ts/properties/src/element-a.ts
@@ -17,6 +17,17 @@ export interface LocalInterface {
@customElement('element-a')
export class ElementA extends LitElement {
+ static properties = {
+ staticProp: {attribute: 'static-prop', type: Number},
+ };
+
+ declare staticProp: number;
+
+ constructor() {
+ super();
+ this.staticProp = 42;
+ }
+
notDecorated: string;
@property()
diff --git a/packages/labs/analyzer/test-files/ts/properties/src/external.ts b/packages/labs/analyzer/test-files/ts/properties/src/external.ts
new file mode 100644
index 0000000000..82952e0cb2
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/properties/src/external.ts
@@ -0,0 +1,7 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export class ImportedClass {}
diff --git a/packages/labs/analyzer/test-files/ts/properties/tsconfig.json b/packages/labs/analyzer/test-files/ts/properties/tsconfig.json
new file mode 100644
index 0000000000..204701fc83
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/properties/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "lib": ["es2020", "DOM"],
+ "module": "ES2020",
+ "rootDir": "./src",
+ "outDir": "./",
+ "moduleResolution": "node",
+ "experimentalDecorators": true,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": []
+}
diff --git a/packages/labs/analyzer/test-files/types/package.json b/packages/labs/analyzer/test-files/ts/types/package.json
similarity index 100%
rename from packages/labs/analyzer/test-files/types/package.json
rename to packages/labs/analyzer/test-files/ts/types/package.json
diff --git a/packages/labs/analyzer/test-files/types/src/external.ts b/packages/labs/analyzer/test-files/ts/types/src/external.ts
similarity index 74%
rename from packages/labs/analyzer/test-files/types/src/external.ts
rename to packages/labs/analyzer/test-files/ts/types/src/external.ts
index aa87a5f728..0c9793c6d6 100644
--- a/packages/labs/analyzer/test-files/types/src/external.ts
+++ b/packages/labs/analyzer/test-files/ts/types/src/external.ts
@@ -10,3 +10,7 @@ export class ImportedClass {
export interface ImportedInterface {
someData: number;
}
+
+export const returnsClass = () => {
+ return new ImportedClass();
+};
diff --git a/packages/labs/analyzer/test-files/types/src/module.ts b/packages/labs/analyzer/test-files/ts/types/src/module.ts
similarity index 92%
rename from packages/labs/analyzer/test-files/types/src/module.ts
rename to packages/labs/analyzer/test-files/ts/types/src/module.ts
index a830f01aa8..79f3921049 100644
--- a/packages/labs/analyzer/test-files/types/src/module.ts
+++ b/packages/labs/analyzer/test-files/ts/types/src/module.ts
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import {ImportedClass, ImportedInterface} from './external.js';
-import {LitElement} from 'lit';
+import {ImportedClass, ImportedInterface, returnsClass} from './external.js';
+import {LitElement, html} from 'lit';
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
export const testString: string = 'hi';
@@ -73,3 +73,5 @@ const [separatelyExportedDestructArr, [separatelyExportedDestructArrNested]] = [
[new LitElement()],
];
export {separatelyExportedDestructArr, separatelyExportedDestructArrNested};
+
+export const importedType = Math.random() ? returnsClass() : html``;
diff --git a/packages/labs/analyzer/test-files/ts/types/tsconfig.json b/packages/labs/analyzer/test-files/ts/types/tsconfig.json
new file mode 100644
index 0000000000..204701fc83
--- /dev/null
+++ b/packages/labs/analyzer/test-files/ts/types/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "lib": ["es2020", "DOM"],
+ "module": "ES2020",
+ "rootDir": "./src",
+ "outDir": "./",
+ "moduleResolution": "node",
+ "experimentalDecorators": true,
+ "skipDefaultLibCheck": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": []
+}
diff --git a/packages/labs/cli-localize/.gitignore b/packages/labs/cli-localize/.gitignore
new file mode 100644
index 0000000000..ca54677a04
--- /dev/null
+++ b/packages/labs/cli-localize/.gitignore
@@ -0,0 +1,2 @@
+/lib/
+/node_modules/
diff --git a/packages/labs/cli-localize/CHANGELOG.md b/packages/labs/cli-localize/CHANGELOG.md
new file mode 100644
index 0000000000..d7aecf675a
--- /dev/null
+++ b/packages/labs/cli-localize/CHANGELOG.md
@@ -0,0 +1,9 @@
+# @lit-labs/cli
+
+## 0.1.0
+
+### Minor Changes
+
+- [#2936](https://github.com/lit/lit/pull/2936) [`7a9fc0f5`](https://github.com/lit/lit/commit/7a9fc0f57e43c2eab44e9442e5896f951a8c751a) - Locally version and lazily install the localize command.
+
+## 0.0.1
diff --git a/packages/labs/cli-localize/README.md b/packages/labs/cli-localize/README.md
new file mode 100644
index 0000000000..3b9adf7fe4
--- /dev/null
+++ b/packages/labs/cli-localize/README.md
@@ -0,0 +1,7 @@
+# @lit-labs/cli-localize
+
+Powers the `lit localize` command.
+
+Don't use this directly, but install `@lit-labs/cli` and run `lit localize` from it.
+
+That command will load this package from the nearest node_modules directory, or offer to install it if it's not found.
diff --git a/packages/labs/cli-localize/package-lock.json b/packages/labs/cli-localize/package-lock.json
new file mode 100644
index 0000000000..7715f04c85
--- /dev/null
+++ b/packages/labs/cli-localize/package-lock.json
@@ -0,0 +1,660 @@
+{
+ "name": "@lit-labs/cli-localize",
+ "version": "0.0.1",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@lit-labs/cli-localize",
+ "version": "0.0.1",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/localize-tools": "^0.6.3"
+ },
+ "devDependencies": {
+ "typescript": "~4.6.2"
+ },
+ "engines": {
+ "node": ">=14.8.0"
+ }
+ },
+ "node_modules/@lit/localize": {
+ "version": "0.11.3",
+ "resolved": "https://registry.npmjs.org/@lit/localize/-/localize-0.11.3.tgz",
+ "integrity": "sha512-zWq74Sa/HvSRAtXv9UaBbZd7B1wP82rV/71krYJ+INdexye+nILjPUiGHYHb0G54+v7YpyZi61Fm5yI+kfcJnw==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.0.0",
+ "lit": "^2.0.0"
+ }
+ },
+ "node_modules/@lit/localize-tools": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@lit/localize-tools/-/localize-tools-0.6.3.tgz",
+ "integrity": "sha512-cJ5RazZw2GxUoSRqeZt5oIZfIvf217r/VFWXx7DkcNCELDdr29MCF9Bvl4gLK5X6GG0i4WjNccEIn3TO4i/BMg==",
+ "dependencies": {
+ "@lit/localize": "^0.11.0",
+ "@xmldom/xmldom": "^0.7.0",
+ "fast-glob": "^3.2.7",
+ "fs-extra": "^10.0.0",
+ "jsonschema": "^1.4.0",
+ "lit": "^2.2.0",
+ "minimist": "^1.2.5",
+ "parse5": "^6.0.1",
+ "source-map-support": "^0.5.19",
+ "typescript": "^4.3.5"
+ },
+ "bin": {
+ "lit-localize": "bin/lit-localize.js"
+ }
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.3.2.tgz",
+ "integrity": "sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA=="
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
+ "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonschema": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/lit": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.2.6.tgz",
+ "integrity": "sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-element": "^3.2.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.2.0.tgz",
+ "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.2.6.tgz",
+ "integrity": "sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
+ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ }
+ },
+ "dependencies": {
+ "@lit/localize": {
+ "version": "0.11.3",
+ "resolved": "https://registry.npmjs.org/@lit/localize/-/localize-0.11.3.tgz",
+ "integrity": "sha512-zWq74Sa/HvSRAtXv9UaBbZd7B1wP82rV/71krYJ+INdexye+nILjPUiGHYHb0G54+v7YpyZi61Fm5yI+kfcJnw==",
+ "requires": {
+ "@lit/reactive-element": "^1.0.0",
+ "lit": "^2.0.0"
+ }
+ },
+ "@lit/localize-tools": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@lit/localize-tools/-/localize-tools-0.6.3.tgz",
+ "integrity": "sha512-cJ5RazZw2GxUoSRqeZt5oIZfIvf217r/VFWXx7DkcNCELDdr29MCF9Bvl4gLK5X6GG0i4WjNccEIn3TO4i/BMg==",
+ "requires": {
+ "@lit/localize": "^0.11.0",
+ "@xmldom/xmldom": "^0.7.0",
+ "fast-glob": "^3.2.7",
+ "fs-extra": "^10.0.0",
+ "jsonschema": "^1.4.0",
+ "lit": "^2.2.0",
+ "minimist": "^1.2.5",
+ "parse5": "^6.0.1",
+ "source-map-support": "^0.5.19",
+ "typescript": "^4.3.5"
+ }
+ },
+ "@lit/reactive-element": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.3.2.tgz",
+ "integrity": "sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA=="
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
+ },
+ "@xmldom/xmldom": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
+ "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A=="
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ }
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "requires": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "jsonschema": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ=="
+ },
+ "lit": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.2.6.tgz",
+ "integrity": "sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==",
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-element": "^3.2.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "lit-element": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.2.0.tgz",
+ "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==",
+ "requires": {
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.2.0"
+ }
+ },
+ "lit-html": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.2.6.tgz",
+ "integrity": "sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==",
+ "requires": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "typescript": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
+ "integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg=="
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
+ }
+ }
+}
diff --git a/packages/labs/cli-localize/package.json b/packages/labs/cli-localize/package.json
new file mode 100644
index 0000000000..a5fcfe4e11
--- /dev/null
+++ b/packages/labs/cli-localize/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "@lit-labs/cli-localize",
+ "description": "Implements the `lit localize` command. Use from @lit-labs/cli",
+ "version": "0.1.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "author": "Google LLC",
+ "license": "BSD-3-Clause",
+ "bugs": "https://github.com/lit/lit/issues",
+ "type": "module",
+ "main": "lib/index.js",
+ "repository": "lit/lit",
+ "scripts": {
+ "build": "wireit",
+ "test": "wireit",
+ "test:compile": "wireit",
+ "build:deps": "wireit",
+ "build:compile": "wireit"
+ },
+ "wireit": {
+ "build": {
+ "dependencies": [
+ "build:deps",
+ "build:compile"
+ ]
+ },
+ "build:compile": {
+ "command": "tsc --skipLibCheck || echo ''",
+ "#comment": "This never fails and always emits output so that we can run tests of code that doesn't type check",
+ "clean": "if-file-deleted",
+ "files": [
+ "tsconfig.json",
+ "src/**/*"
+ ],
+ "output": [
+ "lib",
+ "test",
+ "index.{js,js.map,d.ts}"
+ ]
+ },
+ "build:deps": {
+ "dependencies": [
+ "../../localize-tools:build:ts"
+ ]
+ },
+ "test": {
+ "dependencies": [
+ "test:compile"
+ ]
+ },
+ "test:compile": {
+ "dependencies": [
+ "build:deps"
+ ],
+ "command": "tsc --pretty --noEmit",
+ "files": [
+ "tsconfig.json",
+ "src/**/*"
+ ],
+ "output": []
+ }
+ },
+ "dependencies": {
+ "@lit/localize-tools": "^0.6.3"
+ },
+ "devDependencies": {
+ "typescript": "~4.6.2"
+ },
+ "engines": {
+ "node": ">=14.8.0"
+ },
+ "files": [
+ "lib"
+ ],
+ "homepage": "https://github.com/lit/lit",
+ "keywords": [
+ "lit",
+ "localization",
+ "localize",
+ "lit-cli"
+ ]
+}
diff --git a/packages/labs/cli/src/lib/localize/build.ts b/packages/labs/cli-localize/src/commands.ts
similarity index 60%
rename from packages/labs/cli/src/lib/localize/build.ts
rename to packages/labs/cli-localize/src/commands.ts
index 12fdb9ed95..1bed84ff82 100644
--- a/packages/labs/cli/src/lib/localize/build.ts
+++ b/packages/labs/cli-localize/src/commands.ts
@@ -4,6 +4,17 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
+/**
+ * The implementations of our commands exposed in the Lit CLI.
+ *
+ * These are in their own module so that we can separate out lightweight
+ * metadata about the commands (which are used for the --help command,
+ * and argument parsing), from their actual implementations here, because
+ * loading all of our deps of the implementation takes time, and loading all
+ * of the deps of all of the implementations of _every_ command would seriously
+ * slow down the CLI.
+ */
+
import {
readConfigFileAndWriteSchema,
Config,
@@ -14,8 +25,9 @@ import {TransformLitLocalizer} from '@lit/localize-tools/lib/modes/transform.js'
import {RuntimeLitLocalizer} from '@lit/localize-tools/lib/modes/runtime.js';
import {KnownError, unreachable} from '@lit/localize-tools/lib/error.js';
import {LitLocalizer} from '@lit/localize-tools/lib/index.js';
+import {printDiagnostics} from '@lit/localize-tools/lib/typescript.js';
-export const run = async (configPath: string, console: Console) => {
+export const build = async (configPath: string, console: Console) => {
const config = readConfigFileAndWriteSchema(configPath);
const localizer = makeLocalizer(config);
console.log('Building');
@@ -35,6 +47,22 @@ export const run = async (configPath: string, console: Console) => {
await localizer.build();
};
+export const extract = async (configPath: string, console: Console) => {
+ const config = readConfigFileAndWriteSchema(configPath);
+ const localizer = makeLocalizer(config);
+ // TODO(aomarks) Don't even require the user to have configured their output
+ // mode if they're just doing extraction.
+ console.log('Extracting messages');
+ const {messages, errors} = localizer.extractSourceMessages();
+ if (errors.length > 0) {
+ printDiagnostics(errors);
+ throw new KnownError('Error analyzing program');
+ }
+ console.log(`Extracted ${messages.length} messages`);
+ console.log(`Writing interchange files`);
+ await localizer.writeInterchangeFiles();
+};
+
const makeLocalizer = (config: Config): LitLocalizer => {
switch (config.output.mode) {
case 'transform':
diff --git a/packages/labs/cli-localize/src/index.ts b/packages/labs/cli-localize/src/index.ts
new file mode 100644
index 0000000000..fa24d7b1e6
--- /dev/null
+++ b/packages/labs/cli-localize/src/index.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * Defines our interface within the Lit CLI.
+ */
+export const getCommand = () => {
+ return {
+ kind: 'resolved',
+ name: 'localize',
+ description: 'Lit localize',
+ subcommands: [
+ {
+ kind: 'resolved',
+ name: 'extract',
+ description: 'Extracts lit-localize messages',
+ options: [
+ {
+ name: 'config',
+ description: 'The path to the localize config file',
+ defaultValue: './lit-localize.json',
+ },
+ ],
+ async run({config}: {config: string}, console: Console) {
+ const commands = await import('./commands.js');
+ await commands.extract(config, console);
+ },
+ },
+ {
+ kind: 'resolved',
+ name: 'build',
+ description: 'Build lit-localize projects',
+ options: [
+ {
+ name: 'config',
+ description: 'The path to the localize config file',
+ defaultValue: './lit-localize.json',
+ },
+ ],
+ async run({config}: {config: string}, console: Console) {
+ const commands = await import('./commands.js');
+ await commands.build(config, console);
+ },
+ },
+ ],
+ async run(_options: unknown, console: Console) {
+ console.error(
+ 'Use one of the localize subcommands, like `lit localize build` or ' +
+ '`lit localize extract`. Run `lit help localize` for more help.'
+ );
+ return {
+ exitCode: 1,
+ };
+ },
+ };
+};
diff --git a/packages/labs/cli-localize/tsconfig.json b/packages/labs/cli-localize/tsconfig.json
new file mode 100644
index 0000000000..91a03f01d9
--- /dev/null
+++ b/packages/labs/cli-localize/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "module": "es2022",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "lib": ["es2020"],
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "preserveConstEnums": true,
+ "forceConsistentCasingInFileNames": true,
+ "rootDir": "src/",
+ "outDir": "lib/",
+ "declaration": true,
+ "sourceMap": true,
+ "incremental": true,
+ "tsBuildInfoFile": "./lib/.tsbuildinfo",
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*"],
+ "exclude": []
+}
diff --git a/packages/labs/cli/.prettierignore b/packages/labs/cli/.prettierignore
new file mode 100644
index 0000000000..dd13a6f370
--- /dev/null
+++ b/packages/labs/cli/.prettierignore
@@ -0,0 +1 @@
+/test-goldens/
\ No newline at end of file
diff --git a/packages/labs/cli/CHANGELOG.md b/packages/labs/cli/CHANGELOG.md
index cb356847fb..7c89d22b65 100644
--- a/packages/labs/cli/CHANGELOG.md
+++ b/packages/labs/cli/CHANGELOG.md
@@ -1,5 +1,25 @@
# @lit-labs/cli
+## 0.2.1
+
+### Patch Changes
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3304](https://github.com/lit/lit/pull/3304) [`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a) - Added support for analyzing JavaScript files.
+
+- [#2936](https://github.com/lit/lit/pull/2936) [`7a9fc0f5`](https://github.com/lit/lit/commit/7a9fc0f57e43c2eab44e9442e5896f951a8c751a) - Locally version and lazily install the localize command.
+
+### Patch Changes
+
+- Updated dependencies [[`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a), [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e), [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9)]:
+ - @lit-labs/analyzer@0.3.0
+
## 0.1.0
### Minor Changes
diff --git a/packages/labs/cli/bin/lit.js b/packages/labs/cli/bin/lit.js
index dfc30b0cb4..c322726d92 100755
--- a/packages/labs/cli/bin/lit.js
+++ b/packages/labs/cli/bin/lit.js
@@ -20,7 +20,9 @@ process.on('unhandledRejection', (error) => {
// eslint-disable-next-line no-undef
const args = process.argv.slice(2);
-const cli = new LitCli(args);
+// eslint-disable-next-line no-undef
+const cwd = process.cwd();
+const cli = new LitCli(args, {cwd});
const result = await cli.run();
// eslint-disable-next-line no-undef
process.exit(result?.exitCode ?? 0);
diff --git a/packages/labs/cli/package.json b/packages/labs/cli/package.json
index 405601dab9..571f6b866e 100644
--- a/packages/labs/cli/package.json
+++ b/packages/labs/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@lit-labs/cli",
"description": "Tooling for Lit development",
- "version": "0.1.0",
+ "version": "0.2.1",
"publishConfig": {
"access": "public"
},
@@ -41,7 +41,7 @@
},
"build:deps": {
"dependencies": [
- "../../localize-tools:build:ts",
+ "../cli-localize:build",
"../../tests:build",
"../analyzer:build",
"../gen-wrapper-react:build",
@@ -62,13 +62,14 @@
"command": "tsc --pretty --noEmit",
"files": [
"tsconfig.json",
- "src/**/*"
+ "src/**/*",
+ "test-goldens/**/*"
],
"output": []
},
"test:actual": {
"#comment": "The quotes around the file regex must be double quotes on windows!",
- "command": "uvu test \"_test\\.js$\"",
+ "command": "cross-env NODE_OPTIONS=--enable-source-maps uvu test \"_test\\.js$\"",
"dependencies": [
"build"
],
@@ -77,7 +78,7 @@
}
},
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit/localize-tools": "^0.6.1",
"chalk": "^5.0.1",
diff --git a/packages/labs/cli/src/lib/commands/help.ts b/packages/labs/cli/src/lib/commands/help.ts
index 001d0a8619..2ca1a8e820 100644
--- a/packages/labs/cli/src/lib/commands/help.ts
+++ b/packages/labs/cli/src/lib/commands/help.ts
@@ -88,7 +88,13 @@ export const makeHelpCommand = (cli: LitCli): ResolvedCommand => {
],
async run(options: CommandOptions, console: Console) {
- const commandNames = options['command'] as Array | null;
+ let commandNames = options['command'] as Array | string | null;
+
+ if (typeof commandNames === 'string') {
+ commandNames = commandNames?.split(' ') ?? [];
+ }
+
+ commandNames = commandNames?.map((c) => c.trim()) ?? null;
if (commandNames == null) {
console.debug('no command given, printing general help...', {options});
diff --git a/packages/labs/cli/src/lib/commands/init.ts b/packages/labs/cli/src/lib/commands/init.ts
new file mode 100644
index 0000000000..a9a5667844
--- /dev/null
+++ b/packages/labs/cli/src/lib/commands/init.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {Command, CommandOptions} from '../command.js';
+import {run} from '../init/element-starter/index.js';
+import {LitCli} from '../lit-cli.js';
+
+export type Language = 'ts' | 'js';
+export interface InitCommandOptions {
+ lang: Language;
+ name: string;
+ dir: string;
+}
+
+export const makeInitCommand = (cli: LitCli): Command => {
+ return {
+ kind: 'resolved',
+ name: 'init',
+ description: 'Initialize a Lit project',
+ subcommands: [
+ {
+ kind: 'resolved',
+ name: 'element',
+ description: 'Generate a shareable element starter package',
+ options: [
+ {
+ name: 'lang',
+ defaultValue: 'js',
+ description:
+ 'Which language to use for the element. Supported languages: js, ts',
+ },
+ {
+ name: 'name',
+ defaultValue: 'my-element',
+ description:
+ 'Tag name of the Element to generate (must include a hyphen).',
+ },
+ {
+ name: 'dir',
+ defaultValue: '.',
+ description: 'Directory in which to generate the element package.',
+ },
+ ],
+ async run(options: CommandOptions, console: Console) {
+ const name = options.name as string;
+ /*
+ * This is a basic check to ensure that the name is a valid custom
+ * element name. Will make sure you you start off with a character and
+ * at least one hyphen plus more characters. Will not check for the
+ * following invalid use cases:
+ * - starting with a digit
+ *
+ * Will not allow the following valid use cases:
+ * - including a unicode character as not the first character
+ */
+ const customElementMatch = name.match(/\w+(-\w+)+/g);
+ if (!customElementMatch || customElementMatch[0] !== name) {
+ throw new Error(
+ `"${name}" is not a valid custom-element name. (Must include a hyphen and ascii characters)`
+ );
+ }
+ return await run(
+ options as unknown as InitCommandOptions,
+ console,
+ cli
+ );
+ },
+ },
+ ],
+ async run(_options: CommandOptions, console: Console) {
+ // by default run the element command
+ return await run(
+ {lang: 'js', name: 'my-element', dir: '.'},
+ console,
+ cli
+ );
+ },
+ };
+};
diff --git a/packages/labs/cli/src/lib/commands/labs.ts b/packages/labs/cli/src/lib/commands/labs.ts
index 572b954148..b7dc97b7d8 100644
--- a/packages/labs/cli/src/lib/commands/labs.ts
+++ b/packages/labs/cli/src/lib/commands/labs.ts
@@ -23,7 +23,7 @@ export const makeLabsCommand = (cli: LitCli): Command => {
multiple: true,
defaultValue: './',
description:
- 'Folder containing a package to generate wrappers for.',
+ 'Folder containing a package to generate wrappers for. For TypeScript projects, if the package folder does not contain a tsconfig.json, this option may also specify a specific tsconfig.json to use.',
},
{
name: 'framework',
@@ -31,6 +31,12 @@ export const makeLabsCommand = (cli: LitCli): Command => {
description:
'Framework to generate wrappers for. Supported frameworks: react, vue.',
},
+ {
+ name: 'manifest',
+ type: Boolean,
+ description:
+ 'Generate a custom-elements.json manifest for this package.',
+ },
{
name: 'out',
defaultValue: './gen',
@@ -41,16 +47,18 @@ export const makeLabsCommand = (cli: LitCli): Command => {
{
package: packages,
framework: frameworks,
+ manifest,
out: outDir,
}: {
package: string[];
framework: string[];
+ manifest: boolean;
out: string;
},
console: Console
) {
const gen = await import('../generate/generate.js');
- await gen.run({cli, packages, frameworks, outDir}, console);
+ await gen.run({cli, packages, frameworks, manifest, outDir}, console);
},
},
],
diff --git a/packages/labs/cli/src/lib/commands/localize.ts b/packages/labs/cli/src/lib/commands/localize.ts
index f060870119..af8fe19bc3 100644
--- a/packages/labs/cli/src/lib/commands/localize.ts
+++ b/packages/labs/cli/src/lib/commands/localize.ts
@@ -4,53 +4,12 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import {ResolvedCommand, CommandOptions} from '../command.js';
+import {ReferenceToCommand} from '../command.js';
-export const localize: ResolvedCommand = {
- kind: 'resolved',
+export const localize: ReferenceToCommand = {
+ kind: 'reference',
name: 'localize',
description: 'Lit localize',
- subcommands: [
- {
- kind: 'resolved',
- name: 'extract',
- description: 'Extracts lit-localize messages',
- options: [
- {
- name: 'config',
- description: 'The path to the localize config file',
- defaultValue: './lit-localize.json',
- },
- ],
- async run({config}: {config: string}, console: Console) {
- const extract = await import('../localize/extract.js');
- await extract.run(config, console);
- },
- },
- {
- kind: 'resolved',
- name: 'build',
- description: 'Build lit-localize projects',
- options: [
- {
- name: 'config',
- description: 'The path to the localize config file',
- defaultValue: './lit-localize.json',
- },
- ],
- async run({config}: {config: string}, console: Console) {
- const build = await import('../localize/build.js');
- await build.run(config, console);
- },
- },
- ],
- async run(_options: CommandOptions, console: Console) {
- console.error(
- 'Use one of the localize subcommands, like `lit localize build` or ' +
- '`lit localize extract`. Run `lit help localize` for more help.'
- );
- return {
- exitCode: 1,
- };
- },
+ importSpecifier: '@lit-labs/cli-localize',
+ installFrom: '@lit-labs/cli-localize',
};
diff --git a/packages/labs/cli/src/lib/generate/generate.ts b/packages/labs/cli/src/lib/generate/generate.ts
index 11ea9a0372..0b2fdef86e 100644
--- a/packages/labs/cli/src/lib/generate/generate.ts
+++ b/packages/labs/cli/src/lib/generate/generate.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import {Analyzer} from '@lit-labs/analyzer';
+import {createPackageAnalyzer} from '@lit-labs/analyzer';
import {AbsolutePath} from '@lit-labs/analyzer/lib/paths.js';
import {FileTree, writeFileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
import {LitCli} from '../lit-cli.js';
@@ -28,9 +28,17 @@ const vueCommand: Command = {
importSpecifier: '@lit-labs/gen-wrapper-vue/index.js',
};
+const manifestCommand: Command = {
+ name: 'manifest',
+ description: 'Generate custom-elements.json manifest.',
+ kind: 'reference',
+ installFrom: '@lit-labs/gen-manifest',
+ importSpecifier: '@lit-labs/gen-manifest/index.js',
+};
+
// A generate command has a generate method instead of a run method.
interface GenerateCommand extends Omit {
- generate(options: {analysis: Package}, console: Console): Promise;
+ generate(options: {package: Package}, console: Console): Promise;
}
const frameworkCommands = {
@@ -45,29 +53,39 @@ export const run = async (
cli,
packages,
frameworks: frameworkNames,
+ manifest,
outDir,
- }: {packages: string[]; frameworks: string[]; outDir: string; cli: LitCli},
+ }: {
+ packages: string[];
+ frameworks: string[];
+ manifest: boolean;
+ outDir: string;
+ cli: LitCli;
+ },
console: Console
) => {
for (const packageRoot of packages) {
// Ensure separators in input paths are normalized and resolved to absolute
const root = path.normalize(path.resolve(packageRoot)) as AbsolutePath;
const out = path.normalize(path.resolve(outDir)) as AbsolutePath;
- const analyzer = new Analyzer(root);
- const analysis = analyzer.analyzePackage();
- if (!analysis.packageJson.name) {
+ const analyzer = createPackageAnalyzer(root);
+ const pkg = analyzer.getPackage();
+ if (!pkg.packageJson.name) {
throw new Error(
`Package at '${packageRoot}' did not have a name in package.json. The 'gen' command requires that packages have a name.`
);
}
const generatorReferences = [];
- for (const name of frameworkNames as FrameworkName[]) {
+ for (const name of (frameworkNames ?? []) as FrameworkName[]) {
const framework = frameworkCommands[name];
if (framework == null) {
throw new Error(`No generator exists for framework '${framework}'`);
}
generatorReferences.push(framework);
}
+ if (manifest) {
+ generatorReferences.push(manifestCommand);
+ }
// Optimistically try to import all generators in parallel.
// If any aren't installed we need to ask for permission to install it
// below, but in the common happy case this will do all the loading work.
@@ -92,7 +110,7 @@ export const run = async (
generators.push(resolved as unknown as GenerateCommand);
}
const options = {
- analysis,
+ package: pkg,
};
const results = await Promise.allSettled(
generators.map(async (generator) => {
diff --git a/packages/labs/cli/src/lib/init/element-starter/index.ts b/packages/labs/cli/src/lib/init/element-starter/index.ts
new file mode 100644
index 0000000000..482135a10f
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/index.ts
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree, writeFileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {generateTsconfig} from './templates/tsconfig.json.js';
+import {generatePackageJson} from './templates/package.json.js';
+import {generateIndex} from './templates/demo/index.html.js';
+import {generateGitignore} from './templates/gitignore.js';
+import {generateNpmignore} from './templates/npmignore.js';
+import {generateElement} from './templates/lib/element.js';
+import {CommandResult} from '../../command.js';
+import {InitCommandOptions} from '../../commands/init.js';
+import {LitCli} from '../../lit-cli.js';
+import path from 'path';
+
+export const generateLitElementStarter = async (
+ options: InitCommandOptions
+): Promise => {
+ const {name, lang} = options;
+ let files = {
+ ...generatePackageJson(name, lang),
+ ...generateIndex(name),
+ ...generateGitignore(lang),
+ ...generateNpmignore(),
+ ...generateElement(name, lang),
+ };
+ if (lang === 'ts') {
+ files = {
+ ...files,
+ ...generateTsconfig(),
+ };
+ }
+ return files;
+};
+
+export const run = async (
+ options: InitCommandOptions,
+ console: Console,
+ cli: LitCli
+): Promise => {
+ const files = await generateLitElementStarter(options);
+ const outPath = path.join(cli.cwd, options.dir, options.name);
+ await writeFileTree(outPath, files);
+ const relativePath = path.relative(cli.cwd, outPath);
+ console.log(`Created sharable element in ${relativePath}/.`);
+ return {
+ exitCode: 0,
+ };
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/demo/index.html.ts b/packages/labs/cli/src/lib/init/element-starter/templates/demo/index.html.ts
new file mode 100644
index 0000000000..4e40ac0659
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/demo/index.html.ts
@@ -0,0 +1,25 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {html} from '@lit-labs/gen-utils/lib/str-utils.js';
+
+export const generateIndex = (elementName: string): FileTree => {
+ return {
+ demo: {
+ 'index.html': html`
+
+
+ ${elementName} demo
+
+
+
+ <${elementName}>${elementName}>
+
+`,
+ },
+ };
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/gitignore.ts b/packages/labs/cli/src/lib/init/element-starter/templates/gitignore.ts
new file mode 100644
index 0000000000..04383247dd
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/gitignore.ts
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {Language} from '../../../commands/init.js';
+
+export const generateGitignore = (lang: Language): FileTree => {
+ return {
+ '.gitignore': `node_modules${
+ lang !== 'ts'
+ ? ''
+ : `
+lib`
+ }`,
+ };
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/lib/element.ts b/packages/labs/cli/src/lib/init/element-starter/templates/lib/element.ts
new file mode 100644
index 0000000000..2fbe758f8b
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/lib/element.ts
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {
+ javascript,
+ kabobToPascalCase,
+} from '@lit-labs/gen-utils/lib/str-utils.js';
+import {Language} from '../../../../commands/init.js';
+
+export const generateElement = (
+ elementName: string,
+ language: Language
+): FileTree => {
+ const directory = language === 'js' ? 'lib' : 'src';
+ return {
+ [directory]: {
+ [`${elementName}.${language}`]:
+ language === 'js'
+ ? generateTemplate(elementName, language)
+ : generateTemplate(elementName, language),
+ },
+ };
+};
+
+const generateTemplate = (elementName: string, lang: Language) => {
+ const className = kabobToPascalCase(elementName);
+ return javascript`import {LitElement, html, css} from 'lit';${
+ lang === 'js'
+ ? ''
+ : `
+import {property, customElement} from 'lit/decorators.js';`
+ }
+
+/**
+ * An example element.
+ *
+ * @fires count-changed - Indicates when the count changes
+ * @slot - This element has a slot
+ * @csspart button - The button
+ * @cssprop --${elementName}-font-size - The button's font size
+ */${
+ lang === 'js'
+ ? ''
+ : `
+@customElement('${elementName}')`
+ }
+export class ${className} extends LitElement {
+ static styles = css\`
+ :host {
+ display: block;
+ border: solid 1px gray;
+ padding: 16px;
+ }
+
+ button {
+ font-size: var(--${elementName}-font-size, 16px);
+ }
+ \`;
+
+ ${
+ lang === 'js'
+ ? `static properties = {
+ /**
+ * The number of times the button has been clicked.
+ * @type {number}
+ */
+ count: {type: Number},
+ }
+
+ constructor() {
+ super();
+ this.count = 0;
+ }`
+ : `/**
+ * The number of times the button has been clicked.
+ */
+ @property({type: Number})
+ count = 0;`
+ }
+
+ render() {
+ return html\`
+ Hello World
+
+
+ \`;
+ }
+
+ ${lang === 'js' ? '' : 'protected '}_onClick() {
+ this.count++;
+ const event = new Event('count-changed', {bubbles: true});
+ this.dispatchEvent(event);
+ }
+}
+${
+ lang === 'js'
+ ? `
+customElements.define('${elementName}', ${className});`
+ : ``
+}`;
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/npmignore.ts b/packages/labs/cli/src/lib/init/element-starter/templates/npmignore.ts
new file mode 100644
index 0000000000..2a4f0dc799
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/npmignore.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+
+export const generateNpmignore = (): FileTree => {
+ return {
+ '.npmignore': `node_modules
+.vscode
+README.md
+index.html
+src`,
+ };
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/package.json.ts b/packages/labs/cli/src/lib/init/element-starter/templates/package.json.ts
new file mode 100644
index 0000000000..5f558c2176
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/package.json.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {Language} from '../../../commands/init.js';
+import {litVersion} from '../../../lit-version.js';
+
+export const generatePackageJson = (
+ elementName: string,
+ language: Language
+): FileTree => {
+ return {
+ 'package.json': `{
+ "name": "${elementName}",
+ "version": "0.0.1",
+ "description": "A Minimal Lit Element starter kit",
+ "type": "module",
+ "main": "lib/${elementName}.js",
+ "scripts": {
+ "serve": "wds --node-resolve -nwo demo"${
+ language !== 'ts'
+ ? ''
+ : `,
+ "build": "tsc",
+ "build:watch": "tsc --watch",
+ "dev": "npm run serve & npm run build:watch"`
+ }
+ },
+ "keywords": [
+ "web-component",
+ "lit-element",
+ "lit"
+ ],
+ "dependencies": {
+ "lit": "^${litVersion}"
+ },
+ "devDependencies": {
+ "@web/dev-server": "^0.1.32"${
+ language !== 'ts'
+ ? ''
+ : `,
+ "typescript": "^4.7.4"`
+ }
+ },
+ "exports": {
+ "./lib/${elementName}.js": {
+ "default": "./lib/${elementName}.js"
+ }
+ }
+}`,
+ };
+};
diff --git a/packages/labs/cli/src/lib/init/element-starter/templates/tsconfig.json.ts b/packages/labs/cli/src/lib/init/element-starter/templates/tsconfig.json.ts
new file mode 100644
index 0000000000..f9fff23b81
--- /dev/null
+++ b/packages/labs/cli/src/lib/init/element-starter/templates/tsconfig.json.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+
+export const generateTsconfig = (): FileTree => {
+ return {
+ 'tsconfig.json': `{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "es2022",
+ "lib": ["es2022", "DOM", "DOM.Iterable"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "rootDir": "src",
+ "outDir": "lib",
+ "strict": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false
+ },
+ "include": ["src/**/*.ts"]
+}`,
+ };
+};
diff --git a/packages/labs/cli/src/lib/lit-cli.ts b/packages/labs/cli/src/lib/lit-cli.ts
index 18b00d0587..ae3cc5af9c 100644
--- a/packages/labs/cli/src/lib/lit-cli.ts
+++ b/packages/labs/cli/src/lib/lit-cli.ts
@@ -19,12 +19,14 @@ import {globalOptions, mergeOptions} from './options.js';
import {makeHelpCommand} from './commands/help.js';
import {localize} from './commands/localize.js';
import {makeLabsCommand} from './commands/labs.js';
+import {makeInitCommand} from './commands/init.js';
import {createRequire} from 'module';
import * as childProcess from 'child_process';
export interface Options {
+ // Mandatory, so that all tests must specify it.
+ cwd: string;
console?: LitConsole;
- cwd?: string;
stdin?: NodeJS.ReadableStream;
}
@@ -33,12 +35,12 @@ export class LitCli {
readonly args: readonly string[];
readonly console: LitConsole;
/** The current working directory. */
- private readonly cwd: string;
+ readonly cwd: string;
private readonly stdin: NodeJS.ReadableStream;
- constructor(args: string[], options?: Options) {
- this.stdin = options?.stdin ?? process.stdin;
- this.cwd = options?.cwd ?? process.cwd();
+ constructor(args: string[], options: Options) {
+ this.cwd = options.cwd;
+ this.stdin = options.stdin ?? process.stdin;
this.console =
options?.console ?? new LitConsole(process.stdout, process.stderr);
this.console.logLevel = 'info';
@@ -62,6 +64,7 @@ export class LitCli {
this.addCommand(localize);
this.addCommand(makeLabsCommand(this));
this.addCommand(makeHelpCommand(this));
+ this.addCommand(makeInitCommand(this));
}
addCommand(command: Command) {
@@ -92,7 +95,10 @@ export class LitCli {
const result = await this.getCommand(this.commands, this.args);
if ('invalidCommand' in result) {
- return helpCommand.run({command: [result.invalidCommand]}, this.console);
+ return await helpCommand.run(
+ {command: [result.invalidCommand]},
+ this.console
+ );
} else if ('commandNotInstalled' in result) {
this.console.error(`Command not installed.`);
return {exitCode: 1};
@@ -123,11 +129,11 @@ export class LitCli {
this.console.debug(
`'--help' option found, running 'help' for given command...`
);
- return helpCommand.run({command: commandName}, this.console);
+ return await helpCommand.run({command: commandName}, this.console);
}
this.console.debug('Running command...');
- return command.run(commandOptions, this.console);
+ return await command.run(commandOptions, this.console);
}
}
@@ -281,6 +287,7 @@ export class LitCli {
return false;
}
const installFrom = reference.installFrom ?? reference.importSpecifier;
+ this.console.log(`Installing ${installFrom}...`);
const child = childProcess.spawn(
// https://stackoverflow.com/questions/43230346/error-spawn-npm-enoent
/^win/.test(process.platform) ? 'npm.cmd' : 'npm',
diff --git a/packages/labs/cli/src/lib/lit-version.ts b/packages/labs/cli/src/lib/lit-version.ts
new file mode 100644
index 0000000000..d8b86acb93
--- /dev/null
+++ b/packages/labs/cli/src/lib/lit-version.ts
@@ -0,0 +1 @@
+export const litVersion = '2.2.8';
diff --git a/packages/labs/cli/src/lib/localize/extract.ts b/packages/labs/cli/src/lib/localize/extract.ts
deleted file mode 100644
index 111ed2fb6f..0000000000
--- a/packages/labs/cli/src/lib/localize/extract.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-import {
- readConfigFileAndWriteSchema,
- Config,
- TransformOutputConfig,
- RuntimeOutputConfig,
-} from '@lit/localize-tools/lib/config.js';
-import {TransformLitLocalizer} from '@lit/localize-tools/lib/modes/transform.js';
-import {RuntimeLitLocalizer} from '@lit/localize-tools/lib/modes/runtime.js';
-import {printDiagnostics} from '@lit/localize-tools/lib/typescript.js';
-import {KnownError, unreachable} from '@lit/localize-tools/lib/error.js';
-import {LitLocalizer} from '@lit/localize-tools/lib/index.js';
-
-export const run = async (configPath: string, console: Console) => {
- const config = readConfigFileAndWriteSchema(configPath);
- const localizer = makeLocalizer(config);
- // TODO(aomarks) Don't even require the user to have configured their output
- // mode if they're just doing extraction.
- console.log('Extracting messages');
- const {messages, errors} = localizer.extractSourceMessages();
- if (errors.length > 0) {
- printDiagnostics(errors);
- throw new KnownError('Error analyzing program');
- }
- console.log(`Extracted ${messages.length} messages`);
- console.log(`Writing interchange files`);
- await localizer.writeInterchangeFiles();
-};
-
-const makeLocalizer = (config: Config): LitLocalizer => {
- switch (config.output.mode) {
- case 'transform':
- return new TransformLitLocalizer(
- // TODO(aomarks) Unfortunate that TypeScript doesn't automatically do
- // this type narrowing. Because the union is on a nested property?
- config as Config & {output: TransformOutputConfig}
- );
- case 'runtime':
- return new RuntimeLitLocalizer(
- config as Config & {output: RuntimeOutputConfig}
- );
- default:
- throw new KnownError(
- `Internal error: unknown mode ${
- (unreachable(config.output as never) as Config['output']).mode
- }`
- );
- }
-};
diff --git a/packages/labs/cli/src/test/cli-test-utils.ts b/packages/labs/cli/src/test/cli-test-utils.ts
index 95c556da98..0187c74e43 100644
--- a/packages/labs/cli/src/test/cli-test-utils.ts
+++ b/packages/labs/cli/src/test/cli-test-utils.ts
@@ -89,6 +89,10 @@ export const symlinkAllCommands = async (rig: FilesystemTestRig) => {
relPath: ['..', '..', '..', 'gen-wrapper-vue'],
packageName: ['@lit-labs', 'gen-wrapper-vue'],
},
+ {
+ relPath: ['..', '..', '..', 'cli-localize'],
+ packageName: ['@lit-labs', 'cli-localize'],
+ },
];
await Promise.all(
symLinks.map(async ({packageName, relPath}) => {
diff --git a/packages/labs/cli/src/test/gen/react_test.ts b/packages/labs/cli/src/test/gen/react_test.ts
index 8f27ead544..e9d39fa48e 100644
--- a/packages/labs/cli/src/test/gen/react_test.ts
+++ b/packages/labs/cli/src/test/gen/react_test.ts
@@ -55,7 +55,7 @@ test('basic wrapper generation', async ({rig, testConsole}) => {
testConsole.alsoLogToGlobalConsole = true;
await cli.run();
- assert.equal(testConsole.errorStream.buffer.join(''), '');
+ assert.snapshot(testConsole.errorStream.text, '');
// Note, this is only a very basic test that wrapper generation succeeds when
// executed via the CLI. For detailed tests, see tests in
diff --git a/packages/labs/cli/src/test/help_test.ts b/packages/labs/cli/src/test/help_test.ts
index ab186af1c1..33cf78a23d 100644
--- a/packages/labs/cli/src/test/help_test.ts
+++ b/packages/labs/cli/src/test/help_test.ts
@@ -4,12 +4,11 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import 'source-map-support/register.js';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
import {LitCli} from '../lib/lit-cli.js';
import {LitConsole} from '../lib/console.js';
-import {BufferedWritable} from './cli-test-utils.js';
+import {BufferedWritable, symlinkAllCommands} from './cli-test-utils.js';
import {ReferenceToCommand} from '../lib/command.js';
import {ConsoleConstructorOptions} from 'console';
import * as stream from 'stream';
@@ -50,41 +49,47 @@ test.after.each(async (ctx) => {
await ctx.fs.cleanup();
});
-test('help with no command', async ({console, stdin}) => {
- const cli = new LitCli(['help'], {console, stdin: stdin});
+test('help with no command', async ({fs, console, stdin}) => {
+ const cli = new LitCli(['help'], {console, stdin: stdin, cwd: fs.rootDir});
await cli.run();
const output = console.outputStream.text;
- assert.equal(console.errorStream.buffer.length, 0);
+ assert.snapshot(console.errorStream.text, '');
assert.match(output, 'Lit CLI');
assert.match(output, 'Available Commands');
assert.match(output, 'localize');
});
-test('help with localize command', async ({console, stdin}) => {
- const cli = new LitCli(['help', 'localize'], {console, stdin});
+test('help with localize command', async ({fs, console, stdin}) => {
+ await symlinkAllCommands(fs);
+ const cli = new LitCli(['help', 'localize'], {
+ console,
+ stdin,
+ cwd: fs.rootDir,
+ });
await cli.run();
const output = console.outputStream.text;
- assert.equal(console.errorStream.buffer.length, 0);
assert.match(output, 'lit localize');
assert.match(output, 'Sub-Commands');
assert.match(output, 'extract');
assert.match(output, 'build');
});
-test('help with localize extract command', async ({console, stdin}) => {
+test('help with localize extract command', async ({fs, console, stdin}) => {
+ await symlinkAllCommands(fs);
const cli = new LitCli(['help', 'localize', 'extract'], {
console,
stdin,
+ cwd: fs.rootDir,
});
await cli.run();
const output = console.outputStream.text;
- assert.equal(console.errorStream.buffer.length, 0);
+ assert.snapshot(console.errorStream.text, '');
assert.match(output, 'lit localize extract');
assert.match(output, '--config');
});
@@ -109,7 +114,7 @@ test('help includes unresolved external command descriptions', async ({
cli.addCommand(fooCommandReference);
await cli.run();
const output = console.outputStream.text;
- assert.equal(console.errorStream.buffer.length, 0);
+ assert.snapshot(console.errorStream.text, '');
assert.match(
output,
/\s+foo\s+This is the description in the `foo` reference./
@@ -183,7 +188,7 @@ test(`help for a resolved external command`, async ({console, fs, stdin}) => {
cli.addCommand(fooCommandReference);
await cli.run();
output = console.outputStream.text;
- assert.equal(console.errorStream.buffer.length, 0);
+ assert.snapshot(console.errorStream.text, '');
assert.match(
output,
/\s+foo\s+this is the resolved foo command from the node_modules directory/
@@ -224,7 +229,7 @@ test('we install a referenced command with permission', async ({
});
cli.addCommand({...fooCommandReference, installFrom: '../foo-package'});
await cli.run();
- assert.equal(console.errorStream.text, '');
+ assert.snapshot(console.errorStream.text, '');
// The npm install happend.
assert.match(console.outputStream.text, 'added 1 package');
// After installation, we were able to resolve the command.
diff --git a/packages/labs/cli/src/test/init/element_test.ts b/packages/labs/cli/src/test/init/element_test.ts
new file mode 100644
index 0000000000..14254391b8
--- /dev/null
+++ b/packages/labs/cli/src/test/init/element_test.ts
@@ -0,0 +1,159 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+import {LitCli} from '../../lib/lit-cli.js';
+import {suite} from '../uvu-wrapper.js';
+import {FilesystemTestRig} from '@lit-internal/tests/utils/filesystem-test-rig.js';
+import {symlinkAllCommands, TestConsole} from '../cli-test-utils.js';
+import {assertGoldensMatch} from '@lit-internal/tests/utils/assert-goldens.js';
+import path from 'path';
+
+interface TestContext {
+ testConsole: TestConsole;
+ rig: FilesystemTestRig;
+}
+
+const test = suite();
+
+test.before.each(async (ctx) => {
+ const rig = new FilesystemTestRig();
+ await rig.setup();
+ ctx.rig = rig;
+ ctx.testConsole = new TestConsole();
+});
+
+test.after.each(async ({rig}) => {
+ await rig.cleanup();
+});
+
+test('element generation default', async ({rig, testConsole}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(['init', 'element'], {
+ cwd: rig.rootDir,
+ console: testConsole,
+ });
+ testConsole.alsoLogToGlobalConsole = true;
+ await cli.run();
+
+ assert.equal(testConsole.errorStream.buffer.join(''), '');
+
+ await assertGoldensMatch(
+ path.join(rig.rootDir, 'my-element'),
+ path.join('test-goldens/init', 'js'),
+ {
+ noFormat: true,
+ }
+ );
+});
+
+test('lit init command runs lit init element with defaults', async ({
+ rig,
+ testConsole,
+}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(['init'], {
+ cwd: rig.rootDir,
+ console: testConsole,
+ });
+ testConsole.alsoLogToGlobalConsole = true;
+ await cli.run();
+
+ assert.equal(testConsole.errorStream.buffer.join(''), '');
+
+ await assertGoldensMatch(
+ path.join(rig.rootDir, 'my-element'),
+ path.join('test-goldens/init', 'js'),
+ {
+ noFormat: true,
+ }
+ );
+});
+
+test('element generation named', async ({rig, testConsole}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(['init', 'element', '--name', 'le-element'], {
+ cwd: rig.rootDir,
+ console: testConsole,
+ });
+ testConsole.alsoLogToGlobalConsole = true;
+ await cli.run();
+
+ assert.equal(testConsole.errorStream.buffer.join(''), '');
+
+ await assertGoldensMatch(
+ path.join(rig.rootDir, 'le-element'),
+ path.join('test-goldens/init', 'js-named'),
+ {
+ noFormat: true,
+ }
+ );
+});
+
+test('element generation invalid name', async ({rig, testConsole}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(['init', 'element', '--name', 'element'], {
+ cwd: rig.rootDir,
+ console: testConsole,
+ });
+ testConsole.alsoLogToGlobalConsole = true;
+ let errorThrown = false;
+
+ try {
+ await cli.run();
+ } catch {
+ // Expected
+ errorThrown = true;
+ }
+
+ assert.ok(errorThrown, 'No invalid name error thrown');
+});
+
+test('element generation TS named', async ({rig, testConsole}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(
+ ['init', 'element', '--name', 'el-element', '--lang', 'ts'],
+ {
+ cwd: rig.rootDir,
+ console: testConsole,
+ }
+ );
+ testConsole.alsoLogToGlobalConsole = true;
+ await cli.run();
+
+ assert.equal(testConsole.errorStream.buffer.join(''), '');
+
+ await assertGoldensMatch(
+ path.join(rig.rootDir, 'el-element'),
+ path.join('test-goldens/init', 'ts-named'),
+ {
+ noFormat: true,
+ }
+ );
+});
+
+test('default config in subdirectory', async ({rig, testConsole}) => {
+ await symlinkAllCommands(rig);
+ const cli = new LitCli(['init', 'element', '--dir', 'subdir'], {
+ cwd: rig.rootDir,
+ console: testConsole,
+ });
+ testConsole.alsoLogToGlobalConsole = true;
+ await cli.run();
+
+ assert.equal(testConsole.errorStream.buffer.join(''), '');
+
+ await assertGoldensMatch(
+ path.join(rig.rootDir, 'subdir', 'my-element'),
+ path.join('test-goldens/init', 'js'),
+ {
+ noFormat: true,
+ }
+ );
+});
+
+test.run();
diff --git a/packages/labs/cli/src/test/uvu-wrapper.ts b/packages/labs/cli/src/test/uvu-wrapper.ts
index 14df06c00d..8343029781 100644
--- a/packages/labs/cli/src/test/uvu-wrapper.ts
+++ b/packages/labs/cli/src/test/uvu-wrapper.ts
@@ -37,19 +37,21 @@ function timeout(
test: uvu.Callback
): uvu.Callback {
return async (ctx) => {
- let timeoutId: ReturnType;
- const result = Promise.race([
- test(ctx),
- new Promise((_, reject) => {
- timeoutId = setTimeout(
- () => reject(new Error(`Test timed out: ${JSON.stringify(name)}`)),
- ms
- );
- }),
- ]);
- result.finally(() => {
- clearTimeout(timeoutId);
- });
- return result;
+ let timeoutId: ReturnType | undefined;
+ try {
+ return await Promise.race([
+ test(ctx),
+ new Promise((_, reject) => {
+ timeoutId = setTimeout(
+ () => reject(new Error(`Test timed out: ${JSON.stringify(name)}`)),
+ ms
+ );
+ }),
+ ]);
+ } finally {
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ }
};
}
diff --git a/packages/labs/cli/test-goldens/init/js-named/.gitignore b/packages/labs/cli/test-goldens/init/js-named/.gitignore
new file mode 100644
index 0000000000..b512c09d47
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js-named/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/js-named/.npmignore b/packages/labs/cli/test-goldens/init/js-named/.npmignore
new file mode 100644
index 0000000000..535a7f97ec
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js-named/.npmignore
@@ -0,0 +1,5 @@
+node_modules
+.vscode
+README.md
+index.html
+src
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/js-named/demo/index.html b/packages/labs/cli/test-goldens/init/js-named/demo/index.html
new file mode 100644
index 0000000000..30490ad199
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js-named/demo/index.html
@@ -0,0 +1,10 @@
+
+
+
+ le-element demo
+
+
+
+
+
+
diff --git a/packages/labs/cli/test-goldens/init/js-named/lib/le-element.js b/packages/labs/cli/test-goldens/init/js-named/lib/le-element.js
new file mode 100644
index 0000000000..4cbca10550
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js-named/lib/le-element.js
@@ -0,0 +1,54 @@
+import {LitElement, html, css} from 'lit';
+
+/**
+ * An example element.
+ *
+ * @fires count-changed - Indicates when the count changes
+ * @slot - This element has a slot
+ * @csspart button - The button
+ * @cssprop --le-element-font-size - The button's font size
+ */
+export class LeElement extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ border: solid 1px gray;
+ padding: 16px;
+ }
+
+ button {
+ font-size: var(--le-element-font-size, 16px);
+ }
+ `;
+
+ static properties = {
+ /**
+ * The number of times the button has been clicked.
+ * @type {number}
+ */
+ count: {type: Number},
+ }
+
+ constructor() {
+ super();
+ this.count = 0;
+ }
+
+ render() {
+ return html`
+ Hello World
+
+
+ `;
+ }
+
+ _onClick() {
+ this.count++;
+ const event = new Event('count-changed', {bubbles: true});
+ this.dispatchEvent(event);
+ }
+}
+
+customElements.define('le-element', LeElement);
diff --git a/packages/labs/cli/test-goldens/init/js-named/package.json b/packages/labs/cli/test-goldens/init/js-named/package.json
new file mode 100644
index 0000000000..0f290243eb
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js-named/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "le-element",
+ "version": "0.0.1",
+ "description": "A Minimal Lit Element starter kit",
+ "type": "module",
+ "main": "lib/le-element.js",
+ "scripts": {
+ "serve": "wds --node-resolve -nwo demo"
+ },
+ "keywords": [
+ "web-component",
+ "lit-element",
+ "lit"
+ ],
+ "dependencies": {
+ "lit": "^2.2.8"
+ },
+ "devDependencies": {
+ "@web/dev-server": "^0.1.32"
+ },
+ "exports": {
+ "./lib/le-element.js": {
+ "default": "./lib/le-element.js"
+ }
+ }
+}
diff --git a/packages/labs/cli/test-goldens/init/js/.gitignore b/packages/labs/cli/test-goldens/init/js/.gitignore
new file mode 100644
index 0000000000..b512c09d47
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/js/.npmignore b/packages/labs/cli/test-goldens/init/js/.npmignore
new file mode 100644
index 0000000000..535a7f97ec
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js/.npmignore
@@ -0,0 +1,5 @@
+node_modules
+.vscode
+README.md
+index.html
+src
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/js/demo/index.html b/packages/labs/cli/test-goldens/init/js/demo/index.html
new file mode 100644
index 0000000000..9ef5976353
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js/demo/index.html
@@ -0,0 +1,10 @@
+
+
+
+ my-element demo
+
+
+
+
+
+
diff --git a/packages/labs/cli/test-goldens/init/js/lib/my-element.js b/packages/labs/cli/test-goldens/init/js/lib/my-element.js
new file mode 100644
index 0000000000..8861fc64a1
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js/lib/my-element.js
@@ -0,0 +1,54 @@
+import {LitElement, html, css} from 'lit';
+
+/**
+ * An example element.
+ *
+ * @fires count-changed - Indicates when the count changes
+ * @slot - This element has a slot
+ * @csspart button - The button
+ * @cssprop --my-element-font-size - The button's font size
+ */
+export class MyElement extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ border: solid 1px gray;
+ padding: 16px;
+ }
+
+ button {
+ font-size: var(--my-element-font-size, 16px);
+ }
+ `;
+
+ static properties = {
+ /**
+ * The number of times the button has been clicked.
+ * @type {number}
+ */
+ count: {type: Number},
+ }
+
+ constructor() {
+ super();
+ this.count = 0;
+ }
+
+ render() {
+ return html`
+ Hello World
+
+
+ `;
+ }
+
+ _onClick() {
+ this.count++;
+ const event = new Event('count-changed', {bubbles: true});
+ this.dispatchEvent(event);
+ }
+}
+
+customElements.define('my-element', MyElement);
diff --git a/packages/labs/cli/test-goldens/init/js/package.json b/packages/labs/cli/test-goldens/init/js/package.json
new file mode 100644
index 0000000000..cd3ff42100
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/js/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "my-element",
+ "version": "0.0.1",
+ "description": "A Minimal Lit Element starter kit",
+ "type": "module",
+ "main": "lib/my-element.js",
+ "scripts": {
+ "serve": "wds --node-resolve -nwo demo"
+ },
+ "keywords": [
+ "web-component",
+ "lit-element",
+ "lit"
+ ],
+ "dependencies": {
+ "lit": "^2.2.8"
+ },
+ "devDependencies": {
+ "@web/dev-server": "^0.1.32"
+ },
+ "exports": {
+ "./lib/my-element.js": {
+ "default": "./lib/my-element.js"
+ }
+ }
+}
diff --git a/packages/labs/cli/test-goldens/init/ts-named/.gitignore b/packages/labs/cli/test-goldens/init/ts-named/.gitignore
new file mode 100644
index 0000000000..9b26ed04f1
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+lib
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/ts-named/.npmignore b/packages/labs/cli/test-goldens/init/ts-named/.npmignore
new file mode 100644
index 0000000000..535a7f97ec
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/.npmignore
@@ -0,0 +1,5 @@
+node_modules
+.vscode
+README.md
+index.html
+src
\ No newline at end of file
diff --git a/packages/labs/cli/test-goldens/init/ts-named/demo/index.html b/packages/labs/cli/test-goldens/init/ts-named/demo/index.html
new file mode 100644
index 0000000000..32d3637b6e
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/demo/index.html
@@ -0,0 +1,10 @@
+
+
+
+ el-element demo
+
+
+
+
+
+
diff --git a/packages/labs/cli/test-goldens/init/ts-named/package.json b/packages/labs/cli/test-goldens/init/ts-named/package.json
new file mode 100644
index 0000000000..b36c4aefeb
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "el-element",
+ "version": "0.0.1",
+ "description": "A Minimal Lit Element starter kit",
+ "type": "module",
+ "main": "lib/el-element.js",
+ "scripts": {
+ "serve": "wds --node-resolve -nwo demo",
+ "build": "tsc",
+ "build:watch": "tsc --watch",
+ "dev": "npm run serve & npm run build:watch"
+ },
+ "keywords": [
+ "web-component",
+ "lit-element",
+ "lit"
+ ],
+ "dependencies": {
+ "lit": "^2.2.8"
+ },
+ "devDependencies": {
+ "@web/dev-server": "^0.1.32",
+ "typescript": "^4.7.4"
+ },
+ "exports": {
+ "./lib/el-element.js": {
+ "default": "./lib/el-element.js"
+ }
+ }
+}
diff --git a/packages/labs/cli/test-goldens/init/ts-named/src/el-element.ts b/packages/labs/cli/test-goldens/init/ts-named/src/el-element.ts
new file mode 100644
index 0000000000..fd82cd1a0b
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/src/el-element.ts
@@ -0,0 +1,47 @@
+import {LitElement, html, css} from 'lit';
+import {property, customElement} from 'lit/decorators.js';
+
+/**
+ * An example element.
+ *
+ * @fires count-changed - Indicates when the count changes
+ * @slot - This element has a slot
+ * @csspart button - The button
+ * @cssprop --el-element-font-size - The button's font size
+ */
+@customElement('el-element')
+export class ElElement extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ border: solid 1px gray;
+ padding: 16px;
+ }
+
+ button {
+ font-size: var(--el-element-font-size, 16px);
+ }
+ `;
+
+ /**
+ * The number of times the button has been clicked.
+ */
+ @property({type: Number})
+ count = 0;
+
+ render() {
+ return html`
+ Hello World
+
+
+ `;
+ }
+
+ protected _onClick() {
+ this.count++;
+ const event = new Event('count-changed', {bubbles: true});
+ this.dispatchEvent(event);
+ }
+}
diff --git a/packages/labs/cli/test-goldens/init/ts-named/tsconfig.json b/packages/labs/cli/test-goldens/init/ts-named/tsconfig.json
new file mode 100644
index 0000000000..102bdf01d6
--- /dev/null
+++ b/packages/labs/cli/test-goldens/init/ts-named/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "es2022",
+ "lib": ["es2022", "DOM", "DOM.Iterable"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "rootDir": "src",
+ "outDir": "lib",
+ "strict": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/labs/context/README.md b/packages/labs/context/README.md
index 76929b6cb0..8d3076fb6b 100644
--- a/packages/labs/context/README.md
+++ b/packages/labs/context/README.md
@@ -32,18 +32,18 @@ export const loggerContext = createContext('logger');
Now we can define a consumer for this context - some component in our app needs the logger.
-Here we're using the `@contextProvided` property decorator to make a `ContextConsumer` controller
+Here we're using the `@consume` property decorator to make a `ContextConsumer` controller
and update its value when the context changes:
#### **`my-element.ts`**:
```ts
-import {contextRequest} from '@lit-labs/context';
import {LitElement, property} from 'lit';
+import {consume} from '@lit-labs/context';
import {Logger, loggerContext} from './logger.js';
export class MyElement extends LitElement {
- @contextProvided({context: loggerContext, subscribe: true})
+ @consume({context: loggerContext, subscribe: true})
@property({attribute: false})
public logger?: Logger;
@@ -80,18 +80,18 @@ export class MyElement extends LitElement {
Finally we want to be able to provide this context from somewhere higher in the DOM.
-Here we're using a `@contextProvider` property decorator to make a `ContextProvider`
+Here we're using a `@provide` property decorator to make a `ContextProvider`
controller and update its value when the property value changes.
#### **`my-app.ts`**:
```ts
import {LitElement} from 'lit';
-import {contextProvider} from '@lit-labs/context';
-import {loggerContext, Logger} from './my-logger.js';
+import {provide} from '@lit-labs/context';
+import {loggerContext, Logger} from './logger.js';
export class MyApp extends LitElement {
- @contextProvider({context: loggerContext})
+ @provide({context: loggerContext})
@property({attribute: false})
public logger: Logger = {
log: (msg) => {
@@ -112,7 +112,7 @@ We can also use the `ContextProvider` controller directly:
```ts
import {LitElement} from 'lit';
import {ContextProvider} from '@lit-labs/context';
-import {loggerContext, Logger} from './my-logger.js';
+import {loggerContext, Logger} from './logger.js';
export class MyApp extends LitElement {
// create a provider controller and a default logger
diff --git a/packages/labs/context/src/index.ts b/packages/labs/context/src/index.ts
index 8584b7fd47..d3a2646a24 100644
--- a/packages/labs/context/src/index.ts
+++ b/packages/labs/context/src/index.ts
@@ -9,11 +9,29 @@ export {
ContextRequestEvent as ContextEvent,
} from './lib/context-request-event.js';
-export {ContextKey, ContextType, createContext} from './lib/context-key.js';
+export {
+ Context,
+ ContextKey,
+ ContextType,
+ createContext,
+} from './lib/create-context.js';
export {ContextConsumer} from './lib/controllers/context-consumer.js';
export {ContextProvider} from './lib/controllers/context-provider.js';
export {ContextRoot} from './lib/context-root.js';
-export {contextProvider} from './lib/decorators/context-provider.js';
-export {contextProvided} from './lib/decorators/context-provided.js';
+export {provide} from './lib/decorators/provide.js';
+export {consume} from './lib/decorators/consume.js';
+
+import {provide} from './lib/decorators/provide.js';
+import {consume} from './lib/decorators/consume.js';
+
+/**
+ * @deprecated use `provide` instead
+ */
+export const contextProvider = provide;
+
+/**
+ * @deprecated use `consume` instead
+ */
+export const contextProvided = consume;
diff --git a/packages/labs/context/src/lib/context-key.ts b/packages/labs/context/src/lib/context-key.ts
deleted file mode 100644
index e816878f58..0000000000
--- a/packages/labs/context/src/lib/context-key.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @license
- * Copyright 2021 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-/**
- * The ContextKey type defines a type brand to associate a key value with the context value type
- */
-export type ContextKey = KeyType & {__context__: ValueType};
-
-/**
- * A helper type which can extract a Context value type from a Context type
- */
-export type ContextType> =
- Key extends ContextKey ? ValueType : never;
-
-/**
- * A helper method for creating a context key with the appropriate type
- *
- * @param key a context key value
- * @returns the context key value with the correct type
- */
-export function createContext(key: unknown) {
- return key as ContextKey;
-}
diff --git a/packages/labs/context/src/lib/context-request-event.ts b/packages/labs/context/src/lib/context-request-event.ts
index 95c290d53c..ad4f9d904c 100644
--- a/packages/labs/context/src/lib/context-request-event.ts
+++ b/packages/labs/context/src/lib/context-request-event.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import {ContextType, ContextKey} from './context-key.js';
+import {ContextType, Context} from './create-context.js';
declare global {
interface HTMLElementEventMap {
@@ -12,7 +12,7 @@ declare global {
* A 'context-request' event can be emitted by any element which desires
* a context value to be injected by an external provider.
*/
- 'context-request': ContextRequestEvent>;
+ 'context-request': ContextRequestEvent>;
}
}
@@ -28,9 +28,9 @@ export type ContextCallback = (
/**
* Interface definition for a ContextRequest
*/
-export interface ContextRequest> {
- readonly context: Context;
- readonly callback: ContextCallback>;
+export interface ContextRequest> {
+ readonly context: C;
+ readonly callback: ContextCallback>;
readonly subscribe?: boolean;
}
@@ -47,9 +47,9 @@ export interface ContextRequest> {
* If no `subscribe` value is present in the event, then the provider can assume that this is a 'one time'
* request for the context and can therefore not track the consumer.
*/
-export class ContextRequestEvent>
+export class ContextRequestEvent>
extends Event
- implements ContextRequest
+ implements ContextRequest
{
/**
*
@@ -58,8 +58,8 @@ export class ContextRequestEvent>
* @param subscribe an optional argument, if true indicates we want to subscribe to future updates
*/
public constructor(
- public readonly context: Context,
- public readonly callback: ContextCallback>,
+ public readonly context: C,
+ public readonly callback: ContextCallback>,
public readonly subscribe?: boolean
) {
super('context-request', {bubbles: true, composed: true});
diff --git a/packages/labs/context/src/lib/context-root.ts b/packages/labs/context/src/lib/context-root.ts
index 2bba275c12..acb428e58d 100644
--- a/packages/labs/context/src/lib/context-root.ts
+++ b/packages/labs/context/src/lib/context-root.ts
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
-import {ContextKey} from './context-key.js';
+import {Context} from './create-context.js';
import {ContextRequest, ContextRequestEvent} from './context-request-event.js';
import {ContextProviderEvent} from './controllers/context-provider.js';
-type UnknownContextKey = ContextKey;
+type UnknownContextKey = Context;
/**
* A context request, with associated source element, with all objects as weak references.
@@ -50,7 +50,7 @@ export class ContextRoot {
}
private onContextProvider = (
- ev: ContextProviderEvent>
+ ev: ContextProviderEvent>
) => {
const pendingRequests = this.pendingContextRequests.get(ev.context);
if (!pendingRequests) {
@@ -74,7 +74,7 @@ export class ContextRoot {
};
private onContextRequest = (
- ev: ContextRequestEvent>
+ ev: ContextRequestEvent>
) => {
// events that are not subscribing should not be captured
if (!ev.subscribe) {
diff --git a/packages/labs/context/src/lib/controllers/context-consumer.ts b/packages/labs/context/src/lib/controllers/context-consumer.ts
index f7f6feeeaa..9775b53c83 100644
--- a/packages/labs/context/src/lib/controllers/context-consumer.ts
+++ b/packages/labs/context/src/lib/controllers/context-consumer.ts
@@ -5,7 +5,7 @@
*/
import {ContextRequestEvent} from '../context-request-event.js';
-import {ContextKey, ContextType} from '../context-key.js';
+import {Context, ContextType} from '../create-context.js';
import {ReactiveController, ReactiveElement} from 'lit';
/**
@@ -17,21 +17,18 @@ import {ReactiveController, ReactiveElement} from 'lit';
* disconnected.
*/
export class ContextConsumer<
- Context extends ContextKey,
+ C extends Context,
HostElement extends ReactiveElement
> implements ReactiveController
{
private provided = false;
- public value?: ContextType = undefined;
+ public value?: ContextType = undefined;
constructor(
protected host: HostElement,
- private context: Context,
- private callback?: (
- value: ContextType,
- dispose?: () => void
- ) => void,
+ private context: C,
+ private callback?: (value: ContextType, dispose?: () => void) => void,
private subscribe: boolean = false
) {
this.host.addController(this);
diff --git a/packages/labs/context/src/lib/controllers/context-provider.ts b/packages/labs/context/src/lib/controllers/context-provider.ts
index e1c3a8262e..10cd5693df 100644
--- a/packages/labs/context/src/lib/controllers/context-provider.ts
+++ b/packages/labs/context/src/lib/controllers/context-provider.ts
@@ -5,7 +5,7 @@
*/
import {ContextRequestEvent} from '../context-request-event.js';
-import {ContextKey, ContextType} from '../context-key.js';
+import {Context, ContextType} from '../create-context.js';
import {ValueNotifier} from '../value-notifier.js';
import {ReactiveController, ReactiveElement} from 'lit';
@@ -15,18 +15,18 @@ declare global {
* A 'context-provider' event can be emitted by any element which hosts
* a context provider to indicate it is available for use.
*/
- 'context-provider': ContextProviderEvent>;
+ 'context-provider': ContextProviderEvent>;
}
}
export class ContextProviderEvent<
- Context extends ContextKey
+ C extends Context
> extends Event {
/**
*
* @param context the context which this provider can provide
*/
- public constructor(public readonly context: Context) {
+ public constructor(public readonly context: C) {
super('context-provider', {bubbles: true, composed: true});
}
}
@@ -39,7 +39,7 @@ export class ContextProviderEvent<
* the host is connected to the DOM and registers the received callbacks
* against its observable Context implementation.
*/
-export class ContextProvider>
+export class ContextProvider>
extends ValueNotifier>
implements ReactiveController
{
@@ -49,12 +49,12 @@ export class ContextProvider>
initialValue?: ContextType
) {
super(initialValue);
- this.host.addController(this);
this.attachListeners();
+ this.host.addController(this);
}
public onContextRequest = (
- ev: ContextRequestEvent>
+ ev: ContextRequestEvent>
): void => {
// Only call the callback if the context matches.
// Also, in case an element is a consumer AND a provider
diff --git a/packages/labs/context/src/lib/create-context.ts b/packages/labs/context/src/lib/create-context.ts
new file mode 100644
index 0000000000..5e8840ba49
--- /dev/null
+++ b/packages/labs/context/src/lib/create-context.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright 2021 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * The Context type defines a type brand to associate a key value with the context value type
+ */
+export type Context = KeyType & {__context__: ValueType};
+
+/**
+ * @deprecated use Context instead
+ */
+export type ContextKey = Context;
+
+/**
+ * A helper type which can extract a Context value type from a Context type
+ */
+export type ContextType> =
+ Key extends Context ? ValueType : never;
+
+/**
+ * Creates a typed Context.
+ *
+ * Contexts are compared with with strict equality.
+ *
+ * If you want two separate `createContext()` calls to referer to the same
+ * context, then use a key that will by equal under strict equality like a
+ * string for `Symbol.for()`:
+ *
+ * ```ts
+ * // true
+ * createContext('my-context') === createContext('my-context')
+ * // true
+ * createContext(Symbol.for('my-context')) === createContext(Symbol.for('my-context'))
+ * ```
+ *
+ * If you want a context to be unique so that it's guaranteed to not collide
+ * with other contexts, use a key that's unique under strict equality, like
+ * a `Symbol()` or object.:
+ *
+ * ```
+ * // false
+ * createContext({}) === createContext({})
+ * // false
+ * createContext(Symbol('my-context')) === createContext(Symbol('my-context'))
+ * ```
+ *
+ * @param key a context key value
+ * @template ValueType the type of value that can be provided by this context.
+ * @returns the context key value with the correct type
+ */
+export function createContext(key: unknown) {
+ return key as Context;
+}
diff --git a/packages/labs/context/src/lib/decorators/context-provided.ts b/packages/labs/context/src/lib/decorators/consume.ts
similarity index 84%
rename from packages/labs/context/src/lib/decorators/context-provided.ts
rename to packages/labs/context/src/lib/decorators/consume.ts
index aa922d99df..b544363370 100644
--- a/packages/labs/context/src/lib/decorators/context-provided.ts
+++ b/packages/labs/context/src/lib/decorators/consume.ts
@@ -7,7 +7,7 @@
import {ReactiveElement} from '@lit/reactive-element';
import {decorateProperty} from '@lit/reactive-element/decorators/base.js';
import {ContextConsumer} from '../controllers/context-consumer.js';
-import {ContextKey} from '../context-key.js';
+import {Context} from '../create-context.js';
/*
* IMPORTANT: For compatibility with tsickle and the Closure JS compiler, all
@@ -27,10 +27,11 @@ import {ContextKey} from '../context-key.js';
* @example
*
* ```ts
+ * import {consume} from '@lit-labs/context';
* import {loggerContext, Logger} from 'community-protocols/logger';
*
* class MyElement {
- * @contextProvided({context: loggerContext})
+ * @consume({context: loggerContext})
* logger?: Logger;
*
* doThing() {
@@ -40,14 +41,15 @@ import {ContextKey} from '../context-key.js';
* ```
* @category Decorator
*/
-export function contextProvided({
+export function consume({
context: context,
subscribe,
}: {
- context: ContextKey;
+ context: Context;
subscribe?: boolean;
}): (
- protoOrDescriptor: ReactiveElement & Record,
+ // Partial<> allows for providing the value to an optional field
+ protoOrDescriptor: ReactiveElement & Partial>,
name?: K
// Note TypeScript requires the return type to be `void|any`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/labs/context/src/lib/decorators/context-provider.ts b/packages/labs/context/src/lib/decorators/provide.ts
similarity index 92%
rename from packages/labs/context/src/lib/decorators/context-provider.ts
rename to packages/labs/context/src/lib/decorators/provide.ts
index 60cab8af31..01feecdd34 100644
--- a/packages/labs/context/src/lib/decorators/context-provider.ts
+++ b/packages/labs/context/src/lib/decorators/provide.ts
@@ -6,7 +6,7 @@
import {ReactiveElement} from '@lit/reactive-element';
import {decorateProperty} from '@lit/reactive-element/decorators/base.js';
-import {ContextKey} from '../context-key.js';
+import {Context} from '../create-context.js';
import {ContextProvider} from '../controllers/context-provider.js';
/*
@@ -29,10 +29,11 @@ import {ContextProvider} from '../controllers/context-provider.js';
* @example
*
* ```ts
+ * import {consume} from '@lit-labs/context';
* import {loggerContext} from 'community-protocols/logger';
*
* class MyElement {
- * @contextProvided(loggerContext)
+ * @provide(loggerContext)
* logger;
*
* doThing() {
@@ -42,10 +43,10 @@ import {ContextProvider} from '../controllers/context-provider.js';
* ```
* @category Decorator
*/
-export function contextProvider({
+export function provide({
context: context,
}: {
- context: ContextKey;
+ context: Context;
}): (
protoOrDescriptor: ReactiveElement & Record,
name?: K
diff --git a/packages/labs/context/src/test/context-provider_test.ts b/packages/labs/context/src/test/context-provider_test.ts
index 0a4031b869..5d069e1818 100644
--- a/packages/labs/context/src/test/context-provider_test.ts
+++ b/packages/labs/context/src/test/context-provider_test.ts
@@ -7,15 +7,20 @@
import {LitElement, html, TemplateResult} from 'lit';
import {property} from 'lit/decorators/property.js';
-import {ContextKey, contextProvided, contextProvider} from '@lit-labs/context';
+import {Context, consume, provide} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
-const simpleContext = 'simple-context' as ContextKey<'simple-context', number>;
+const simpleContext = 'simple-context' as Context<'simple-context', number>;
class ContextConsumerElement extends LitElement {
- @contextProvided({context: simpleContext, subscribe: true})
+ @consume({context: simpleContext, subscribe: true})
@property({type: Number})
- public value = 0;
+ public value?: number;
+
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ @consume({context: simpleContext, subscribe: true})
+ @property({type: Number})
+ public value2?: string;
protected render(): TemplateResult {
return html`Value ${this.value}`;
@@ -24,7 +29,7 @@ class ContextConsumerElement extends LitElement {
customElements.define('context-consumer', ContextConsumerElement);
class ContextProviderElement extends LitElement {
- @contextProvider({context: simpleContext})
+ @provide({context: simpleContext})
@property({type: Number, reflect: true})
public value = 0;
@@ -38,7 +43,7 @@ class ContextProviderElement extends LitElement {
}
customElements.define('context-provider', ContextProviderElement);
-suite('@contextProvided', () => {
+suite('@consume', () => {
let consumer: ContextConsumerElement;
let provider: ContextProviderElement;
let container: HTMLElement;
@@ -81,7 +86,7 @@ suite('@contextProvided', () => {
});
});
-suite('@contextProvided: multiple instances', () => {
+suite('@consume: multiple instances', () => {
let consumers: ContextConsumerElement[];
let providers: ContextProviderElement[];
let container: HTMLElement;
diff --git a/packages/labs/context/src/test/context-request_test.ts b/packages/labs/context/src/test/context-request_test.ts
index 0afa993482..4d1f74aef7 100644
--- a/packages/labs/context/src/test/context-request_test.ts
+++ b/packages/labs/context/src/test/context-request_test.ts
@@ -11,7 +11,7 @@ import {
ContextConsumer,
ContextProvider,
createContext,
- contextProvided,
+ consume,
} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
@@ -31,12 +31,12 @@ class SimpleContextProvider extends LitElement {
class SimpleContextConsumer extends LitElement {
// a one-time property fullfilled by context
- @contextProvided({context: simpleContext})
+ @consume({context: simpleContext})
@property({type: Number})
public onceValue = 0;
// a subscribed property fulfilled by context
- @contextProvided({context: simpleContext, subscribe: true})
+ @consume({context: simpleContext, subscribe: true})
@property({type: Number})
public subscribedValue = 0;
diff --git a/packages/labs/context/src/test/late-provider_test.ts b/packages/labs/context/src/test/late-provider_test.ts
index e311666e53..ff40e35d53 100644
--- a/packages/labs/context/src/test/late-provider_test.ts
+++ b/packages/labs/context/src/test/late-provider_test.ts
@@ -5,24 +5,26 @@
*/
import {LitElement, html, TemplateResult} from 'lit';
-import {property} from 'lit/decorators/property.js';
+import {customElement, property} from 'lit/decorators.js';
import {
- ContextKey,
- contextProvided,
- contextProvider,
+ Context,
+ consume,
+ provide,
ContextRoot,
+ ContextProvider,
} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
-const simpleContext = 'simple-context' as ContextKey<'simple-context', number>;
+const simpleContext = 'simple-context' as Context<'simple-context', number>;
+@customElement('context-consumer')
class ContextConsumerElement extends LitElement {
- @contextProvided({context: simpleContext, subscribe: true})
+ @consume({context: simpleContext, subscribe: true})
@property({type: Number})
public value = 0;
- @contextProvided({context: simpleContext})
+ @consume({context: simpleContext})
@property({type: Number})
public onceValue = 0;
@@ -30,10 +32,9 @@ class ContextConsumerElement extends LitElement {
return html`Value ${this.value}`;
}
}
-customElements.define('context-consumer', ContextConsumerElement);
class LateContextProviderElement extends LitElement {
- @contextProvider({context: simpleContext})
+ @provide({context: simpleContext})
@property({type: Number, reflect: true})
public value = 0;
@@ -46,59 +47,96 @@ class LateContextProviderElement extends LitElement {
}
}
+@customElement('lazy-context-provider')
+export class LazyContextProviderElement extends LitElement {
+ protected render() {
+ return html``;
+ }
+}
+
suite('late context provider', () => {
- let consumer: ContextConsumerElement;
- let provider: LateContextProviderElement;
+ // let consumer: ContextConsumerElement;
+ // let provider: LateContextProviderElement;
let container: HTMLElement;
+
setup(async () => {
container = document.createElement('div');
+ document.body.append(container);
- // add a root context to catch late providers and re-dispatch requests
+ // Add a root context to catch late providers and re-dispatch requests
new ContextRoot().attach(container);
+ });
+ teardown(() => {
+ container.remove();
+ });
+
+ test(`handles late upgrade properly`, async () => {
container.innerHTML = `
-
-
-
- `;
- document.body.appendChild(container);
+
+
+
+ `;
- provider = container.querySelector(
+ const provider = container.querySelector(
'late-context-provider'
) as LateContextProviderElement;
- consumer = container.querySelector(
+ const consumer = container.querySelector(
'context-consumer'
) as ContextConsumerElement;
await consumer.updateComplete;
- assert.isDefined(consumer);
- });
-
- teardown(() => {
- document.body.removeChild(container);
- });
-
- test(`handles late upgrade properly`, async () => {
- // initially consumer has initial value
+ // Initially consumer has initial value
assert.strictEqual(consumer.value, 0);
assert.strictEqual(consumer.onceValue, 0);
- // do upgrade
+
+ // Define provider element
customElements.define('late-context-provider', LateContextProviderElement);
- // await update of provider component
+
await provider.updateComplete;
- // await update of consumer component
await consumer.updateComplete;
- // should now have provided context
+
+ // `value` should now be provided
assert.strictEqual(consumer.value, 1000);
+
// but only to the subscribed value
assert.strictEqual(consumer.onceValue, 0);
- // confirm subscription is established
+
+ // Confirm subscription is established
provider.value = 500;
await consumer.updateComplete;
assert.strictEqual(consumer.value, 500);
+
// and once was not updated
assert.strictEqual(consumer.onceValue, 0);
});
+
+ test('lazy added provider', async () => {
+ container.innerHTML = `
+
+
+
+ `;
+
+ const provider = container.querySelector(
+ 'lazy-context-provider'
+ ) as LazyContextProviderElement;
+
+ const consumer = container.querySelector(
+ 'context-consumer'
+ ) as ContextConsumerElement;
+
+ await consumer.updateComplete;
+
+ // Add a provider after the elements are setup
+ new ContextProvider(provider, simpleContext, 1000);
+
+ await provider.updateComplete;
+ await consumer.updateComplete;
+
+ // `value` should now be provided
+ assert.strictEqual(consumer.value, 1000);
+ });
});
diff --git a/packages/labs/context/src/test/provider-and-consumer_test.ts b/packages/labs/context/src/test/provider-and-consumer_test.ts
index 7340c2a656..e3d7e254ba 100644
--- a/packages/labs/context/src/test/provider-and-consumer_test.ts
+++ b/packages/labs/context/src/test/provider-and-consumer_test.ts
@@ -7,17 +7,17 @@
import {LitElement, html, TemplateResult} from 'lit';
import {property} from 'lit/decorators/property.js';
-import {ContextKey, contextProvided, contextProvider} from '@lit-labs/context';
+import {Context, consume, provide} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
-const simpleContext = 'simple-context' as ContextKey<'simple-context', number>;
+const simpleContext = 'simple-context' as Context<'simple-context', number>;
class ContextConsumerAndProviderElement extends LitElement {
- @contextProvided({context: simpleContext, subscribe: true})
+ @consume({context: simpleContext, subscribe: true})
@property({type: Number})
public provided = 0;
- @contextProvider({context: simpleContext})
+ @provide({context: simpleContext})
@property({type: Number})
public value = 0;
diff --git a/packages/labs/context/src/test/provider-consumer_test.ts b/packages/labs/context/src/test/provider-consumer_test.ts
index 3fb2ba4f4c..791344565d 100644
--- a/packages/labs/context/src/test/provider-consumer_test.ts
+++ b/packages/labs/context/src/test/provider-consumer_test.ts
@@ -7,10 +7,10 @@
import {LitElement, html, TemplateResult} from 'lit';
import {property} from 'lit/decorators/property.js';
-import {ContextProvider, ContextKey, ContextConsumer} from '@lit-labs/context';
+import {ContextProvider, Context, ContextConsumer} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
-const simpleContext = 'simple-context' as ContextKey<'simple-context', number>;
+const simpleContext = 'simple-context' as Context<'simple-context', number>;
class SimpleContextProvider extends LitElement {
private provider = new ContextProvider(this, simpleContext, 1000);
diff --git a/packages/labs/gen-manifest/.gitignore b/packages/labs/gen-manifest/.gitignore
new file mode 100644
index 0000000000..d62eb03630
--- /dev/null
+++ b/packages/labs/gen-manifest/.gitignore
@@ -0,0 +1,6 @@
+/index.js
+/index.js.map
+/index.d.ts
+/index.d.ts.map
+/test/
+/gen-output/
diff --git a/packages/labs/gen-manifest/CHANGELOG.md b/packages/labs/gen-manifest/CHANGELOG.md
new file mode 100644
index 0000000000..5d1c0490c8
--- /dev/null
+++ b/packages/labs/gen-manifest/CHANGELOG.md
@@ -0,0 +1,10 @@
+# @lit-labs/gen-manifest
+
+## 0.0.2
+
+### Patch Changes
+
+- [#2990](https://github.com/lit/lit/pull/2990) [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771) - Added initial implementation of custom elements manifest generator (WIP).
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
diff --git a/packages/labs/gen-manifest/README.md b/packages/labs/gen-manifest/README.md
new file mode 100644
index 0000000000..819c588a9d
--- /dev/null
+++ b/packages/labs/gen-manifest/README.md
@@ -0,0 +1,5 @@
+# @lit-labs/gen-manifest
+
+Utility library for generating a [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest) for Lit components.
+
+For a command-line interface to using the generator, see `@lit-labs/cli`.
diff --git a/packages/labs/gen-manifest/goldens/test-element-a/custom-elements.json b/packages/labs/gen-manifest/goldens/test-element-a/custom-elements.json
new file mode 100644
index 0000000000..8d94d86acc
--- /dev/null
+++ b/packages/labs/gen-manifest/goldens/test-element-a/custom-elements.json
@@ -0,0 +1,477 @@
+{
+ "schemaVersion": "1.0.0",
+ "modules": [
+ {
+ "kind": "javascript-module",
+ "path": "detail-type.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [],
+ "exports": [],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "element-a.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "ElementA",
+ "description": "This is a description of my element. It's pretty great. The description has\ntext that spans multiple lines.",
+ "summary": "My awesome element",
+ "superclass": {"name": "LitElement", "package": "lit"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false,
+ "tagName": "element-a",
+ "attributes": [],
+ "events": [{"name": "a-changed", "type": {"text": "TODO"}}],
+ "slots": [
+ {"name": "default", "summary": "The default slot"},
+ {"name": "stuff", "summary": "A slot for stuff"}
+ ],
+ "cssParts": [
+ {"name": "header", "summary": "The header"},
+ {"name": "footer", "summary": "The footer"}
+ ],
+ "cssProperties": [
+ {"name": "--foreground-color", "summary": "The foreground color"},
+ {"name": "--background-color", "summary": "The background color"}
+ ],
+ "demos": [],
+ "customElement": true
+ },
+ {
+ "kind": "variable",
+ "name": "localTypeVar",
+ "summary": "TODO",
+ "description": "TODO",
+ "type": {
+ "text": "ElementA",
+ "references": [
+ {
+ "name": "ElementA",
+ "package": "@lit-internal/test-element-a",
+ "module": "element-a.js",
+ "start": 0,
+ "end": 8
+ }
+ ]
+ }
+ },
+ {
+ "kind": "variable",
+ "name": "packageTypeVar",
+ "summary": "TODO",
+ "description": "TODO",
+ "type": {
+ "text": "Foo",
+ "references": [
+ {
+ "name": "Foo",
+ "package": "@lit-internal/test-element-a",
+ "module": "package-stuff.js",
+ "start": 0,
+ "end": 3
+ },
+ {
+ "name": "Bar",
+ "package": "@lit-internal/test-element-a",
+ "module": "package-stuff.js",
+ "start": 4,
+ "end": 7
+ }
+ ]
+ }
+ },
+ {
+ "kind": "variable",
+ "name": "externalTypeVar",
+ "summary": "TODO",
+ "description": "TODO",
+ "type": {
+ "text": "LitElement",
+ "references": [
+ {"name": "LitElement", "package": "lit", "start": 0, "end": 10}
+ ]
+ }
+ },
+ {
+ "kind": "variable",
+ "name": "globalTypeVar",
+ "summary": "TODO",
+ "description": "TODO",
+ "type": {
+ "text": "HTMLElement",
+ "references": [
+ {
+ "name": "HTMLElement",
+ "package": "global:",
+ "start": 0,
+ "end": 11
+ }
+ ]
+ }
+ }
+ ],
+ "exports": [
+ {"kind": "js", "name": "ElementA", "declaration": {"name": "ElementA"}},
+ {
+ "kind": "js",
+ "name": "localTypeVar",
+ "declaration": {"name": "localTypeVar"}
+ },
+ {
+ "kind": "js",
+ "name": "packageTypeVar",
+ "declaration": {"name": "packageTypeVar"}
+ },
+ {
+ "kind": "js",
+ "name": "externalTypeVar",
+ "declaration": {"name": "externalTypeVar"}
+ },
+ {
+ "kind": "js",
+ "name": "globalTypeVar",
+ "declaration": {"name": "globalTypeVar"}
+ },
+ {
+ "kind": "js",
+ "name": "Foo",
+ "declaration": {
+ "name": "Foo",
+ "package": "@lit-internal/test-element-a",
+ "module": "package-stuff.js"
+ }
+ },
+ {
+ "kind": "js",
+ "name": "Baz",
+ "declaration": {
+ "name": "Bar",
+ "package": "@lit-internal/test-element-a",
+ "module": "package-stuff.js"
+ }
+ },
+ {
+ "kind": "js",
+ "name": "local",
+ "declaration": {"name": "localTypeVar"}
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "element-a",
+ "declaration": {"name": "ElementA"}
+ }
+ ],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "element-events.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "EventSubclass",
+ "superclass": {"name": "Event", "package": "global:"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false
+ },
+ {
+ "kind": "class",
+ "name": "ElementEvents",
+ "description": "My awesome element",
+ "superclass": {"name": "LitElement", "package": "lit"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false,
+ "tagName": "element-events",
+ "attributes": [],
+ "events": [
+ {
+ "name": "string-custom-event",
+ "type": {
+ "text": "CustomEvent",
+ "references": [
+ {
+ "name": "CustomEvent",
+ "package": "global:",
+ "start": 0,
+ "end": 11
+ }
+ ]
+ }
+ },
+ {
+ "name": "number-custom-event",
+ "type": {
+ "text": "CustomEvent",
+ "references": [
+ {
+ "name": "CustomEvent",
+ "package": "global:",
+ "start": 0,
+ "end": 11
+ }
+ ]
+ }
+ },
+ {
+ "name": "my-detail-custom-event",
+ "type": {
+ "text": "CustomEvent",
+ "references": [
+ {
+ "name": "CustomEvent",
+ "package": "global:",
+ "start": 0,
+ "end": 11
+ },
+ {
+ "name": "MyDetail",
+ "package": "@lit-internal/test-element-a",
+ "module": "detail-type.js",
+ "start": 12,
+ "end": 20
+ }
+ ]
+ }
+ },
+ {
+ "name": "event-subclass",
+ "type": {
+ "text": "EventSubclass",
+ "references": [
+ {
+ "name": "EventSubclass",
+ "package": "@lit-internal/test-element-a",
+ "module": "element-events.js",
+ "start": 0,
+ "end": 13
+ }
+ ]
+ }
+ },
+ {
+ "name": "special-event",
+ "type": {
+ "text": "SpecialEvent",
+ "references": [
+ {
+ "name": "SpecialEvent",
+ "package": "@lit-internal/test-element-a",
+ "module": "special-event.js",
+ "start": 0,
+ "end": 12
+ }
+ ]
+ }
+ },
+ {
+ "name": "template-result-custom-event",
+ "type": {
+ "text": "CustomEvent",
+ "references": [
+ {
+ "name": "CustomEvent",
+ "package": "global:",
+ "start": 0,
+ "end": 11
+ },
+ {
+ "name": "TemplateResult",
+ "package": "lit",
+ "start": 12,
+ "end": 26
+ }
+ ]
+ }
+ }
+ ],
+ "slots": [],
+ "cssParts": [],
+ "cssProperties": [],
+ "demos": [],
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "SpecialEvent",
+ "declaration": {
+ "name": "SpecialEvent",
+ "package": "@lit-internal/test-element-a",
+ "module": "special-event.js"
+ }
+ },
+ {
+ "kind": "js",
+ "name": "MyDetail",
+ "declaration": {
+ "name": "MyDetail",
+ "package": "@lit-internal/test-element-a",
+ "module": "detail-type.js"
+ }
+ },
+ {
+ "kind": "js",
+ "name": "EventSubclass",
+ "declaration": {"name": "EventSubclass"}
+ },
+ {
+ "kind": "js",
+ "name": "ElementEvents",
+ "declaration": {"name": "ElementEvents"}
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "element-events",
+ "declaration": {"name": "ElementEvents"}
+ }
+ ],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "element-props.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "ElementProps",
+ "description": "My awesome element",
+ "superclass": {"name": "LitElement", "package": "lit"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false,
+ "tagName": "element-props",
+ "attributes": [],
+ "events": [{"name": "a-changed", "type": {"text": "TODO"}}],
+ "slots": [],
+ "cssParts": [],
+ "cssProperties": [],
+ "demos": [],
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "ElementProps",
+ "declaration": {"name": "ElementProps"}
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "element-props",
+ "declaration": {"name": "ElementProps"}
+ }
+ ],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "element-slots.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "ElementSlots",
+ "description": "My awesome element",
+ "superclass": {"name": "LitElement", "package": "lit"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false,
+ "tagName": "element-slots",
+ "attributes": [],
+ "events": [],
+ "slots": [],
+ "cssParts": [],
+ "cssProperties": [],
+ "demos": [],
+ "customElement": true
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "ElementSlots",
+ "declaration": {"name": "ElementSlots"}
+ },
+ {
+ "kind": "custom-element-definition",
+ "name": "element-slots",
+ "declaration": {"name": "ElementSlots"}
+ }
+ ],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "package-stuff.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "Bar",
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false
+ },
+ {
+ "kind": "class",
+ "name": "Foo",
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false
+ }
+ ],
+ "exports": [
+ {"kind": "js", "name": "Bar", "declaration": {"name": "Bar"}},
+ {"kind": "js", "name": "Foo", "declaration": {"name": "Foo"}}
+ ],
+ "deprecated": false
+ },
+ {
+ "kind": "javascript-module",
+ "path": "special-event.js",
+ "summary": "TODO",
+ "description": "TODO",
+ "declarations": [
+ {
+ "kind": "class",
+ "name": "SpecialEvent",
+ "superclass": {"name": "Event", "package": "global:"},
+ "mixins": [],
+ "members": [],
+ "source": {"href": "TODO"},
+ "deprecated": false
+ }
+ ],
+ "exports": [
+ {
+ "kind": "js",
+ "name": "SpecialEvent",
+ "declaration": {"name": "SpecialEvent"}
+ }
+ ],
+ "deprecated": false
+ }
+ ]
+}
diff --git a/packages/labs/gen-manifest/package.json b/packages/labs/gen-manifest/package.json
new file mode 100644
index 0000000000..cd804128ba
--- /dev/null
+++ b/packages/labs/gen-manifest/package.json
@@ -0,0 +1,78 @@
+{
+ "private": true,
+ "name": "@lit-labs/gen-manifest",
+ "description": "Code generator for generating Custom Elements Manifests for Lit components",
+ "version": "0.0.2",
+ "author": "Google LLC",
+ "license": "BSD-3-Clause",
+ "bugs": "https://github.com/lit/lit/issues",
+ "type": "module",
+ "main": "lib/index.js",
+ "repository": "lit/lit",
+ "scripts": {
+ "build": "wireit",
+ "test": "wireit",
+ "test:update-goldens": "UPDATE_TEST_GOLDENS=true npm run test"
+ },
+ "wireit": {
+ "build": {
+ "command": "tsc --build --pretty",
+ "dependencies": [
+ "../../tests:build",
+ "../analyzer:build",
+ "../gen-utils:build"
+ ],
+ "files": [
+ "src/**/*",
+ "tsconfig.json",
+ "../test-projects/test-element-a"
+ ],
+ "output": [
+ "test",
+ "index.{js,js.map,d.ts,d.ts.map}",
+ "tsconfig.tsbuildinfo"
+ ],
+ "clean": "if-file-deleted"
+ },
+ "test": {
+ "dependencies": [
+ "build"
+ ],
+ "files": [
+ "goldens/**/*",
+ "../test-projects/test-element-a"
+ ],
+ "#comment": "The quotes around the file regex must be double quotes on windows!",
+ "command": "cross-env NODE_OPTIONS=--enable-source-maps uvu test \"_test\\.js$\"",
+ "output": []
+ }
+ },
+ "dependencies": {
+ "@lit-labs/analyzer": "^0.4.0",
+ "@lit-labs/gen-utils": "^0.1.0",
+ "custom-elements-manifest": "^2.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^17.0.31",
+ "@lit-internal/tests": "^0.0.0",
+ "uvu": "^0.5.3"
+ },
+ "engines": {
+ "node": ">=14.8.0"
+ },
+ "files": [
+ "index.js"
+ ],
+ "homepage": "https://github.com/lit/lit",
+ "keywords": [
+ "lit",
+ "lit-html",
+ "lit-element",
+ "LitElement",
+ "generator",
+ "cli",
+ "manifest",
+ "custom-elements-manifest",
+ "CEM"
+ ]
+}
diff --git a/packages/labs/gen-manifest/src/index.ts b/packages/labs/gen-manifest/src/index.ts
new file mode 100644
index 0000000000..dd7e61ccb4
--- /dev/null
+++ b/packages/labs/gen-manifest/src/index.ts
@@ -0,0 +1,220 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {
+ ClassDeclaration,
+ Declaration,
+ Event,
+ LitElementDeclaration,
+ Module,
+ Package,
+ Reference,
+ Type,
+ VariableDeclaration,
+ LitElementExport,
+} from '@lit-labs/analyzer';
+import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import type * as cem from 'custom-elements-manifest/schema';
+
+const ifDefined = (model: O, name: K) => {
+ const obj: Partial> = {};
+ if (model[name] !== undefined) {
+ obj[name] = model[name];
+ }
+ return obj;
+};
+
+/**
+ * Our command for the Lit CLI.
+ *
+ * See ../../cli/src/lib/generate/generate.ts
+ */
+export const getCommand = () => {
+ return {
+ name: 'manifest',
+ description: 'Generate custom-elements.json manifest.',
+ kind: 'resolved',
+ async generate(options: {package: Package}): Promise {
+ return await generateManifest(options.package);
+ },
+ };
+};
+
+export const generateManifest = async (
+ analysis: Package
+): Promise => {
+ return {
+ 'custom-elements.json': JSON.stringify(convertPackage(analysis)),
+ };
+};
+
+const convertPackage = (pkg: Package): cem.Package => {
+ return {
+ schemaVersion: '1.0.0',
+ modules: [...pkg.modules.map(convertModule)],
+ };
+};
+
+const convertModule = (module: Module): cem.Module => {
+ return {
+ kind: 'javascript-module',
+ path: module.jsPath,
+ summary: 'TODO', // TODO
+ description: 'TODO', // TODO
+ declarations: [...module.declarations.map(convertDeclaration)],
+ exports: [
+ ...module.exportNames.map((name) =>
+ convertJavascriptExport(name, module.getExportReference(name))
+ ),
+ ...module.getCustomElementExports().map(convertCustomElementExport),
+ ],
+ deprecated: false, // TODO
+ };
+};
+
+const convertDeclaration = (declaration: Declaration): cem.Declaration => {
+ if (declaration.isLitElementDeclaration()) {
+ return convertLitElementDeclaration(declaration);
+ } else if (declaration.isClassDeclaration()) {
+ return convertClassDeclaration(declaration);
+ } else if (declaration.isVariableDeclaration()) {
+ return convertVariableDeclaration(declaration);
+ } else {
+ // TODO: FunctionDeclaration
+ // TODO: MixinDeclaration
+ // TODO: CustomElementMixinDeclaration;
+ throw new Error(
+ `Unknown declaration: ${(declaration as Object).constructor.name}`
+ );
+ }
+};
+
+const convertJavascriptExport = (
+ name: string,
+ reference: Reference
+): cem.JavaScriptExport => {
+ return {
+ kind: 'js',
+ name,
+ declaration: convertReference(reference),
+ };
+};
+
+const convertCustomElementExport = (
+ declaration: LitElementExport
+): cem.CustomElementExport => {
+ return {
+ kind: 'custom-element-definition',
+ name: declaration.tagname,
+ declaration: {
+ name: declaration.name,
+ },
+ };
+};
+
+const convertLitElementDeclaration = (
+ declaration: LitElementDeclaration
+): cem.CustomElementDeclaration => {
+ return {
+ ...convertClassDeclaration(declaration),
+ tagName: declaration.tagname,
+ attributes: [
+ // TODO
+ ],
+ events: Array.from(declaration.events.values()).map(convertEvent),
+ slots: Array.from(declaration.slots.values()),
+ cssParts: Array.from(declaration.cssParts.values()),
+ cssProperties: Array.from(declaration.cssProperties.values()),
+ demos: [
+ // TODO
+ ],
+ customElement: true,
+ };
+};
+
+const convertClassDeclaration = (
+ declaration: ClassDeclaration
+): cem.ClassDeclaration => {
+ const {superClass} = declaration.heritage;
+ return {
+ kind: 'class',
+ name: declaration.name!, // TODO(kschaaf) name isn't optional in CEM
+ ...ifDefined(declaration, 'description'),
+ ...ifDefined(declaration, 'summary'),
+ superclass: superClass ? convertReference(superClass) : undefined,
+ mixins: [], // TODO
+ members: [
+ // TODO: ClassField
+ // TODO: ClassMethod
+ ],
+ source: {href: 'TODO'}, // TODO
+ deprecated: false, // TODO
+ };
+};
+
+const convertVariableDeclaration = (
+ declaration: VariableDeclaration
+): cem.VariableDeclaration => {
+ return {
+ kind: 'variable',
+ name: declaration.name,
+ summary: 'TODO', // TODO
+ description: 'TODO', // TODO
+ type: declaration.type ? convertType(declaration.type) : {text: 'TODO'}, // TODO(kschaaf) type isn't optional in CEM
+ };
+};
+
+const convertEvent = (event: Event): cem.Event => {
+ return {
+ name: event.name,
+ type: event.type ? convertType(event.type) : {text: 'TODO'}, // TODO(kschaaf) type isn't optional in CEM
+ };
+};
+
+const convertType = (type: Type): cem.Type => {
+ return {
+ text: type.text,
+ references: convertTypeReference(type.text, type.references),
+ };
+};
+
+const convertTypeReference = (
+ text: string,
+ references: Reference[]
+): cem.TypeReference[] => {
+ const cemReferences: cem.TypeReference[] = [];
+ let curr = 0;
+ for (const ref of references) {
+ const start = text.indexOf(ref.name, curr);
+ if (start < 0) {
+ throw new Error(
+ `Could not find type reference '${ref.name}' in type '${text}'`
+ );
+ }
+ curr = start + ref.name.length;
+ cemReferences.push({
+ ...convertReference(ref),
+ start,
+ end: curr,
+ });
+ }
+ return cemReferences;
+};
+
+const convertReference = (reference: Reference): cem.TypeReference => {
+ const refObj: cem.TypeReference = {
+ name: reference.name,
+ };
+ if (reference.isGlobal) {
+ refObj.package = 'global:';
+ } else if (reference.package !== undefined) {
+ refObj.package = reference.package;
+ }
+ if (reference.module !== undefined) {
+ refObj.module = reference.module;
+ }
+ return refObj;
+};
diff --git a/packages/labs/gen-manifest/src/test/generate_test.ts b/packages/labs/gen-manifest/src/test/generate_test.ts
new file mode 100644
index 0000000000..20ae0c4ea3
--- /dev/null
+++ b/packages/labs/gen-manifest/src/test/generate_test.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {test} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as fs from 'fs';
+import * as path from 'path';
+import {AbsolutePath, createPackageAnalyzer} from '@lit-labs/analyzer';
+import {writeFileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
+import {generateManifest} from '../index.js';
+import {assertGoldensMatch} from '@lit-internal/tests/utils/assert-goldens.js';
+
+const testProjects = '../test-projects';
+const outputFolder = 'gen-output';
+
+test('basic manifest generation', async () => {
+ const project = 'test-element-a';
+ const inputPackage = path.resolve(testProjects, project);
+
+ if (fs.existsSync(outputFolder)) {
+ fs.rmSync(outputFolder, {recursive: true});
+ }
+
+ const analyzer = createPackageAnalyzer(inputPackage as AbsolutePath);
+ const pkg = analyzer.getPackage();
+ await writeFileTree(outputFolder, await generateManifest(pkg));
+
+ await assertGoldensMatch(outputFolder, path.join('goldens', project), {
+ formatGlob: '**/*.json',
+ });
+});
+
+test.run();
diff --git a/packages/labs/gen-manifest/tsconfig.json b/packages/labs/gen-manifest/tsconfig.json
new file mode 100644
index 0000000000..58bfedd3d8
--- /dev/null
+++ b/packages/labs/gen-manifest/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "incremental": true,
+ "tsBuildInfoFile": "tsconfig.tsbuildinfo",
+ "target": "ES2020",
+ "module": "ES2020",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "lib": ["es2020"],
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "preserveConstEnums": true,
+ "forceConsistentCasingInFileNames": true,
+ "rootDir": "src/",
+ "outDir": "./",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*"],
+ "exclude": []
+}
diff --git a/packages/labs/gen-utils/CHANGELOG.md b/packages/labs/gen-utils/CHANGELOG.md
index 24034edd84..48fff5299d 100644
--- a/packages/labs/gen-utils/CHANGELOG.md
+++ b/packages/labs/gen-utils/CHANGELOG.md
@@ -1,5 +1,19 @@
# @lit-labs/gen-utils
+## 0.1.2
+
+### Patch Changes
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
+
+## 0.1.1
+
+### Patch Changes
+
+- Updated dependencies [[`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a), [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e), [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9)]:
+ - @lit-labs/analyzer@0.3.0
+
## 0.1.0
### Minor Changes
diff --git a/packages/labs/gen-utils/package.json b/packages/labs/gen-utils/package.json
index 68f216e183..bf4c1c6e51 100644
--- a/packages/labs/gen-utils/package.json
+++ b/packages/labs/gen-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@lit-labs/gen-utils",
"description": "Utilities for lit code generators",
- "version": "0.1.0",
+ "version": "0.1.2",
"publishConfig": {
"access": "public"
},
@@ -46,7 +46,7 @@
}
},
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0"
+ "@lit-labs/analyzer": "^0.4.0"
},
"devDependencies": {
"@lit-internal/tests": "^0.0.0"
diff --git a/packages/labs/gen-utils/src/lib/str-utils.ts b/packages/labs/gen-utils/src/lib/str-utils.ts
index 4fe219c496..e8da435358 100644
--- a/packages/labs/gen-utils/src/lib/str-utils.ts
+++ b/packages/labs/gen-utils/src/lib/str-utils.ts
@@ -21,6 +21,12 @@ const concat = (strings: TemplateStringsArray, ...values: unknown[]) =>
*/
export const javascript = concat;
+/**
+ * Use https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
+ * for HTML syntax highlighting
+ */
+export const html = concat;
+
/**
* Converts string to initial cap.
*/
diff --git a/packages/labs/gen-utils/src/test/package-utils_test.ts b/packages/labs/gen-utils/src/test/package-utils_test.ts
index a77f8a25d9..6143e3adf9 100644
--- a/packages/labs/gen-utils/src/test/package-utils_test.ts
+++ b/packages/labs/gen-utils/src/test/package-utils_test.ts
@@ -42,14 +42,36 @@ test('install package with monorepo link', async ({tempFs}) => {
await tempFs.write('package.json', {
dependencies: {
lit: '^2.0.0',
+ 'lit-html': '^2.0.0',
+ 'lit-element': '^3.0.0',
+ '@lit/reactive-element': '^1.0.0',
},
});
await installPackage(tempFs.rootDir, {
lit: '../../lit',
+ 'lit-html': '../../lit-html',
+ 'lit-element': '../../lit-element',
+ '@lit/reactive-element': '../../reactive-element',
});
assert.ok((await tempFs.read('node_modules', 'lit', 'index.js')).length > 0);
+ assert.ok(
+ (await tempFs.read('node_modules', 'lit-html', 'lit-html.js')).length > 0
+ );
+ assert.ok(
+ (await tempFs.read('node_modules', 'lit-element', 'index.js')).length > 0
+ );
+ assert.ok(
+ (
+ await tempFs.read(
+ 'node_modules',
+ '@lit',
+ 'reactive-element',
+ 'reactive-element.js'
+ )
+ ).length > 0
+ );
});
test('build package', async ({tempFs}) => {
diff --git a/packages/labs/gen-wrapper-angular/CHANGELOG.md b/packages/labs/gen-wrapper-angular/CHANGELOG.md
index f372b28b15..b295c8d8a8 100644
--- a/packages/labs/gen-wrapper-angular/CHANGELOG.md
+++ b/packages/labs/gen-wrapper-angular/CHANGELOG.md
@@ -1,5 +1,21 @@
# @lit-labs/gen-wrapper-angular
+## 0.0.3
+
+### Patch Changes
+
+- [#3384](https://github.com/lit/lit/pull/3384) [`9f802646`](https://github.com/lit/lit/commit/9f802646d955198cbaf6e521283fe137e7f5b7a6) - Updates generated wrappers to better support types for properties and events, tested via a suite of test elements.
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
+
+## 0.0.2
+
+### Patch Changes
+
+- Updated dependencies [[`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a), [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e), [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9)]:
+ - @lit-labs/analyzer@0.3.0
+
## 0.0.1
### Patch Changes
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/.gitignore b/packages/labs/gen-wrapper-angular/goldens/test-element-a/.gitignore
index ea02704223..0e55a4408e 100644
--- a/packages/labs/gen-wrapper-angular/goldens/test-element-a/.gitignore
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/.gitignore
@@ -1 +1,4 @@
-element-a.js
\ No newline at end of file
+element-a.js
+element-events.js
+element-props.js
+element-slots.js
\ No newline at end of file
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/package.json b/packages/labs/gen-wrapper-angular/goldens/test-element-a/package.json
index 424e62fdad..ddf6d31f57 100644
--- a/packages/labs/gen-wrapper-angular/goldens/test-element-a/package.json
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/package.json
@@ -17,6 +17,9 @@
"typescript": "~4.7.4"
},
"files": [
- "element-a.js"
+ "element-a.js",
+ "element-events.js",
+ "element-props.js",
+ "element-slots.js"
]
}
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-a.ts b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-a.ts
index a52a8df9c3..d339c78d9e 100644
--- a/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-a.ts
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-a.ts
@@ -1,11 +1,12 @@
import {
Component,
ElementRef,
- EventEmitter,
- Input,
NgZone,
+ Input,
+ EventEmitter,
Output,
} from '@angular/core';
+
import type {ElementA as ElementAElement} from '@lit-internal/test-element-a/element-a.js';
import '@lit-internal/test-element-a/element-a.js';
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-events.ts b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-events.ts
new file mode 100644
index 0000000000..faa11b9f0a
--- /dev/null
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-events.ts
@@ -0,0 +1,88 @@
+import {
+ Component,
+ ElementRef,
+ NgZone,
+ Input,
+ EventEmitter,
+ Output,
+} from '@angular/core';
+
+import type {ElementEvents as ElementEventsElement} from '@lit-internal/test-element-a/element-events.js';
+import '@lit-internal/test-element-a/element-events.js';
+
+@Component({
+ selector: 'element-events',
+ template: '',
+})
+export class ElementEvents {
+ private _el: ElementEventsElement;
+ private _ngZone: NgZone;
+
+ constructor(e: ElementRef, ngZone: NgZone) {
+ this._el = e.nativeElement;
+ this._ngZone = ngZone;
+
+ this._el.addEventListener('string-custom-event', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.stringCustomEventEvent.emit(e);
+ });
+
+ this._el.addEventListener('number-custom-event', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.numberCustomEventEvent.emit(e);
+ });
+
+ this._el.addEventListener('my-detail-custom-event', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.myDetailCustomEventEvent.emit(e);
+ });
+
+ this._el.addEventListener('event-subclass', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.eventSubclassEvent.emit(e);
+ });
+
+ this._el.addEventListener('special-event', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.specialEventEvent.emit(e);
+ });
+
+ this._el.addEventListener('template-result-custom-event', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.templateResultCustomEventEvent.emit(e);
+ });
+ }
+
+ @Input()
+ set foo(v: string | undefined) {
+ this._ngZone.runOutsideAngular(() => (this._el.foo = v));
+ }
+
+ get foo() {
+ return this._el.foo;
+ }
+
+ @Output()
+ stringCustomEventEvent = new EventEmitter();
+
+ @Output()
+ numberCustomEventEvent = new EventEmitter();
+
+ @Output()
+ myDetailCustomEventEvent = new EventEmitter();
+
+ @Output()
+ eventSubclassEvent = new EventEmitter();
+
+ @Output()
+ specialEventEvent = new EventEmitter();
+
+ @Output()
+ templateResultCustomEventEvent = new EventEmitter();
+}
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-props.ts b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-props.ts
new file mode 100644
index 0000000000..61abb35bf0
--- /dev/null
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-props.ts
@@ -0,0 +1,80 @@
+import {
+ Component,
+ ElementRef,
+ NgZone,
+ Input,
+ EventEmitter,
+ Output,
+} from '@angular/core';
+import {MyType} from '@lit-internal/test-element-a/element-props.js';
+export type {MyType} from '@lit-internal/test-element-a/element-props.js';
+import type {ElementProps as ElementPropsElement} from '@lit-internal/test-element-a/element-props.js';
+import '@lit-internal/test-element-a/element-props.js';
+
+@Component({
+ selector: 'element-props',
+ template: '',
+})
+export class ElementProps {
+ private _el: ElementPropsElement;
+ private _ngZone: NgZone;
+
+ constructor(e: ElementRef, ngZone: NgZone) {
+ this._el = e.nativeElement;
+ this._ngZone = ngZone;
+
+ this._el.addEventListener('a-changed', (e: Event) => {
+ // TODO(justinfagnani): we need to let the element say how to get a value
+ // from an event, ex: e.value
+ this.aChangedEvent.emit(e);
+ });
+ }
+
+ @Input()
+ set aStr(v: string) {
+ this._ngZone.runOutsideAngular(() => (this._el.aStr = v));
+ }
+
+ get aStr() {
+ return this._el.aStr;
+ }
+
+ @Input()
+ set aNum(v: number) {
+ this._ngZone.runOutsideAngular(() => (this._el.aNum = v));
+ }
+
+ get aNum() {
+ return this._el.aNum;
+ }
+
+ @Input()
+ set aBool(v: boolean) {
+ this._ngZone.runOutsideAngular(() => (this._el.aBool = v));
+ }
+
+ get aBool() {
+ return this._el.aBool;
+ }
+
+ @Input()
+ set aStrArray(v: string[]) {
+ this._ngZone.runOutsideAngular(() => (this._el.aStrArray = v));
+ }
+
+ get aStrArray() {
+ return this._el.aStrArray;
+ }
+
+ @Input()
+ set aMyType(v: MyType) {
+ this._ngZone.runOutsideAngular(() => (this._el.aMyType = v));
+ }
+
+ get aMyType() {
+ return this._el.aMyType;
+ }
+
+ @Output()
+ aChangedEvent = new EventEmitter();
+}
diff --git a/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-slots.ts b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-slots.ts
new file mode 100644
index 0000000000..36dc5648fb
--- /dev/null
+++ b/packages/labs/gen-wrapper-angular/goldens/test-element-a/src/element-slots.ts
@@ -0,0 +1,27 @@
+import {Component, ElementRef, NgZone, Input} from '@angular/core';
+
+import type {ElementSlots as ElementSlotsElement} from '@lit-internal/test-element-a/element-slots.js';
+import '@lit-internal/test-element-a/element-slots.js';
+
+@Component({
+ selector: 'element-slots',
+ template: '',
+})
+export class ElementSlots {
+ private _el: ElementSlotsElement;
+ private _ngZone: NgZone;
+
+ constructor(e: ElementRef, ngZone: NgZone) {
+ this._el = e.nativeElement;
+ this._ngZone = ngZone;
+ }
+
+ @Input()
+ set mainDefault(v: string) {
+ this._ngZone.runOutsideAngular(() => (this._el.mainDefault = v));
+ }
+
+ get mainDefault() {
+ return this._el.mainDefault;
+ }
+}
diff --git a/packages/labs/gen-wrapper-angular/package.json b/packages/labs/gen-wrapper-angular/package.json
index 5f36dda2d7..027829fb07 100644
--- a/packages/labs/gen-wrapper-angular/package.json
+++ b/packages/labs/gen-wrapper-angular/package.json
@@ -2,7 +2,7 @@
"private": true,
"name": "@lit-labs/gen-wrapper-angular",
"description": "Code generator for Angular wrappers for Lit components",
- "version": "0.0.1",
+ "version": "0.0.3",
"publishConfig": {
"access": "public"
},
@@ -15,7 +15,8 @@
"scripts": {
"build": "wireit",
"test": "wireit",
- "test:gen": "wireit"
+ "test:gen": "wireit",
+ "test:update-goldens": "UPDATE_TEST_GOLDENS=true npm run test:gen"
},
"wireit": {
"build": {
@@ -43,18 +44,20 @@
},
"test:gen": {
"command": "uvu test \".*_test\\.js$\"",
- "files": [],
+ "files": [
+ "goldens/**/*"
+ ],
"output": [
"gen-output"
],
"dependencies": [
"build",
- "../test-projects/test-element-a:build"
+ "../test-projects/test-element-a:pack"
]
}
},
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0"
},
"devDependencies": {
diff --git a/packages/labs/gen-wrapper-angular/src/lib/wrapper-module-template.ts b/packages/labs/gen-wrapper-angular/src/lib/wrapper-module-template.ts
index d328c1c4eb..1fa70f68be 100644
--- a/packages/labs/gen-wrapper-angular/src/lib/wrapper-module-template.ts
+++ b/packages/labs/gen-wrapper-angular/src/lib/wrapper-module-template.ts
@@ -7,22 +7,56 @@
import {
LitElementDeclaration,
PackageJson,
+ getImportsStringForReferences,
+} from '@lit-labs/analyzer';
+import {
+ ReactiveProperty as ModelProperty,
+ Event as EventModel,
+ Reference,
} from '@lit-labs/analyzer/lib/model.js';
import {javascript} from '@lit-labs/gen-utils/lib/str-utils.js';
+const getTypeReferencesForMap = (
+ map: Map
+) => Array.from(map.values()).flatMap((e) => e.type?.references ?? []);
+
+const getElementTypeImports = (declarations: LitElementDeclaration[]) => {
+ const refs: Reference[] = [];
+ declarations.forEach((declaration) => {
+ const {/*events,*/ reactiveProperties} = declaration;
+ refs.push(
+ // TODO(sorvell): Add event types.
+ //...getTypeReferencesForMap(events),
+ ...getTypeReferencesForMap(reactiveProperties)
+ );
+ });
+ return getImportsStringForReferences(refs);
+};
+
+// TODO(sorvell): add support for getting exports in analyzer.
+const getElementTypeExportsFromImports = (imports: string) =>
+ imports.replace(/(?:^import)/gm, 'export type');
+
export const wrapperModuleTemplate = (
packageJson: PackageJson,
moduleJsPath: string,
elements: LitElementDeclaration[]
) => {
+ const imports = [`Component`, `ElementRef`, `NgZone`];
+ if (elements.filter((e) => e.reactiveProperties.size).length > 0) {
+ imports.push(`Input`);
+ }
+ if (elements.filter((e) => e.events.size).length > 0) {
+ imports.push(`EventEmitter`, `Output`);
+ }
+ const typeImports = getElementTypeImports(elements);
+ const typeExports = getElementTypeExportsFromImports(typeImports);
return javascript`import {
- Component,
- ElementRef,
- EventEmitter,
- Input,
- NgZone,
- Output,
+ ${imports.join(',\n ')}
+
} from '@angular/core';
+${typeImports}
+${typeExports}
${elements.map(
(
element
@@ -61,7 +95,7 @@ export class ${name} {
${Array.from(reactiveProperties.entries()).map(
([propertyName, property]) => javascript`
@Input()
- set ${propertyName}(v: ${property.type.text}) {
+ set ${propertyName}(v: ${property.type?.text ?? 'any'}) {
this._ngZone.runOutsideAngular(() => (this._el.${propertyName} = v));
}
diff --git a/packages/labs/gen-wrapper-angular/src/test/generation/generate_test.ts b/packages/labs/gen-wrapper-angular/src/test/generation/generate_test.ts
index c84e08ce32..63e9e3dcd1 100644
--- a/packages/labs/gen-wrapper-angular/src/test/generation/generate_test.ts
+++ b/packages/labs/gen-wrapper-angular/src/test/generation/generate_test.ts
@@ -9,7 +9,7 @@ import {test} from 'uvu';
import * as assert from 'uvu/assert';
import * as fs from 'fs';
import * as path from 'path';
-import {Analyzer} from '@lit-labs/analyzer';
+import {createPackageAnalyzer} from '@lit-labs/analyzer';
import {AbsolutePath} from '@lit-labs/analyzer/lib/paths.js';
import {
installPackage,
@@ -32,9 +32,9 @@ test('basic wrapper generation', async () => {
fs.rmSync(outputPackage, {recursive: true});
}
- const analyzer = new Analyzer(inputPackage as AbsolutePath);
- const analysis = analyzer.analyzePackage();
- await writeFileTree(outputFolder, await generateAngularWrapper(analysis));
+ const analyzer = createPackageAnalyzer(inputPackage as AbsolutePath);
+ const pkg = analyzer.getPackage();
+ await writeFileTree(outputFolder, await generateAngularWrapper(pkg));
const wrapperSourceFile = fs.readFileSync(
path.join(outputPackage, 'src', 'element-a.ts')
diff --git a/packages/labs/gen-wrapper-react/CHANGELOG.md b/packages/labs/gen-wrapper-react/CHANGELOG.md
index 7ccab11caa..46f74431f9 100644
--- a/packages/labs/gen-wrapper-react/CHANGELOG.md
+++ b/packages/labs/gen-wrapper-react/CHANGELOG.md
@@ -1,5 +1,31 @@
# @lit-labs/gen-wrapper-react
+## 0.2.1
+
+### Patch Changes
+
+- [#3384](https://github.com/lit/lit/pull/3384) [`9f802646`](https://github.com/lit/lit/commit/9f802646d955198cbaf6e521283fe137e7f5b7a6) - Updates generated wrappers to better support types for properties and events, tested via a suite of test elements.
+
+- [#3377](https://github.com/lit/lit/pull/3377) [`0af4e79b`](https://github.com/lit/lit/commit/0af4e79b51d34d959488ceae4caa2240a76c15e0) - Adds type info for props/events for Vue/React wrappers. Vue wrapper properly handles defaults.
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3288](https://github.com/lit/lit/pull/3288) [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e) - Refactored Analyzer into better fit for use in plugins. Analyzer class now takes a ts.Program, and PackageAnalyzer takes a package path and creates a program to analyze a package on the filesystem.
+
+- [#3254](https://github.com/lit/lit/pull/3254) [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9) - Fixes bug where global install of CLI resulted in incompatible use of analyzer between CLI packages. Fixes #3234.
+
+### Patch Changes
+
+- [#3310](https://github.com/lit/lit/pull/3310) [`b225bd3a`](https://github.com/lit/lit/commit/b225bd3ae1f03d46119650c997719b86742831fe) - Test-output points to the same dependencies as monorepo.
+
+- Updated dependencies [[`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a), [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e), [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9)]:
+ - @lit-labs/analyzer@0.3.0
+
## 0.1.0
### Minor Changes
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/.gitignore b/packages/labs/gen-wrapper-react/goldens/test-element-a/.gitignore
index ea02704223..0e55a4408e 100644
--- a/packages/labs/gen-wrapper-react/goldens/test-element-a/.gitignore
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/.gitignore
@@ -1 +1,4 @@
-element-a.js
\ No newline at end of file
+element-a.js
+element-events.js
+element-props.js
+element-slots.js
\ No newline at end of file
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/package.json b/packages/labs/gen-wrapper-react/goldens/test-element-a/package.json
index 311395a61a..2554ece72b 100644
--- a/packages/labs/gen-wrapper-react/goldens/test-element-a/package.json
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/package.json
@@ -18,6 +18,9 @@
"typescript": "~4.7.4"
},
"files": [
- "element-a.{js,js.map,d.ts,d.ts.map}"
+ "element-a.{js,js.map,d.ts,d.ts.map}",
+ "element-events.{js,js.map,d.ts,d.ts.map}",
+ "element-props.{js,js.map,d.ts,d.ts.map}",
+ "element-slots.{js,js.map,d.ts,d.ts.map}"
]
}
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-a.ts b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-a.ts
index fc7618c38b..fa648e42b5 100644
--- a/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-a.ts
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-a.ts
@@ -1,8 +1,8 @@
import * as React from 'react';
-import {createComponent} from '@lit-labs/react';
+import {createComponent, EventName} from '@lit-labs/react';
import {ElementA as ElementAElement} from '@lit-internal/test-element-a/element-a.js';
export const ElementA = createComponent(React, 'element-a', ElementAElement, {
- onAChanged: 'a-changed',
+ onAChanged: 'a-changed' as EventName>,
});
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-events.ts b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-events.ts
new file mode 100644
index 0000000000..7e936a1110
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-events.ts
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import {createComponent, EventName} from '@lit-labs/react';
+
+import {ElementEvents as ElementEventsElement} from '@lit-internal/test-element-a/element-events.js';
+import {MyDetail} from '@lit-internal/test-element-a/detail-type.js';
+import {EventSubclass} from '@lit-internal/test-element-a/element-events.js';
+import {SpecialEvent} from '@lit-internal/test-element-a/special-event.js';
+import {TemplateResult} from 'lit';
+export type {MyDetail} from '@lit-internal/test-element-a/detail-type.js';
+export type {EventSubclass} from '@lit-internal/test-element-a/element-events.js';
+export type {SpecialEvent} from '@lit-internal/test-element-a/special-event.js';
+export type {TemplateResult} from 'lit';
+
+export const ElementEvents = createComponent(
+ React,
+ 'element-events',
+ ElementEventsElement,
+ {
+ onStringCustomEvent: 'string-custom-event' as EventName<
+ CustomEvent
+ >,
+ onNumberCustomEvent: 'number-custom-event' as EventName<
+ CustomEvent
+ >,
+ onMyDetailCustomEvent: 'my-detail-custom-event' as EventName<
+ CustomEvent
+ >,
+ onEventSubclass: 'event-subclass' as EventName,
+ onSpecialEvent: 'special-event' as EventName,
+ onTemplateResultCustomEvent: 'template-result-custom-event' as EventName<
+ CustomEvent
+ >,
+ }
+);
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-props.ts b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-props.ts
new file mode 100644
index 0000000000..155ecdc87d
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-props.ts
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import {createComponent, EventName} from '@lit-labs/react';
+
+import {ElementProps as ElementPropsElement} from '@lit-internal/test-element-a/element-props.js';
+
+export const ElementProps = createComponent(
+ React,
+ 'element-props',
+ ElementPropsElement,
+ {
+ onAChanged: 'a-changed' as EventName>,
+ }
+);
diff --git a/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-slots.ts b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-slots.ts
new file mode 100644
index 0000000000..d7112468a6
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/goldens/test-element-a/src/element-slots.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import {createComponent} from '@lit-labs/react';
+
+import {ElementSlots as ElementSlotsElement} from '@lit-internal/test-element-a/element-slots.js';
+
+export const ElementSlots = createComponent(
+ React,
+ 'element-slots',
+ ElementSlotsElement,
+ {}
+);
diff --git a/packages/labs/gen-wrapper-react/package.json b/packages/labs/gen-wrapper-react/package.json
index 083371c560..9aa70d027b 100644
--- a/packages/labs/gen-wrapper-react/package.json
+++ b/packages/labs/gen-wrapper-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@lit-labs/gen-wrapper-react",
"description": "Code generator for React wrapper for Lit components",
- "version": "0.1.0",
+ "version": "0.2.1",
"publishConfig": {
"access": "public"
},
@@ -60,7 +60,7 @@
}
},
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0"
},
"devDependencies": {
diff --git a/packages/labs/gen-wrapper-react/src/index.ts b/packages/labs/gen-wrapper-react/src/index.ts
index 01a28bf28a..1ae9aabb99 100644
--- a/packages/labs/gen-wrapper-react/src/index.ts
+++ b/packages/labs/gen-wrapper-react/src/index.ts
@@ -10,7 +10,9 @@ import {
PackageJson,
LitElementDeclaration,
ModuleWithLitElementDeclarations,
+ getImportsStringForReferences,
} from '@lit-labs/analyzer';
+import {Event as EventModel} from '@lit-labs/analyzer/lib/model.js';
import {FileTree} from '@lit-labs/gen-utils/lib/file-utils.js';
import {javascript, kabobToOnEvent} from '@lit-labs/gen-utils/lib/str-utils.js';
@@ -24,8 +26,8 @@ export const getCommand = () => {
name: 'react',
description: 'Generate React wrapper components from Lit elements',
kind: 'resolved',
- async generate(options: {analysis: Package}): Promise {
- return await generateReactWrapper(options.analysis);
+ async generate(options: {package: Package}): Promise {
+ return await generateReactWrapper(options.package);
},
};
};
@@ -149,22 +151,41 @@ const tsconfigTemplate = () => {
);
};
+const getTypeImports = (declarations: LitElementDeclaration[]) => {
+ // We only need type imports for events.
+ const refs = declarations.flatMap(({events}) =>
+ Array.from(events.values()).flatMap((e) => e.type?.references ?? [])
+ );
+ return getImportsStringForReferences(refs);
+};
+
+// TODO(sorvell): add support for getting exports in analyzer.
+const getElementTypeExportsFromImports = (imports: string) =>
+ imports.replace(/(?:^import)/gm, 'export type');
+
const wrapperModuleTemplate = (
packageJson: PackageJson,
moduleJsPath: string,
elements: LitElementDeclaration[]
) => {
+ const hasEvents = elements.filter(({events}) => events.size).length > 0;
+ const typeImports = getTypeImports(elements);
+ const typeExports = getElementTypeExportsFromImports(typeImports);
return javascript`
-import * as React from 'react';
-import {createComponent} from '@lit-labs/react';
-${elements.map(
- (element) => javascript`
-import {${element.name} as ${element.name}Element} from '${packageJson.name}/${moduleJsPath}';
-`
-)}
+ import * as React from 'react';
+ import {createComponent${
+ hasEvents ? `, EventName` : ``
+ }} from '@lit-labs/react';
+ ${elements.map(
+ (element) => javascript`
+ import {${element.name} as ${element.name}Element} from '${packageJson.name}/${moduleJsPath}';
+ ${typeImports}
+ ${typeExports}
+ `
+ )}
-${elements.map((element) => wrapperTemplate(element))}
-`;
+ ${elements.map((element) => wrapperTemplate(element))}
+ `;
};
// TODO(kschaaf): Should this be configurable?
@@ -172,22 +193,19 @@ const packageNameToReactPackageName = (pkgName: string) => `${pkgName}-react`;
const wrapperTemplate = ({name, tagname, events}: LitElementDeclaration) => {
return javascript`
-export const ${name} = createComponent(
- React,
- '${tagname}',
- ${name}Element,
- {
- ${Array.from(events.keys()).map(
- (eventName) => javascript`
- ${kabobToOnEvent(eventName)}: '${
- // TODO(kschaaf): add cast to `as EventName` once the
- // analyzer reports the event type correctly (currently we have the
- // type string without an AST reference to get its import, etc.)
- // https://github.com/lit/lit/issues/2850
- eventName
- }',`
- )}
- }
-);
-`;
+ export const ${name} = createComponent(
+ React,
+ '${tagname}',
+ ${name}Element,
+ {
+ ${Array.from(events.values()).map((event: EventModel) => {
+ const {name, type} = event;
+ return javascript`
+ ${kabobToOnEvent(name)}: '${name}' as EventName<${
+ type?.text || `CustomEvent`
+ }>,`;
+ })}
+ }
+ );
+ `;
};
diff --git a/packages/labs/gen-wrapper-react/src/test-gen/generate_test.ts b/packages/labs/gen-wrapper-react/src/test-gen/generate_test.ts
index b876137b2a..0cff941164 100644
--- a/packages/labs/gen-wrapper-react/src/test-gen/generate_test.ts
+++ b/packages/labs/gen-wrapper-react/src/test-gen/generate_test.ts
@@ -9,7 +9,7 @@ import {test} from 'uvu';
import * as assert from 'uvu/assert';
import * as fs from 'fs';
import * as path from 'path';
-import {Analyzer} from '@lit-labs/analyzer';
+import {createPackageAnalyzer} from '@lit-labs/analyzer';
import {AbsolutePath} from '@lit-labs/analyzer/lib/paths.js';
import {
installPackage,
@@ -32,9 +32,9 @@ test('basic wrapper generation', async () => {
fs.rmSync(outputPackage, {recursive: true});
}
- const analyzer = new Analyzer(inputPackage as AbsolutePath);
- const analysis = analyzer.analyzePackage();
- await writeFileTree(outputFolder, await generateReactWrapper(analysis));
+ const analyzer = createPackageAnalyzer(inputPackage as AbsolutePath);
+ const pkg = analyzer.getPackage();
+ await writeFileTree(outputFolder, await generateReactWrapper(pkg));
const wrapperSourceFile = fs.readFileSync(
path.join(outputPackage, 'src/element-a.ts')
diff --git a/packages/labs/gen-wrapper-react/test-output/package.json b/packages/labs/gen-wrapper-react/test-output/package.json
index f3b9294009..283c2380ec 100644
--- a/packages/labs/gen-wrapper-react/test-output/package.json
+++ b/packages/labs/gen-wrapper-react/test-output/package.json
@@ -7,10 +7,15 @@
"@esm-bundle/chai": "^4.1.5",
"@types/chai": "^4.0.1",
"@types/mocha": "^9.0.0",
- "@types/react": "^18.0.9",
- "@types/react-dom": "^18.0.3",
- "react": "^18.1.0",
- "react-dom": "^18.1.0",
+ "@types/react": "file:../../../../node_modules/@types/react",
+ "@types/react-dom": "file:../../../../node_modules/@types/react-dom",
+ "react": "file:../../../../node_modules/react",
+ "react-dom": "file:../../../../node_modules/react-dom",
+ "@lit-labs/react": "file:../../react",
+ "lit": "file:../../../lit",
+ "lit-html": "file:../../../lit-html",
+ "lit-element": "file:../../../lit-element",
+ "@lit/reactive-element": "file:../../../reactive-element",
"@lit-internal/test-element-a": "file:../../test-projects/test-element-a/lit-internal-test-element-a-1.0.0.tgz",
"@lit-internal/test-element-a-react": "file:../gen-output/test-element-a-react/lit-internal-test-element-a-react-1.0.0.tgz"
},
@@ -66,7 +71,7 @@
"build",
"../../../tests:build"
],
- "command": "node ../../../tests/run-web-tests.js \"tests/**/*_test.js\" --config ../../../tests/web-test-runner.config.js",
+ "command": "node ../../../tests/run-web-tests.js \"tests/tests.js\" --config ../../../tests/web-test-runner.config.js",
"output": []
}
}
diff --git a/packages/labs/gen-wrapper-react/test-output/rollup.config.js b/packages/labs/gen-wrapper-react/test-output/rollup.config.js
index c266e7895a..a0f05e1b5a 100644
--- a/packages/labs/gen-wrapper-react/test-output/rollup.config.js
+++ b/packages/labs/gen-wrapper-react/test-output/rollup.config.js
@@ -14,7 +14,7 @@ import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
export default {
- input: ['js/tests/test-element-a_test.js'],
+ input: ['js/tests/tests.js'],
output: {
dir: './tests',
format: 'esm',
diff --git a/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-events_test.tsx b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-events_test.tsx
new file mode 100644
index 0000000000..5bd69fd61a
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-events_test.tsx
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+
+import React from 'react';
+// eslint-disable-next-line import/extensions
+import {render, unmountComponentAtNode} from 'react-dom';
+import {
+ ElementEvents,
+ SpecialEvent,
+ MyDetail,
+ EventSubclass,
+ TemplateResult,
+} from '@lit-internal/test-element-a-react/element-events.js';
+import {ElementEvents as ElementEventsElement} from '@lit-internal/test-element-a/element-events.js';
+
+suite('test-element-events', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ unmountComponentAtNode(container);
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('can listen to events', async () => {
+ let stringCustomEventPayload: CustomEvent | undefined = undefined;
+ let numberCustomEventPayload: CustomEvent | undefined = undefined;
+ let myDetailCustomEventPayload: CustomEvent | undefined =
+ undefined;
+ let templateResultCustomEventPayload:
+ | CustomEvent
+ | undefined = undefined;
+ let eventSubclassPayload: EventSubclass | undefined = undefined;
+ let specialEventPayload: SpecialEvent | undefined = undefined;
+ const props = {
+ onStringCustomEvent: (e: CustomEvent) =>
+ (stringCustomEventPayload = e),
+ onNumberCustomEvent: (e: CustomEvent) =>
+ (numberCustomEventPayload = e),
+ onMyDetailCustomEvent: (e: CustomEvent) =>
+ (myDetailCustomEventPayload = e),
+ onTemplateResultCustomEvent: (e: CustomEvent) =>
+ (templateResultCustomEventPayload = e),
+ onEventSubclass: (e: EventSubclass) => (eventSubclassPayload = e),
+ onSpecialEvent: (e: SpecialEvent) => (specialEventPayload = e),
+ };
+
+ render(
+
+
+ ,
+ container
+ );
+ const el = container.querySelector(
+ 'element-events'
+ )! as ElementEventsElement;
+ await el.updateComplete;
+ let expected_stringCustomEventPayload = 'test-detail';
+ el.fireStringCustomEvent(expected_stringCustomEventPayload);
+ assert.equal(
+ stringCustomEventPayload!.detail,
+ expected_stringCustomEventPayload
+ );
+ expected_stringCustomEventPayload = 'test-detail2';
+ el.fireStringCustomEvent(expected_stringCustomEventPayload);
+ assert.equal(
+ stringCustomEventPayload!.detail,
+ expected_stringCustomEventPayload
+ );
+ const expected_numberCustomEventPayload = 55;
+ el.fireNumberCustomEvent(expected_numberCustomEventPayload);
+ assert.equal(
+ numberCustomEventPayload!.detail,
+ expected_numberCustomEventPayload
+ );
+ const expected_myDetailCustomEventPayload: MyDetail = {a: 'aa', b: 555};
+ el.fireMyDetailCustomEvent(expected_myDetailCustomEventPayload);
+ assert.equal(
+ myDetailCustomEventPayload!.detail,
+ expected_myDetailCustomEventPayload
+ );
+ // Note, default payload is html`` which results in {strings: [], values: []}
+ el.fireTemplateResultCustomEvent();
+ const {strings, values} = templateResultCustomEventPayload!.detail;
+ assert.equal(strings.length, 1);
+ assert.equal(strings[0], '');
+ assert.equal(values.length, 0);
+ const str = 'strstr';
+ const num = 5555;
+ el.fireEventSubclass(str, num);
+ assert.equal(eventSubclassPayload!.aStr, str);
+ assert.equal(eventSubclassPayload!.aNumber, num);
+ el.fireSpecialEvent(num);
+ assert.equal(specialEventPayload!.aNumber, num);
+ });
+});
diff --git a/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-props_test.tsx b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-props_test.tsx
new file mode 100644
index 0000000000..5056fd7041
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-props_test.tsx
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+
+import React from 'react';
+// eslint-disable-next-line import/extensions
+import {render, unmountComponentAtNode} from 'react-dom';
+import {ElementProps} from '@lit-internal/test-element-a-react/element-props.js';
+import {ElementProps as ElementPropsElement} from '@lit-internal/test-element-a/element-props.js';
+
+suite('test-element-props', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ unmountComponentAtNode(container);
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('renders correctly', async () => {
+ const props = {
+ aStr: 'Hi',
+ aBool: true,
+ aMyType: {
+ a: 'a',
+ b: 2,
+ c: false,
+ d: ['1'],
+ e: 'unknown',
+ strOrNum: 5,
+ },
+ };
+ render(
+
+
+ ,
+ container
+ );
+ const el = container.querySelector('element-props')! as ElementPropsElement;
+ await el.updateComplete;
+ const shadowRoot = el.shadowRoot!;
+ Object.entries(props).forEach(([k, v]) => {
+ const e = shadowRoot.getElementById(k as string)!;
+ assert.equal(el[k as keyof ElementPropsElement], v);
+ assert.equal(
+ e.textContent,
+ typeof v === 'object' ? JSON.stringify(v) : String(v)
+ );
+ });
+ });
+});
diff --git a/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-slots_test.tsx b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-slots_test.tsx
new file mode 100644
index 0000000000..2353730aa8
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/test-output/src/tests/test-element-slots_test.tsx
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+
+import React from 'react';
+// eslint-disable-next-line import/extensions
+import {render, unmountComponentAtNode} from 'react-dom';
+import {ElementSlots} from '@lit-internal/test-element-a-react/element-slots.js';
+import {ElementSlots as ElementSlotsElement} from '@lit-internal/test-element-a/element-slots.js';
+
+suite('test-element-slots', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ unmountComponentAtNode(container);
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('renders correctly', async () => {
+ render(
+
+
+
+
+ Footer
+ Default
+
+ ,
+ container
+ );
+ const ce = container.querySelector('element-slots')! as ElementSlotsElement;
+ await ce.updateComplete;
+ const shadowRoot = ce.shadowRoot!;
+ // header renders: `#header1, #header2`
+ const headerSlot = shadowRoot.querySelector(
+ 'slot[name="header"]'
+ )!;
+ assert.instanceOf(headerSlot, HTMLSlotElement);
+ const headerAssigned = headerSlot.assignedElements();
+ assert.equal(headerAssigned.length, 2);
+ headerAssigned.forEach((e, i) => assert(e.id, `header${i + 1}`));
+ // main renders fallback
+ const mainSlot =
+ shadowRoot.querySelector('slot[name="main"]')!;
+ const mainAssigned = mainSlot.assignedNodes({flatten: true});
+ assert.equal(mainAssigned.length, 1);
+ assert.equal(mainAssigned[0].textContent, `mainDefault`);
+ // footer: text wrapped in div
+ const footerSlot = shadowRoot.querySelector(
+ 'slot[name="footer"]'
+ )!;
+ const footerAssigned = footerSlot.assignedElements();
+ assert.equal(footerAssigned.length, 1);
+ assert.equal(footerAssigned[0].textContent?.trim(), `Footer`);
+ // default: text
+ const defaultSlot =
+ shadowRoot.querySelector('slot:not([name])')!;
+ const defaultAssigned = defaultSlot
+ .assignedNodes()
+ .map((e) => e.textContent?.trim())
+ .filter((e) => e);
+ assert.equal(defaultAssigned.length, 1);
+ assert.equal(defaultAssigned[0], `Default`);
+ });
+});
diff --git a/packages/labs/gen-wrapper-react/test-output/src/tests/tests.ts b/packages/labs/gen-wrapper-react/test-output/src/tests/tests.ts
new file mode 100644
index 0000000000..f799579b92
--- /dev/null
+++ b/packages/labs/gen-wrapper-react/test-output/src/tests/tests.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import './test-element-a_test.js';
+import './test-element-props_test.js';
+import './test-element-events_test.js';
+import './test-element-slots_test.js';
diff --git a/packages/labs/gen-wrapper-vue/CHANGELOG.md b/packages/labs/gen-wrapper-vue/CHANGELOG.md
index e2e2336bae..b6a4d131e6 100644
--- a/packages/labs/gen-wrapper-vue/CHANGELOG.md
+++ b/packages/labs/gen-wrapper-vue/CHANGELOG.md
@@ -1,5 +1,31 @@
# @lit-labs/gen-wrapper-vue
+## 0.2.1
+
+### Patch Changes
+
+- [#3384](https://github.com/lit/lit/pull/3384) [`9f802646`](https://github.com/lit/lit/commit/9f802646d955198cbaf6e521283fe137e7f5b7a6) - Updates generated wrappers to better support types for properties and events, tested via a suite of test elements.
+
+- [#3377](https://github.com/lit/lit/pull/3377) [`0af4e79b`](https://github.com/lit/lit/commit/0af4e79b51d34d959488ceae4caa2240a76c15e0) - Adds type info for props/events for Vue/React wrappers. Vue wrapper properly handles defaults.
+
+- Updated dependencies [[`fc2b1c88`](https://github.com/lit/lit/commit/fc2b1c885211e4334d5ae5637570df85dd2e3f9e), [`ad361cc2`](https://github.com/lit/lit/commit/ad361cc22303f759afbefe60512df34fffdee771)]:
+ - @lit-labs/analyzer@0.4.0
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3304](https://github.com/lit/lit/pull/3304) [`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a) - Added support for analyzing JavaScript files.
+
+- [#3288](https://github.com/lit/lit/pull/3288) [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e) - Refactored Analyzer into better fit for use in plugins. Analyzer class now takes a ts.Program, and PackageAnalyzer takes a package path and creates a program to analyze a package on the filesystem.
+
+- [#3254](https://github.com/lit/lit/pull/3254) [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9) - Fixes bug where global install of CLI resulted in incompatible use of analyzer between CLI packages. Fixes #3234.
+
+### Patch Changes
+
+- Updated dependencies [[`31bed8d6`](https://github.com/lit/lit/commit/31bed8d6542c44a64bad8282b9ce5e5d6514e44a), [`569a6237`](https://github.com/lit/lit/commit/569a6237377eeef0c8dced2c369c77ebdd81218e), [`fc2fd4c8`](https://github.com/lit/lit/commit/fc2fd4c8f4a25b9a85073afcb38614209e079bb9)]:
+ - @lit-labs/analyzer@0.3.0
+
## 0.1.1
### Patch Changes
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/.gitignore b/packages/labs/gen-wrapper-vue/goldens/test-element-a/.gitignore
index 38d837e530..c3731c49a0 100644
--- a/packages/labs/gen-wrapper-vue/goldens/test-element-a/.gitignore
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/.gitignore
@@ -1 +1,4 @@
-ElementA.*
\ No newline at end of file
+/ElementA.*
+/ElementEvents.*
+/ElementProps.*
+/ElementSlots.*
\ No newline at end of file
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/package.json b/packages/labs/gen-wrapper-vue/goldens/test-element-a/package.json
index 18bb9694a4..572e8d272b 100644
--- a/packages/labs/gen-wrapper-vue/goldens/test-element-a/package.json
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/package.json
@@ -11,17 +11,20 @@
"version": "1.0.0",
"dependencies": {
"@lit-internal/test-element-a": "^1.0.0",
- "vue": "^3.2.25",
+ "vue": "^3.2.41",
"@lit-labs/vue-utils": "^0.1.0"
},
"devDependencies": {
"typescript": "~4.7.4",
- "@vitejs/plugin-vue": "^2.3.1",
- "@rollup/plugin-typescript": "^8.3.2",
- "vite": "^2.9.2",
- "vue-tsc": "^0.29.8"
+ "@vitejs/plugin-vue": "^3.1.2",
+ "@rollup/plugin-typescript": "^9.0.1",
+ "vite": "^3.1.8",
+ "vue-tsc": "^1.0.8"
},
"files": [
- "ElementA.{js,js.map,d.ts,vue}"
+ "ElementA.*",
+ "ElementEvents.*",
+ "ElementProps.*",
+ "ElementSlots.*"
]
}
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementA.vue b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementA.vue
index 6389ed3bd0..8f8408786f 100644
--- a/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementA.vue
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementA.vue
@@ -1,27 +1,48 @@
-
+
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementEvents.vue b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementEvents.vue
new file mode 100644
index 0000000000..de4fdefe11
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementEvents.vue
@@ -0,0 +1,79 @@
+
+
+
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementProps.vue b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementProps.vue
new file mode 100644
index 0000000000..0177262885
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementProps.vue
@@ -0,0 +1,56 @@
+
+
+
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementSlots.vue b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementSlots.vue
new file mode 100644
index 0000000000..a5237d8628
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/src/ElementSlots.vue
@@ -0,0 +1,41 @@
+
+
diff --git a/packages/labs/gen-wrapper-vue/goldens/test-element-a/vite.config.ts b/packages/labs/gen-wrapper-vue/goldens/test-element-a/vite.config.ts
index 6878297d69..7c1f4c1f67 100644
--- a/packages/labs/gen-wrapper-vue/goldens/test-element-a/vite.config.ts
+++ b/packages/labs/gen-wrapper-vue/goldens/test-element-a/vite.config.ts
@@ -5,10 +5,17 @@ import typescript from '@rollup/plugin-typescript';
export default {
build: {
rollupOptions: {
- // Ensures no deps are bundled with this build.
- external: () => true,
- input: ['./src/ElementA.vue'],
- preserveModules: true,
+ // Ensures no deps are bundled with build.
+ // Source paths are expected to start with `./` or `/` but may be
+ // `x:` on Windows.
+ external: (id: string) => !id.match(/^((\w:)|(\.?[\\/]))/),
+ input: [
+ './src/ElementA.vue',
+ './src/ElementEvents.vue',
+ './src/ElementProps.vue',
+ './src/ElementSlots.vue',
+ ],
+ preserveModules: false,
preserveEntrySignatures: true,
output: {
format: 'es',
diff --git a/packages/labs/gen-wrapper-vue/package.json b/packages/labs/gen-wrapper-vue/package.json
index 2a4fa4b311..d7da92f6e1 100644
--- a/packages/labs/gen-wrapper-vue/package.json
+++ b/packages/labs/gen-wrapper-vue/package.json
@@ -1,7 +1,7 @@
{
"name": "@lit-labs/gen-wrapper-vue",
"description": "Code generator for Vue wrapper for Lit components",
- "version": "0.1.1",
+ "version": "0.2.1",
"publishConfig": {
"access": "public"
},
@@ -61,7 +61,7 @@
}
},
"dependencies": {
- "@lit-labs/analyzer": "^0.2.0",
+ "@lit-labs/analyzer": "^0.4.0",
"@lit-labs/gen-utils": "^0.1.0",
"@lit-labs/vue-utils": "^0.1.0"
},
diff --git a/packages/labs/gen-wrapper-vue/src/index.ts b/packages/labs/gen-wrapper-vue/src/index.ts
index e218f1158d..b574d743d5 100644
--- a/packages/labs/gen-wrapper-vue/src/index.ts
+++ b/packages/labs/gen-wrapper-vue/src/index.ts
@@ -22,8 +22,8 @@ export const getCommand = () => {
name: 'vue',
description: 'Generate Vue wrapper components from Lit elements',
kind: 'resolved',
- async generate(options: {analysis: Package}): Promise {
- return generateVueWrapper(options.analysis);
+ async generate(options: {package: Package}): Promise {
+ return generateVueWrapper(options.package);
},
};
};
@@ -58,7 +58,7 @@ export const generateVueWrapper = async (pkg: Package): Promise => {
const packageNameToVuePackageName = (pkgName: string) => `${pkgName}-vue`;
const gitIgnoreTemplate = (moduleNames: string[]) =>
- moduleNames.map((f) => `${f}.*`).join('\n');
+ moduleNames.map((f) => `/${f}.*`).join('\n');
const getVueFileName = (dir: string, name: string) => `${dir}/${name}.vue`;
diff --git a/packages/labs/gen-wrapper-vue/src/lib/package-json-template.ts b/packages/labs/gen-wrapper-vue/src/lib/package-json-template.ts
index baf593f1d7..b21fb81297 100644
--- a/packages/labs/gen-wrapper-vue/src/lib/package-json-template.ts
+++ b/packages/labs/gen-wrapper-vue/src/lib/package-json-template.ts
@@ -33,18 +33,18 @@ export const packageJsonTemplate = (
dependencies: {
// TODO(kschaaf): make component version range configurable?
[pkgJson.name!]: '^' + pkgJson.version!,
- vue: '^3.2.25',
+ vue: '^3.2.41',
'@lit-labs/vue-utils': '^0.1.0',
},
devDependencies: {
// Use typescript from source package, assuming it exists
typescript: pkgJson?.devDependencies?.typescript ?? '~4.7.4',
- '@vitejs/plugin-vue': '^2.3.1',
- '@rollup/plugin-typescript': '^8.3.2',
- vite: '^2.9.2',
- 'vue-tsc': '^0.29.8',
+ '@vitejs/plugin-vue': '^3.1.2',
+ '@rollup/plugin-typescript': '^9.0.1',
+ vite: '^3.1.8',
+ 'vue-tsc': '^1.0.8',
},
- files: [...moduleNames.map((f) => `${f}.{js,js.map,d.ts,vue}`)],
+ files: [...moduleNames.map((f) => `${f}.*`)],
},
null,
2
diff --git a/packages/labs/gen-wrapper-vue/src/lib/vite.config-template.ts b/packages/labs/gen-wrapper-vue/src/lib/vite.config-template.ts
index a871e3f8f1..adf1ee2f98 100644
--- a/packages/labs/gen-wrapper-vue/src/lib/vite.config-template.ts
+++ b/packages/labs/gen-wrapper-vue/src/lib/vite.config-template.ts
@@ -29,14 +29,16 @@ import typescript from '@rollup/plugin-typescript';
export default {
build: {
rollupOptions: {
- // Ensures no deps are bundled with this build.
- external: () => true,
+ // Ensures no deps are bundled with build.
+ // Source paths are expected to start with \`./\` or \`/\` but may be
+ // \`x:\` on Windows.
+ external: (id: string) => !id.match(/^((\\w:)|(\\.?[\\\\/]))/),
input: [
${Object.keys(sfcFiles)
.map((path) => `'./${path}'`)
.join(', ')}
],
- preserveModules: true,
+ preserveModules: false,
preserveEntrySignatures: true,
output: {
format: 'es',
diff --git a/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template-sfc.ts b/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template-sfc.ts
index 69f7f329e4..28405d8f8e 100644
--- a/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template-sfc.ts
+++ b/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template-sfc.ts
@@ -6,9 +6,13 @@
import {
LitElementDeclaration,
- ReactiveProperty as ModelProperty,
- Event as ModelEvent,
PackageJson,
+ getImportsStringForReferences,
+} from '@lit-labs/analyzer';
+
+import {
+ ReactiveProperty as ModelProperty,
+ Event as EventModel,
} from '@lit-labs/analyzer/lib/model.js';
import {javascript, kabobToOnEvent} from '@lit-labs/gen-utils/lib/str-utils.js';
@@ -16,9 +20,6 @@ import {javascript, kabobToOnEvent} from '@lit-labs/gen-utils/lib/str-utils.js';
* Generates a Vue wrapper component as a Vue single file component. This
* approach relies on the Vue compiler to generate a Javascript property types
* object for Vue runtime type checking from the Typescript property types.
- *
- * TODO(sorvell): This is also a Typescript module generator that is unused.
- * Need to decide which approach is best and delete the unused generator.
*/
export const wrapperModuleTemplateSFC = (
packageJson: PackageJson,
@@ -32,73 +33,130 @@ export const wrapperModuleTemplateSFC = (
]);
};
-// TODO(sorvell): place into model directly?
-const getFieldModifierString = (node: ModelProperty['node']) =>
- node.questionToken ? '?' : node.exclamationToken ? '!' : '';
+const defaultEventType = `CustomEvent`;
-const getEventType = (event: ModelEvent) => event.type?.text || `unknown`;
+const getEventInfo = (event: EventModel) => {
+ const {name, type: modelType} = event;
+ const onName = kabobToOnEvent(name);
+ const type = modelType?.text ?? defaultEventType;
+ return {onName, type};
+};
-const wrapDefineProps = (props: Map) =>
- Array.from(props.values())
- .map(
- (prop) =>
- `${prop.name}${getFieldModifierString(prop.node)}: ${prop.type?.text}`
- )
- .join(',\n');
+const renderPropsInterface = (props: Map) =>
+ `export interface Props {
+ ${Array.from(props.values())
+ .map((prop) => `${prop.name}?: ${prop.type?.text || 'any'}`)
+ .join(';\n ')}
+ }`;
-// TODO(sorvell): Improve event handling, currently just forwarding the event,
-// but this should be its "payload."
-const wrapEvents = (events: Map) =>
+const wrapEvents = (events: Map) =>
Array.from(events.values())
- .map(
- (event) => `(e: '${event.name}', payload: ${getEventType(event)}): void`
- )
+ .map((event) => {
+ const {type} = getEventInfo(event);
+ return `(e: '${event.name}', payload: ${type}): void`;
+ })
.join(',\n');
+
/**
* Generates VNode props for events. Note that vue automatically maps
* event names from e.g. `event-name` to `onEventName`.
*/
-const renderEvents = (events: Map) =>
- Array.from(events.values())
- .map(
- (event) =>
- `${kabobToOnEvent(event.name)}: (event: ${
- event.type?.text || `CustomEvent`
- }) => emit('${event.name}', (event.detail || event) as ${getEventType(
- event
- )})`
- )
- .join(',\n');
+const renderEvents = (events: Map) =>
+ javascript`{
+ ${Array.from(events.values())
+ .map((event) => {
+ const {onName, type} = getEventInfo(event);
+ return `${onName}: (event: ${type}) => emit('${event.name}', event as ${type})`;
+ })
+ .join(',\n')}
+ }`;
+
+const getTypeReferencesForMap = (
+ map: Map
+) => Array.from(map.values()).flatMap((e) => e.type?.references ?? []);
+
+const getElementTypeImports = (declaration: LitElementDeclaration) => {
+ const {events, reactiveProperties} = declaration;
+ const refs = [
+ ...getTypeReferencesForMap(events),
+ ...getTypeReferencesForMap(reactiveProperties),
+ ];
+ return getImportsStringForReferences(refs);
+};
+
+// TODO(sorvell): add support for getting exports in analyzer.
+const getElementTypeExportsFromImports = (imports: string) =>
+ imports.replace(/(?:^import)/gm, 'export type');
// TODO(sorvell): Add support for `v-bind`.
+// TODO(sorvell): Investigate if it's possible to save the ~15 lines related to
+// handling defaults by factoring the defaults directive and associated code
+// into the vue-utils package.
const wrapperTemplate = (
- {tagname, events, reactiveProperties}: LitElementDeclaration,
+ declaration: LitElementDeclaration,
wcPath: string
) => {
- return javascript`
+ const {tagname, events, reactiveProperties} = declaration;
+ const typeImports = getElementTypeImports(declaration);
+ const typeExports = getElementTypeExportsFromImports(typeImports);
+ return javascript`${
+ typeExports
+ ? javascript`
+ `
+ : ''
+ }
- `;
+ `;
};
diff --git a/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template.ts b/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template.ts
deleted file mode 100644
index dd06cd8eee..0000000000
--- a/packages/labs/gen-wrapper-vue/src/lib/wrapper-module-template.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-import {
- LitElementDeclaration,
- ReactiveProperty as ModelProperty,
- Event as ModelEvent,
- PackageJson,
-} from '@lit-labs/analyzer/lib/model.js';
-import {
- javascript,
- kabobToOnEvent,
- toInitialCap,
-} from '@lit-labs/gen-utils/lib/str-utils.js';
-
-/**
- * Generates a Vue wrapper component as a Typescript module. This approach
- * requires generating a Javascript property types object for Vue runtime
- * type checking to work.
- *
- * TODO(sorvell): This is currently unused and instead the wrapper is generated
- * as a Vue SFC. Need to decide which approach is best and delete the unused
- * generator.
- */
-export const wrapperModuleTemplate = (
- packageJson: PackageJson,
- moduleJsPath: string,
- elements: LitElementDeclaration[]
-) => {
- return javascript`
- import { h, defineComponent, openBlock, createBlock } from "vue";
- import { assignSlotNodes, Slots, eventProp } from "@lit-labs/vue-utils/wrapper-utils.js";
- import '${packageJson.name}/${moduleJsPath}';
-
- ${elements.map((element) => wrapperTemplate(element))}
-`;
-};
-
-// TODO(sorvell): need to extract Javascript type from typescript for Vue.
-// can workaround this by using an SFC and the macro `defineProperties<>()`
-const tsTypeToVuePropType = (type: string) => {
- const required = type.indexOf('undefined') === -1;
- let jsType = type.replace('undefined', '').trim().replace(/[|]$/, '');
- jsType = jsType
- .split(/\s+/)
- .map((s) => toInitialCap(s).replace(/[|]/g, '||'))
- .join(' ')
- .trim();
- return `{type: ${jsType}, required: ${required}}`;
-};
-
-const wrapProps = (props: Map) =>
- Array.from(props.values())
- .map(
- (prop) =>
- `${prop.name}: ${tsTypeToVuePropType(
- prop.type?.text || 'string|undefined'
- )}`
- )
- .join(',\n');
-
-// TODO(sorvell): Improve event handling, currently just forwarding the event,
-// but this should be its "payload."
-const wrapEvents = (events: Map) =>
- Array.from(events.values())
- .map(
- (event) =>
- `${kabobToOnEvent(event.name)}: eventProp<(event: ${
- event.type?.text || `CustomEvent`
- }) => void>()`
- )
- .join(',\n');
-
-/**
- * Generates VNode props for events. Note that vue automatically maps
- * event names from e.g. `event-name` to `onEventName`.
- */
-const renderEvents = (events: Map) =>
- Array.from(events.values())
- .map(
- (event) =>
- `${kabobToOnEvent(event.name)}: (event: ${
- event.type?.text || `CustomEvent`
- }) => emit(event.type, event.detail || event)`
- )
- .join(',\n');
-
-// TODO(sorvell): Add support for `v-bind`.
-const wrapperTemplate = ({
- name,
- tagname,
- events,
- reactiveProperties,
-}: LitElementDeclaration) => {
- return javascript`
- const props = {
- ${wrapProps(reactiveProperties)},
- ${wrapEvents(events)}
- };
-
- const ${name} = defineComponent({
- name: "${name}",
- props,
- setup(props, {emit, slots}) {
- const render = () => h(
- "${tagname}",
- {
- ...props,
- ${renderEvents(events)}
- },
- assignSlotNodes(slots as Slots)
- );
- return () => {
- openBlock();
- return createBlock(render);
- };
- }
- });
-
- export default ${name};
-`;
-};
diff --git a/packages/labs/gen-wrapper-vue/src/test-gen/generate_test.ts b/packages/labs/gen-wrapper-vue/src/test-gen/generate_test.ts
index 8b35d82f19..6e9bd88b32 100644
--- a/packages/labs/gen-wrapper-vue/src/test-gen/generate_test.ts
+++ b/packages/labs/gen-wrapper-vue/src/test-gen/generate_test.ts
@@ -9,7 +9,7 @@ import {test} from 'uvu';
import * as assert from 'uvu/assert';
import * as fs from 'fs';
import * as path from 'path';
-import {Analyzer} from '@lit-labs/analyzer';
+import {createPackageAnalyzer} from '@lit-labs/analyzer';
import {AbsolutePath} from '@lit-labs/analyzer/lib/paths.js';
import {
installPackage,
@@ -32,9 +32,9 @@ test('basic wrapper generation', async () => {
fs.rmSync(outputPackage, {recursive: true});
}
- const analyzer = new Analyzer(inputPackage as AbsolutePath);
- const analysis = analyzer.analyzePackage();
- await writeFileTree(outputFolder, await generateVueWrapper(analysis));
+ const analyzer = createPackageAnalyzer(inputPackage as AbsolutePath);
+ const pkg = analyzer.getPackage();
+ await writeFileTree(outputFolder, await generateVueWrapper(pkg));
const wrapperSourceFile = fs.readFileSync(
path.join(outputPackage, 'src/ElementA.vue')
diff --git a/packages/labs/gen-wrapper-vue/test-output/package.json b/packages/labs/gen-wrapper-vue/test-output/package.json
index 15ec8156fc..11b8a97d0e 100644
--- a/packages/labs/gen-wrapper-vue/test-output/package.json
+++ b/packages/labs/gen-wrapper-vue/test-output/package.json
@@ -58,7 +58,7 @@
"../../../tests:build"
],
"files": [],
- "command": "node ../../../tests/run-web-tests.js \"tests/**/*_test.js\" --config ../../../tests/web-test-runner.config.js",
+ "command": "node ../../../tests/run-web-tests.js \"tests/**/tests.js\" --config ../../../tests/web-test-runner.config.js",
"output": []
}
}
diff --git a/packages/labs/gen-wrapper-vue/test-output/rollup.config.js b/packages/labs/gen-wrapper-vue/test-output/rollup.config.js
index c266e7895a..a0f05e1b5a 100644
--- a/packages/labs/gen-wrapper-vue/test-output/rollup.config.js
+++ b/packages/labs/gen-wrapper-vue/test-output/rollup.config.js
@@ -14,7 +14,7 @@ import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
export default {
- input: ['js/tests/test-element-a_test.js'],
+ input: ['js/tests/tests.js'],
output: {
dir: './tests',
format: 'esm',
diff --git a/packages/labs/gen-wrapper-vue/test-output/src/tests/SlotContainer.vue b/packages/labs/gen-wrapper-vue/test-output/src/tests/SlotContainer.vue
new file mode 100644
index 0000000000..8c985f1b4d
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/test-output/src/tests/SlotContainer.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+ Footer
+ Default
+
+
diff --git a/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-events_test.ts b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-events_test.ts
new file mode 100644
index 0000000000..dc1fc699ce
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-events_test.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+import {createApp, reactive, h} from 'vue';
+import {
+ default as ElementEvents,
+ SpecialEvent,
+ MyDetail,
+ EventSubclass,
+ TemplateResult,
+} from '@lit-internal/test-element-a-vue/ElementEvents.js';
+import {ElementEvents as ElementEventsElement} from '@lit-internal/test-element-a/element-events.js';
+
+suite('test-element-events', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('can listen to events', async () => {
+ let stringCustomEventPayload: CustomEvent | undefined = undefined;
+ let numberCustomEventPayload: CustomEvent | undefined = undefined;
+ let myDetailCustomEventPayload: CustomEvent | undefined =
+ undefined;
+ let templateResultCustomEventPayload:
+ | CustomEvent
+ | undefined = undefined;
+ let eventSubclassPayload: EventSubclass | undefined = undefined;
+ let specialEventPayload: SpecialEvent | undefined = undefined;
+
+ const props = {
+ onStringCustomEvent: (e: CustomEvent) =>
+ (stringCustomEventPayload = e),
+ onNumberCustomEvent: (e: CustomEvent) =>
+ (numberCustomEventPayload = e),
+ onMyDetailCustomEvent: (e: CustomEvent) =>
+ (myDetailCustomEventPayload = e),
+ onTemplateResultCustomEvent: (e: CustomEvent) =>
+ (templateResultCustomEventPayload = e),
+ onEventSubclass: (e: EventSubclass) => (eventSubclassPayload = e),
+ onSpecialEvent: (e: SpecialEvent) => (specialEventPayload = e),
+ };
+
+ const reactiveProps = reactive(props);
+ createApp({
+ render() {
+ return h(ElementEvents, reactiveProps);
+ },
+ }).mount(container);
+ const el = container.querySelector(
+ 'element-events'
+ )! as ElementEventsElement;
+ await el.updateComplete;
+ let expected_stringCustomEventPayload = 'test-detail';
+ el.fireStringCustomEvent(expected_stringCustomEventPayload);
+ assert.equal(
+ stringCustomEventPayload!.detail,
+ expected_stringCustomEventPayload
+ );
+ expected_stringCustomEventPayload = 'test-detail2';
+ el.fireStringCustomEvent(expected_stringCustomEventPayload);
+ assert.equal(
+ stringCustomEventPayload!.detail,
+ expected_stringCustomEventPayload
+ );
+ const expected_numberCustomEventPayload = 55;
+ el.fireNumberCustomEvent(expected_numberCustomEventPayload);
+ assert.equal(
+ numberCustomEventPayload!.detail,
+ expected_numberCustomEventPayload
+ );
+ const expected_myDetailCustomEventPayload: MyDetail = {a: 'aa', b: 555};
+ el.fireMyDetailCustomEvent(expected_myDetailCustomEventPayload);
+ assert.equal(
+ myDetailCustomEventPayload!.detail,
+ expected_myDetailCustomEventPayload
+ );
+ // Note, default payload is html`` which results in {strings: [''], values: []}
+ el.fireTemplateResultCustomEvent();
+ const {strings, values} = templateResultCustomEventPayload!.detail;
+ assert.equal(strings.length, 1);
+ assert.equal(strings[0], '');
+ assert.equal(values.length, 0);
+ const str = 'strstr';
+ const num = 5555;
+ el.fireEventSubclass(str, num);
+ assert.equal(eventSubclassPayload!.aStr, str);
+ assert.equal(eventSubclassPayload!.aNumber, num);
+ el.fireSpecialEvent(num);
+ assert.equal(specialEventPayload!.aNumber, num);
+ });
+});
diff --git a/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-props_test.ts b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-props_test.ts
new file mode 100644
index 0000000000..5b72e078bc
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-props_test.ts
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+import {createApp, reactive, h, nextTick} from 'vue';
+import {
+ default as ElementProps,
+ Props as PropsType,
+} from '@lit-internal/test-element-a-vue/ElementProps.js';
+import {ElementProps as ElementPropsElement} from '@lit-internal/test-element-a/element-props.js';
+
+suite('test-element-props', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('renders property changes correctly', async () => {
+ const {aStr, aNum, aBool, aMyType} = document.createElement(
+ 'element-props'
+ ) as ElementPropsElement;
+ const defaults = {aStr, aNum, aBool, aMyType};
+
+ const props: PropsType = {
+ aStr: 'Hi',
+ aBool: true,
+ aMyType: {
+ a: 'a',
+ b: 2,
+ c: false,
+ d: ['1'],
+ e: 'unknown',
+ strOrNum: 5,
+ },
+ };
+
+ const reactiveProps = reactive(props);
+ createApp({
+ render() {
+ return h(ElementProps, reactiveProps);
+ },
+ }).mount(container);
+ const el = container.querySelector('element-props')! as ElementPropsElement;
+ await el.updateComplete;
+ const shadowRoot = el.shadowRoot!;
+ // Check that property values are rendered to DOM as expected.
+ // Note, element under test has nodes with id's same as props to facilitate
+ // this easily.
+ const assertPropsRendered = async (values = reactiveProps) => {
+ await nextTick();
+ Object.entries(values).forEach(([k, v]) => {
+ const e = shadowRoot.getElementById(k as string)!;
+ assert.equal(
+ e.textContent,
+ typeof v === 'object' ? JSON.stringify(v) : String(v)
+ );
+ });
+ };
+ await assertPropsRendered();
+ // Verify default values are applied when props become undefined.
+ // This follows Vue's convention for handling properties.
+ reactiveProps.aStr = undefined;
+ reactiveProps.aNum = undefined;
+ reactiveProps.aBool = undefined;
+ reactiveProps.aMyType = undefined;
+ await assertPropsRendered(defaults);
+ // Can update props from undefined state ok.
+ reactiveProps.aStr = 'n';
+ reactiveProps.aNum = 100;
+ reactiveProps.aBool = false;
+ reactiveProps.aMyType = {
+ a: 'a2',
+ b: 4,
+ c: true,
+ d: ['1', '2', '3'],
+ e: 'unknown2',
+ strOrNum: '55',
+ };
+ await assertPropsRendered();
+ });
+});
diff --git a/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-slots_test.ts b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-slots_test.ts
new file mode 100644
index 0000000000..961badf676
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/test-output/src/tests/test-element-slots_test.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {assert} from '@esm-bundle/chai';
+import {createApp} from 'vue';
+import SlotContainer from './SlotContainer.vue';
+import {ElementSlots as ElementSlotsElement} from '@lit-internal/test-element-a/element-slots.js';
+
+suite('test-element-slots', () => {
+ let container: HTMLElement;
+
+ setup(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ teardown(() => {
+ if (container && container.parentNode) {
+ container.parentNode.removeChild(container);
+ }
+ });
+
+ test('renders slots inside a Vue component', async () => {
+ createApp(SlotContainer).mount(container);
+ const ce = container.querySelector('element-slots')! as ElementSlotsElement;
+ await ce.updateComplete;
+ const shadowRoot = ce.shadowRoot!;
+ // header renders: `#header1, #header2`
+ const headerSlot = shadowRoot.querySelector(
+ 'slot[name="header"]'
+ )!;
+ assert.instanceOf(headerSlot, HTMLSlotElement);
+ const headerAssigned = headerSlot.assignedElements();
+ assert.equal(headerAssigned.length, 2);
+ headerAssigned.forEach((e, i) => assert(e.id, `header${i + 1}`));
+ // main renders fallback
+ const mainSlot =
+ shadowRoot.querySelector('slot[name="main"]')!;
+ const mainAssigned = mainSlot.assignedNodes({flatten: true});
+ assert.equal(mainAssigned.length, 1);
+ assert.equal(mainAssigned[0].textContent, `mainDefault`);
+ // footer: text wrapped in div
+ const footerSlot = shadowRoot.querySelector(
+ 'slot[name="footer"]'
+ )!;
+ const footerAssigned = footerSlot.assignedElements();
+ assert.equal(footerAssigned.length, 1);
+ assert.equal(footerAssigned[0].textContent?.trim(), `Footer`);
+ // default: text
+ const defaultSlot =
+ shadowRoot.querySelector('slot:not([name])')!;
+ const defaultAssigned = defaultSlot
+ .assignedNodes()
+ .map((e) => e.textContent?.trim())
+ .filter((e) => e);
+ assert.equal(defaultAssigned.length, 1);
+ assert.equal(defaultAssigned[0], `Default`);
+ });
+});
diff --git a/packages/labs/gen-wrapper-vue/test-output/src/tests/tests.ts b/packages/labs/gen-wrapper-vue/test-output/src/tests/tests.ts
new file mode 100644
index 0000000000..f799579b92
--- /dev/null
+++ b/packages/labs/gen-wrapper-vue/test-output/src/tests/tests.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import './test-element-a_test.js';
+import './test-element-props_test.js';
+import './test-element-events_test.js';
+import './test-element-slots_test.js';
diff --git a/packages/labs/gen-wrapper-vue/test-output/vite.config.ts b/packages/labs/gen-wrapper-vue/test-output/vite.config.ts
index 18b86fbc20..8a304cf986 100644
--- a/packages/labs/gen-wrapper-vue/test-output/vite.config.ts
+++ b/packages/labs/gen-wrapper-vue/test-output/vite.config.ts
@@ -4,8 +4,8 @@ import vue from '@vitejs/plugin-vue';
export default {
build: {
lib: {
- entry: './src/tests/test-element-a_test.ts',
- fileName: () => `test-element-a_test.js`,
+ entry: './src/tests/tests.ts',
+ fileName: () => `tests.js`,
formats: ['es'],
},
outDir: './tests',
diff --git a/packages/labs/observers/CHANGELOG.md b/packages/labs/observers/CHANGELOG.md
index 5ef307682e..86f00e41a6 100644
--- a/packages/labs/observers/CHANGELOG.md
+++ b/packages/labs/observers/CHANGELOG.md
@@ -1,5 +1,23 @@
# @lit-labs/observers
+## 1.1.0
+
+### Minor Changes
+
+- [#3294](https://github.com/lit/lit/pull/3294) [`96c05f25`](https://github.com/lit/lit/commit/96c05f258183066b34d2253c57552ef41ed4581a) - Fix value property of type `unknown` on exported controllers. The type of
+ `value` is now generic and can be inferred from the return type of your passed
+ in `callback`. The default callback `() => true` was removed, and is now
+ undefined by default.
+
+- [#3323](https://github.com/lit/lit/pull/3323) [`0f787b29`](https://github.com/lit/lit/commit/0f787b290af1ce68498ddb8fb0ab32b9d6698dc6) - Add unobserve method to `ResizeController` and `IntersectionController`.
+
+### Patch Changes
+
+- [#3293](https://github.com/lit/lit/pull/3293) [`7e22bc2e`](https://github.com/lit/lit/commit/7e22bc2e3918e36c0e46aa6430c17eb8f557968f) - Fix controllers not observing changes to target element if initialized after the host has connected.
+
+- [#3321](https://github.com/lit/lit/pull/3321) [`e90e8fe9`](https://github.com/lit/lit/commit/e90e8fe99423c264827564dcc98236d0329a118a) - Controllers now track all observed targets and will restore observing targets
+ when host is reconnected.
+
## 1.0.2
### Patch Changes
diff --git a/packages/labs/observers/package.json b/packages/labs/observers/package.json
index eab540b682..1df887152c 100644
--- a/packages/labs/observers/package.json
+++ b/packages/labs/observers/package.json
@@ -1,6 +1,6 @@
{
"name": "@lit-labs/observers",
- "version": "1.0.2",
+ "version": "1.1.0",
"description": "A set of reactive controllers that facilitate using the platform observer objects.",
"license": "BSD-3-Clause",
"homepage": "https://lit.dev/",
diff --git a/packages/labs/observers/src/intersection_controller.ts b/packages/labs/observers/src/intersection_controller.ts
index e6fd598302..581302e341 100644
--- a/packages/labs/observers/src/intersection_controller.ts
+++ b/packages/labs/observers/src/intersection_controller.ts
@@ -11,14 +11,14 @@ import {
/**
* The callback function for a IntersectionController.
*/
-export type IntersectionValueCallback = (
+export type IntersectionValueCallback = (
...args: Parameters
-) => unknown;
+) => T;
/**
* The config options for a IntersectionController.
*/
-export interface IntersectionControllerConfig {
+export interface IntersectionControllerConfig {
/**
* Configuration object for the IntersectionObserver.
*/
@@ -35,7 +35,7 @@ export interface IntersectionControllerConfig {
* The callback used to process detected changes into a value stored
* in the controller's `value` property.
*/
- callback?: IntersectionValueCallback;
+ callback?: IntersectionValueCallback;
/**
* An IntersectionObserver reports the initial intersection state
* when observe is called. Setting this flag to true skips processing this
@@ -60,9 +60,9 @@ export interface IntersectionControllerConfig {
* used to process the result into a value which is stored on the controller.
* The controller's `value` is usable during the host's update cycle.
*/
-export class IntersectionController implements ReactiveController {
+export class IntersectionController implements ReactiveController {
private _host: ReactiveControllerHost;
- private _target: Element | null;
+ private _targets: Set = new Set();
private _observer!: IntersectionObserver;
private _skipInitial = false;
/**
@@ -77,22 +77,23 @@ export class IntersectionController implements ReactiveController {
* The result of processing the observer's changes via the `callback`
* function.
*/
- value?: unknown;
+ value?: T;
/**
* Function that returns a value processed from the observer's changes.
* The result is stored in the `value` property.
*/
- callback: IntersectionValueCallback = () => true;
+ callback?: IntersectionValueCallback;
constructor(
- host: ReactiveControllerHost,
- {target, config, callback, skipInitial}: IntersectionControllerConfig
+ host: ReactiveControllerHost & Element,
+ {target, config, callback, skipInitial}: IntersectionControllerConfig
) {
- (this._host = host).addController(this);
+ this._host = host;
// Target defaults to `host` unless explicitly `null`.
- this._target =
- target === null ? target : target ?? (this._host as unknown as Element);
+ if (target !== null) {
+ this._targets.add(target ?? host);
+ }
this._skipInitial = skipInitial ?? this._skipInitial;
- this.callback = callback ?? this.callback;
+ this.callback = callback;
// Check browser support.
if (!window.IntersectionObserver) {
console.warn(
@@ -112,6 +113,7 @@ export class IntersectionController implements ReactiveController {
},
config
);
+ host.addController(this);
}
/**
@@ -119,12 +121,12 @@ export class IntersectionController implements ReactiveController {
* function to produce a result stored in the `value` property.
*/
protected handleChanges(entries: IntersectionObserverEntry[]) {
- this.value = this.callback(entries, this._observer);
+ this.value = this.callback?.(entries, this._observer);
}
hostConnected() {
- if (this._target) {
- this.observe(this._target);
+ for (const target of this._targets) {
+ this.observe(target);
}
}
@@ -146,12 +148,22 @@ export class IntersectionController implements ReactiveController {
* @param target Element to observe
*/
observe(target: Element) {
+ this._targets.add(target);
// Note, this will always trigger the callback since the initial
// intersection state is reported.
this._observer.observe(target);
this._unobservedUpdate = true;
}
+ /**
+ * Unobserve the target element.
+ * @param target Element to unobserve
+ */
+ unobserve(target: Element) {
+ this._targets.delete(target);
+ this._observer.unobserve(target);
+ }
+
/**
* Disconnects the observer. This is done automatically when the host
* disconnects.
diff --git a/packages/labs/observers/src/mutation_controller.ts b/packages/labs/observers/src/mutation_controller.ts
index 6b07b2aee4..e331503e63 100644
--- a/packages/labs/observers/src/mutation_controller.ts
+++ b/packages/labs/observers/src/mutation_controller.ts
@@ -11,14 +11,14 @@ import {
/**
* The callback function for a MutationController.
*/
-export type MutationValueCallback = (
+export type MutationValueCallback = (
...args: Parameters
-) => unknown;
+) => T;
/**
* The config options for a MutationController.
*/
-export interface MutationControllerConfig {
+export interface MutationControllerConfig {
/**
* Configuration object for the MutationObserver.
*/
@@ -35,7 +35,7 @@ export interface MutationControllerConfig {
* The callback used to process detected changes into a value stored
* in the controller's `value` property.
*/
- callback?: MutationValueCallback;
+ callback?: MutationValueCallback;
/**
* By default the `callback` is called without changes when a target is
* observed. This is done to help manage initial state, but this
@@ -59,9 +59,9 @@ export interface MutationControllerConfig {
* used to process the result into a value which is stored on the controller.
* The controller's `value` is usable during the host's update cycle.
*/
-export class MutationController implements ReactiveController {
+export class MutationController implements ReactiveController {
private _host: ReactiveControllerHost;
- private _target: Element | null;
+ private _targets: Set = new Set();
private _config: MutationObserverInit;
private _observer!: MutationObserver;
private _skipInitial = false;
@@ -76,23 +76,24 @@ export class MutationController implements ReactiveController {
* The result of processing the observer's changes via the `callback`
* function.
*/
- value?: unknown;
+ value?: T;
/**
* Function that returns a value processed from the observer's changes.
* The result is stored in the `value` property.
*/
- callback: MutationValueCallback = () => true;
+ callback?: MutationValueCallback;
constructor(
- host: ReactiveControllerHost,
- {target, config, callback, skipInitial}: MutationControllerConfig
+ host: ReactiveControllerHost & Element,
+ {target, config, callback, skipInitial}: MutationControllerConfig
) {
- (this._host = host).addController(this);
+ this._host = host;
// Target defaults to `host` unless explicitly `null`.
- this._target =
- target === null ? target : target ?? (this._host as unknown as Element);
+ if (target !== null) {
+ this._targets.add(target ?? host);
+ }
this._config = config;
this._skipInitial = skipInitial ?? this._skipInitial;
- this.callback = callback ?? this.callback;
+ this.callback = callback;
// Check browser support.
if (!window.MutationObserver) {
console.warn(
@@ -104,6 +105,7 @@ export class MutationController implements ReactiveController {
this.handleChanges(records);
this._host.requestUpdate();
});
+ host.addController(this);
}
/**
@@ -111,12 +113,12 @@ export class MutationController implements ReactiveController {
* function to produce a result stored in the `value` property.
*/
protected handleChanges(records: MutationRecord[]) {
- this.value = this.callback(records, this._observer);
+ this.value = this.callback?.(records, this._observer);
}
hostConnected() {
- if (this._target) {
- this.observe(this._target);
+ for (const target of this._targets) {
+ this.observe(target);
}
}
@@ -145,6 +147,7 @@ export class MutationController implements ReactiveController {
* @param target Element to observe
*/
observe(target: Element) {
+ this._targets.add(target);
this._observer.observe(target, this._config);
this._unobservedUpdate = true;
this._host.requestUpdate();
diff --git a/packages/labs/observers/src/performance_controller.ts b/packages/labs/observers/src/performance_controller.ts
index ed4516adec..c48b3eddd5 100644
--- a/packages/labs/observers/src/performance_controller.ts
+++ b/packages/labs/observers/src/performance_controller.ts
@@ -11,16 +11,16 @@ import {
/**
* The callback function for a PerformanceController.
*/
-export type PerformanceValueCallback = (
+export type PerformanceValueCallback = (
entries: PerformanceEntryList,
observer: PerformanceObserver,
entryList?: PerformanceObserverEntryList
-) => unknown;
+) => T;
/**
* The config options for a PerformanceController.
*/
-export interface PerformanceControllerConfig {
+export interface PerformanceControllerConfig {
/**
* Configuration object for the PerformanceObserver.
*/
@@ -29,7 +29,7 @@ export interface PerformanceControllerConfig {
* The callback used to process detected changes into a value stored
* in the controller's `value` property.
*/
- callback?: PerformanceValueCallback;
+ callback?: PerformanceValueCallback;
/**
* By default the `callback` is called without changes when a target is
* observed. This is done to help manage initial state, but this
@@ -50,7 +50,7 @@ export interface PerformanceControllerConfig {
* used to process the result into a value which is stored on the controller.
* The controller's `value` is usable during the host's update cycle.
*/
-export class PerformanceController implements ReactiveController {
+export class PerformanceController implements ReactiveController {
private _host: ReactiveControllerHost;
private _config: PerformanceObserverInit;
private _observer!: PerformanceObserver;
@@ -66,20 +66,20 @@ export class PerformanceController implements ReactiveController {
* The result of processing the observer's changes via the `callback`
* function.
*/
- value?: unknown;
+ value?: T;
/**
* Function that returns a value processed from the observer's changes.
* The result is stored in the `value` property.
*/
- callback: PerformanceValueCallback = () => true;
+ callback?: PerformanceValueCallback;
constructor(
host: ReactiveControllerHost,
- {config, callback, skipInitial}: PerformanceControllerConfig
+ {config, callback, skipInitial}: PerformanceControllerConfig
) {
- (this._host = host).addController(this);
+ this._host = host;
this._config = config;
this._skipInitial = skipInitial ?? this._skipInitial;
- this.callback = callback ?? this.callback;
+ this.callback = callback;
// Check browser support.
if (!window.PerformanceObserver) {
console.warn(
@@ -93,6 +93,7 @@ export class PerformanceController implements ReactiveController {
this._host.requestUpdate();
}
);
+ host.addController(this);
}
/**
@@ -103,7 +104,7 @@ export class PerformanceController implements ReactiveController {
entries: PerformanceEntryList,
entryList?: PerformanceObserverEntryList
) {
- this.value = this.callback(entries, this._observer, entryList);
+ this.value = this.callback?.(entries, this._observer, entryList);
}
hostConnected() {
diff --git a/packages/labs/observers/src/resize_controller.ts b/packages/labs/observers/src/resize_controller.ts
index a47f3dbce0..5dbbdb0649 100644
--- a/packages/labs/observers/src/resize_controller.ts
+++ b/packages/labs/observers/src/resize_controller.ts
@@ -11,14 +11,14 @@ import {
/**
* The callback function for a ResizeController.
*/
-export type ResizeValueCallback = (
+export type ResizeValueCallback = (
...args: Parameters
-) => unknown;
+) => T;
/**
* The config options for a ResizeController.
*/
-export interface ResizeControllerConfig {
+export interface ResizeControllerConfig {
/**
* Configuration object for the ResizeController.
*/
@@ -35,7 +35,7 @@ export interface ResizeControllerConfig {
* The callback used to process detected changes into a value stored
* in the controller's `value` property.
*/
- callback?: ResizeValueCallback;
+ callback?: ResizeValueCallback;
/**
* By default the `callback` is called without changes when a target is
* observed. This is done to help manage initial state, but this
@@ -58,9 +58,9 @@ export interface ResizeControllerConfig {
* used to process the result into a value which is stored on the controller.
* The controller's `value` is usable during the host's update cycle.
*/
-export class ResizeController implements ReactiveController {
+export class ResizeController implements ReactiveController {
private _host: ReactiveControllerHost;
- private _target: Element | null;
+ private _targets: Set = new Set();
private _config?: ResizeObserverOptions;
private _observer!: ResizeObserver;
private _skipInitial = false;
@@ -75,23 +75,24 @@ export class ResizeController implements ReactiveController {
* The result of processing the observer's changes via the `callback`
* function.
*/
- value?: unknown;
+ value?: T;
/**
* Function that returns a value processed from the observer's changes.
* The result is stored in the `value` property.
*/
- callback: ResizeValueCallback = () => true;
+ callback?: ResizeValueCallback;
constructor(
- host: ReactiveControllerHost,
- {target, config, callback, skipInitial}: ResizeControllerConfig
+ host: ReactiveControllerHost & Element,
+ {target, config, callback, skipInitial}: ResizeControllerConfig
) {
- (this._host = host).addController(this);
+ this._host = host;
// Target defaults to `host` unless explicitly `null`.
- this._target =
- target === null ? target : target ?? (this._host as unknown as Element);
+ if (target !== null) {
+ this._targets.add(target ?? host);
+ }
this._config = config;
this._skipInitial = skipInitial ?? this._skipInitial;
- this.callback = callback ?? this.callback;
+ this.callback = callback;
// Check browser support.
if (!window.ResizeObserver) {
console.warn(
@@ -103,6 +104,7 @@ export class ResizeController implements ReactiveController {
this.handleChanges(entries);
this._host.requestUpdate();
});
+ host.addController(this);
}
/**
@@ -110,12 +112,12 @@ export class ResizeController implements ReactiveController {
* function to produce a result stored in the `value` property.
*/
protected handleChanges(entries: ResizeObserverEntry[]) {
- this.value = this.callback(entries, this._observer);
+ this.value = this.callback?.(entries, this._observer);
}
hostConnected() {
- if (this._target) {
- this.observe(this._target);
+ for (const target of this._targets) {
+ this.observe(target);
}
}
@@ -139,11 +141,21 @@ export class ResizeController implements ReactiveController {
* @param target Element to observe
*/
observe(target: Element) {
+ this._targets.add(target);
this._observer.observe(target, this._config);
this._unobservedUpdate = true;
this._host.requestUpdate();
}
+ /**
+ * Unobserve the target element.
+ * @param target Element to unobserve
+ */
+ unobserve(target: Element) {
+ this._targets.delete(target);
+ this._observer.unobserve(target);
+ }
+
/**
* Disconnects the observer. This is done automatically when the host
* disconnects.
diff --git a/packages/labs/observers/src/test/intersection_controller_test.ts b/packages/labs/observers/src/test/intersection_controller_test.ts
index afeaeabc4e..d8867e22d0 100644
--- a/packages/labs/observers/src/test/intersection_controller_test.ts
+++ b/packages/labs/observers/src/test/intersection_controller_test.ts
@@ -12,6 +12,7 @@ import {
import {
IntersectionController,
IntersectionControllerConfig,
+ IntersectionValueCallback,
} from '@lit-labs/observers/intersection_controller.js';
import {generateElementName, nextFrame} from './test-helpers.js';
import {assert} from '@esm-bundle/chai';
@@ -24,10 +25,16 @@ if (DEV_MODE) {
ReactiveElement.disableWarning?.('change-in-update');
}
-// TODO: disable these tests until can figure out issues with Sauce Safari
-// version. They do pass on latest Safari locally.
-//(window.IntersectionObserver ? suite : suite.skip)
-suite.skip('IntersectionController', () => {
+const canTest = () => {
+ // TODO: disable tests on Sauce Safari until can figure out issues.
+ // The tests pass on latest Safari locally.
+ const isSafari =
+ navigator.userAgent.includes('Safari/') &&
+ navigator.userAgent.includes('Version/');
+ return !isSafari && window.IntersectionObserver;
+};
+
+(canTest() ? suite : suite.skip)('IntersectionController', () => {
let container: HTMLElement;
interface TestElement extends ReactiveElement {
@@ -49,7 +56,10 @@ suite.skip('IntersectionController', () => {
constructor() {
super();
const config = getControllerConfig(this);
- this.observer = new IntersectionController(this, config);
+ this.observer = new IntersectionController(this, {
+ callback: () => true,
+ ...config,
+ });
}
override update(props: PropertyValues) {
@@ -91,6 +101,8 @@ suite.skip('IntersectionController', () => {
const intersectionComplete = async () => {
await nextFrame();
await nextFrame();
+ // For Firefox to pass tests we need a setTimeout.
+ await new Promise((resolve) => setTimeout(resolve, 0));
};
const intersectOut = (el: HTMLElement) => {
@@ -337,6 +349,36 @@ suite.skip('IntersectionController', () => {
assert.isTrue(el.observerValue);
});
+ test('observed targets are re-observed on host connected', async () => {
+ const el = await getTestElement(() => ({target: null}));
+ const d1 = document.createElement('div');
+ const d2 = document.createElement('div');
+
+ el.renderRoot.appendChild(d1);
+ el.renderRoot.appendChild(d2);
+
+ el.observer.observe(d1);
+ el.observer.observe(d2);
+
+ await intersectionComplete();
+ el.resetObserverValue();
+ el.remove();
+
+ container.appendChild(el);
+
+ // Reports change to first observed target.
+ el.resetObserverValue();
+ intersectOut(d1);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+
+ // Reports change to second observed target.
+ el.resetObserverValue();
+ intersectOut(d2);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ });
+
test('observed target respects `skipInitial`', async () => {
const el = await getTestElement(() => ({
target: null,
@@ -358,7 +400,7 @@ suite.skip('IntersectionController', () => {
assert.isTrue(el.observerValue);
});
- test('observed target not re-observed on connection', async () => {
+ test('observed target re-observed on connection', async () => {
const el = await getTestElement(() => ({
target: null,
skipInitial: true,
@@ -375,20 +417,145 @@ suite.skip('IntersectionController', () => {
await intersectionComplete();
assert.isUndefined(el.observerValue);
- // Does not report change when re-connected
+ // Reports changes when re-connected
container.appendChild(el);
await intersectionComplete();
assert.isUndefined(el.observerValue);
intersectOut(d1);
await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ });
+
+ test('can observe changes when initialized after host connected', async () => {
+ class TestFirstUpdated extends ReactiveElement {
+ observer!: IntersectionController;
+ observerValue: true | undefined = undefined;
+ override firstUpdated() {
+ this.observer = new IntersectionController(this, {
+ callback: () => true,
+ });
+ }
+ override updated() {
+ this.observerValue = this.observer.value;
+ }
+ resetObserverValue() {
+ this.observer.value = this.observerValue = undefined;
+ }
+ }
+ customElements.define(generateElementName(), TestFirstUpdated);
+ const el = (await renderTestElement(TestFirstUpdated)) as TestFirstUpdated;
+
+ // Reports initial change by default
+ assert.isTrue(el.observerValue);
+
+ // Reports change when not intersecting
+ el.resetObserverValue();
+ intersectOut(el);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+
+ // Reports change when intersecting
+ el.resetObserverValue();
assert.isUndefined(el.observerValue);
+ intersectIn(el);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ });
+
+ test('can observe external element after host connected', async () => {
+ const d = document.createElement('div');
+ container.appendChild(d);
+ class A extends ReactiveElement {
+ observer!: IntersectionController;
+ observerValue: true | undefined = undefined;
+ override firstUpdated() {
+ this.observer = new IntersectionController(this, {
+ target: d,
+ skipInitial: true,
+ callback: () => true,
+ });
+ }
+ override updated() {
+ this.observerValue = this.observer.value;
+ }
+ resetObserverValue() {
+ this.observer.value = this.observerValue = undefined;
+ }
+ }
+ customElements.define(generateElementName(), A);
+ const el = (await renderTestElement(A)) as A;
- // Can re-observe after connection, respecting `skipInitial`
+ assert.equal(el.observerValue, undefined);
+ // Observe intersect out
+ intersectOut(d);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ el.resetObserverValue();
+
+ // Observe intersect in
+ intersectIn(d);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ });
+
+ test('IntersectionController type-checks', async () => {
+ // This test only checks compile-type behavior. There are no runtime checks.
+ const el = await getTestElement();
+ const A = new IntersectionController(el, {
+ // @ts-expect-error Type 'string' is not assignable to type 'number'
+ callback: () => '',
+ });
+ if (A) {
+ // Suppress no-unused-vars warnings
+ }
+
+ const B = new IntersectionController(el, {
+ callback: () => '',
+ });
+ // @ts-expect-error Type 'number' is not assignable to type 'string'.
+ B.value = 2;
+
+ const C = new IntersectionController(
+ el,
+ {} as IntersectionController
+ );
+ // @ts-expect-error Type 'number' is not assignable to type 'string'.
+ C.value = 3;
+
+ const narrowTypeCb: IntersectionValueCallback = () => '';
+ const D = new IntersectionController(el, {callback: narrowTypeCb});
+
+ D.value = null;
+ D.value = undefined;
+ D.value = '';
+ // @ts-expect-error Type 'number' is not assignable to type 'string'
+ D.value = 3;
+ });
+
+ test('observed target can be unobserved', async () => {
+ const el = await getTestElement(() => ({target: null}));
+ const d1 = document.createElement('div');
+
+ // Reports initial changes when observe called.
el.observer.observe(d1);
+ el.renderRoot.appendChild(d1);
+ await intersectionComplete();
+ assert.isTrue(el.observerValue);
+ el.resetObserverValue();
+ await intersectionComplete();
+
+ // Does not report change when unobserved
+ el.observer.unobserve(d1);
+ intersectOut(d1);
await intersectionComplete();
assert.isUndefined(el.observerValue);
+
+ el.remove();
+ container.appendChild(el);
+
+ // Does not report changes when re-connected
intersectIn(d1);
await intersectionComplete();
- assert.isTrue(el.observerValue);
+ assert.isUndefined(el.observerValue);
});
});
diff --git a/packages/labs/observers/src/test/mutation_controller_test.ts b/packages/labs/observers/src/test/mutation_controller_test.ts
index 4fb4b98655..da38ff9e24 100644
--- a/packages/labs/observers/src/test/mutation_controller_test.ts
+++ b/packages/labs/observers/src/test/mutation_controller_test.ts
@@ -12,6 +12,7 @@ import {
import {
MutationController,
MutationControllerConfig,
+ MutationValueCallback,
} from '@lit-labs/observers/mutation_controller.js';
import {generateElementName, nextFrame} from './test-helpers.js';
import {assert} from '@esm-bundle/chai';
@@ -53,7 +54,10 @@ const canTest =
constructor() {
super();
const config = getControllerConfig(this);
- this.observer = new MutationController(this, config);
+ this.observer = new MutationController(this, {
+ callback: () => true,
+ ...config,
+ });
}
override update(props: PropertyValues) {
@@ -315,6 +319,36 @@ const canTest =
assert.isTrue(el.observerValue);
});
+ test('observed targets are re-observed on host connected', async () => {
+ const el = await getTestElement(() => ({
+ target: null,
+ config: {attributes: true},
+ }));
+ el.resetObserverValue();
+ const d1 = document.createElement('div');
+ const d2 = document.createElement('div');
+
+ el.observer.observe(d1);
+ el.observer.observe(d2);
+
+ await nextFrame();
+ el.remove();
+
+ container.appendChild(el);
+
+ // Reports change to first observed target.
+ el.resetObserverValue();
+ d1.setAttribute('a', 'a');
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+
+ // Reports change to second observed target.
+ el.resetObserverValue();
+ d2.setAttribute('a', 'a1');
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+ });
+
test('observed target respects `skipInitial`', async () => {
const el = await getTestElement(() => ({
target: null,
@@ -335,7 +369,7 @@ const canTest =
assert.isTrue(el.observerValue);
});
- test('observed target not re-observed on connection', async () => {
+ test('observed target re-observed on connection', async () => {
const el = await getTestElement(() => ({
target: null,
config: {attributes: true},
@@ -356,16 +390,131 @@ const canTest =
await nextFrame();
assert.isUndefined(el.observerValue);
- // Does not report change when re-connected
+ // Does report change when re-connected
container.appendChild(el);
d1.setAttribute('a', 'a1');
await nextFrame();
- assert.isUndefined(el.observerValue);
+ assert.isTrue(el.observerValue);
// Can re-observe after connection.
+ el.resetObserverValue();
el.observer.observe(d1);
d1.setAttribute('a', 'a2');
await nextFrame();
assert.isTrue(el.observerValue);
});
+
+ test('can observe changes when initialized after host connected', async () => {
+ class TestFirstUpdated extends ReactiveElement {
+ observer!: MutationController;
+ observerValue: true | undefined = undefined;
+ override firstUpdated() {
+ this.observer = new MutationController(this, {
+ config: {attributes: true},
+ callback: () => true,
+ });
+ }
+ override updated() {
+ this.observerValue = this.observer.value;
+ }
+ resetObserverValue() {
+ this.observer.value = this.observerValue = undefined;
+ }
+ }
+ customElements.define(generateElementName(), TestFirstUpdated);
+
+ const el = (await renderTestElement(TestFirstUpdated)) as TestFirstUpdated;
+
+ // Reports initial change by default
+ assert.isTrue(el.observerValue);
+
+ // Reports attribute change
+ el.resetObserverValue();
+ el.setAttribute('hi', 'hi');
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+
+ // Reports another attribute change
+ el.resetObserverValue();
+ el.requestUpdate();
+ await nextFrame();
+ assert.isUndefined(el.observerValue);
+ el.setAttribute('bye', 'bye');
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+ });
+
+ test('can observe external element after host connected', async () => {
+ class A extends ReactiveElement {
+ observer!: MutationController;
+ observerValue: true | undefined = undefined;
+ override firstUpdated() {
+ this.observer = new MutationController(this, {
+ target: document.body,
+ config: {childList: true},
+ skipInitial: true,
+ callback: () => true,
+ });
+ }
+ override updated() {
+ this.observerValue = this.observer.value;
+ }
+ resetObserverValue() {
+ this.observer.value = this.observerValue = undefined;
+ }
+ }
+ customElements.define(generateElementName(), A);
+
+ const el = (await renderTestElement(A)) as A;
+ assert.equal(el.observerValue, undefined);
+ const d = document.createElement('div');
+ document.body.appendChild(d);
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+ el.resetObserverValue();
+ d.remove();
+ await nextFrame();
+ assert.isTrue(el.observerValue);
+ });
+
+ test('MutationController type-checks', async () => {
+ // This test only checks compile-type behavior. There are no runtime checks.
+ const el = await getTestElement(() => ({
+ target: null,
+ config: {attributes: true},
+ }));
+ const A = new MutationController(el, {
+ // @ts-expect-error Type 'string' is not assignable to type 'number'
+ callback: () => '',
+ config: {attributes: true},
+ });
+ if (A) {
+ // Suppress no-unused-vars warnings
+ }
+
+ const B = new MutationController(el, {
+ callback: () => '',
+ config: {attributes: true},
+ });
+ // @ts-expect-error Type 'number' is not assignable to type 'string'.
+ B.value = 2;
+
+ const C = new MutationController(el, {
+ config: {attributes: true},
+ }) as MutationController;
+ // @ts-expect-error Type 'number' is not assignable to type 'string'.
+ C.value = 3;
+
+ const narrowTypeCb: MutationValueCallback = () => '';
+ const D = new MutationController(el, {
+ callback: narrowTypeCb,
+ config: {attributes: true},
+ });
+
+ D.value = null;
+ D.value = undefined;
+ D.value = '';
+ // @ts-expect-error Type 'number' is not assignable to type 'string'
+ D.value = 3;
+ });
});
diff --git a/packages/labs/observers/src/test/performance_controller_test.ts b/packages/labs/observers/src/test/performance_controller_test.ts
index ac59923ffc..028c56eb51 100644
--- a/packages/labs/observers/src/test/performance_controller_test.ts
+++ b/packages/labs/observers/src/test/performance_controller_test.ts
@@ -12,6 +12,7 @@ import {
import {
PerformanceController,
PerformanceControllerConfig,
+ PerformanceValueCallback,
} from '@lit-labs/observers/performance_controller.js';
import {generateElementName, nextFrame} from './test-helpers.js';
import {assert} from '@esm-bundle/chai';
@@ -35,7 +36,7 @@ const generateMeasure = async (sync = false) => {
const observerComplete = async (el?: HTMLElement) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- (el as any)?.observer.flush();
+ (el as any)?.observer?.flush();
await nextFrame();
await nextFrame();
};
@@ -51,9 +52,16 @@ const observerComplete = async (el?: HTMLElement) => {
// return ok;
// };
-// TODO: disable these tests until can figure out issues with Sauce Safari
-// version. They do pass on latest Safari locally.
-suite.skip('PerformanceController', () => {
+const canTest = () => {
+ // TODO: disable tests on Sauce Safari until can figure out issues.
+ // The tests pass on latest Safari locally.
+ const isSafari =
+ navigator.userAgent.includes('Safari/') &&
+ navigator.userAgent.includes('Version/');
+ return !isSafari && window.PerformanceObserver;
+};
+
+(canTest() ? suite : suite.skip)('PerformanceController', () => {
let container: HTMLElement;
interface TestElement extends ReactiveElement {
@@ -75,7 +83,10 @@ suite.skip('PerformanceController', () => {
constructor() {
super();
const config = getControllerConfig(this);
- this.observer = new PerformanceController(this, config);
+ this.observer = new PerformanceController(this, {
+ callback: () => true,
+ ...config,
+ });
}
override update(props: PropertyValues) {
@@ -224,4 +235,84 @@ suite.skip('PerformanceController', () => {
await observerComplete(el);
assert.match(el.observerValue as string, /2:[\d]/);
});
+
+ test('can observe changes when initialized after host connected', async () => {
+ class TestFirstUpdated extends ReactiveElement {
+ observer!: PerformanceController;
+ observerValue: true | undefined = undefined;
+ override firstUpdated() {
+ this.observer = new PerformanceController(this, {
+ config: {entryTypes: ['measure']},
+ callback: () => true,
+ });
+ }
+ override updated() {
+ this.observerValue = this.observer.value;
+ }
+ resetObserverValue() {
+ this.observer.value = this.observerValue = undefined;
+ }
+ }
+ customElements.define(generateElementName(), TestFirstUpdated);
+ const el = (await renderTestElement(TestFirstUpdated)) as TestFirstUpdated;
+
+ // Reports initial change by default
+ assert.isTrue(el.observerValue);
+
+ // Reports measure
+ el.resetObserverValue();
+ await generateMeasure();
+ await observerComplete(el);
+ assert.isTrue(el.observerValue);
+
+ // Reports another measure after noop update
+ el.resetObserverValue();
+ el.requestUpdate();
+ await observerComplete(el);
+ assert.isUndefined(el.observerValue);
+ await generateMeasure();
+ await observerComplete(el);
+ assert.isTrue(el.observerValue);
+ });
+
+ test('PerformanceController