@pictogrammers/element-esbuild 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pictogrammers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Element-esbuild
2
+
3
+ Element [esbuild](https://esbuild.github.io/) tooling for developing Web Components with the [Element](https://github.com/Pictogrammers/Element/) utility.
4
+
5
+ ## Features
6
+
7
+ - Application - Bundle a single application
8
+ - Components - Bundle a shared component library
9
+ - `__examples__` components map to the library
10
+
11
+ Default folder structure for both:
12
+
13
+ - `src/index.html` - optional
14
+ - `src/favicon.svg` - optional
15
+ - `src/components/*` - required
16
+ - `namespace/` - required
17
+ - `app/app.ts` - required for app
18
+ - `__examples__`
19
+ - `basic/basic.ts` - for component library
20
+
21
+ ## `element.config.ts`
22
+
23
+ Example configuration for a app.
24
+
25
+ ```typescript
26
+ export default {
27
+ // root hello/app/app.ts
28
+ namespace: 'hello', // for applications
29
+ }
30
+ ```
31
+
32
+ Example configuration for a component library.
33
+
34
+ ```typescript
35
+ export default {
36
+ repo: 'https://...',
37
+ navigation: [{
38
+ label: 'Components',
39
+ // default group
40
+ }, {
41
+ label: 'Overlays',
42
+ extends: ['MyOverlay'], // group all components extend class
43
+ include: ['MyOverlay'], // also include MyOverlay component
44
+ exclude: ['MyExclude'],
45
+ namespaces: ['my'], // filter to only my-* components
46
+ }],
47
+ }
48
+ ```
49
+
50
+ Leaving off the `namespace` will treat the repo as a component library. Each component will be built individually instead of a single application. The component's `__examples__` folders will render as demo.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // Build
3
+ console.log('build');
4
+
5
+ // import { build } from 'esbuild';
6
+ /*
7
+ build({
8
+ entryPoints,
9
+ bundle: true,
10
+ platform: 'browser',
11
+ outfile: './dist/main.js',
12
+ sourcemap: true,
13
+ target: 'esnext',
14
+ loader: {
15
+ '.html': 'text',
16
+ '.css': 'text'
17
+ },
18
+ }).catch((err) => {
19
+ process.stderr.write(err.stderr);
20
+ process.exit(1);
21
+ });
22
+ */
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Publish
3
+ console.log('publish');
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { context } from 'esbuild';
4
+ import chokidar from 'chokidar';
5
+ import { pathToFileURL, fileURLToPath } from 'node:url';
6
+ import { join, sep, dirname } from 'node:path';
7
+ import { copyFile, readFile, writeFile } from 'node:fs/promises';
8
+
9
+ import { fileExists } from '../scripts/fileExists.ts';
10
+ import { folderExists } from '../scripts/folderExists.ts';
11
+ import { playgroundPlugin } from '../scripts/playgroundPlugin.ts';
12
+ import { htmlDependentsPlugin } from '../scripts/htmlDependentsPlugin.ts';
13
+ import { rebuildNotifyPlugin } from '../scripts/rebuildNotifyPlugin.ts';
14
+
15
+ const plugins = [htmlDependentsPlugin, rebuildNotifyPlugin];
16
+
17
+ const bold = (text: string) => `\x1b[1m${text}\x1b[0m`;
18
+ const green = (text: string) => `\x1b[32m${text}\x1b[0m`;
19
+ const red = (text: string) => `\x1b[31m${text}\x1b[0m`;
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const defaultDir = join(__dirname, '..', 'default');
24
+ const configFile = 'element.config.ts';
25
+ const rootDir = process.cwd();
26
+ const publishDir = 'publish';
27
+ const fullConfigPath = pathToFileURL(configFile);
28
+ if (!(await fileExists(configFile))) {
29
+ console.log(red('Missing element.config.ts in root.'), 'Add with content:');
30
+ console.log('export default {');
31
+ console.log(` namespace: 'hello',`);
32
+ console.log('}');
33
+ process.exit();
34
+ }
35
+ const config = await import(fullConfigPath.href);
36
+
37
+ const {
38
+ namespace,
39
+ title,
40
+ navigation,
41
+ repo,
42
+ repoComponent,
43
+ } = config.default;
44
+ const distDir = 'dist';
45
+ const srcDir = 'src';
46
+ const componentsDir = 'components';
47
+
48
+ const entryPoints: string[] = [];
49
+ if (namespace) {
50
+ console.log(green('Building app...'));
51
+ entryPoints.push(`./${srcDir}/${componentsDir}/${namespace}/app/app.ts`);
52
+ // Handle index.html
53
+ const indexFile = 'index.html';
54
+ if (await fileExists(join(rootDir, srcDir, indexFile))) {
55
+ await copyFile(
56
+ join(rootDir, srcDir, indexFile),
57
+ join(rootDir, distDir, indexFile)
58
+ );
59
+ } else {
60
+ let indexContent = await readFile(join(defaultDir, indexFile), 'utf8');
61
+ indexContent = indexContent.replace(
62
+ '<title>Default</title>',
63
+ `<title>${title ?? 'Default'}</title>`
64
+ );
65
+ indexContent = indexContent.replace(
66
+ '<namespace-app></namespace-app>',
67
+ `<${namespace}-app></${namespace}-app>`
68
+ );
69
+ await writeFile(join(rootDir, distDir, indexFile), indexContent);
70
+ }
71
+ // Handle favicon.svg
72
+ const faviconSvg = 'favicon.svg';
73
+ if (await fileExists(join(rootDir, srcDir, faviconSvg))) {
74
+ await copyFile(
75
+ join(rootDir, srcDir, faviconSvg),
76
+ join(rootDir, distDir, faviconSvg)
77
+ );
78
+ } else {
79
+ await copyFile(
80
+ join(defaultDir, faviconSvg),
81
+ join(rootDir, distDir, faviconSvg)
82
+ );
83
+ }
84
+ } else {
85
+ console.log(green('Building components...'));
86
+ if (!(await folderExists(join(srcDir, componentsDir)))) {
87
+ console.log(red('Missing required "src/components" directory.'))
88
+ process.exit();
89
+ }
90
+ const playgroundFile = 'playground.html';
91
+ const indexFile = 'index.html';
92
+ entryPoints.push('playground-entry');
93
+ plugins.push(
94
+ playgroundPlugin({
95
+ after: async (namespaces: any[]) => {
96
+ let indexContent = '';
97
+ if (await fileExists(join(rootDir, srcDir, indexFile))) {
98
+ indexContent = await readFile(join(rootDir, srcDir, indexFile), 'utf8');
99
+ } else {
100
+ indexContent = await readFile(join(defaultDir, playgroundFile), 'utf8');
101
+ }
102
+ indexContent = indexContent.replace(
103
+ '<title>Default</title>',
104
+ `<title>${title ?? 'Default'}</title>`
105
+ );
106
+ indexContent = indexContent.replace(
107
+ '<h1>Default</h1>',
108
+ `<h1>${title ?? 'Default'}</h1>`
109
+ );
110
+ const navItems = structuredClone(navigation ?? []);
111
+ for (let navItem of navItems) {
112
+ navItem.items = [];
113
+ }
114
+ let defaultItem;
115
+ if (navItems.length === 0) {
116
+ defaultItem = { label: 'Components', items: [] };
117
+ navItems.push(defaultItem);
118
+ } else {
119
+ defaultItem = navItems.find((x: any) => !x.extends && !x.components && !x.namespaces);
120
+ if (!defaultItem) {
121
+ defaultItem = { label: 'Other', items: [] };
122
+ navItems.push(defaultItem);
123
+ }
124
+ }
125
+ const componentMap = new Map();
126
+ // Loop and organize into lists
127
+ namespaces.forEach(({ components }: any) => {
128
+ components.forEach(({ component, namespace, tag, readme, examples, className, classExtends }: any) => {
129
+ // Front end data
130
+ componentMap.set(className, {
131
+ className, // MyComponent
132
+ classExtends, // MyModal
133
+ component, // component
134
+ namespace, // my
135
+ tag, // my-component
136
+ readme, // # My Component
137
+ examples: examples.map((example: any) => example.className)
138
+ });
139
+ examples.forEach((example: any) => {
140
+ componentMap.set(example.className, {
141
+ className: example.className, // XMyComponentBasic
142
+ classExtends: example.classExtends, // HtmlElement
143
+ component: example.component, // myComponentBasic
144
+ namespace: example.namespace, // x
145
+ tag: example.tag, // x-my-component-basic
146
+ example: example.example, // Basic
147
+ });
148
+ });
149
+ // Quick insert any direct includes
150
+ for (let navItem of navItems) {
151
+ if (navItem.include && navItem.include.includes(className)) {
152
+ navItem.items.push({
153
+ namespace,
154
+ component,
155
+ className,
156
+ tag,
157
+ });
158
+ return;
159
+ }
160
+ }
161
+ // Move on to any other nav groups
162
+ for (let navItem of navItems) {
163
+ // skip default nav group
164
+ if (navItem === defaultItem) {
165
+ continue;
166
+ }
167
+ // ignore if excluded
168
+ if (navItem.exclude && navItem.exclude.includes(className)) {
169
+ continue;
170
+ }
171
+ // ignore if not in namespace
172
+ if (navItem.namespaces && !navItem.namespaces.includes(namespace)) {
173
+ continue;
174
+ }
175
+ // ignore if not extending the required class
176
+ if (navItem.extends && !navItem.extends.includes(classExtends)) {
177
+ continue;
178
+ }
179
+ navItem.items.push({
180
+ namespace,
181
+ component,
182
+ className,
183
+ tag,
184
+ });
185
+ return;
186
+ }
187
+ defaultItem.items.push({
188
+ namespace,
189
+ component,
190
+ examples,
191
+ className,
192
+ tag,
193
+ });
194
+ });
195
+ });
196
+ // Replace left nav
197
+ indexContent = indexContent.replace(/([ ]*)<!-- \[Navigation\] -->/, (match: any, indent: any) => {
198
+ return navItems.map(({ label, items }: any) => {
199
+ return [
200
+ indent,
201
+ `<div>${label}</div>`,
202
+ '<ul>',
203
+ items.map(({component, namespace, tag, className}: any) => {
204
+ return [
205
+ `<li data-tag="${tag}" data-class-name="${className}" data-component="${component}"><a href="#${tag}">${component}</a></li>`
206
+ ].join(`\n${indent}`);
207
+ }).join(`\n${indent}`),
208
+ '</ul>'
209
+ ].join(`\n${indent}`)
210
+ }).join(`\n${indent}`);
211
+ });
212
+ // Repo
213
+ if (repo) {
214
+ const github = 'M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z';
215
+ const generic = 'M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12.75,13.5C15.5,13.5 16.24,11.47 16.43,10.4C17.34,10.11 18,9.26 18,8.25C18,7 17,6 15.75,6C14.5,6 13.5,7 13.5,8.25C13.5,9.19 14.07,10 14.89,10.33C14.67,11 14,12 12,12C10.62,12 9.66,12.35 9,12.84V8.87C9.87,8.56 10.5,7.73 10.5,6.75C10.5,5.5 9.5,4.5 8.25,4.5C7,4.5 6,5.5 6,6.75C6,7.73 6.63,8.56 7.5,8.87V15.13C6.63,15.44 6,16.27 6,17.25C6,18.5 7,19.5 8.25,19.5C9.5,19.5 10.5,18.5 10.5,17.25C10.5,16.32 9.94,15.5 9.13,15.18C9.41,14.5 10.23,13.5 12.75,13.5M8.25,16.5A0.75,0.75 0 0,1 9,17.25A0.75,0.75 0 0,1 8.25,18A0.75,0.75 0 0,1 7.5,17.25A0.75,0.75 0 0,1 8.25,16.5M8.25,6A0.75,0.75 0 0,1 9,6.75A0.75,0.75 0 0,1 8.25,7.5A0.75,0.75 0 0,1 7.5,6.75A0.75,0.75 0 0,1 8.25,6M15.75,7.5A0.75,0.75 0 0,1 16.5,8.25A0.75,0.75 0 0,1 15.75,9A0.75,0.75 0 0,1 15,8.25A0.75,0.75 0 0,1 15.75,7.5Z';
216
+ const repoIcon = repo.includes('github.com') ? github : generic;
217
+ indexContent = indexContent.replace(/([ ]*)<!-- \[Repo\] -->/, (match: any, indent: any) => {
218
+ return [
219
+ indent,
220
+ `<a href="${repo}">`,
221
+ ' <svg viewBox="0 0 24 24">',
222
+ ` <path fill="currentColor" d="${repoIcon}" />`,
223
+ ' </svg>',
224
+ ' <span>View Repo</span>',
225
+ '</a>'
226
+ ].join(`\n${indent}`)
227
+ });
228
+ indexContent = indexContent.replace(/const repo = '';/, (match: any) => {
229
+ return `const repo = '${repo}';`;
230
+ });
231
+ indexContent = indexContent.replace(/const repoComponent = '';/, (match: any) => {
232
+ return `const repoComponent = '${repoComponent.replace(/\$repo/g, repo)}';`;
233
+ });
234
+ indexContent = indexContent.replace(/const repoIcon = '';/, (match: any) => {
235
+ return `const repoIcon = '${repoIcon}';`;
236
+ });
237
+ }
238
+ // Components
239
+ const classNames = [...componentMap.keys()];
240
+ indexContent = indexContent.replace(/([ ]*)const componentMap = new Map\(\);/, (match: any, indent: any) => {
241
+ return [
242
+ `${indent}const componentMap = new Map();`,
243
+ ...classNames.map((className: any) => {
244
+ const data = componentMap.get(className);
245
+ return `componentMap.set('${className}', ${JSON.stringify(data)});`;
246
+ })
247
+ ].join(`${indent}\n`)
248
+ });
249
+ // Components
250
+ indexContent = indexContent.replace(/([ ]*)const navigation = \[\];/, (match: any, indent: any) => {
251
+ return`${indent}const navigation = ${JSON.stringify(navItems, null, ' ')};`;
252
+ });
253
+ await writeFile(join(rootDir, distDir, indexFile), indexContent);
254
+ }
255
+ })
256
+ );
257
+ }
258
+
259
+ let ctx = await context({
260
+ entryPoints,
261
+ outfile: `./${distDir}/main.js`,
262
+ bundle: true,
263
+ format: 'esm', // Use ES Modules
264
+ target: 'es2024', // Target ES6 syntax
265
+ minify: false,
266
+ loader: {
267
+ '.css': 'text'
268
+ },
269
+ plugins,
270
+ });
271
+
272
+ // initial rebuild
273
+ await ctx.rebuild();
274
+
275
+ // any change to src should trigger rebuild
276
+ const watcher = chokidar.watch('src', {
277
+ ignoreInitial: true, // Don't trigger on startup
278
+ });
279
+
280
+ watcher.on('all', async (event, path) => {
281
+ // Copy to publish folder for component projects
282
+ if (!namespace) {
283
+ const parts = path.split(sep);
284
+ if (parts.length > 4 && parts[0] === srcDir && parts[1] === componentsDir) {
285
+ console.log(`Copy "${parts.slice(2).join('/')}" to publish/*`);
286
+ await copyFile(join(rootDir, ...parts), join(rootDir, publishDir, ...parts.slice(2)));
287
+ }
288
+ }
289
+ try {
290
+ await ctx.rebuild();
291
+ } catch (e) {
292
+ console.error('Rebuild failed:', e);
293
+ }
294
+ });
295
+
296
+ let { port } = await ctx.serve({
297
+ servedir: distDir,
298
+ });
299
+ console.log(green('Dev server started at'), `localhost:${port}`);
@@ -0,0 +1,20 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2
+ <style>
3
+ #main {
4
+ fill: #FFFFFF;
5
+ }
6
+ #square {
7
+ fill: #453C4F;
8
+ }
9
+ @media (prefers-color-scheme: dark) {
10
+ #main {
11
+ fill: #453C4F;
12
+ }
13
+ #square {
14
+ fill: #FFFFFF;
15
+ }
16
+ }
17
+ </style>
18
+ <rect id="square" x="2" y="2" width="60" height="60" rx="6" fill-opacity="0.9"/>
19
+ <path id="main" d="M 8,0L 56,0C 60.4183,0 64,3.58173 64,8L 64,56C 64,60.4183 60.4183,64 56,64L 8,64C 3.58172,64 0,60.4183 0,56L 0,8C 0,3.58173 3.58172,0 8,0 Z M 12,12.0001L 12,52.0001L 32,52.0002L 32,44.0003L 20,44.0001L 20,36.0001L 32,36.0002L 32,28.0003L 20,28.0001L 20,20.0001L 32,20.0002L 32,12.0003L 12,12.0001 Z M 40,12.0001L 40,52.0001L 48,52.0001L 48,12.0001L 40,12.0001 Z "/>
20
+ </svg>
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="shortcut icon" type="image/svg" href="favicon.svg"/>
6
+ <title>Default</title>
7
+ <style>
8
+ :root {
9
+ --app-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
10
+ }
11
+ body {
12
+ margin: 0;
13
+ font-family: var(--app-font-family);
14
+ }
15
+ </style>
16
+ <script type="module" src="main.js" defer></script>
17
+ </head>
18
+ <body>
19
+ <namespace-app></namespace-app>
20
+ </body>
21
+ </html>
@@ -0,0 +1,575 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <link rel="icon" href="favicon.svg" type="image/svg+xml" sizes="any" />
7
+ <title>Default</title>
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ font-family: system-ui, sans-serif;
12
+ }
13
+
14
+ .wrapper {
15
+ display: grid;
16
+ grid-template-columns: 12rem calc(100% - 12rem);
17
+ grid-template-rows: 3rem auto;
18
+ }
19
+
20
+ code {
21
+ font-family: 'Consolas', 'Courier New', Courier, monospace;
22
+ border: 1px solid #ddd;
23
+ padding: 0.15rem 0.25rem;
24
+ border-radius: 0.25rem;
25
+ }
26
+
27
+ header {
28
+ display: flex;
29
+ grid-row: 1;
30
+ grid-column: 1 / span 2;
31
+ background: #453C4F;
32
+ color: #FFF;
33
+ align-items: center;
34
+ }
35
+
36
+ header>svg {
37
+ display: inline-flex;
38
+ width: 1.5rem;
39
+ height: 1.5rem;
40
+ margin: 0 0.75rem 0 1rem;
41
+ }
42
+
43
+ header>h1 {
44
+ display: inline-flex;
45
+ color: #FFF;
46
+ font-size: 1.5rem;
47
+ margin: 0;
48
+ font-weight: normal;
49
+ }
50
+
51
+ header>span {
52
+ flex: 1;
53
+ }
54
+
55
+ header>a {
56
+ display: inline-flex;
57
+ border: 1px solid #FFF;
58
+ padding: 0.25rem 0.5rem;
59
+ flex-direction: row;
60
+ border-radius: 0.25rem;
61
+ color: #FFF;
62
+ float: right;
63
+ text-decoration: none;
64
+ margin-right: 1rem;
65
+ align-items: center;
66
+ }
67
+
68
+ header>a>svg {
69
+ display: inline-flex;
70
+ width: 1.5rem;
71
+ height: 1.5rem;
72
+ margin: 0 0.5rem 0 0;
73
+ }
74
+
75
+ header>a:hover {
76
+ background: #FFF;
77
+ color: #453C4F;
78
+ }
79
+
80
+ header select {
81
+ padding: 0.375rem 0.25rem;
82
+ border-radius: 0.25rem;
83
+ border: 1px solid #FFF;
84
+ background: transparent;
85
+ color: #FFF;
86
+ margin-right: 0.5rem;
87
+ font-size: 1rem;
88
+ }
89
+
90
+ header select option {
91
+ color: #222;
92
+ background: #FFF;
93
+ }
94
+
95
+ aside {
96
+ grid-row: 2;
97
+ grid-column: 1;
98
+ padding: 1.5rem 1rem 1rem 1rem;
99
+ }
100
+
101
+ aside>div {
102
+ font-weight: bold;
103
+ }
104
+
105
+ aside>ul {
106
+ list-style: none;
107
+ padding: 0 0 0 0.5rem;
108
+ line-height: 1.5rem;
109
+ margin: 0.25rem 0 0.5rem 0;
110
+ }
111
+
112
+ aside a {
113
+ position: relative;
114
+ color: #414F56;
115
+ text-decoration: none;
116
+ }
117
+
118
+ aside a:hover {
119
+ text-decoration: underline;
120
+ }
121
+
122
+ main {
123
+ grid-row: 2;
124
+ grid-column: 2;
125
+ padding: 0 1rem 1rem 1rem;
126
+ }
127
+
128
+ main>p {
129
+ line-height: 1.6rem;
130
+ }
131
+
132
+ main>section {
133
+ padding: 1.5rem 1rem 1rem 1rem;
134
+ border-radius: 0.5rem;
135
+ border: 1px solid #ccc;
136
+ margin-top: 2rem;
137
+ }
138
+
139
+ main>h2 {
140
+ position: relative;
141
+ margin: 1.25rem 0 0.5rem 0;
142
+ border-bottom: 1px solid #ddd;
143
+ padding-bottom: 0.5rem;
144
+ font-size: 1.5rem;
145
+ font-weight: normal;
146
+ }
147
+
148
+ main>h2.open {
149
+ border-bottom: none;
150
+ }
151
+
152
+ main>h2>a {
153
+ text-decoration: none;
154
+ color: #aaa;
155
+ margin: 0 0 0 0.5rem;
156
+ }
157
+
158
+ main>h2>a:hover {
159
+ color: #453C4F;
160
+ }
161
+
162
+ main>h2>a>svg {
163
+ width: 24px;
164
+ height: 24px;
165
+ vertical-align: middle;
166
+ margin-top: -2px;
167
+ }
168
+
169
+ main>h2>button {
170
+ margin: 0;
171
+ border-radius: 0.25rem 0.25rem 0 0;
172
+ border: 1px solid #ccc;
173
+ background: #fff;
174
+ padding: 0.25rem 0.5rem;
175
+ position: absolute;
176
+ bottom: -1px;
177
+ right: 0.5rem;
178
+ color: #453C4F;
179
+ outline: none;
180
+ }
181
+
182
+ main>h2>button.active,
183
+ main>h2>button:focus,
184
+ main>h2>button:hover {
185
+ background: #453C4F;
186
+ border-top-color: #453C4F;
187
+ border-right-color: #453C4F;
188
+ border-bottom-color: transparent;
189
+ border-left-color: #453C4F;
190
+ color: #fff;
191
+ }
192
+
193
+ main>h2>button:focus::before {
194
+ pointer-events: none;
195
+ content: '';
196
+ position: absolute;
197
+ top: -1px;
198
+ right: -1px;
199
+ bottom: -1px;
200
+ left: -1px;
201
+ border-radius: 0.25rem 0.25rem 0 0;
202
+ box-shadow: 0 0 0 3px var(--pg-focus-color, rgb(79, 143, 249, 0.6));
203
+ }
204
+
205
+ main>h2>button>svg {
206
+ width: 24px;
207
+ height: 24px;
208
+ vertical-align: middle;
209
+ }
210
+
211
+ main>h2>button+span.line {
212
+ display: none;
213
+ position: absolute;
214
+ left: 0;
215
+ right: 0;
216
+ bottom: -0.25rem;
217
+ height: 0.25rem;
218
+ background: transparent;
219
+ transition: background-color linear 0.2s;
220
+ }
221
+
222
+ main>h2>button:not(.active):focus+span.line,
223
+ main>h2>button:not(.active):hover+span.line {
224
+ display: block;
225
+ background: #453C4F;
226
+ }
227
+
228
+ main>h2>span.label {
229
+ padding: 0.75rem 0.5rem 0.5rem 0.25rem;
230
+ font-size: 1rem;
231
+ display: inline;
232
+ color: #aaa;
233
+ position: absolute;
234
+ right: 4rem;
235
+ }
236
+
237
+ main>div.markdown {
238
+ display: none;
239
+ }
240
+
241
+ main>div.markdown.show {
242
+ display: block;
243
+ border: 0.25rem solid #453C4F;
244
+ margin-top: -0.5rem;
245
+ padding: 0 1rem;
246
+ border-radius: 0.5rem;
247
+ }
248
+
249
+ main>section>h3 {
250
+ font-size: 1.25rem;
251
+ font-weight: normal;
252
+ padding: 0;
253
+ display: inline-block;
254
+ position: absolute;
255
+ margin: -2.5rem 0 0 1rem;
256
+ }
257
+
258
+ main>section>h3::before {
259
+ content: ' ';
260
+ background: #fff;
261
+ left: -1rem;
262
+ right: -1rem;
263
+ position: absolute;
264
+ border-radius: 0 0 0.5rem 0.5rem;
265
+ margin: 0;
266
+ height: 1rem;
267
+ border-bottom: 1px solid #ccc;
268
+ border-left: 1px solid #ccc;
269
+ border-right: 1px solid #ccc;
270
+ top: calc(1rem - 1px);
271
+ }
272
+
273
+ main>section>h3>span {
274
+ position: relative;
275
+ }
276
+
277
+ h1 small,
278
+ h2 small,
279
+ h3 small {
280
+ color: #999;
281
+ font-weight: normal;
282
+ font-size: 0.875rem;
283
+ }
284
+
285
+ .example {
286
+ vertical-align: top;
287
+ background: #F1F1F1;
288
+ border-radius: 0.25rem;
289
+ border: 1px dashed #222;
290
+ padding: 0.5rem;
291
+ margin-bottom: 0.5rem;
292
+ margin-right: 1rem;
293
+ box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.1) inset;
294
+ }
295
+
296
+ .example-flex {
297
+ display: flex;
298
+ }
299
+
300
+ body>button {
301
+ border: 1px solid #DDD;
302
+ background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
303
+ color: #24292e;
304
+ border: 1px solid rgba(27, 31, 35, .2);
305
+ border-radius: .25em;
306
+ padding: 0.25rem 0.4rem 0.2rem 0.4rem;
307
+ }
308
+
309
+ body>button:hover {
310
+ background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
311
+ background-position: -.5em;
312
+ border-color: rgba(27, 31, 35, .35);
313
+ }
314
+
315
+ #search {
316
+ font-family: system-ui;
317
+ padding: 0.25rem 0.5rem 0.25rem 2.0rem;
318
+ border: 2px solid #453C4F;
319
+ border-radius: 0.25rem;
320
+ background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" fill="rgb(69 60 79)" /></svg>');
321
+ background-repeat: no-repeat;
322
+ background-position: 0.25rem 50%;
323
+ background-size: 1.5rem;
324
+ margin-bottom: 0.5rem;
325
+ width: calc(100% - 2.5rem);
326
+ }
327
+
328
+ #search:focus {
329
+ box-shadow: 0 0 0 3px rgb(79, 143, 249, 0.5);
330
+ }
331
+
332
+ .hide {
333
+ display: none;
334
+ }
335
+ </style>
336
+ <script type="module" src="main.js" defer></script>
337
+ </head>
338
+
339
+ <body>
340
+ <template id="templateInfo">
341
+ <p>This page is generated from <code>npm start</code>. To render only specific components use
342
+ <code>npm start c-button</code>.
343
+ </p>
344
+ </template>
345
+ <template id="templateComponent">
346
+ <h2>
347
+ <span data-title></span>
348
+ <a data-title title="" href="">
349
+ <svg viewBox="0 0 24 24">
350
+ <path fill="currentColor"
351
+ d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z">
352
+ </svg>
353
+ </a>
354
+ <a data-repo title="View Code" href="" target="_blank">
355
+ <svg viewBox="0 0 24 24">
356
+ <path fill="currentColor"
357
+ d="M14 2H6C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2M18 20H6V4H13V9H18V20M9.54 15.65L11.63 17.74L10.35 19L7 15.65L10.35 12.3L11.63 13.56L9.54 15.65M17 15.65L13.65 19L12.38 17.74L14.47 15.65L12.38 13.56L13.65 12.3L17 15.65Z">
358
+ </svg>
359
+ </a>
360
+ <span data-extends class="label"></span>
361
+ <button data-readme title="Expand Documentation">
362
+ <svg viewBox="0 0 24 24">
363
+ <path fill="currentColor"
364
+ d="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z">
365
+ </svg>
366
+ </button>
367
+ <span class="line"></span>
368
+ </h2>
369
+ <div data-container class="markdown"></div>
370
+ </template>
371
+ <template id="templateExample">
372
+ <section>
373
+ <h3><span></span></h3>
374
+ </section>
375
+ </template>
376
+ <script>
377
+ const repo = '';
378
+ const repoIcon = '';
379
+ const repoComponent = '';
380
+ // Contains all component meta data
381
+ // [MyComponent, {
382
+ // name: 'MyComponent',
383
+ // tag: 'my-component',
384
+ // examples: [{
385
+ // name: 'XMyComponentBasic',
386
+ // tag: 'x-my-component-basic',
387
+ // label: 'Basic',
388
+ // }]
389
+ // }]
390
+ const componentMap = new Map();
391
+ // Navigation
392
+ const navigation = [];
393
+ // Stuff
394
+ function kebabToPascalRegex(kebabString) {
395
+ return kebabString.replace(/(^|\W+)(\w)/g, (match, sep, char) => {
396
+ return char.toUpperCase();
397
+ });
398
+ }
399
+ const markdownCache = {};
400
+ function createComponent({ tag, className, namespace, component, examples, classExtends, readme }) {
401
+ const template = document.getElementById('templateComponent');
402
+ const $template = document.importNode(template.content, true);
403
+ $template.querySelector('h2>span[data-title]').textContent = tag;
404
+ $template.querySelector('h2>a[data-title]').title = `Link to ${tag}`;
405
+ $template.querySelector('h2>a[data-title]').href = `#${tag}`;
406
+ $template.querySelector('h2>a[data-title]').id = tag;
407
+ $template.querySelector('h2>span[data-extends]').textContent = classExtends;
408
+ const normalizeRepoComponent = repoComponent.replace(/\$namespace/, namespace)
409
+ .replace(/\$component/, component);
410
+ $template.querySelector('h2>a[data-repo]').href = normalizeRepoComponent;
411
+ const h2 = $template.querySelector('h2');
412
+ const button = $template.querySelector('button[data-readme]');
413
+ const container = $template.querySelector('div[data-container]');
414
+ button.addEventListener('click', () => {
415
+ // Create
416
+ if (!(className in markdownCache)) {
417
+ // todo: embed always!!!
418
+ var element = document.createElement('pg-markdown');
419
+ container.appendChild(element);
420
+ element.text = readme ? readme.replace(/#[^\r\n]+/, '') : 'Missing README.md';
421
+ markdownCache[className] = false;
422
+ }
423
+ // Flip
424
+ markdownCache[className] = !markdownCache[className];
425
+ h2.classList.toggle('open', markdownCache[className]);
426
+ button.classList.toggle('active', markdownCache[className]);
427
+ button.title = `${markdownCache[className] ? 'Collapse' : 'Expand'} Documentation`;
428
+ container.classList.toggle('show', markdownCache[className]);
429
+ });
430
+ return $template;
431
+ }
432
+ function createExample({ tag, example }) {
433
+ const template = document.getElementById('templateExample');
434
+ const $template = document.importNode(template.content, true);
435
+ $template.querySelector('h3>span').textContent = example;
436
+ const $example = document.createElement(tag);
437
+ $template.querySelector('section').appendChild($example);
438
+ return $template;
439
+ }
440
+ function init() {
441
+ // elements and templates
442
+ const $main = document.querySelector('main');
443
+ const templateInfo = document.getElementById('templateInfo');
444
+ const $templateInfo = document.importNode(templateInfo.content, true);
445
+ // Clear main
446
+ while ($main.firstChild) {
447
+ $main.removeChild($main.firstChild);
448
+ }
449
+ // If linking to a single component render 1 component
450
+ const hash = window.location.hash;
451
+ const className = kebabToPascalRegex(hash);
452
+ if (hash && componentMap.has(className)) {
453
+ const componentMeta = componentMap.get(className);
454
+ $main.appendChild(createComponent(componentMeta));
455
+ componentMeta.examples.forEach((exampleClassName) => {
456
+ const exampleComponentMeta = componentMap.get(exampleClassName);
457
+ $main.appendChild(createExample(exampleComponentMeta));
458
+ });
459
+ return;
460
+ }
461
+ // Render dev tool info
462
+ $main.appendChild($templateInfo);
463
+ // Render all components
464
+ navigation.forEach(({ items }) => {
465
+ items.forEach(({ className }) => {
466
+ const componentMeta = componentMap.get(className);
467
+ $main.appendChild(createComponent(componentMeta));
468
+ componentMeta.examples.forEach((exampleClassName) => {
469
+ const exampleComponentMeta = componentMap.get(exampleClassName);
470
+ $main.appendChild(createExample(exampleComponentMeta));
471
+ });
472
+ });
473
+ });
474
+ }
475
+ function debounce(func, wait) {
476
+ let timeout; // This variable is maintained using closure
477
+
478
+ return function (...args) {
479
+ const context = this;
480
+ // Clear the previous timeout if the function is called again
481
+ clearTimeout(timeout);
482
+ // Set a new timeout
483
+ timeout = setTimeout(() => {
484
+ func.apply(context, args);
485
+ }, wait);
486
+ };
487
+ }
488
+ window.addEventListener('load', () => {
489
+ init();
490
+ window.addEventListener('hashchange', function() {
491
+ init();
492
+ });
493
+ const listItems = [...document.querySelectorAll('aside li')];
494
+ const links = [...document.querySelectorAll('aside a')];
495
+ links.forEach((link) => {
496
+ link.addEventListener('click', (e) => {
497
+ window.scrollTo({
498
+ top: 0,
499
+ behavior: 'instant',
500
+ });
501
+ });
502
+ });
503
+ /*
504
+ const themeSelect = document.getElementById('theme');
505
+ function setTheme(value) {
506
+ localStorage.setItem('theme', value);
507
+ if (value !== '') {
508
+ const s = document.createElement('link');
509
+ s.setAttribute('rel', 'stylesheet');
510
+ s.setAttribute('type', 'text/css');
511
+ s.setAttribute('href', './theme-ui3.css');
512
+ document.head.appendChild(s);
513
+ }
514
+ }
515
+ themeSelect.addEventListener('change', (e) => {
516
+ const value = e.target.value;
517
+ setTheme(value);
518
+ window.location.reload();
519
+ });
520
+ const themeCache = localStorage.getItem('theme');
521
+ if (themeCache !== null) {
522
+ setTheme(themeCache);
523
+ themeSelect.value = themeCache;
524
+ }*/
525
+ const debounceSearch = debounce((e) => {
526
+ const query = e.target.value.toLowerCase();
527
+ // Hide and show non-matches
528
+ listItems.forEach((item) => {
529
+ if (query === '') {
530
+ item.classList.toggle('hide', false);
531
+ return;
532
+ }
533
+ const { className, tag, component } = item.dataset;
534
+ let visible = false;
535
+ if (className.toLowerCase().includes(query)) {
536
+ visible = true;
537
+ }
538
+ if (tag.toLowerCase().includes(query)) {
539
+ visible = true;
540
+ }
541
+ if (component.toLowerCase().includes(query)) {
542
+ visible = true;
543
+ }
544
+ item.classList.toggle('hide', !visible);
545
+ });
546
+ }, 300);
547
+ document.getElementById('search').addEventListener('input', debounceSearch);
548
+ document.getElementById('search').focus();
549
+ });
550
+ </script>
551
+ <div class="wrapper">
552
+ <header>
553
+ <svg viewBox="0 0 64 64">
554
+ <path fill="currentColor"
555
+ d="M 8,0L 56,0C 60.4183,0 64,3.58173 64,8L 64,56C 64,60.4183 60.4183,64 56,64L 8,64C 3.58172,64 0,60.4183 0,56L 0,8C 0,3.58173 3.58172,0 8,0 Z M 8,44.0001L 14,44.0001L 14,32.0001L 20,32.0001C 26.6274,32.0001 32,26.6275 32,20.0001C 32,13.3726 26.6274,8.00006 20,8.00006L 8,8.00003L 8,44.0001 Z M 20,26.0003L 14,26.0003L 14,14.0003L 20,14.0003C 23.3137,14.0003 26,16.6866 26,20.0003C 26,23.314 23.3137,26.0003 20,26.0003 Z M 44,44.0001L 45.9999,44.0003C 48.2091,44.0003 49.9999,42.2094 49.9999,40.0003L 50,44.0001C 50,49.5231 45.5228,54.0002 40,54.0002C 36.085,54.0002 32.6955,51.7505 31.0537,48.4732L 25.686,51.1571C 28.3128,56.4006 33.736,60.0002 40,60.0002C 48.8365,60.0002 56,52.8368 56,44.0001L 56,24.0001C 56,21.791 54.2092,20.0001 52,20.0001L 44,20.0001C 37.3726,20.0001 32,25.3727 32,32.0001C 32,38.6275 37.3726,44.0001 44,44.0001 Z M 50,26L 49.9999,38.0001L 44,38C 40.6863,38 38,35.3137 38,32C 38,28.6863 40.6863,26 44,26L 50,26 Z" />
556
+ </svg>
557
+ <h1>Default</h1>
558
+ <span></span>
559
+ <!--<label>
560
+ <select id="theme">
561
+ <option value="">None</option>
562
+ <option value="ui3">UI3</option>
563
+ </select>
564
+ </label>-->
565
+ <!-- [Repo] -->
566
+ </header>
567
+ <aside>
568
+ <input type="text" id="search" autocomplete="off" autocorrect="off" />
569
+ <!-- [Navigation] -->
570
+ </aside>
571
+ <main></main>
572
+ </div>
573
+ </body>
574
+
575
+ </html>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2
+ <rect id="square" x="2" y="2" width="60" height="60" rx="6" fill-opacity="0.9"/>
3
+ <path id="main" d="M 8,0L 56,0C 60.4183,0 64,3.58173 64,8L 64,56C 64,60.4183 60.4183,64 56,64L 8,64C 3.58172,64 0,60.4183 0,56L 0,8C 0,3.58173 3.58172,0 8,0 Z M 12,12.0001L 12,52.0001L 32,52.0002L 32,44.0003L 20,44.0001L 20,36.0001L 32,36.0002L 32,28.0003L 20,28.0001L 20,20.0001L 32,20.0002L 32,12.0003L 12,12.0001 Z M 40,12.0001L 40,52.0001L 48,52.0001L 48,12.0001L 40,12.0001 Z "/>
4
+ </svg>
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@pictogrammers/element-esbuild",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Element esbuild",
6
+ "homepage": "https://github.com/Pictogrammers/Element-esbuild#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/Pictogrammers/Element-esbuild/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Pictogrammers/Element-esbuild.git"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Austin Andrews",
16
+ "main": "index.ts",
17
+ "bin": {
18
+ "element-start": "./bin/element-start.ts",
19
+ "element-build": "./bin/element-build.ts",
20
+ "element-publish": "./bin/element-publish.ts"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.0.3",
24
+ "esbuild": "0.27.2",
25
+ "esbuild-html-plugin": "^1.1.0",
26
+ "tsx": "^4.21.0",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "dependencies": {
30
+ "chokidar": "^5.0.0"
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ export function camelToDash(str: string) {
2
+ return str.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
3
+ }
@@ -0,0 +1,6 @@
1
+ export function capitalizeFirstCharacter(str: string): string {
2
+ if (!str) {
3
+ return str; // Handle empty or null strings
4
+ }
5
+ return str.charAt(0).toUpperCase() + str.slice(1);
6
+ }
@@ -0,0 +1,3 @@
1
+ export function dashToCamel(str: string) {
2
+ return str.replace(/-([a-z])/g, m => m[1].toUpperCase());
3
+ }
@@ -0,0 +1,11 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+
4
+ export async function fileExists(filePath: string): Promise<boolean> {
5
+ try {
6
+ await access(filePath, constants.F_OK);
7
+ return true;
8
+ } catch (error) {
9
+ return false;
10
+ }
11
+ }
@@ -0,0 +1,14 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+
4
+ export async function folderExists(folderPath: string): Promise<boolean> {
5
+ try {
6
+ // Check if the path exists (F_OK) and Node.js can access it
7
+ await access(folderPath, constants.F_OK);
8
+ // If no error is thrown, the folder exists
9
+ return true;
10
+ } catch (error) {
11
+ // If an error occurs, it generally means the folder does not exist or permissions are insufficient
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ import {readdir} from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ export async function getDirectories(dirPath: string) {
5
+ const directories: string[] = [];
6
+ try {
7
+ // Read the directory and get dirent objects (which include file type information)
8
+ const entries = await readdir(dirPath, { withFileTypes: true });
9
+ // Loop through each entry
10
+ for (const entry of entries) {
11
+ // Check if the entry is a directory
12
+ if (entry.isDirectory()) {
13
+ directories.push(entry.name);
14
+ }
15
+ }
16
+ } catch (err) {
17
+ console.error('Error reading directory:', err);
18
+ }
19
+ return directories;
20
+ }
@@ -0,0 +1,45 @@
1
+ import { sep } from 'node:path';
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { dashToCamel } from "./dashToCamel.ts";
5
+
6
+ const root = process.cwd();
7
+ const rootDepth = root.split(sep).length;
8
+
9
+ export const htmlDependentsPlugin = {
10
+ name: 'html-dependents-plugin',
11
+ setup(build: any) {
12
+ // Intercept files with the .html extension
13
+ build.onLoad({ filter: /\.html$/ }, async (args: any) => {
14
+ const parts = args.path.split(sep).splice(rootDepth);
15
+ const [src, componentsDir, currentNamspace, currentComponent, ...file] = parts;
16
+ // Read the file contents as text
17
+ const contents = await readFile(args.path, 'utf8');
18
+ const matches = contents.matchAll(/<\/(?<namespace>\w+)-(?<value>[^>]+)/g);
19
+ const components = new Map<string, string[]>();
20
+ for (const match of matches) {
21
+ const { namespace, value } = match.groups as any;
22
+ const component = dashToCamel(value);
23
+ components.set(`${namespace}-${component}`, [namespace, component]);
24
+ }
25
+ const imports: string[] = [];
26
+ components.forEach(([namespace, component]) => {
27
+ const depth = parts.length - 4;
28
+ const backPaths = (new Array(depth)).fill('..');
29
+ if (namespace === currentNamspace) {
30
+ if (component === currentComponent) {
31
+ return;
32
+ }
33
+ imports.push(`import './${backPaths.join('/')}/${component}/${component}';`);
34
+ } else {
35
+ imports.push(`import './${backPaths.join('/')}/${namespace}/${component}/${component}';`);
36
+ }
37
+ });
38
+ imports.push(`export default \`${contents}\`;`);
39
+ return {
40
+ contents: imports.join('\n'),
41
+ loader: 'js',
42
+ };
43
+ });
44
+ },
45
+ };
@@ -0,0 +1,107 @@
1
+ import { join, sep, dirname } from 'node:path';
2
+
3
+ import { getDirectories } from './getDirectories.ts';
4
+ import { fileExists } from './fileExists.ts';
5
+ import { folderExists } from './folderExists.ts';
6
+ import { readFile, writeFile } from 'node:fs/promises';
7
+ import { capitalizeFirstCharacter } from './capitalizeFirstChracter.ts';
8
+ import { camelToDash } from './camelToDash.ts';
9
+
10
+ const red = (text: string) => `\x1b[31m${text}\x1b[0m`;
11
+
12
+ const rootDir = process.cwd();
13
+ const distDir = 'dist';
14
+ const srcDir = 'src';
15
+ const componentsDir = 'components';
16
+
17
+ export function playgroundPlugin(options: any) {
18
+ return {
19
+ name: 'playground-plugin',
20
+ setup(build: any) {
21
+ const entryPointName = 'playground-entry';
22
+ const virtualNamespace = 'playground-module'; // Use a custom namespace
23
+
24
+ // 1. Intercept the entry point resolution
25
+ build.onResolve({ filter: new RegExp(`^${entryPointName}$`) }, (args: any) => {
26
+ // Return a path with the custom namespace
27
+ return {
28
+ path: entryPointName,
29
+ namespace: virtualNamespace,
30
+ };
31
+ });
32
+
33
+ // 2. Load the virtual module content
34
+ build.onLoad({ filter: /.*/, namespace: virtualNamespace }, async (args: any) => {
35
+ const entryPoints: string[] = [];
36
+ const namespaces = await getDirectories(join(srcDir, componentsDir));
37
+ if (namespaces.length === 0) {
38
+ console.log(red('Missing at least 1 namespace folder under "src/components/"'));
39
+ process.exit();
40
+ }
41
+ const meta = [];
42
+ for (let namespace of namespaces) {
43
+ const metaNamespace = {
44
+ namespace,
45
+ components: [],
46
+ } as any;
47
+ meta.push(metaNamespace);
48
+ const namespaceDir = join(srcDir, componentsDir, namespace);
49
+ const components = await getDirectories(namespaceDir);
50
+ for (let component of components) {
51
+ if (await fileExists(join(srcDir, componentsDir, namespace, component, `${component}.ts`))) {
52
+ let readme = '';
53
+ if (await fileExists(join(srcDir, componentsDir, namespace, component, 'README.md'))) {
54
+ // todo: memory?
55
+ readme = await readFile(join(srcDir, componentsDir, namespace, component, 'README.md'), 'utf8');
56
+ }
57
+ const metaComponent = {
58
+ namespace,
59
+ component,
60
+ tag: `${namespace}-${camelToDash(component)}`,
61
+ examples: [] as any[],
62
+ className: '',
63
+ classExtends: '',
64
+ readme,
65
+ };
66
+ entryPoints.push(`import '${componentsDir}/${namespace}/${component}/${component}';`);
67
+ if (await folderExists(join(namespaceDir, component, '__examples__'))) {
68
+ const examples = await getDirectories(join(namespaceDir, component, '__examples__'));
69
+ for (let example of examples) {
70
+ if (await fileExists(join(namespaceDir, component, '__examples__', example, `${example}.ts`))) {
71
+ entryPoints.push(`import '${componentsDir}/${namespace}/${component}/__examples__/${example}/${example}';`);
72
+ metaComponent.examples.push({
73
+ namespace,
74
+ component,
75
+ example,
76
+ tag: `x-${namespace}-${camelToDash(component)}-${camelToDash(example)}`,
77
+ className: `X${capitalizeFirstCharacter(namespace)}${capitalizeFirstCharacter(component)}${capitalizeFirstCharacter(example)}`,
78
+ });
79
+ } else {
80
+ console.log(red(`Missing ${componentsDir}/${namespace}/${component}/__examples__/${example}/${example}.ts`));
81
+ }
82
+ }
83
+ }
84
+ // ToDo: Lazy, probably way better ways to do this in ESBuild!!!
85
+ const data = await readFile(join(srcDir, componentsDir, namespace, component, `${component}.ts`), 'utf8');
86
+ const matches = data.match(/class (\w+) extends (\w+)/);
87
+ if (!matches) {
88
+ console.log(red(`Component "${namespace}-${component}" must extend HtmlElement or base class`));
89
+ process.exit();
90
+ }
91
+ metaComponent.className = matches[1];
92
+ metaComponent.classExtends = matches[2];
93
+ metaNamespace.components.push(metaComponent);
94
+ }
95
+ }
96
+ }
97
+ // Update index.html in dist
98
+ options.after(meta);
99
+ // Define your virtual file content here
100
+ return {
101
+ contents: entryPoints.join('\n'),
102
+ resolveDir: join(process.cwd(), srcDir),
103
+ };
104
+ });
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,15 @@
1
+ const green = (text: string) => `\x1b[32m${text}\x1b[0m`;
2
+
3
+ export const rebuildNotifyPlugin = {
4
+ name: 'rebuild-notify',
5
+ setup(build: any) {
6
+ build.onEnd((result: any) => {
7
+ if (result.errors.length > 0) {
8
+ console.error(`Build ended with ${result.errors.length} errors`);
9
+ } else {
10
+ console.log(green('Build succeeded!'));
11
+ }
12
+ // You can add logic here to restart a server, send a signal, etc.
13
+ });
14
+ },
15
+ };