custom-elements-ts 0.1.0 → 0.2.0
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/.eslintrc.json +3 -2
- package/README.md +10 -3
- package/assets/readme-header.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/demos/site/event-log/event-log.element.ts +3 -14
- package/demos/site/index.html +39 -17
- package/demos/site/index.ts +6 -0
- package/demos/site/llms.txt +53 -0
- package/demos/site/og-image.png +0 -0
- package/demos/site/scroll-restoration.ts +28 -0
- package/demos/site/source-toggle/source-toggle.ts +88 -0
- package/demos/site/source-viewer/source-viewer.element.scss +292 -0
- package/demos/site/source-viewer/source-viewer.element.ts +138 -0
- package/demos/site/source-viewer/sources.generated.ts +11 -0
- package/demos/site/styles/site.css +144 -32
- package/demos/todo-dashboard/todo-dashboard.element.scss +8 -3
- package/demos/todo-dashboard/todo-dashboard.element.ts +1 -0
- package/demos/todo-dashboard/todo-item.element.ts +1 -0
- package/package.json +3 -3
- package/src/index.ts +3 -2
- package/src/signal.ts +66 -0
- package/src/state.ts +20 -8
- package/src/template-runtime.ts +404 -56
- package/tests/map-shallow-state.spec.ts +184 -0
- package/tests/signal.spec.ts +117 -0
- package/tools/build.js +14 -5
- package/tools/generate-sources.js +76 -0
- package/tools/rollup-config.js +2 -2
- package/tools/start.js +13 -7
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CustomElement, State, TemplateResult, html, map } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
const nextMicrotask = () => Promise.resolve();
|
|
5
|
+
|
|
6
|
+
interface Item {
|
|
7
|
+
id: number;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const rowSpy = vi.fn();
|
|
12
|
+
|
|
13
|
+
@CustomElement({
|
|
14
|
+
tag: 'mapped-list-element',
|
|
15
|
+
shadow: false,
|
|
16
|
+
})
|
|
17
|
+
class MappedListElement extends HTMLElement {
|
|
18
|
+
@State({ deep: false }) items: Item[] = [];
|
|
19
|
+
|
|
20
|
+
render(): TemplateResult {
|
|
21
|
+
return html`<ul>
|
|
22
|
+
${map(this.items, (item) => {
|
|
23
|
+
rowSpy(item.id);
|
|
24
|
+
return html`<li data-id=${item.id}>${item.label}</li>`;
|
|
25
|
+
})}
|
|
26
|
+
</ul>`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('map() identity-based list rendering', () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
document.body.innerHTML = '';
|
|
33
|
+
rowSpy.mockClear();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mount = async (): Promise<MappedListElement> => {
|
|
37
|
+
const el = document.createElement('mapped-list-element') as MappedListElement;
|
|
38
|
+
document.body.appendChild(el);
|
|
39
|
+
await nextMicrotask();
|
|
40
|
+
return el;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
it('renders mapped items and skips rows whose item is identical on rerender', async () => {
|
|
44
|
+
const el = await mount();
|
|
45
|
+
const a = { id: 1, label: 'one' };
|
|
46
|
+
const b = { id: 2, label: 'two' };
|
|
47
|
+
el.items = [a, b];
|
|
48
|
+
await nextMicrotask();
|
|
49
|
+
|
|
50
|
+
const listItems = el.querySelectorAll('li');
|
|
51
|
+
expect(listItems.length).toBe(2);
|
|
52
|
+
expect(listItems[1].textContent).toBe('two');
|
|
53
|
+
expect(rowSpy).toHaveBeenCalledTimes(2);
|
|
54
|
+
|
|
55
|
+
// Replace only the second item; the first row must be skipped entirely.
|
|
56
|
+
rowSpy.mockClear();
|
|
57
|
+
el.items = [a, { id: 2, label: 'TWO' }];
|
|
58
|
+
await nextMicrotask();
|
|
59
|
+
|
|
60
|
+
expect(rowSpy).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(rowSpy).toHaveBeenCalledWith(2);
|
|
62
|
+
expect(el.querySelectorAll('li')[1].textContent).toBe('TWO');
|
|
63
|
+
expect(el.querySelectorAll('li')[0].textContent).toBe('one');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('detaches only the removed row on a contiguous middle removal', async () => {
|
|
67
|
+
const el = await mount();
|
|
68
|
+
const items = [
|
|
69
|
+
{ id: 1, label: 'one' },
|
|
70
|
+
{ id: 2, label: 'two' },
|
|
71
|
+
{ id: 3, label: 'three' },
|
|
72
|
+
{ id: 4, label: 'four' },
|
|
73
|
+
];
|
|
74
|
+
el.items = items;
|
|
75
|
+
await nextMicrotask();
|
|
76
|
+
const before = Array.from(el.querySelectorAll('li'));
|
|
77
|
+
expect(before.length).toBe(4);
|
|
78
|
+
|
|
79
|
+
// Remove the second item: the surviving rows must keep their exact DOM
|
|
80
|
+
// nodes and the row function must not run at all.
|
|
81
|
+
rowSpy.mockClear();
|
|
82
|
+
el.items = [items[0], items[2], items[3]];
|
|
83
|
+
await nextMicrotask();
|
|
84
|
+
|
|
85
|
+
const after = Array.from(el.querySelectorAll('li'));
|
|
86
|
+
expect(after.length).toBe(3);
|
|
87
|
+
expect(rowSpy).not.toHaveBeenCalled();
|
|
88
|
+
expect(after[0]).toBe(before[0]);
|
|
89
|
+
expect(after[1]).toBe(before[2]);
|
|
90
|
+
expect(after[2]).toBe(before[3]);
|
|
91
|
+
expect(after.map((li) => li.textContent)).toEqual(['one', 'three', 'four']);
|
|
92
|
+
|
|
93
|
+
// A later unrelated update still works with the spliced bookkeeping.
|
|
94
|
+
el.items = [items[0], { id: 9, label: 'nine' }, items[3]];
|
|
95
|
+
await nextMicrotask();
|
|
96
|
+
expect(Array.from(el.querySelectorAll('li')).map((li) => li.textContent)).toEqual([
|
|
97
|
+
'one',
|
|
98
|
+
'nine',
|
|
99
|
+
'four',
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('grows, shrinks, and fast-clears mapped lists', async () => {
|
|
104
|
+
const el = await mount();
|
|
105
|
+
const items = [
|
|
106
|
+
{ id: 1, label: 'one' },
|
|
107
|
+
{ id: 2, label: 'two' },
|
|
108
|
+
{ id: 3, label: 'three' },
|
|
109
|
+
];
|
|
110
|
+
el.items = items;
|
|
111
|
+
await nextMicrotask();
|
|
112
|
+
expect(el.querySelectorAll('li').length).toBe(3);
|
|
113
|
+
|
|
114
|
+
// Shrink: first item kept by identity, rest removed.
|
|
115
|
+
rowSpy.mockClear();
|
|
116
|
+
el.items = [items[0]];
|
|
117
|
+
await nextMicrotask();
|
|
118
|
+
expect(el.querySelectorAll('li').length).toBe(1);
|
|
119
|
+
expect(rowSpy).not.toHaveBeenCalled();
|
|
120
|
+
|
|
121
|
+
// Grow again.
|
|
122
|
+
el.items = [items[0], { id: 9, label: 'nine' }];
|
|
123
|
+
await nextMicrotask();
|
|
124
|
+
const grown = el.querySelectorAll('li');
|
|
125
|
+
expect(grown.length).toBe(2);
|
|
126
|
+
expect(grown[1].textContent).toBe('nine');
|
|
127
|
+
|
|
128
|
+
// Clear to empty, then repopulate.
|
|
129
|
+
el.items = [];
|
|
130
|
+
await nextMicrotask();
|
|
131
|
+
expect(el.querySelectorAll('li').length).toBe(0);
|
|
132
|
+
|
|
133
|
+
el.items = [{ id: 5, label: 'five' }];
|
|
134
|
+
await nextMicrotask();
|
|
135
|
+
expect(el.querySelectorAll('li').length).toBe(1);
|
|
136
|
+
expect(el.querySelector('li')!.textContent).toBe('five');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
@CustomElement({
|
|
141
|
+
tag: 'shallow-state-element',
|
|
142
|
+
shadow: false,
|
|
143
|
+
})
|
|
144
|
+
class ShallowStateElement extends HTMLElement {
|
|
145
|
+
@State({ deep: false }) data: Item[] = [];
|
|
146
|
+
renderCount = 0;
|
|
147
|
+
|
|
148
|
+
render(): TemplateResult {
|
|
149
|
+
this.renderCount++;
|
|
150
|
+
return html`<p>${this.data.length ? this.data[0].label : 'empty'}</p>`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('@State({ deep: false })', () => {
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
document.body.innerHTML = '';
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not proxy values and only rerenders on reassignment', async () => {
|
|
160
|
+
const el = document.createElement('shallow-state-element') as ShallowStateElement;
|
|
161
|
+
document.body.appendChild(el);
|
|
162
|
+
await nextMicrotask();
|
|
163
|
+
|
|
164
|
+
const raw = [{ id: 1, label: 'one' }];
|
|
165
|
+
el.data = raw;
|
|
166
|
+
await nextMicrotask();
|
|
167
|
+
// Shallow state stores the value as-is (no proxy wrapper).
|
|
168
|
+
expect(el.data).toBe(raw);
|
|
169
|
+
expect(el.querySelector('p')!.textContent).toBe('one');
|
|
170
|
+
const renders = el.renderCount;
|
|
171
|
+
|
|
172
|
+
// Nested mutation must not schedule a render.
|
|
173
|
+
el.data[0].label = 'mutated';
|
|
174
|
+
await nextMicrotask();
|
|
175
|
+
expect(el.renderCount).toBe(renders);
|
|
176
|
+
expect(el.querySelector('p')!.textContent).toBe('one');
|
|
177
|
+
|
|
178
|
+
// Reassignment rerenders.
|
|
179
|
+
el.data = el.data.slice();
|
|
180
|
+
await nextMicrotask();
|
|
181
|
+
expect(el.renderCount).toBe(renders + 1);
|
|
182
|
+
expect(el.querySelector('p')!.textContent).toBe('mutated');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { CustomElement, Signal, State, TemplateResult, html, signal } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
const nextMicrotask = () => Promise.resolve();
|
|
5
|
+
|
|
6
|
+
interface Row {
|
|
7
|
+
id: number;
|
|
8
|
+
label: Signal<string>;
|
|
9
|
+
selected: Signal<string | null>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@CustomElement({
|
|
13
|
+
tag: 'signal-row-element',
|
|
14
|
+
shadow: false,
|
|
15
|
+
})
|
|
16
|
+
class SignalRowElement extends HTMLElement {
|
|
17
|
+
@State({ deep: false }) rows: Row[] = [];
|
|
18
|
+
renderCount = 0;
|
|
19
|
+
|
|
20
|
+
render(): TemplateResult {
|
|
21
|
+
this.renderCount++;
|
|
22
|
+
return html`<ul>
|
|
23
|
+
${this.rows.map(
|
|
24
|
+
(row) => html`<li class=${row.selected} .title=${row.label}>${row.label}</li>`
|
|
25
|
+
)}
|
|
26
|
+
</ul>`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('signal bindings', () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
document.body.innerHTML = '';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('updates text, attribute, and property directly without re-rendering', async () => {
|
|
36
|
+
const el = document.createElement('signal-row-element') as SignalRowElement;
|
|
37
|
+
document.body.appendChild(el);
|
|
38
|
+
await nextMicrotask();
|
|
39
|
+
|
|
40
|
+
const row: Row = { id: 1, label: signal('one'), selected: signal<string | null>(null) };
|
|
41
|
+
el.rows = [row];
|
|
42
|
+
await nextMicrotask();
|
|
43
|
+
|
|
44
|
+
const li = el.querySelector('li')!;
|
|
45
|
+
expect(li.textContent).toBe('one');
|
|
46
|
+
expect(li.hasAttribute('class')).toBe(false);
|
|
47
|
+
expect(li.title).toBe('one');
|
|
48
|
+
const renders = el.renderCount;
|
|
49
|
+
|
|
50
|
+
// Signal writes go straight to the DOM — no render pass.
|
|
51
|
+
row.label.value = 'uno';
|
|
52
|
+
row.selected.value = 'danger';
|
|
53
|
+
expect(el.renderCount).toBe(renders);
|
|
54
|
+
expect(li.textContent).toBe('uno');
|
|
55
|
+
expect(li.getAttribute('class')).toBe('danger');
|
|
56
|
+
expect(li.title).toBe('uno');
|
|
57
|
+
|
|
58
|
+
// Null removes the attribute again.
|
|
59
|
+
row.selected.value = null;
|
|
60
|
+
expect(li.hasAttribute('class')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('keeps bindings across re-renders with the same signal and rebinds on a new one', async () => {
|
|
64
|
+
const el = document.createElement('signal-row-element') as SignalRowElement;
|
|
65
|
+
document.body.appendChild(el);
|
|
66
|
+
await nextMicrotask();
|
|
67
|
+
|
|
68
|
+
const row: Row = { id: 1, label: signal('a'), selected: signal<string | null>(null) };
|
|
69
|
+
el.rows = [row];
|
|
70
|
+
await nextMicrotask();
|
|
71
|
+
|
|
72
|
+
// Re-render with the same signal instances: binding must survive.
|
|
73
|
+
el.rows = [row];
|
|
74
|
+
await nextMicrotask();
|
|
75
|
+
row.label.value = 'b';
|
|
76
|
+
expect(el.querySelector('li')!.textContent).toBe('b');
|
|
77
|
+
|
|
78
|
+
// Re-render with a different signal: old one must be detached.
|
|
79
|
+
const oldLabel = row.label;
|
|
80
|
+
const replacement: Row = { id: 1, label: signal('fresh'), selected: row.selected };
|
|
81
|
+
el.rows = [replacement];
|
|
82
|
+
await nextMicrotask();
|
|
83
|
+
const li = el.querySelector('li')!;
|
|
84
|
+
expect(li.textContent).toBe('fresh');
|
|
85
|
+
oldLabel.value = 'stale write';
|
|
86
|
+
expect(li.textContent).toBe('fresh');
|
|
87
|
+
replacement.label.value = 'newer';
|
|
88
|
+
expect(li.textContent).toBe('newer');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('unsubscribes disposed parts and skips notifications with equal values', async () => {
|
|
92
|
+
const el = document.createElement('signal-row-element') as SignalRowElement;
|
|
93
|
+
document.body.appendChild(el);
|
|
94
|
+
await nextMicrotask();
|
|
95
|
+
|
|
96
|
+
const label = signal('x');
|
|
97
|
+
const row: Row = { id: 1, label, selected: signal<string | null>(null) };
|
|
98
|
+
el.rows = [row];
|
|
99
|
+
await nextMicrotask();
|
|
100
|
+
expect(el.querySelector('li')).not.toBeNull();
|
|
101
|
+
|
|
102
|
+
let calls = 0;
|
|
103
|
+
label.subscribe(() => calls++);
|
|
104
|
+
label.value = 'x'; // equal value: no notification
|
|
105
|
+
expect(calls).toBe(0);
|
|
106
|
+
label.value = 'y';
|
|
107
|
+
expect(calls).toBe(1);
|
|
108
|
+
|
|
109
|
+
// Clearing the list disposes parts; writes afterwards must not throw.
|
|
110
|
+
el.rows = [];
|
|
111
|
+
await nextMicrotask();
|
|
112
|
+
expect(el.querySelector('li')).toBeNull();
|
|
113
|
+
expect(() => {
|
|
114
|
+
label.value = 'z';
|
|
115
|
+
}).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
});
|
package/tools/build.js
CHANGED
|
@@ -7,16 +7,17 @@ const rimraf = require('rimraf');
|
|
|
7
7
|
const { copyFile } = require('fs').promises;
|
|
8
8
|
const glob = require('glob');
|
|
9
9
|
const { readFileSync, writeFileSync } = require('fs');
|
|
10
|
+
const { generateSources } = require('./generate-sources');
|
|
10
11
|
|
|
11
12
|
const DEST_PATH = 'dist';
|
|
12
|
-
// Inline
|
|
13
|
-
// demos and have their templateUrl/styleUrl references resolved.
|
|
13
|
+
// Inline demo sources so sibling demo imports resolve.
|
|
14
14
|
const SRC_PATH = `demos/**/*.ts`;
|
|
15
15
|
const SRC_TMP_PATH = `.tmp`;
|
|
16
16
|
|
|
17
17
|
const STATIC_ASSET_EXTS = new Set([
|
|
18
18
|
'.css', '.svg', '.png', '.jpg', '.jpeg', '.gif',
|
|
19
|
-
'.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf'
|
|
19
|
+
'.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf',
|
|
20
|
+
'.txt'
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
function copyStaticAssets(srcDir, destDir) {
|
|
@@ -36,7 +37,14 @@ function copyDemoShell() {
|
|
|
36
37
|
if (!existsSync(DEST_PATH)) mkdirSync(DEST_PATH, { recursive: true });
|
|
37
38
|
const indexSrc = `demos/${ELEMENT_NAME}/index.html`;
|
|
38
39
|
if (existsSync(indexSrc)) {
|
|
39
|
-
|
|
40
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
|
|
41
|
+
let html = readFileSync(indexSrc, 'utf-8');
|
|
42
|
+
// Keep the landing page's version badge in sync with package.json.
|
|
43
|
+
html = html.replace(
|
|
44
|
+
/<span class="badge">v[^<]*<\/span>/,
|
|
45
|
+
`<span class="badge">v${pkg.version}</span>`
|
|
46
|
+
);
|
|
47
|
+
writeFileSync(path.join(DEST_PATH, 'index.html'), html);
|
|
40
48
|
}
|
|
41
49
|
copyStaticAssets(`demos/${ELEMENT_NAME}`, DEST_PATH);
|
|
42
50
|
}
|
|
@@ -111,9 +119,10 @@ async function rollupGenerate(config) {
|
|
|
111
119
|
|
|
112
120
|
Promise.all([clean(DEST_PATH), clean(SRC_TMP_PATH)])
|
|
113
121
|
.then(() => inlineSources(SRC_PATH, SRC_TMP_PATH))
|
|
122
|
+
.then(() => generateSources(SRC_TMP_PATH))
|
|
114
123
|
.then(() => rollupGenerate(config))
|
|
115
124
|
.then(() => copyDemoShell())
|
|
116
125
|
.catch(err => {
|
|
117
126
|
console.error('Build failed:', err);
|
|
118
127
|
process.exit(1);
|
|
119
|
-
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the source map consumed by <cts-source-viewer>.
|
|
3
|
+
*/
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('fs');
|
|
6
|
+
|
|
7
|
+
// First file is the default tab.
|
|
8
|
+
const ENTRIES = [
|
|
9
|
+
{
|
|
10
|
+
slug: 'counter',
|
|
11
|
+
folder: 'demos/counter',
|
|
12
|
+
files: ['counter.element.ts'],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
slug: 'todo-dashboard',
|
|
16
|
+
folder: 'demos/todo-dashboard',
|
|
17
|
+
files: [
|
|
18
|
+
'todo-dashboard.element.ts',
|
|
19
|
+
'todo-stats.element.ts',
|
|
20
|
+
'todo-filters.element.ts',
|
|
21
|
+
'todo-item.element.ts',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const OUTPUT_REL = path.join('site', 'source-viewer', 'sources.generated.ts');
|
|
27
|
+
|
|
28
|
+
function buildSourcesMap() {
|
|
29
|
+
const map = {};
|
|
30
|
+
for (const entry of ENTRIES) {
|
|
31
|
+
map[entry.slug] = entry.files
|
|
32
|
+
.map((name) => {
|
|
33
|
+
const fullPath = path.join(entry.folder, name);
|
|
34
|
+
if (!existsSync(fullPath)) {
|
|
35
|
+
console.warn(`generate-sources: missing ${fullPath}; skipping.`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { name, source: readFileSync(fullPath, 'utf-8') };
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
return map;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderModule(map) {
|
|
46
|
+
// JSON can be embedded directly as a TS object literal.
|
|
47
|
+
const body = JSON.stringify(map, null, 2);
|
|
48
|
+
return [
|
|
49
|
+
'// AUTO-GENERATED by tools/generate-sources.js — do not edit by hand.',
|
|
50
|
+
'// Regenerated on every build/start so the live viewer always shows',
|
|
51
|
+
'// the source that ships with the bundle.',
|
|
52
|
+
'',
|
|
53
|
+
'export interface SourceFile {',
|
|
54
|
+
' name: string;',
|
|
55
|
+
' source: string;',
|
|
56
|
+
'}',
|
|
57
|
+
'',
|
|
58
|
+
`export const SOURCES: Record<string, SourceFile[]> = ${body};`,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} destRoot Usually `.tmp`.
|
|
65
|
+
*/
|
|
66
|
+
function generateSources(destRoot) {
|
|
67
|
+
const map = buildSourcesMap();
|
|
68
|
+
const outPath = path.join(destRoot, OUTPUT_REL);
|
|
69
|
+
const outDir = path.dirname(outPath);
|
|
70
|
+
if (!existsSync(outDir)) {
|
|
71
|
+
mkdirSync(outDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(outPath, renderModule(map));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { generateSources, ENTRIES };
|
package/tools/rollup-config.js
CHANGED
|
@@ -13,7 +13,7 @@ const prodModeParams = ['--prod', '--prod=true', '--prod true'];
|
|
|
13
13
|
const ELEMENT_PATH = `${ELEMENT_NAME}/index.ts`;
|
|
14
14
|
const INPUT_PATH = path.join('.tmp', ELEMENT_PATH);
|
|
15
15
|
|
|
16
|
-
if (ELEMENT_NAME
|
|
16
|
+
if (ELEMENT_NAME === undefined) {
|
|
17
17
|
console.log('specify which element to start');
|
|
18
18
|
console.log(' ↳ eg. npm start element-name');
|
|
19
19
|
process.exit();
|
|
@@ -67,4 +67,4 @@ if (isProcess(prodModeParams)) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
exports.config = config;
|
|
70
|
-
exports.ELEMENT_NAME = ELEMENT_NAME;
|
|
70
|
+
exports.ELEMENT_NAME = ELEMENT_NAME;
|
package/tools/start.js
CHANGED
|
@@ -6,16 +6,19 @@ const rollup = require('rollup');
|
|
|
6
6
|
const rimraf = require('rimraf');
|
|
7
7
|
const glob = require('glob');
|
|
8
8
|
const { config, ELEMENT_NAME } = require('./rollup-config');
|
|
9
|
+
const { generateSources } = require('./generate-sources');
|
|
9
10
|
|
|
10
11
|
const STATIC_ASSET_EXTS = new Set([
|
|
11
12
|
'.css', '.svg', '.png', '.jpg', '.jpeg', '.gif',
|
|
12
|
-
'.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf'
|
|
13
|
+
'.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf',
|
|
14
|
+
'.txt'
|
|
13
15
|
]);
|
|
14
16
|
|
|
17
|
+
const INLINE_SOURCE_EXTS = new Set(['.ts', '.scss']);
|
|
18
|
+
|
|
15
19
|
const DEST_PATH = 'dist';
|
|
16
|
-
// Inline
|
|
17
|
-
|
|
18
|
-
const SRC_PATH = `demos/**/*.ts`;
|
|
20
|
+
// Inline demo sources so sibling demo imports resolve.
|
|
21
|
+
const SRC_PATH = `demos/**/*.ts`;
|
|
19
22
|
const SRC_TMP_PATH = `.tmp`;
|
|
20
23
|
|
|
21
24
|
// Simple dev server
|
|
@@ -151,9 +154,10 @@ const copy = () => {
|
|
|
151
154
|
const fileWatcher = () => {
|
|
152
155
|
// Watch src and demos directories
|
|
153
156
|
watch('src', { recursive: true }, async (eventType, filename) => {
|
|
154
|
-
if (filename &&
|
|
157
|
+
if (filename && INLINE_SOURCE_EXTS.has(path.extname(filename).toLowerCase())) {
|
|
155
158
|
console.log(`File changed: ${filename}`);
|
|
156
159
|
await inlineSources(SRC_PATH, SRC_TMP_PATH);
|
|
160
|
+
await generateSources(SRC_TMP_PATH);
|
|
157
161
|
await rollupGenerate(config);
|
|
158
162
|
}
|
|
159
163
|
});
|
|
@@ -161,9 +165,10 @@ const fileWatcher = () => {
|
|
|
161
165
|
watch('demos', { recursive: true }, async (eventType, filename) => {
|
|
162
166
|
if (!filename) return;
|
|
163
167
|
const ext = path.extname(filename).toLowerCase();
|
|
164
|
-
if (ext
|
|
168
|
+
if (INLINE_SOURCE_EXTS.has(ext)) {
|
|
165
169
|
console.log(`File changed: ${filename}`);
|
|
166
170
|
await inlineSources(SRC_PATH, SRC_TMP_PATH);
|
|
171
|
+
await generateSources(SRC_TMP_PATH);
|
|
167
172
|
await rollupGenerate(config);
|
|
168
173
|
} else if (ext === '.html') {
|
|
169
174
|
console.log(`HTML changed: ${filename}`);
|
|
@@ -177,6 +182,7 @@ const fileWatcher = () => {
|
|
|
177
182
|
|
|
178
183
|
Promise.all([clean(DEST_PATH), clean(SRC_TMP_PATH)])
|
|
179
184
|
.then(() => Promise.all([inlineSources(SRC_PATH, SRC_TMP_PATH), copy()]))
|
|
185
|
+
.then(() => generateSources(SRC_TMP_PATH))
|
|
180
186
|
.then(() => {
|
|
181
187
|
rollupGenerate(config);
|
|
182
188
|
DevServer.start();
|
|
@@ -185,4 +191,4 @@ Promise.all([clean(DEST_PATH), clean(SRC_TMP_PATH)])
|
|
|
185
191
|
.catch(err => {
|
|
186
192
|
console.error('Start failed:', err);
|
|
187
193
|
process.exit(1);
|
|
188
|
-
});
|
|
194
|
+
});
|