diff --git a/.changeset/tall-mirrors-notice.md b/.changeset/tall-mirrors-notice.md new file mode 100644 index 0000000000..974d0cd818 --- /dev/null +++ b/.changeset/tall-mirrors-notice.md @@ -0,0 +1,7 @@ +--- +'lit': minor +'lit-html': minor +'@lit/reactive-element': minor +--- + +Lit and its underlying libraries can now be imported directly from Node without crashing, without the need to load the @lit-labs/ssr dom-shim library. Note that actually rendering from a Node context still requires the @lit-labs/ssr dom-shim, and the appropriate integration between @lit-labs/ssr and your framework/tool. diff --git a/.eslintignore b/.eslintignore index 4ec7c50df3..81d5ab79cf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -62,6 +62,7 @@ packages/lit-element/polyfill-support.* packages/lit-element/private-ssr-support.* packages/lit-html/development/ +packages/lit-html/node/ packages/lit-html/version-stability-build/ packages/lit-html/directives/ packages/lit-html/node_modules/ @@ -118,6 +119,7 @@ packages/localize-tools/testdata/*/output/ packages/reactive-element/decorators/ packages/reactive-element/development/ +packages/reactive-element/node/ packages/reactive-element/test/ packages/reactive-element/css-tag.* packages/reactive-element/decorators.* diff --git a/.prettierignore b/.prettierignore index 58d33fa037..1a9631ebbb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -62,6 +62,7 @@ packages/lit-element/polyfill-support.* packages/lit-element/private-ssr-support.* packages/lit-html/development/ +packages/lit-html/node/ packages/lit-html/version-stability-build/ packages/lit-html/directives/ packages/lit-html/node_modules/ @@ -104,6 +105,7 @@ packages/localize-tools/testdata/*/output/ packages/reactive-element/decorators/ packages/reactive-element/development/ +packages/reactive-element/node/ packages/reactive-element/test/ packages/reactive-element/css-tag.* packages/reactive-element/decorators.* diff --git a/packages/lit-element/package.json b/packages/lit-element/package.json index 2f3b02f561..1fc5bab937 100644 --- a/packages/lit-element/package.json +++ b/packages/lit-element/package.json @@ -104,7 +104,8 @@ "prepublishOnly": "npm run check-version", "test": "wireit", "test:dev": "wireit", - "test:prod": "wireit" + "test:prod": "wireit", + "test:node": "wireit" }, "wireit": { "build": { @@ -198,6 +199,7 @@ "dependencies": [ "test:dev", "test:prod", + "test:node", "check-version" ] }, @@ -219,6 +221,15 @@ ], "files": [], "output": [] + }, + "test:node": { + "command": "node development/test/node-imports.js", + "dependencies": [ + "build:ts", + "build:rollup" + ], + "files": [], + "output": [] } }, "files": [ diff --git a/packages/lit-element/src/test/node-imports.ts b/packages/lit-element/src/test/node-imports.ts new file mode 100644 index 0000000000..59f35e60a0 --- /dev/null +++ b/packages/lit-element/src/test/node-imports.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +// This file will be loaded by Node from the node:test script to verify that all +// exports of this package can be imported without crashing in Node. + +import 'lit-element'; +import 'lit-element/experimental-hydrate-support.js'; +import 'lit-element/private-ssr-support.js'; +import 'lit-element/decorators.js'; +import 'lit-element/decorators/custom-element.js'; +import 'lit-element/decorators/event-options.js'; +import 'lit-element/decorators/state.js'; +import 'lit-element/decorators/property.js'; +import 'lit-element/decorators/query.js'; +import 'lit-element/decorators/query-all.js'; +import 'lit-element/decorators/query-assigned-elements.js'; +import 'lit-element/decorators/query-assigned-nodes.js'; +import 'lit-element/decorators/query-async.js'; + +import {LitElement, html} from 'lit-element'; +import {customElement} from 'lit-element/decorators.js'; + +@customElement('my-element') +export class MyElement extends LitElement { + override render() { + return html`Hello World`; + } +} + +export class MyOtherElement extends LitElement { + override render() { + return html`Hello World`; + } +} +customElements.define('my-other-element', MyOtherElement); diff --git a/packages/lit-html/.gitignore b/packages/lit-html/.gitignore index 049a253061..843b4373d0 100644 --- a/packages/lit-html/.gitignore +++ b/packages/lit-html/.gitignore @@ -1,4 +1,5 @@ /development/ +/node/ /version-stability-build/ /directives/ /node_modules/ diff --git a/packages/lit-html/package.json b/packages/lit-html/package.json index 893d22ae8f..e7e6a02f1a 100644 --- a/packages/lit-html/package.json +++ b/packages/lit-html/package.json @@ -15,141 +15,169 @@ "exports": { ".": { "types": "./development/lit-html.d.ts", + "node": "./node/lit-html.js", "development": "./development/lit-html.js", "default": "./lit-html.js" }, "./async-directive.js": { "types": "./development/async-directive.d.ts", + "node": "./node/async-directive.js", "development": "./development/async-directive.js", "default": "./async-directive.js" }, "./directive-helpers.js": { "types": "./development/directive-helpers.d.ts", + "node": "./node/directive-helpers.js", "development": "./development/directive-helpers.js", "default": "./directive-helpers.js" }, "./directive.js": { "types": "./development/directive.d.ts", + "node": "./node/directive.js", "development": "./development/directive.js", "default": "./directive.js" }, "./directives/async-append.js": { "types": "./development/directives/async-append.d.ts", + "node": "./node/directives/async-append.js", "development": "./development/directives/async-append.js", "default": "./directives/async-append.js" }, "./directives/async-replace.js": { "types": "./development/directives/async-replace.d.ts", + "node": "./node/directives/async-replace.js", "development": "./development/directives/async-replace.js", "default": "./directives/async-replace.js" }, "./directives/cache.js": { "types": "./development/directives/cache.d.ts", + "node": "./node/directives/cache.js", "development": "./development/directives/cache.js", "default": "./directives/cache.js" }, "./directives/choose.js": { "types": "./development/directives/choose.d.ts", + "node": "./node/directives/choose.js", "development": "./development/directives/choose.js", "default": "./directives/choose.js" }, "./directives/class-map.js": { "types": "./development/directives/class-map.d.ts", + "node": "./node/directives/class-map.js", "development": "./development/directives/class-map.js", "default": "./directives/class-map.js" }, "./directives/guard.js": { "types": "./development/directives/guard.d.ts", + "node": "./node/directives/guard.js", "development": "./development/directives/guard.js", "default": "./directives/guard.js" }, "./directives/if-defined.js": { "types": "./development/directives/if-defined.d.ts", + "node": "./node/directives/if-defined.js", "development": "./development/directives/if-defined.js", "default": "./directives/if-defined.js" }, "./directives/join.js": { "types": "./development/directives/join.d.ts", + "node": "./node/directives/join.js", "development": "./development/directives/join.js", "default": "./directives/join.js" }, "./directives/keyed.js": { "types": "./development/directives/keyed.d.ts", + "node": "./node/directives/keyed.js", "development": "./development/directives/keyed.js", "default": "./directives/keyed.js" }, "./directives/live.js": { "types": "./development/directives/live.d.ts", + "node": "./node/directives/live.js", "development": "./development/directives/live.js", "default": "./directives/live.js" }, "./directives/map.js": { "types": "./development/directives/map.d.ts", + "node": "./node/directives/map.js", "development": "./development/directives/map.js", "default": "./directives/map.js" }, "./directives/range.js": { "types": "./development/directives/range.d.ts", + "node": "./node/directives/range.js", "development": "./development/directives/range.js", "default": "./directives/range.js" }, "./directives/ref.js": { "types": "./development/directives/ref.d.ts", + "node": "./node/directives/ref.js", "development": "./development/directives/ref.js", "default": "./directives/ref.js" }, "./directives/repeat.js": { "types": "./development/directives/repeat.d.ts", + "node": "./node/directives/repeat.js", "development": "./development/directives/repeat.js", "default": "./directives/repeat.js" }, "./directives/style-map.js": { "types": "./development/directives/style-map.d.ts", + "node": "./node/directives/style-map.js", "development": "./development/directives/style-map.js", "default": "./directives/style-map.js" }, "./directives/template-content.js": { "types": "./development/directives/template-content.d.ts", + "node": "./node/directives/template-content.js", "development": "./development/directives/template-content.js", "default": "./directives/template-content.js" }, "./directives/unsafe-html.js": { "types": "./development/directives/unsafe-html.d.ts", + "node": "./node/directives/unsafe-html.js", "development": "./development/directives/unsafe-html.js", "default": "./directives/unsafe-html.js" }, "./directives/unsafe-svg.js": { "types": "./development/directives/unsafe-svg.d.ts", + "node": "./node/directives/unsafe-svg.js", "development": "./development/directives/unsafe-svg.js", "default": "./directives/unsafe-svg.js" }, "./directives/until.js": { "types": "./development/directives/until.d.ts", + "node": "./node/directives/until.js", "development": "./development/directives/until.js", "default": "./directives/until.js" }, "./directives/when.js": { "types": "./development/directives/when.d.ts", + "node": "./node/directives/when.js", "development": "./development/directives/when.js", "default": "./directives/when.js" }, "./experimental-hydrate.js": { "types": "./development/experimental-hydrate.d.ts", + "node": "./node/experimental-hydrate.js", "development": "./development/experimental-hydrate.js", "default": "./experimental-hydrate.js" }, "./polyfill-support.js": { "types": "./development/polyfill-support.d.ts", + "node": "./node/polyfill-support.js", "development": "./development/polyfill-support.js", "default": "./polyfill-support.js" }, "./private-ssr-support.js": { "types": "./development/private-ssr-support.d.ts", + "node": "./node/private-ssr-support.js", "development": "./development/private-ssr-support.js", "default": "./private-ssr-support.js" }, "./static.js": { "types": "./development/static.d.ts", + "node": "./node/static.js", "development": "./development/static.js", "default": "./static.js" } @@ -165,7 +193,8 @@ "prepublishOnly": "npm run check-version", "test": "wireit", "test:dev": "wireit", - "test:prod": "wireit" + "test:prod": "wireit", + "test:node": "wireit" }, "files": [ "/async-directive.{d.ts,d.ts.map,js,js.map}", @@ -178,7 +207,8 @@ "/static.{d.ts,d.ts.map,js,js.map}", "/development/", "!/development/test/", - "/directives/" + "/directives/", + "/node/" ], "wireit": { "build": { @@ -240,7 +270,8 @@ "test/*_test.html", "development/test/*_test.html", "test/polyfill-support/*_test.html", - "development/test/polyfill-support/*_test.html" + "development/test/polyfill-support/*_test.html", + "node/" ] }, "build:version-stability-test": { @@ -281,6 +312,7 @@ "dependencies": [ "test:dev", "test:prod", + "test:node", "check-version" ] }, @@ -303,6 +335,15 @@ ], "files": [], "output": [] + }, + "test:node": { + "command": "node development/test/node-imports.js", + "dependencies": [ + "build:ts", + "build:rollup" + ], + "files": [], + "output": [] } }, "dependencies": { diff --git a/packages/lit-html/rollup.config.js b/packages/lit-html/rollup.config.js index a02cc8a721..1efc527298 100644 --- a/packages/lit-html/rollup.config.js +++ b/packages/lit-html/rollup.config.js @@ -45,6 +45,7 @@ export const defaultConfig = (options = {}) => file: 'polyfill-support', }, ], + includeNodeBuild: true, ...options, }); diff --git a/packages/lit-html/src/lit-html.ts b/packages/lit-html/src/lit-html.ts index 7d0e06e265..aa065d7049 100644 --- a/packages/lit-html/src/lit-html.ts +++ b/packages/lit-html/src/lit-html.ts @@ -10,6 +10,9 @@ import type {Directive, DirectiveResult, PartInfo} from './directive.js'; const DEV_MODE = true; const ENABLE_EXTRA_SECURITY_HOOKS = true; const ENABLE_SHADYDOM_NOPATCH = true; +const NODE_MODE = false; +// Use window for browser builds because IE11 doesn't have globalThis. +const global = NODE_MODE ? globalThis : window; /** * Contains types that are part of the unstable debug API. @@ -190,12 +193,12 @@ interface DebugLoggingWindow { */ const debugLogEvent = DEV_MODE ? (event: LitUnstable.DebugLog.Entry) => { - const shouldEmit = (window as unknown as DebugLoggingWindow) + const shouldEmit = (global as unknown as DebugLoggingWindow) .emitLitDebugLogEvents; if (!shouldEmit) { return; } - window.dispatchEvent( + global.dispatchEvent( new CustomEvent('lit-debug', { detail: event, }) @@ -210,16 +213,16 @@ let debugLogRenderId = 0; let issueWarning: (code: string, warning: string) => void; if (DEV_MODE) { - globalThis.litIssuedWarnings ??= new Set(); + global.litIssuedWarnings ??= new Set(); // Issue a warning, if we haven't already. issueWarning = (code: string, warning: string) => { warning += code ? ` See https://lit.dev/msg/${code} for more information.` : ''; - if (!globalThis.litIssuedWarnings!.has(warning)) { + if (!global.litIssuedWarnings!.has(warning)) { console.warn(warning); - globalThis.litIssuedWarnings!.add(warning); + global.litIssuedWarnings!.add(warning); } }; @@ -231,12 +234,12 @@ if (DEV_MODE) { const wrap = ENABLE_SHADYDOM_NOPATCH && - window.ShadyDOM?.inUse && - window.ShadyDOM?.noPatch === true - ? window.ShadyDOM!.wrap + global.ShadyDOM?.inUse && + global.ShadyDOM?.noPatch === true + ? global.ShadyDOM!.wrap : (node: Node) => node; -const trustedTypes = (globalThis as unknown as Partial).trustedTypes; +const trustedTypes = (global as unknown as Partial).trustedTypes; /** * Our TrustedTypePolicy for HTML which is declared using the html template @@ -341,7 +344,13 @@ const markerMatch = '?' + marker; // syntax because it's slightly smaller, but parses as a comment node. const nodeMarker = `<${markerMatch}>`; -const d = document; +const d = NODE_MODE + ? ({ + createTreeWalker() { + return {}; + }, + } as unknown as Document) + : document; // Creates a dynamic marker. We never have to search for these in the DOM. const createMarker = (v = '') => d.createComment(v); @@ -2147,14 +2156,14 @@ export const _$LH = { // Apply polyfills if available const polyfillSupport = DEV_MODE - ? window.litHtmlPolyfillSupportDevMode - : window.litHtmlPolyfillSupport; + ? global.litHtmlPolyfillSupportDevMode + : global.litHtmlPolyfillSupport; polyfillSupport?.(Template, ChildPart); // IMPORTANT: do not change the property name or the assignment expression. // This line will be used in regexes to search for lit-html usage. -(globalThis.litHtmlVersions ??= []).push('2.2.7'); -if (DEV_MODE && globalThis.litHtmlVersions.length > 1) { +(global.litHtmlVersions ??= []).push('2.2.7'); +if (DEV_MODE && global.litHtmlVersions.length > 1) { issueWarning!( 'multiple-versions', `Multiple versions of Lit loaded. ` + diff --git a/packages/lit-html/src/test/node-imports.ts b/packages/lit-html/src/test/node-imports.ts new file mode 100644 index 0000000000..2c3c953516 --- /dev/null +++ b/packages/lit-html/src/test/node-imports.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +// This file will be loaded by Node from the node:test script to verify that all +// exports of this package can be imported without crashing in Node. + +import 'lit-html'; +import 'lit-html/directives/async-append.js'; +import 'lit-html/directives/async-replace.js'; +import 'lit-html/directives/cache.js'; +import 'lit-html/directives/choose.js'; +import 'lit-html/directives/class-map.js'; +import 'lit-html/directives/guard.js'; +import 'lit-html/directives/if-defined.js'; +import 'lit-html/directives/join.js'; +import 'lit-html/directives/keyed.js'; +import 'lit-html/directives/live.js'; +import 'lit-html/directives/map.js'; +import 'lit-html/directives/range.js'; +import 'lit-html/directives/ref.js'; +import 'lit-html/directives/repeat.js'; +import 'lit-html/directives/style-map.js'; +import 'lit-html/directives/template-content.js'; +import 'lit-html/directives/unsafe-html.js'; +import 'lit-html/directives/unsafe-svg.js'; +import 'lit-html/directives/until.js'; +import 'lit-html/directives/when.js'; +import 'lit-html/directive.js'; +import 'lit-html/directive-helpers.js'; +import 'lit-html/async-directive.js'; +import 'lit-html/static.js'; +import 'lit-html/experimental-hydrate.js'; +import 'lit-html/private-ssr-support.js'; +import 'lit-html/polyfill-support.js'; diff --git a/packages/lit/package.json b/packages/lit/package.json index 36851640a3..99e79b763a 100644 --- a/packages/lit/package.json +++ b/packages/lit/package.json @@ -143,7 +143,8 @@ "checksize": "wireit", "test": "wireit", "test:dev": "wireit", - "test:prod": "wireit" + "test:prod": "wireit", + "test:node": "wireit" }, "wireit": { "build": { @@ -233,7 +234,8 @@ "test": { "dependencies": [ "test:dev", - "test:prod" + "test:prod", + "test:node" ] }, "test:dev": { @@ -258,6 +260,15 @@ ], "files": [], "output": [] + }, + "test:node": { + "command": "node development/test/node-imports.js", + "dependencies": [ + "build:ts", + "build:rollup" + ], + "files": [], + "output": [] } }, "files": [ diff --git a/packages/lit/src/test/node-imports.ts b/packages/lit/src/test/node-imports.ts new file mode 100644 index 0000000000..d443a495d1 --- /dev/null +++ b/packages/lit/src/test/node-imports.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +// This file will be loaded by Node from the node:test script to verify that all +// exports of this package can be imported without crashing in Node. + +import 'lit'; +import 'lit/decorators.js'; +import 'lit/decorators/custom-element.js'; +import 'lit/decorators/event-options.js'; +import 'lit/decorators/property.js'; +import 'lit/decorators/query.js'; +import 'lit/decorators/query-all.js'; +import 'lit/decorators/query-assigned-elements.js'; +import 'lit/decorators/query-assigned-nodes.js'; +import 'lit/decorators/query-async.js'; +import 'lit/decorators/state.js'; +import 'lit/directive-helpers.js'; +import 'lit/directive.js'; +import 'lit/directives/async-append.js'; +import 'lit/directives/async-replace.js'; +import 'lit/directives/cache.js'; +import 'lit/directives/choose.js'; +import 'lit/directives/class-map.js'; +import 'lit/directives/guard.js'; +import 'lit/directives/if-defined.js'; +import 'lit/directives/join.js'; +import 'lit/directives/keyed.js'; +import 'lit/directives/live.js'; +import 'lit/directives/map.js'; +import 'lit/directives/range.js'; +import 'lit/directives/ref.js'; +import 'lit/directives/repeat.js'; +import 'lit/directives/style-map.js'; +import 'lit/directives/template-content.js'; +import 'lit/directives/unsafe-html.js'; +import 'lit/directives/unsafe-svg.js'; +import 'lit/directives/until.js'; +import 'lit/directives/when.js'; +import 'lit/async-directive.js'; +import 'lit/html.js'; +import 'lit/experimental-hydrate-support.js'; +import 'lit/experimental-hydrate.js'; +import 'lit/static-html.js'; diff --git a/packages/reactive-element/.gitignore b/packages/reactive-element/.gitignore index fd3572c58b..963243dc86 100644 --- a/packages/reactive-element/.gitignore +++ b/packages/reactive-element/.gitignore @@ -1,5 +1,6 @@ /decorators/ /development/ +/node/ /test/ /css-tag.* /decorators.* diff --git a/packages/reactive-element/package.json b/packages/reactive-element/package.json index 476af5322e..155f12d18d 100644 --- a/packages/reactive-element/package.json +++ b/packages/reactive-element/package.json @@ -19,76 +19,91 @@ "exports": { ".": { "types": "./development/reactive-element.d.ts", + "node": "./node/reactive-element.js", "development": "./development/reactive-element.js", "default": "./reactive-element.js" }, "./css-tag.js": { "types": "./development/css-tag.d.ts", + "node": "./node/css-tag.js", "development": "./development/css-tag.js", "default": "./css-tag.js" }, "./decorators.js": { "types": "./development/decorators.d.ts", + "node": "./node/decorators.js", "development": "./development/decorators.js", "default": "./decorators.js" }, "./decorators/base.js": { "types": "./development/decorators/base.d.ts", + "node": "./node/decorators/base.js", "development": "./development/decorators/base.js", "default": "./decorators/base.js" }, "./decorators/custom-element.js": { "types": "./development/decorators/custom-element.d.ts", + "node": "./node/decorators/custom-element.js", "development": "./development/decorators/custom-element.js", "default": "./decorators/custom-element.js" }, "./decorators/event-options.js": { "types": "./development/decorators/event-options.d.ts", + "node": "./node/decorators/event-options.js", "development": "./development/decorators/event-options.js", "default": "./decorators/event-options.js" }, "./decorators/property.js": { "types": "./development/decorators/property.d.ts", + "node": "./node/decorators/property.js", "development": "./development/decorators/property.js", "default": "./decorators/property.js" }, "./decorators/query-all.js": { "types": "./development/decorators/query-all.d.ts", + "node": "./node/decorators/query-all.js", "development": "./development/decorators/query-all.js", "default": "./decorators/query-all.js" }, "./decorators/query-assigned-elements.js": { "types": "./development/decorators/query-assigned-elements.d.ts", + "node": "./node/decorators/query-assigned-elements.js", "development": "./development/decorators/query-assigned-elements.js", "default": "./decorators/query-assigned-elements.js" }, "./decorators/query-assigned-nodes.js": { "types": "./development/decorators/query-assigned-nodes.d.ts", + "node": "./node/decorators/query-assigned-nodes.js", "development": "./development/decorators/query-assigned-nodes.js", "default": "./decorators/query-assigned-nodes.js" }, "./decorators/query-async.js": { "types": "./development/decorators/query-async.d.ts", + "node": "./node/decorators/query-async.js", "development": "./development/decorators/query-async.js", "default": "./decorators/query-async.js" }, "./decorators/query.js": { "types": "./development/decorators/query.d.ts", + "node": "./node/decorators/query.js", "development": "./development/decorators/query.js", "default": "./decorators/query.js" }, "./decorators/state.js": { "types": "./development/decorators/state.d.ts", + "node": "./node/decorators/state.js", "development": "./development/decorators/state.js", "default": "./decorators/state.js" }, "./polyfill-support.js": { "types": "./development/polyfill-support.d.ts", + "node": "./node/polyfill-support.js", "development": "./development/polyfill-support.js", "default": "./polyfill-support.js" }, "./reactive-controller.js": { "types": "./development/reactive-controller.d.ts", + "node": "./node/reactive-controller.js", "development": "./development/reactive-controller.js", "default": "./reactive-controller.js" } @@ -104,7 +119,8 @@ "prepublishOnly": "npm run check-version", "test": "wireit", "test:dev": "wireit", - "test:prod": "wireit" + "test:prod": "wireit", + "test:node": "wireit" }, "wireit": { "build": { @@ -164,7 +180,8 @@ "test/*_test.html", "development/test/*_test.html", "test/polyfill-support/*_test.html", - "development/test/polyfill-support/*_test.html" + "development/test/polyfill-support/*_test.html", + "node/" ] }, "build:babel": { @@ -200,6 +217,7 @@ "dependencies": [ "test:dev", "test:prod", + "test:node", "check-version" ] }, @@ -223,6 +241,15 @@ ], "files": [], "output": [] + }, + "test:node": { + "command": "node development/test/node-imports.js", + "dependencies": [ + "build:ts", + "build:rollup" + ], + "files": [], + "output": [] } }, "files": [ @@ -233,7 +260,8 @@ "/reactive-element.{d.ts,d.ts.map,js,js.map}", "/decorators/", "/development/", - "!/development/test/" + "!/development/test/", + "/node/" ], "devDependencies": { "@babel/cli": "^7.14.6", diff --git a/packages/reactive-element/rollup.config.js b/packages/reactive-element/rollup.config.js index 3121d5752f..5ca253a77c 100644 --- a/packages/reactive-element/rollup.config.js +++ b/packages/reactive-element/rollup.config.js @@ -31,4 +31,5 @@ export default litProdConfig({ file: 'polyfill-support', }, ], + includeNodeBuild: true, }); diff --git a/packages/reactive-element/src/css-tag.ts b/packages/reactive-element/src/css-tag.ts index 7b763d7b2a..bbd00674a4 100644 --- a/packages/reactive-element/src/css-tag.ts +++ b/packages/reactive-element/src/css-tag.ts @@ -4,12 +4,15 @@ * SPDX-License-Identifier: BSD-3-Clause */ +const NODE_MODE = false; +const global = NODE_MODE ? globalThis : window; + /** * Whether the current browser supports `adoptedStyleSheets`. */ export const supportsAdoptingStyleSheets = - window.ShadowRoot && - (window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) && + global.ShadowRoot && + (global.ShadyCSS === undefined || global.ShadyCSS.nativeShadow) && 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype; @@ -174,7 +177,7 @@ export const adoptStyles = ( styles.forEach((s) => { const style = document.createElement('style'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const nonce = (window as any)['litNonce']; + const nonce = (global as any)['litNonce']; if (nonce !== undefined) { style.setAttribute('nonce', nonce); } diff --git a/packages/reactive-element/src/decorators/custom-element.ts b/packages/reactive-element/src/decorators/custom-element.ts index e094bb31c4..d83a26d025 100644 --- a/packages/reactive-element/src/decorators/custom-element.ts +++ b/packages/reactive-element/src/decorators/custom-element.ts @@ -18,7 +18,7 @@ import {Constructor, ClassDescriptor} from './base.js'; type CustomElementClass = Omit; const legacyCustomElement = (tagName: string, clazz: CustomElementClass) => { - window.customElements.define(tagName, clazz as CustomElementConstructor); + customElements.define(tagName, clazz as CustomElementConstructor); // Cast as any because TS doesn't recognize the return type as being a // subtype of the decorated class when clazz is typed as // `Constructor` for some reason. @@ -38,7 +38,7 @@ const standardCustomElement = ( elements, // This callback is called once the class is otherwise fully defined finisher(clazz: Constructor) { - window.customElements.define(tagName, clazz); + customElements.define(tagName, clazz); }, }; }; diff --git a/packages/reactive-element/src/decorators/query-assigned-elements.ts b/packages/reactive-element/src/decorators/query-assigned-elements.ts index 25f5849f33..98a65c783b 100644 --- a/packages/reactive-element/src/decorators/query-assigned-elements.ts +++ b/packages/reactive-element/src/decorators/query-assigned-elements.ts @@ -16,11 +16,14 @@ import {decorateProperty} from './base.js'; import type {ReactiveElement} from '../reactive-element.js'; import type {QueryAssignedNodesOptions} from './query-assigned-nodes.js'; +const NODE_MODE = false; +const global = NODE_MODE ? globalThis : window; + /** * A tiny module scoped polyfill for HTMLSlotElement.assignedElements. */ const slotAssignedElements = - window.HTMLSlotElement?.prototype.assignedElements != null + global.HTMLSlotElement?.prototype.assignedElements != null ? (slot: HTMLSlotElement, opts?: AssignedNodesOptions) => slot.assignedElements(opts) : (slot: HTMLSlotElement, opts?: AssignedNodesOptions) => diff --git a/packages/reactive-element/src/reactive-element.ts b/packages/reactive-element/src/reactive-element.ts index 5fe276cd46..15dfe38e94 100644 --- a/packages/reactive-element/src/reactive-element.ts +++ b/packages/reactive-element/src/reactive-element.ts @@ -27,6 +27,15 @@ export type { ReactiveControllerHost, } from './reactive-controller.js'; +const NODE_MODE = false; +const global = NODE_MODE ? globalThis : window; + +if (NODE_MODE) { + global.customElements ??= { + define() {}, + } as unknown as CustomElementRegistry; +} + const DEV_MODE = true; let requestUpdateThenable: (name: string) => { @@ -38,7 +47,7 @@ let requestUpdateThenable: (name: string) => { let issueWarning: (code: string, warning: string) => void; -const trustedTypes = (window as unknown as {trustedTypes?: {emptyScript: ''}}) +const trustedTypes = (global as unknown as {trustedTypes?: {emptyScript: ''}}) .trustedTypes; // Temporary workaround for https://crbug.com/993268 @@ -50,14 +59,14 @@ const emptyStringForBooleanAttribute = trustedTypes : ''; const polyfillSupport = DEV_MODE - ? window.reactiveElementPolyfillSupportDevMode - : window.reactiveElementPolyfillSupport; + ? global.reactiveElementPolyfillSupportDevMode + : global.reactiveElementPolyfillSupport; if (DEV_MODE) { // Ensure warnings are issued only 1x, even if multiple versions of Lit // are loaded. - const issuedWarnings: Set = - (globalThis.litIssuedWarnings ??= new Set()); + const issuedWarnings: Set = (global.litIssuedWarnings ??= + new Set()); // Issue a warning, if we haven't already. issueWarning = (code: string, warning: string) => { @@ -74,7 +83,7 @@ if (DEV_MODE) { ); // Issue polyfill support warning. - if (window.ShadyDOM?.inUse && polyfillSupport === undefined) { + if (global.ShadyDOM?.inUse && polyfillSupport === undefined) { issueWarning( 'polyfill-support-missing', `Shadow DOM is being polyfilled via \`ShadyDOM\` but ` + @@ -139,12 +148,12 @@ interface DebugLoggingWindow { */ const debugLogEvent = DEV_MODE ? (event: ReactiveUnstable.DebugLog.Entry) => { - const shouldEmit = (window as unknown as DebugLoggingWindow) + const shouldEmit = (global as unknown as DebugLoggingWindow) .emitLitDebugLogEvents; if (!shouldEmit) { return; } - window.dispatchEvent( + global.dispatchEvent( new CustomEvent('lit-debug', { detail: event, }) @@ -383,6 +392,11 @@ export type WarningKind = 'change-in-update' | 'migration'; export type Initializer = (element: ReactiveElement) => void; +const htmlElementShimNeeded = NODE_MODE && global.HTMLElement === undefined; +if (htmlElementShimNeeded) { + global.HTMLElement = class HTMLElement {} as unknown as typeof HTMLElement; +} + /** * Base element class which manages element properties and attributes. When * properties change, the `update` method is asynchronously called. This method @@ -1484,6 +1498,10 @@ export abstract class ReactiveElement protected firstUpdated(_changedProperties: PropertyValues) {} } +if (htmlElementShimNeeded) { + delete (global as Partial).HTMLElement; +} + // Apply polyfills if available polyfillSupport?.({ReactiveElement}); @@ -1521,8 +1539,8 @@ if (DEV_MODE) { // IMPORTANT: do not change the property name or the assignment expression. // This line will be used in regexes to search for ReactiveElement usage. -(globalThis.reactiveElementVersions ??= []).push('1.3.4'); -if (DEV_MODE && globalThis.reactiveElementVersions.length > 1) { +(global.reactiveElementVersions ??= []).push('1.3.4'); +if (DEV_MODE && global.reactiveElementVersions.length > 1) { issueWarning!( 'multiple-versions', `Multiple versions of Lit loaded. Loading multiple versions ` + diff --git a/packages/reactive-element/src/test/node-imports.ts b/packages/reactive-element/src/test/node-imports.ts new file mode 100644 index 0000000000..9d91df2af2 --- /dev/null +++ b/packages/reactive-element/src/test/node-imports.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +// This file will be loaded by Node from the node:test script to verify that all +// exports of this package can be imported without crashing in Node. + +import '@lit/reactive-element'; +import '@lit/reactive-element/reactive-controller.js'; +import '@lit/reactive-element/css-tag.js'; +import '@lit/reactive-element/decorators.js'; +import '@lit/reactive-element/decorators/base.js'; +import '@lit/reactive-element/decorators/custom-element.js'; +import '@lit/reactive-element/decorators/event-options.js'; +import '@lit/reactive-element/decorators/state.js'; +import '@lit/reactive-element/decorators/property.js'; +import '@lit/reactive-element/decorators/query.js'; +import '@lit/reactive-element/decorators/query-all.js'; +import '@lit/reactive-element/decorators/query-assigned-elements.js'; +import '@lit/reactive-element/decorators/query-assigned-nodes.js'; +import '@lit/reactive-element/decorators/query-async.js'; + +import {customElement} from '@lit/reactive-element/decorators.js'; +import {ReactiveElement} from '@lit/reactive-element'; + +@customElement('my-element') +export class MyElement extends ReactiveElement {} + +export class MyOtherElement extends ReactiveElement {} +customElements.define('my-other-element', MyOtherElement); diff --git a/rollup-common.js b/rollup-common.js index b255e4ea35..ebe4b40631 100644 --- a/rollup-common.js +++ b/rollup-common.js @@ -255,6 +255,7 @@ export function litProdConfig({ packageName, outputDir = './', copyHtmlTests = true, + includeNodeBuild = false, // eslint-disable-next-line no-undef } = options) { const classPropertyPrefix = PACKAGE_CLASS_PREFIXES[packageName]; @@ -348,6 +349,7 @@ export function litProdConfig({ skipBundleOutput, ], }, + // Production build { input: entryPoints.map((name) => `development/${name}.js`), output: { @@ -423,6 +425,58 @@ export function litProdConfig({ : []), ], }, + // Node build + ...(includeNodeBuild + ? [ + { + input: entryPoints.map((name) => `development/${name}.js`), + output: { + dir: `${outputDir}/node`, + format: 'esm', + preserveModules: true, + sourcemap: !CHECKSIZE, + }, + external, + plugins: [ + replace({ + preventAssignment: true, + values: { + // Setting NODE_MODE to true enables node-specific behaviors, + // i.e. using globalThis instead of window, and shimming APIs + // needed for Lit bootup. + 'const NODE_MODE = false': 'const NODE_MODE = true', + // Other variables should behave like prod mode. + 'const DEV_MODE = true': 'const DEV_MODE = false', + 'const ENABLE_EXTRA_SECURITY_HOOKS = true': + 'const ENABLE_EXTRA_SECURITY_HOOKS = false', + 'const ENABLE_SHADYDOM_NOPATCH = true': + 'const ENABLE_SHADYDOM_NOPATCH = false', + }, + }), + sourcemaps(), + // We want the Node build to be minified because: + // + // 1. It should be very slightly faster, even in Node where bytes + // are not as important as in the browser. + // + // 2. It means we don't need a Node build for lit-element. There + // is no Node-specific logic needed in lit-element. However, + // lit-element and reactive-element must be consistently + // minified or unminified together, because lit-element + // references properties from reactive-element which will + // otherwise have different names. The default export that + // lit-element will use is minified. + terser(terserOptions), + summary({ + showBrotliSize: true, + showGzippedSize: true, + }), + ...(CHECKSIZE ? [skipBundleOutput] : []), + ], + }, + ] + : []), + // CDN bundles ...bundled.map(({file, output, name, format, sourcemapPathTransform}) => litMonoBundleConfig({ file, diff --git a/scripts/update-version-variables.js b/scripts/update-version-variables.js index 1b978cc7f3..5ee56dd9d1 100644 --- a/scripts/update-version-variables.js +++ b/scripts/update-version-variables.js @@ -24,12 +24,12 @@ const updateVersionVariable = async (packageDir, sourcePath, variableName) => { // Replace version number const versionVarRegex = new RegExp( - `\\(globalThis\\.${variableName} \\?\\?= \\[\\]\\)\\.push\\('\\d+\\.\\d+\\.\\d+'\\)` + `(\\(global(?:This)?\\.${variableName} \\?\\?= \\[\\]\\)\\.push\\(')\\d+\\.\\d+\\.\\d+[^']*('\\))` ); let replaced = false; - const newSource = fileSource.replace(versionVarRegex, () => { + const newSource = fileSource.replace(versionVarRegex, (_, pre, post) => { replaced = true; - return `(globalThis.${variableName} ??= []).push('${version}')`; + return pre + version + post; }); if (!replaced) { throw new Error(`Version variable not found: ${filePath} ${variableName}`);