custom-elements-ts 0.0.17 → 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 +47 -0
- package/.github/workflows/ci.yml +49 -0
- package/.prettierrc +7 -0
- package/LICENSE +20 -0
- package/README.md +279 -37
- package/assets/readme-header.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/demos/counter/counter.element.html +1 -0
- package/demos/counter/counter.element.scss +234 -0
- package/demos/counter/counter.element.ts +68 -0
- package/demos/counter/index.html +205 -0
- package/demos/counter/index.ts +1 -0
- package/demos/site/code-example/code-example.element.scss +168 -0
- package/demos/site/code-example/code-example.element.ts +88 -0
- package/demos/site/event-log/event-log.element.scss +179 -0
- package/demos/site/event-log/event-log.element.ts +123 -0
- package/demos/site/favicon.svg +14 -0
- package/demos/site/index.html +368 -0
- package/demos/site/index.ts +19 -0
- package/demos/site/llms.txt +53 -0
- package/demos/site/message/message.element.scss +75 -0
- package/demos/site/message/message.element.ts +76 -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 +1135 -0
- package/demos/site/styles/tokens.css +56 -0
- package/demos/site/toast/toast.element.scss +110 -0
- package/demos/site/toast/toast.element.ts +63 -0
- package/demos/todo-dashboard/index.html +141 -0
- package/demos/todo-dashboard/index.ts +4 -0
- package/demos/todo-dashboard/todo-dashboard.element.scss +1150 -0
- package/demos/todo-dashboard/todo-dashboard.element.ts +333 -0
- package/demos/todo-dashboard/todo-filters.element.ts +54 -0
- package/demos/todo-dashboard/todo-item.element.ts +127 -0
- package/demos/todo-dashboard/todo-stats.element.ts +189 -0
- package/package.json +73 -24
- package/src/custom-element.ts +206 -0
- package/src/index.ts +8 -0
- package/src/listen.ts +70 -0
- package/src/prop.ts +92 -0
- package/src/signal.ts +66 -0
- package/src/state.ts +141 -0
- package/src/template-runtime.ts +783 -0
- package/src/toggle.ts +66 -0
- package/src/tsconfig.json +24 -0
- package/src/util.ts +33 -0
- package/src/watch.ts +14 -0
- package/tests/basic.spec.ts +70 -0
- package/tests/custom-element.spec.ts +77 -0
- package/tests/dispatch.spec.ts +52 -0
- package/tests/init.spec.ts +94 -0
- package/tests/listen.spec.ts +118 -0
- package/tests/map-shallow-state.spec.ts +184 -0
- package/tests/prop.spec.ts +118 -0
- package/tests/signal.spec.ts +117 -0
- package/tests/templating-runtime.spec.ts +575 -0
- package/tests/toggle.spec.ts +92 -0
- package/tests/watch.spec.ts +183 -0
- package/tools/build.js +128 -0
- package/tools/bundle.js +167 -0
- package/tools/generate-sources.js +76 -0
- package/tools/rollup-config.js +70 -0
- package/tools/start.js +194 -0
- package/tsconfig.json +38 -0
- package/vite.config.mts +30 -0
- package/bundles/custom-elements-ts.umd.js +0 -359
- package/bundles/custom-elements-ts.umd.js.map +0 -1
- package/esm2015/custom-elements-ts.js +0 -283
- package/esm2015/custom-elements-ts.js.map +0 -1
- package/esm5/custom-element.d.ts +0 -12
- package/esm5/custom-elements-ts.js +0 -344
- package/esm5/custom-elements-ts.js.map +0 -1
- package/esm5/index.d.ts +0 -5
- package/esm5/listen.d.ts +0 -17
- package/esm5/prop.d.ts +0 -2
- package/esm5/toggle.d.ts +0 -1
- package/esm5/util.d.ts +0 -4
- package/esm5/watch.d.ts +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CustomElement, Prop, Toggle, Watch } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({
|
|
5
|
+
tag: 'watch-element',
|
|
6
|
+
template: '<span>my element</span>',
|
|
7
|
+
style: ':host{border:0}',
|
|
8
|
+
})
|
|
9
|
+
class WatchElement extends HTMLElement {
|
|
10
|
+
@Prop() name: any;
|
|
11
|
+
|
|
12
|
+
@Watch('name')
|
|
13
|
+
setSpan(value: any) {
|
|
14
|
+
const span = this.shadowRoot!.querySelector('span');
|
|
15
|
+
span!.innerHTML = value.new;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set label(value: any) {
|
|
19
|
+
this.setAttribute('label', value);
|
|
20
|
+
}
|
|
21
|
+
get label(): any {
|
|
22
|
+
return this.getAttribute('label') || '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
newLabel = '';
|
|
26
|
+
@Watch('label')
|
|
27
|
+
setLabel(value: any) {
|
|
28
|
+
this.newLabel = value.new;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
newColor = '';
|
|
32
|
+
@Prop() color = 'blue';
|
|
33
|
+
initialColor = '';
|
|
34
|
+
@Watch('color')
|
|
35
|
+
changeColor(value: any) {
|
|
36
|
+
this.initialColor ||= value.new;
|
|
37
|
+
this.newColor = this.color;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
caseChanged = false;
|
|
41
|
+
@Prop() setCase: any;
|
|
42
|
+
@Watch('setCase')
|
|
43
|
+
changeCase(value: any) {
|
|
44
|
+
this.caseChanged = this.setCase === value.new;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Watch('set-kebab')
|
|
48
|
+
changeKebabCase(value: any) {
|
|
49
|
+
this.caseChanged = this.setCase === value.new;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
menuChanged = false;
|
|
53
|
+
@Prop() menus = 'a';
|
|
54
|
+
@Watch('menus')
|
|
55
|
+
changeMenus(value: any) {
|
|
56
|
+
this.menuChanged = this.menus === value.new;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
enabledChanged = false;
|
|
60
|
+
@Toggle() enabled: any;
|
|
61
|
+
@Watch('enabled')
|
|
62
|
+
changeEnable(value: any) {
|
|
63
|
+
this.enabledChanged = this.enabled.toString() === value.new;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('watch decorator', () => {
|
|
68
|
+
let myElementInstance: any;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
const myElement = document.createElement('watch-element');
|
|
72
|
+
myElementInstance = document.body.appendChild(myElement);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
document.body.innerHTML = '';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should re-render setting property', () => {
|
|
80
|
+
myElementInstance.name = 'Aivan';
|
|
81
|
+
expect(myElementInstance.shadowRoot.querySelector('span').innerHTML).toEqual('Aivan');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should call method decorated with @Watch on prop change', () => {
|
|
85
|
+
const watchSpy = vi.spyOn(myElementInstance, 'setSpan');
|
|
86
|
+
myElementInstance.name = 'Aivan';
|
|
87
|
+
expect(watchSpy).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should call method decorated with @Watch on prop change', () => {
|
|
91
|
+
const watchSpy = vi.spyOn(myElementInstance, 'setSpan');
|
|
92
|
+
myElementInstance.setAttribute('name', 'Mario');
|
|
93
|
+
expect(watchSpy).toHaveBeenCalledWith(...[{ old: null, new: 'Mario' }]);
|
|
94
|
+
expect(myElementInstance.name).toEqual('Mario');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should call @Watch on attribute change with new property value', () => {
|
|
98
|
+
myElementInstance.setAttribute('color', 'red');
|
|
99
|
+
expect(myElementInstance.newColor).toEqual('red');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should call @Watch for initial kebab-resolved default prop value', () => {
|
|
103
|
+
expect(myElementInstance.initialColor).toEqual('blue');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should call @Watch for plain get/set with correct value.new', () => {
|
|
107
|
+
myElementInstance.setAttribute('label', 'Name');
|
|
108
|
+
expect(myElementInstance.newLabel).toEqual('Name');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should call non kebab @Watch on kebab attribute change with new property value', () => {
|
|
112
|
+
myElementInstance.setAttribute('set-case', 'kebab');
|
|
113
|
+
expect(myElementInstance.setCase).toEqual('kebab');
|
|
114
|
+
expect(myElementInstance.caseChanged).toBeTruthy();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should call kebab @Watch on kebab attribute change with new property value', () => {
|
|
118
|
+
myElementInstance.setAttribute('set-case', 'snake');
|
|
119
|
+
expect(myElementInstance.setCase).toEqual('snake');
|
|
120
|
+
expect(myElementInstance.caseChanged).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should call @Watch on property change with new property value', () => {
|
|
124
|
+
const menus = [
|
|
125
|
+
{
|
|
126
|
+
text: 'Colors',
|
|
127
|
+
link: '/user-interface/style-guides/colors',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
text: 'Logo',
|
|
131
|
+
link: '/user-interface/style-guides/logo',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
myElementInstance.menus = menus;
|
|
135
|
+
expect(myElementInstance.menus).toEqual(menus);
|
|
136
|
+
expect(myElementInstance.menuChanged).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should call @Watch on toggle attribute with new property value', () => {
|
|
140
|
+
myElementInstance.enabled = true;
|
|
141
|
+
expect(myElementInstance.enabledChanged).toBeTruthy();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should register watch metadata on the constructor for all watched attributes', () => {
|
|
145
|
+
const meta = (WatchElement as any).watchAttributes as Record<string, string>;
|
|
146
|
+
const propsInit = (WatchElement as any).propsInit as Record<string, unknown>;
|
|
147
|
+
|
|
148
|
+
expect(meta['name']).toBe('setSpan');
|
|
149
|
+
expect(meta['label']).toBe('setLabel');
|
|
150
|
+
expect(meta['color']).toBe('changeColor');
|
|
151
|
+
// camelCase key is kebab-cased in metadata
|
|
152
|
+
expect(meta['set-case']).toBe('changeCase');
|
|
153
|
+
// kebab key is preserved
|
|
154
|
+
expect(meta['set-kebab']).toBe('changeKebabCase');
|
|
155
|
+
expect(meta['menus']).toBe('changeMenus');
|
|
156
|
+
expect(meta['enabled']).toBe('changeEnable');
|
|
157
|
+
|
|
158
|
+
// propsInit entries are created (null placeholders)
|
|
159
|
+
['name', 'label', 'color', 'setCase', 'set-kebab', 'menus', 'enabled'].forEach((k) => {
|
|
160
|
+
expect(propsInit).toHaveProperty(k);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Ensure subsequent @Watch registrations on the same attribute update the mapping (last one wins)
|
|
166
|
+
@CustomElement({
|
|
167
|
+
tag: 'dupe-watch-element',
|
|
168
|
+
template: '<span>dupe</span>',
|
|
169
|
+
})
|
|
170
|
+
class DupeWatchElement extends HTMLElement {
|
|
171
|
+
@Watch('foo')
|
|
172
|
+
first(_v: any) {}
|
|
173
|
+
|
|
174
|
+
@Watch('foo')
|
|
175
|
+
second(_v: any) {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
describe('watch decorator duplicate attribute handling', () => {
|
|
179
|
+
it('last @Watch for same attribute should win in metadata map', () => {
|
|
180
|
+
const meta = (DupeWatchElement as any).watchAttributes as Record<string, string>;
|
|
181
|
+
expect(meta['foo']).toBe('second');
|
|
182
|
+
});
|
|
183
|
+
});
|
package/tools/build.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const { existsSync, mkdirSync, copyFileSync } = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { config, ELEMENT_NAME } = require('./rollup-config');
|
|
5
|
+
const rollup = require('rollup');
|
|
6
|
+
const rimraf = require('rimraf');
|
|
7
|
+
const { copyFile } = require('fs').promises;
|
|
8
|
+
const glob = require('glob');
|
|
9
|
+
const { readFileSync, writeFileSync } = require('fs');
|
|
10
|
+
const { generateSources } = require('./generate-sources');
|
|
11
|
+
|
|
12
|
+
const DEST_PATH = 'dist';
|
|
13
|
+
// Inline demo sources so sibling demo imports resolve.
|
|
14
|
+
const SRC_PATH = `demos/**/*.ts`;
|
|
15
|
+
const SRC_TMP_PATH = `.tmp`;
|
|
16
|
+
|
|
17
|
+
const STATIC_ASSET_EXTS = new Set([
|
|
18
|
+
'.css', '.svg', '.png', '.jpg', '.jpeg', '.gif',
|
|
19
|
+
'.ico', '.webp', '.woff', '.woff2', '.ttf', '.otf',
|
|
20
|
+
'.txt'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function copyStaticAssets(srcDir, destDir) {
|
|
24
|
+
const files = glob.sync(`${srcDir}/**/*`, { nodir: true });
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const ext = path.extname(file).toLowerCase();
|
|
27
|
+
if (!STATIC_ASSET_EXTS.has(ext)) continue;
|
|
28
|
+
const rel = path.relative(srcDir, file);
|
|
29
|
+
const dest = path.join(destDir, rel);
|
|
30
|
+
const destSubDir = path.dirname(dest);
|
|
31
|
+
if (!existsSync(destSubDir)) mkdirSync(destSubDir, { recursive: true });
|
|
32
|
+
copyFileSync(file, dest);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyDemoShell() {
|
|
37
|
+
if (!existsSync(DEST_PATH)) mkdirSync(DEST_PATH, { recursive: true });
|
|
38
|
+
const indexSrc = `demos/${ELEMENT_NAME}/index.html`;
|
|
39
|
+
if (existsSync(indexSrc)) {
|
|
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);
|
|
48
|
+
}
|
|
49
|
+
copyStaticAssets(`demos/${ELEMENT_NAME}`, DEST_PATH);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function clean(dir) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
rimraf(dir, (err) => {
|
|
55
|
+
if (err) reject(err);
|
|
56
|
+
else resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function inlineSources(srcPattern, destPath) {
|
|
62
|
+
const files = glob.sync(srcPattern);
|
|
63
|
+
|
|
64
|
+
if (!existsSync(destPath)) {
|
|
65
|
+
mkdirSync(destPath, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
let content = readFileSync(file, 'utf-8');
|
|
70
|
+
const relativePath = path.relative('demos', file);
|
|
71
|
+
const destFile = path.join(destPath, relativePath);
|
|
72
|
+
const destDir = path.dirname(destFile);
|
|
73
|
+
|
|
74
|
+
if (!existsSync(destDir)) {
|
|
75
|
+
mkdirSync(destDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Inline templateUrl and styleUrl references
|
|
79
|
+
content = inlineTemplates(content, path.dirname(file));
|
|
80
|
+
|
|
81
|
+
writeFileSync(destFile, content);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function inlineTemplates(content, basePath) {
|
|
86
|
+
// Match templateUrl: './file.html'
|
|
87
|
+
const templateUrlRegex = /templateUrl:\s*['"`]([^'"`]+)['"`]/g;
|
|
88
|
+
content = content.replace(templateUrlRegex, (match, filePath) => {
|
|
89
|
+
const fullPath = path.join(basePath, filePath);
|
|
90
|
+
if (existsSync(fullPath)) {
|
|
91
|
+
const template = readFileSync(fullPath, 'utf-8')
|
|
92
|
+
.replace(/`/g, '\\`')
|
|
93
|
+
.replace(/\$/g, '\\$');
|
|
94
|
+
return `template: \`${template}\``;
|
|
95
|
+
}
|
|
96
|
+
return match;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Match styleUrl: './file.scss' or './file.css'
|
|
100
|
+
const styleUrlRegex = /styleUrl:\s*['"`]([^'"`]+)['"`]/g;
|
|
101
|
+
content = content.replace(styleUrlRegex, (match, filePath) => {
|
|
102
|
+
const fullPath = path.join(basePath, filePath);
|
|
103
|
+
if (existsSync(fullPath)) {
|
|
104
|
+
let style = readFileSync(fullPath, 'utf-8');
|
|
105
|
+
// For SCSS files, we'd normally compile them, but for now just use as CSS
|
|
106
|
+
style = style.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
107
|
+
return `style: \`${style}\``;
|
|
108
|
+
}
|
|
109
|
+
return match;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function rollupGenerate(config) {
|
|
116
|
+
const bundle = await rollup.rollup(config.inputOptions);
|
|
117
|
+
await bundle.write(config.outputOptions);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Promise.all([clean(DEST_PATH), clean(SRC_TMP_PATH)])
|
|
121
|
+
.then(() => inlineSources(SRC_PATH, SRC_TMP_PATH))
|
|
122
|
+
.then(() => generateSources(SRC_TMP_PATH))
|
|
123
|
+
.then(() => rollupGenerate(config))
|
|
124
|
+
.then(() => copyDemoShell())
|
|
125
|
+
.catch(err => {
|
|
126
|
+
console.error('Build failed:', err);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
package/tools/bundle.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const rollup = require('rollup');
|
|
3
|
+
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
|
4
|
+
const typescript = require('rollup-plugin-typescript2');
|
|
5
|
+
const ts = require('typescript');
|
|
6
|
+
const rimraf = require('rimraf');
|
|
7
|
+
const { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } = require('fs');
|
|
8
|
+
const MagicString = require('magic-string');
|
|
9
|
+
|
|
10
|
+
const LIB_NAME = 'custom-elements-ts';
|
|
11
|
+
|
|
12
|
+
// removing custom comment-strip plugin; modern toolchain handles comments safely
|
|
13
|
+
|
|
14
|
+
const createConfig = () => {
|
|
15
|
+
return ['umd', 'esm5', 'esm2015'].map((format) => {
|
|
16
|
+
const tsConfig = {
|
|
17
|
+
compilerOptions: {
|
|
18
|
+
target: format.includes('esm2015') ? 'es2015' : 'es5',
|
|
19
|
+
declaration: false,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const file = format.includes('umd')
|
|
24
|
+
? path.join('dist', 'bundles', `${LIB_NAME}.umd.js`)
|
|
25
|
+
: path.join('dist', format, `${LIB_NAME}.js`);
|
|
26
|
+
|
|
27
|
+
const formatType = format.includes('umd') ? 'umd' : 'es';
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
inputOptions: {
|
|
31
|
+
treeshake: true,
|
|
32
|
+
input: 'src/index.ts',
|
|
33
|
+
plugins: [
|
|
34
|
+
typescript({
|
|
35
|
+
tsconfig: 'src/tsconfig.json',
|
|
36
|
+
tsconfigOverride: { ...tsConfig },
|
|
37
|
+
check: false,
|
|
38
|
+
cacheRoot: path.join(path.resolve(), 'node_modules/.tmp/.rts2_cache'),
|
|
39
|
+
// ensure tslib helpers are injected correctly for ES5 target
|
|
40
|
+
tslib: require.resolve('tslib'),
|
|
41
|
+
}),
|
|
42
|
+
nodeResolve(),
|
|
43
|
+
],
|
|
44
|
+
onwarn(warning) {
|
|
45
|
+
if (warning.code === 'THIS_IS_UNDEFINED') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log('Rollup warning: ', warning.message);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
outputOptions: {
|
|
52
|
+
sourcemap: true,
|
|
53
|
+
file: file,
|
|
54
|
+
name: LIB_NAME,
|
|
55
|
+
format: formatType,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
async function clean(dir) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
rimraf(dir, (err) => {
|
|
64
|
+
if (err) reject(err);
|
|
65
|
+
else resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function rollupBuild(config) {
|
|
71
|
+
const bundle = await rollup.rollup(config.inputOptions);
|
|
72
|
+
await bundle.write(config.outputOptions);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const emitDtsFiles = () => {
|
|
76
|
+
const configPath = path.resolve('src', 'tsconfig.json');
|
|
77
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
78
|
+
if (configFile.error) {
|
|
79
|
+
throw new Error(ts.formatDiagnosticsWithColorAndContext([configFile.error], formatHost));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
83
|
+
configFile.config,
|
|
84
|
+
ts.sys,
|
|
85
|
+
path.resolve('src'),
|
|
86
|
+
{
|
|
87
|
+
declaration: true,
|
|
88
|
+
declarationDir: path.resolve('dist'),
|
|
89
|
+
emitDeclarationOnly: true,
|
|
90
|
+
outDir: path.resolve('dist'),
|
|
91
|
+
rootDir: path.resolve('src'),
|
|
92
|
+
sourceMap: false,
|
|
93
|
+
},
|
|
94
|
+
configPath
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
|
|
98
|
+
const emitResult = program.emit();
|
|
99
|
+
const diagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
|
|
100
|
+
const errors = diagnostics.filter(
|
|
101
|
+
(diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error
|
|
102
|
+
);
|
|
103
|
+
if (errors.length > 0) {
|
|
104
|
+
throw new Error(ts.formatDiagnosticsWithColorAndContext(errors, formatHost));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const formatHost = {
|
|
109
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
110
|
+
getCurrentDirectory: () => process.cwd(),
|
|
111
|
+
getNewLine: () => '\n',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const copyReadMe = () => {
|
|
115
|
+
if (existsSync('README.md')) {
|
|
116
|
+
copyFileSync('README.md', path.join('dist', 'README.md'));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const buildCopyPackageFile = (libName, paths) => {
|
|
121
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
|
|
122
|
+
const distPkg = {
|
|
123
|
+
name: libName,
|
|
124
|
+
version: pkg.version,
|
|
125
|
+
description: pkg.description,
|
|
126
|
+
main: paths.main,
|
|
127
|
+
module: paths.module,
|
|
128
|
+
esm5: paths.esm5,
|
|
129
|
+
esm2015: paths.esm2015,
|
|
130
|
+
typings: paths.typings,
|
|
131
|
+
peerDependencies: pkg.peerDependencies,
|
|
132
|
+
repository: pkg.repository,
|
|
133
|
+
keywords: pkg.keywords,
|
|
134
|
+
author: pkg.author,
|
|
135
|
+
license: pkg.license,
|
|
136
|
+
bugs: pkg.bugs,
|
|
137
|
+
homepage: pkg.homepage,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (!existsSync('dist')) {
|
|
141
|
+
mkdirSync('dist', { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!existsSync('dist/bundles')) {
|
|
145
|
+
mkdirSync('dist/bundles', { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
writeFileSync(path.join('dist', 'package.json'), JSON.stringify(distPkg, null, 2));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const copyPkgFile = () =>
|
|
152
|
+
buildCopyPackageFile(LIB_NAME, {
|
|
153
|
+
main: `./bundles/${LIB_NAME}.umd.js`,
|
|
154
|
+
esm5: `./esm5/${LIB_NAME}.js`,
|
|
155
|
+
module: `./esm2015/${LIB_NAME}.js`,
|
|
156
|
+
esm2015: `./esm2015/${LIB_NAME}.js`,
|
|
157
|
+
typings: 'index.d.ts',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
Promise.all([clean('dist'), clean('.tmp')])
|
|
161
|
+
.then(() => Promise.all(createConfig().map((config) => rollupBuild(config))))
|
|
162
|
+
.then(() => emitDtsFiles())
|
|
163
|
+
.then(() => Promise.all([copyPkgFile(), copyReadMe()]))
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
console.error('Bundle failed:', err);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
|
@@ -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 };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const typescript = require('rollup-plugin-typescript2');
|
|
3
|
+
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
|
4
|
+
const terserPlugin = require('@rollup/plugin-terser');
|
|
5
|
+
const terser = terserPlugin.terser || terserPlugin;
|
|
6
|
+
const { existsSync } = require('fs');
|
|
7
|
+
|
|
8
|
+
const ELEMENT_NAME = process.argv[2];
|
|
9
|
+
const DEST_PATH = 'dist';
|
|
10
|
+
|
|
11
|
+
const prodModeParams = ['--prod', '--prod=true', '--prod true'];
|
|
12
|
+
|
|
13
|
+
const ELEMENT_PATH = `${ELEMENT_NAME}/index.ts`;
|
|
14
|
+
const INPUT_PATH = path.join('.tmp', ELEMENT_PATH);
|
|
15
|
+
|
|
16
|
+
if (ELEMENT_NAME === undefined) {
|
|
17
|
+
console.log('specify which element to start');
|
|
18
|
+
console.log(' ↳ eg. npm start element-name');
|
|
19
|
+
process.exit();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!existsSync(`demos/${ELEMENT_PATH}`)) {
|
|
23
|
+
console.log('element does not exist');
|
|
24
|
+
process.exit();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toPascalCase = (text) => {
|
|
28
|
+
return text.replace(/-\w/g, m => m[1].toUpperCase())
|
|
29
|
+
.replace(/^\w/, c => c.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isProcess(params) {
|
|
33
|
+
return params.some(param => process.argv.includes(param));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = {
|
|
37
|
+
inputOptions: {
|
|
38
|
+
treeshake: true,
|
|
39
|
+
input: INPUT_PATH,
|
|
40
|
+
plugins: [
|
|
41
|
+
typescript({
|
|
42
|
+
useTsconfigDeclarationDir: true,
|
|
43
|
+
check: false,
|
|
44
|
+
cacheRoot: path.join(path.resolve(), 'node_modules/.tmp/.rts2_cache')
|
|
45
|
+
}),
|
|
46
|
+
nodeResolve()
|
|
47
|
+
],
|
|
48
|
+
onwarn(warning) {
|
|
49
|
+
if (warning.code === 'THIS_IS_UNDEFINED') { return; }
|
|
50
|
+
console.log("Rollup warning: ", warning.message);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
outputOptions: {
|
|
54
|
+
sourcemap: true,
|
|
55
|
+
exports: 'named',
|
|
56
|
+
name: toPascalCase(ELEMENT_NAME),
|
|
57
|
+
file: `${DEST_PATH}/${ELEMENT_NAME}.umd.js`,
|
|
58
|
+
format: 'umd'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (isProcess(prodModeParams)) {
|
|
63
|
+
const options = {
|
|
64
|
+
mangle: { keep_fnames: true }
|
|
65
|
+
};
|
|
66
|
+
config.inputOptions.plugins.push(terser(options));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
exports.config = config;
|
|
70
|
+
exports.ELEMENT_NAME = ELEMENT_NAME;
|