thunderous 2.3.13 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,71 +1,55 @@
1
1
  {
2
- "name": "thunderous",
3
- "version": "2.3.13",
4
- "description": "",
5
- "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "files": [
10
- "/dist",
11
- "/package.json",
12
- "/README.md",
13
- "/LICENSE"
14
- ],
15
- "author": "Jonathan DeWitt",
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/Thunder-Solutions/Thunderous"
19
- },
20
- "keywords": [
21
- "thunderous",
22
- "web components",
23
- "functional",
24
- "signals",
25
- "custom elements"
26
- ],
27
- "bugs": {
28
- "url": "https://github.com/Thunder-Solutions/Thunderous/issues"
29
- },
30
- "homepage": "https://github.com/Thunder-Solutions/Thunderous#readme",
31
- "license": "MIT",
32
- "scripts": {
33
- "demo": "cd demo && npm start",
34
- "demo:ssr": "cd demo && npm run ssr",
35
- "www": "cd www && npm run dev",
36
- "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
37
- "test": "find src -name '*.test.ts' | xargs c8 node --import tsx --test",
38
- "typecheck": "tsc --noEmit",
39
- "lint": "eslint . && prettier --check .",
40
- "lint:fix": "eslint . --fix && prettier --write .",
41
- "version:patch": "npm run build && npm version patch && cd demo && npm version patch && cd ../www && npm version patch && git add -A && git commit -m 'bump version'",
42
- "version:minor": "npm run build && npm version minor && cd demo && npm version minor && cd ../www && npm version minor && git add -A && git commit -m 'bump version'",
43
- "version:major": "npm run build && npm version major && cd demo && npm version major && cd ../www && npm version major && git add -A && git commit -m 'bump version'",
44
- "publish": "npm run typecheck && npm run lint && npm test && npm run build && npm publish"
45
- },
46
- "devDependencies": {
47
- "@types/dompurify": "^3.2.0",
48
- "@types/eslint": "^8.56.10",
49
- "@types/node": "^22.10.1",
50
- "@typescript-eslint/eslint-plugin": "^8.17.0",
51
- "@typescript-eslint/parser": "^8.17.0",
52
- "c8": "^10.1.2",
53
- "eslint": "^8.57.0",
54
- "eslint-plugin-import": "^2.31.0",
55
- "eslint-plugin-node": "^11.1.0",
56
- "eslint-plugin-promise": "^7.1.0",
57
- "express": "^4.17.1",
58
- "prettier": "^3.3.3",
59
- "tsup": "^8.3.0",
60
- "tsx": "^4.19.2",
61
- "typescript": "^5.7.2"
62
- },
63
- "peerDependencies": {
64
- "@webcomponents/scoped-custom-element-registry": "^0.0.10"
65
- },
66
- "peerDependenciesMeta": {
67
- "@webcomponents/scoped-custom-element-registry": {
68
- "optional": true
69
- }
70
- }
71
- }
2
+ "name": "thunderous",
3
+ "version": "2.4.2",
4
+ "description": "A lightweight, functional web components library that brings the power of signals to your UI.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "/dist",
11
+ "/package.json",
12
+ "/README.md",
13
+ "/LICENSE"
14
+ ],
15
+ "author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/Thunder-Solutions/Thunderous"
19
+ },
20
+ "keywords": [
21
+ "thunderous",
22
+ "web components",
23
+ "functional",
24
+ "signals",
25
+ "custom elements"
26
+ ],
27
+ "bugs": {
28
+ "url": "https://github.com/Thunder-Solutions/Thunderous/issues"
29
+ },
30
+ "homepage": "https://github.com/Thunder-Solutions/Thunderous#readme",
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "@webcomponents/scoped-custom-element-registry": "^0.0.10"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@webcomponents/scoped-custom-element-registry": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "scripts": {
41
+ "demo": "cd demo && npm start",
42
+ "demo:ssr": "cd demo && npm run ssr",
43
+ "build": "tsup src/index.ts --format cjs,esm --dts --no-clean",
44
+ "test": "npm run test:server && npm run test:client",
45
+ "test:server": "find src/__test__/server -name '*.test.ts' | xargs c8 tsx --test",
46
+ "test:client": "PLAYWRIGHT_BROWSERS_PATH=../../.browsers playwright test",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "eslint .",
49
+ "lint:fix": "eslint . --fix",
50
+ "format": "prettier --check . --ignore-path ../../.gitignore",
51
+ "format:fix": "prettier --write . --ignore-path ../../.gitignore",
52
+ "preversion": "npm run typecheck && npm run lint && npm test && npm run build",
53
+ "version": "node postversion.js"
54
+ }
55
+ }
@@ -1,48 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'assert';
3
- import { customElement } from '../custom-element';
4
- import { html } from '../render';
5
- import { createRegistry } from '../registry';
6
- import { NOOP } from '../utilities';
7
- await describe('customElement', async () => {
8
- await it('does not throw on the server', () => {
9
- assert.doesNotThrow(() => customElement(() => html `<div></div>`));
10
- });
11
- await it('returns an element result class', () => {
12
- const MyElement = customElement(() => html `<div></div>`);
13
- assert.ok(MyElement);
14
- const keys = Object.keys(MyElement);
15
- assert(keys.every((key) => ['define', 'register', 'eject'].includes(key)));
16
- });
17
- await it('supports scoped registries', () => {
18
- const registry = createRegistry({ scoped: true });
19
- assert.doesNotThrow(() => customElement(() => html `<div></div>`, { shadowRootOptions: { registry } }));
20
- });
21
- await it('returns self for chaining', () => {
22
- const MyElement = customElement(() => html `<div></div>`);
23
- const registry = createRegistry();
24
- assert.strictEqual(MyElement.define('my-element'), MyElement);
25
- assert.strictEqual(MyElement.register(registry), MyElement);
26
- });
27
- await it('registers the element with a registry', () => {
28
- const registry = createRegistry();
29
- const MyElement = customElement(() => html `<div></div>`)
30
- .register(registry)
31
- .define('my-element');
32
- assert.strictEqual(registry.getTagName(MyElement), 'MY-ELEMENT');
33
- });
34
- await it('logs an error when registering after defining in a scoped registry', (testContext) => {
35
- testContext.mock.method(console, 'error', NOOP);
36
- const errorMock = console.error.mock;
37
- const registry = createRegistry({ scoped: true });
38
- const MyElement = customElement(() => html `<div></div>`);
39
- MyElement.define('my-element');
40
- MyElement.register(registry);
41
- assert.strictEqual(errorMock.callCount(), 1);
42
- assert.strictEqual(errorMock.calls[0].arguments[0], 'Must call `register()` before `define()` for scoped registries.');
43
- });
44
- await it('throws an error when ejecting on the server', () => {
45
- const MyElement = customElement(() => html `<div></div>`);
46
- assert.throws(() => MyElement.eject());
47
- });
48
- });
@@ -1,60 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'assert';
3
- import { createRegistry } from '../registry';
4
- import { customElement } from '../custom-element';
5
- import { html } from '../render';
6
- import { NOOP } from '../utilities';
7
- await describe('createRegistry', async () => {
8
- await it('creates a global registry', () => {
9
- const registry = createRegistry();
10
- assert.ok(registry);
11
- assert.strictEqual(registry.scoped, false);
12
- });
13
- await it('creates a scoped registry', () => {
14
- const registry = createRegistry({ scoped: true });
15
- assert.ok(registry);
16
- assert.strictEqual(registry.scoped, true);
17
- });
18
- await it('defines a custom element', () => {
19
- const registry = createRegistry();
20
- const MyElement = customElement(() => html `<div></div>`);
21
- assert.doesNotThrow(() => registry.define('my-element', MyElement));
22
- });
23
- await it('warns about duplicate custom elements', (testContext) => {
24
- testContext.mock.method(console, 'warn', NOOP);
25
- const warnMock = console.warn.mock;
26
- const registry = createRegistry();
27
- const MyElement = customElement(() => html `<div></div>`);
28
- const MyElement2 = customElement(() => html `<div></div>`);
29
- registry.define('my-element', MyElement);
30
- registry.define('my-element', MyElement2);
31
- assert.strictEqual(warnMock.callCount(), 1);
32
- assert.strictEqual(warnMock.calls[0].arguments[0], 'Custom element tag name "MY-ELEMENT" was already defined. Skipping...');
33
- registry.define('my-element-2', MyElement);
34
- assert.strictEqual(warnMock.callCount(), 2);
35
- assert.strictEqual(warnMock.calls[1].arguments[0], 'MY-ELEMENT-2 was already defined. Skipping...');
36
- });
37
- await it('gets the tag name of a custom element', () => {
38
- const registry = createRegistry();
39
- const tagName = 'my-element';
40
- const MyElement = customElement(() => html `<div></div>`);
41
- registry.define(tagName, MyElement);
42
- const result = registry.getTagName(MyElement);
43
- assert.strictEqual(result, tagName.toUpperCase());
44
- });
45
- await it('gets all tag names defined in the registry', () => {
46
- const registry = createRegistry();
47
- const MyElement = customElement(() => html `<div></div>`);
48
- const MyElement2 = customElement(() => html `<div></div>`);
49
- registry.define('my-element', MyElement);
50
- registry.define('my-element-2', MyElement2);
51
- const result = registry.getAllTagNames();
52
- assert.deepStrictEqual(result, ['MY-ELEMENT', 'MY-ELEMENT-2']);
53
- });
54
- await it('throws an error if ejected on the server', () => {
55
- const registry = createRegistry();
56
- const MyElement = customElement(() => html `<div></div>`);
57
- registry.define('my-element', MyElement);
58
- assert.throws(() => registry.eject(), { message: 'Cannot eject a registry on the server.' });
59
- });
60
- });
@@ -1,48 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'assert';
3
- import { html, css } from '../render';
4
- import { NOOP } from '../utilities';
5
- await describe('html', async () => {
6
- await it('renders a simple string', () => {
7
- const result = html `<div></div>`;
8
- assert.strictEqual(result, '<div></div>');
9
- });
10
- await it('renders a string with interpolated values', () => {
11
- const result = html `<div>${'Hello, world!'} ${1} ${true}</div>`;
12
- assert.strictEqual(result, '<div>Hello, world! 1 true</div>');
13
- });
14
- await it('renders a string with signals', () => {
15
- const mockGetter = () => 'Hello, world!';
16
- const result = html `<div>${mockGetter}</div>`;
17
- assert.strictEqual(result, '<div>Hello, world!</div>');
18
- });
19
- });
20
- await describe('css', async () => {
21
- await it('renders a simple string', () => {
22
- // prettier-ignore
23
- const result = css `div { color: red; }`;
24
- assert.strictEqual(result, 'div { color: red; }');
25
- });
26
- await it('renders a string with interpolated values', () => {
27
- // prettier-ignore
28
- const result = css `div { --str: ${'str'}; --num: ${1}; --bool: ${true}; }`;
29
- assert.strictEqual(result, 'div { --str: str; --num: 1; --bool: true; }');
30
- });
31
- await it('logs an error if a non-primitive value is interpolated', (testContext) => {
32
- testContext.mock.method(console, 'error', NOOP);
33
- const errorMock = console.error.mock;
34
- const obj = {};
35
- // prettier-ignore
36
- const result = css `div { --obj: ${obj}; }`;
37
- assert.strictEqual(result, 'div { --obj: ; }');
38
- assert.strictEqual(errorMock.callCount(), 1);
39
- assert.strictEqual(errorMock.calls[0].arguments[0], 'Objects are not valid in CSS values. Received:');
40
- assert.strictEqual(errorMock.calls[0].arguments[1], obj);
41
- });
42
- await it('renders a string with signals', () => {
43
- const mockGetter = () => 'red';
44
- // prettier-ignore
45
- const result = css `div { color: ${mockGetter}; }`;
46
- assert.strictEqual(result, 'div { color: red; }');
47
- });
48
- });
@@ -1,196 +0,0 @@
1
- import { clientOnlyCallback, getServerRenderArgs, insertTemplates, onServerDefine, serverCss, serverDefine, serverDefineFns, wrapTemplate, } from '../server-side';
2
- import { describe, mock, it } from 'node:test';
3
- import assert from 'assert';
4
- import { createRegistry } from '../registry';
5
- import { DEFAULT_RENDER_OPTIONS } from '../constants';
6
- import { NOOP } from '../utilities';
7
- import { customElement } from '../custom-element';
8
- import { html } from '../render';
9
- const stripWhitespace = (template) => template.trim().replace(/\s\s+/g, ' ');
10
- await describe('getServerRenderArgs', async () => {
11
- await it('throws on the server when accessing client-only properties', () => {
12
- const args = getServerRenderArgs('my-element-1');
13
- assert.throws(() => args.elementRef.children, {
14
- message: 'The `elementRef` property is not available on the server.',
15
- });
16
- assert.throws(() => args.root.children, { message: 'The `root` property is not available on the server.' });
17
- assert.throws(() => args.internals.ariaChecked, {
18
- message: 'The `internals` property is not available on the server.',
19
- });
20
- });
21
- await it('returns nothing from customCallback on the server', () => {
22
- const args = getServerRenderArgs('my-element-2');
23
- assert.strictEqual(args.customCallback(NOOP), '');
24
- });
25
- await it('tracks CSS strings on the server using adoptStyleSheet', () => {
26
- const args = getServerRenderArgs('my-element-3');
27
- // build the expected result
28
- const cssStr = ':host { color: red; }';
29
- const expectedServerCss = new Map();
30
- expectedServerCss.set('my-element-3', [cssStr]);
31
- // @ts-expect-error // this will be a string on the server.
32
- args.adoptStyleSheet(':host { color: red; }');
33
- assert.deepStrictEqual(serverCss, expectedServerCss);
34
- });
35
- await it('tracks CSS strings on the server with registries using adoptStyleSheet', () => {
36
- const registry = createRegistry();
37
- const args = getServerRenderArgs('my-element-4', registry);
38
- // build the expected result
39
- const cssStr1 = ':host { color: blue; }';
40
- const expectedServerCss = new Map();
41
- expectedServerCss.set('my-element-4', [cssStr1]);
42
- // @ts-expect-error // this will be a string on the server.
43
- args.adoptStyleSheet(cssStr1);
44
- assert.deepStrictEqual(registry.__serverCss, expectedServerCss);
45
- });
46
- });
47
- await describe('wrapTemplate', async () => {
48
- await it('wraps the render result in a template tag', () => {
49
- const template = stripWhitespace(wrapTemplate({
50
- tagName: 'my-element-5',
51
- serverRender: () => 'Hello, world!',
52
- options: DEFAULT_RENDER_OPTIONS,
53
- }));
54
- const expectedTemplate = stripWhitespace(/* html */ `
55
- <template
56
- shadowrootmode="closed"
57
- shadowrootdelegatesfocus="false"
58
- shadowrootclonable="false"
59
- shadowrootserializable="false"
60
- >
61
- Hello, world!
62
- </template>
63
- `);
64
- assert.strictEqual(template, expectedTemplate);
65
- });
66
- await it('wraps the render result in a template tag with CSS', () => {
67
- const args = getServerRenderArgs('my-element-6');
68
- // @ts-expect-error // this will be a string on the server.
69
- args.adoptStyleSheet(':host { color: green; }');
70
- const template = stripWhitespace(wrapTemplate({
71
- tagName: 'my-element-6',
72
- serverRender: () => 'Hello, world!',
73
- options: DEFAULT_RENDER_OPTIONS,
74
- }));
75
- const expectedTemplate = stripWhitespace(/* html */ `
76
- <template
77
- shadowrootmode="closed"
78
- shadowrootdelegatesfocus="false"
79
- shadowrootclonable="false"
80
- shadowrootserializable="false"
81
- >
82
- <style>:host { color: green; }</style>Hello, world!
83
- </template>
84
- `);
85
- assert.strictEqual(template, expectedTemplate);
86
- });
87
- await it('wraps the render result in a template tag when shadow root is not attached', () => {
88
- const template = stripWhitespace(wrapTemplate({
89
- tagName: 'my-element-7',
90
- serverRender: () => 'Hello, world!',
91
- options: {
92
- ...DEFAULT_RENDER_OPTIONS,
93
- attachShadow: false,
94
- },
95
- }));
96
- const expectedTemplate = 'Hello, world!';
97
- assert.strictEqual(template, expectedTemplate);
98
- });
99
- });
100
- await describe('insertTemplates', async () => {
101
- await it('inserts the template into the input string', () => {
102
- const inputString = /* html */ `<my-element-7></my-element-7>`;
103
- const template = /* html */ `<div>Hello, world!</div>`;
104
- const result = stripWhitespace(insertTemplates('my-element-7', template, inputString));
105
- const expectedResult = stripWhitespace(/* html */ `
106
- <my-element-7><div>Hello, world!</div></my-element-7>
107
- `);
108
- assert.strictEqual(result, expectedResult);
109
- });
110
- await it('inserts the template into the input string and parses attribute references', () => {
111
- const inputString = /* html */ `<my-element-8 test="Hello, world!"></my-element-8>`;
112
- const template = /* html */ `<div>{{attr:test}}</div>`;
113
- const result = stripWhitespace(insertTemplates('my-element-8', template, inputString));
114
- const expectedResult = stripWhitespace(/* html */ `
115
- <my-element-8 test="Hello, world!"><div>Hello, world!</div></my-element-8>
116
- `);
117
- assert.strictEqual(result, expectedResult);
118
- });
119
- });
120
- await describe('onServerDefine', async () => {
121
- await it('adds the function to the set', () => {
122
- const fn = NOOP;
123
- onServerDefine(fn);
124
- assert.strictEqual(serverDefineFns.size, 1);
125
- assert.strictEqual(serverDefineFns.has(fn), true);
126
- serverDefineFns.clear();
127
- });
128
- });
129
- await describe('serverDefine', async () => {
130
- await it('calls the serverDefineFns with the result of serverRender', () => {
131
- const fn = mock.fn((tagName, template) => {
132
- assert.strictEqual(tagName, 'my-element-9');
133
- assert.strictEqual(template, 'Hello, world!');
134
- });
135
- onServerDefine(fn);
136
- serverDefine({
137
- tagName: 'my-element-9',
138
- serverRender: () => 'Hello, world!',
139
- options: {
140
- ...DEFAULT_RENDER_OPTIONS,
141
- attachShadow: false,
142
- },
143
- elementResult: customElement(() => html `<div></div>`),
144
- });
145
- assert.strictEqual(fn.mock.calls.length, 1);
146
- assert.strictEqual(fn.mock.calls[0].arguments[0], 'my-element-9');
147
- serverDefineFns.clear();
148
- });
149
- await it('sets the server render options on the parent registry', () => {
150
- const parentRegistry = createRegistry();
151
- const serverRender = () => 'Hello, world!';
152
- serverDefine({
153
- tagName: 'my-element-10',
154
- serverRender,
155
- options: DEFAULT_RENDER_OPTIONS,
156
- parentRegistry,
157
- elementResult: customElement(() => html `<div></div>`),
158
- });
159
- const expectedServerRenderOpts = new Map([
160
- ['my-element-10', { serverRender, ...DEFAULT_RENDER_OPTIONS }],
161
- ]);
162
- assert.strictEqual(parentRegistry.__serverRenderOpts.size, 1);
163
- assert.deepStrictEqual(parentRegistry.__serverRenderOpts, expectedServerRenderOpts);
164
- });
165
- await it('renders scoped registries correctly', () => {
166
- const scopedRegistry = createRegistry({ scoped: true });
167
- onServerDefine((tagName, template) => {
168
- assert.strictEqual(tagName, 'my-element-12');
169
- assert.strictEqual(template, '<my-element-11>inner</my-element-11>');
170
- });
171
- serverDefine({
172
- tagName: 'my-element-11',
173
- serverRender: () => 'inner',
174
- options: { ...DEFAULT_RENDER_OPTIONS, attachShadow: false },
175
- parentRegistry: scopedRegistry,
176
- elementResult: customElement(() => html `<div></div>`),
177
- });
178
- serverDefine({
179
- tagName: 'my-element-12',
180
- serverRender: () => '<my-element-11></my-element-11>',
181
- options: { ...DEFAULT_RENDER_OPTIONS, attachShadow: false },
182
- scopedRegistry,
183
- elementResult: customElement(() => html `<div></div>`),
184
- });
185
- serverDefineFns.clear();
186
- });
187
- });
188
- await describe('clientOnlyCallback', async () => {
189
- await it('does nothing on the server when called directly', () => {
190
- let runCount = 0;
191
- clientOnlyCallback(() => {
192
- runCount++;
193
- });
194
- assert.strictEqual(runCount, 0);
195
- });
196
- });