@elenajs/bundler 0.9.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/img/elena-dark.png" alt="Elena" width="558" height="220">
4
+ </source>
5
+ <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
6
+ </source>
7
+ <img src="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
8
+ </picture>
9
+
10
+ ### Bundler for Progressive Web Component libraries built with Elena.
11
+
12
+ <br/>
13
+
14
+ <a href="https://arielsalminen.com"><img src="https://img.shields.io/badge/creator-@arielle-F95B1F" alt="Creator @arielle"/></a>
15
+ <a href="https://www.npmjs.com/package/@elenajs/bundler"><img src="https://img.shields.io/npm/v/@elenajs/bundler.svg" alt="Latest version on npm" /></a>
16
+ <a href="https://github.com/getelena/elena/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="Elena is released under the MIT license." /></a>
17
+ <a href="https://github.com/getelena/elena/actions/workflows/tests.yml"><img src="https://github.com/getelena/elena/actions/workflows/tests.yml/badge.svg" alt="Tests status" /></a>
18
+
19
+ </div>
20
+
21
+ <br/>
22
+
23
+ <p align="center"><strong>@elenajs/bundler</strong> is the build tool for <a href="https://elenajs.com">Elena</a> Progressive Web Component libraries. It bundles JavaScript and TypeScript source files, minifies CSS, generates a <a href="https://custom-elements-manifest.open-wc.org/">Custom Elements Manifest</a>, and produces TypeScript declarations for Elena components.</p>
24
+
25
+ <br/>
26
+
27
+ ## Table of contents
28
+
29
+ - **[Install](#install)**
30
+ - **[CLI usage](#cli-usage)**
31
+ - **[Configuration](#configuration)**
32
+ - **[Options](#options)**
33
+ - **[Build output](#build-output)**
34
+ - **[Programmatic API](#programmatic-api)**
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install --save-dev @elenajs/bundler
40
+ ```
41
+
42
+ ## CLI usage
43
+
44
+ The bundler provides an `elena` CLI binary with `build` and `watch` commands:
45
+
46
+ ```bash
47
+ npx elena build
48
+ ```
49
+
50
+ If no command is provided, `build` is assumed:
51
+
52
+ ```bash
53
+ npx elena
54
+ ```
55
+
56
+ To start a watch session that rebuilds on file changes:
57
+
58
+ ```bash
59
+ npx elena watch
60
+ ```
61
+
62
+ ### Flags
63
+
64
+ | Flag | Description |
65
+ | ------------------ | ------------------------------------------------------------------------------------------- |
66
+ | `--config <path>` | Path to a config file. Defaults to `elena.config.mjs` or `elena.config.js` in the project root. |
67
+
68
+ Example:
69
+
70
+ ```bash
71
+ npx elena build --config config/elena.config.mjs
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ Create an `elena.config.mjs` (or `elena.config.js`) at the root of your package:
77
+
78
+ ```js
79
+ /**
80
+ * ░ [ELENA]: Bundler configuration
81
+ *
82
+ * @type {import("@elenajs/bundler").ElenaConfig}
83
+ */
84
+ export default {
85
+ // Source directory scanned for .js/.ts entry files and .css files.
86
+ input: "src",
87
+
88
+ // Rollup output options.
89
+ output: {
90
+ dir: "dist",
91
+ format: "esm",
92
+ sourcemap: true,
93
+ },
94
+
95
+ // Entry for the single-file bundle. Set to false to disable.
96
+ bundle: "src/index.js",
97
+
98
+ // Additional Rollup plugins appended after Elena's built-in set.
99
+ // plugins: [],
100
+
101
+ // Custom Elements Manifest options. Set to false to skip entirely.
102
+ // analyze: {
103
+ // plugins: [],
104
+ // },
105
+
106
+ // Browserslist targets for transpilation. Enables syntax transforms
107
+ // (e.g. class fields, optional chaining) to widen browser support.
108
+ // target: ["chrome 71", "firefox 69", "safari 12.1"],
109
+
110
+ // Custom Terser minifier options, merged with the defaults.
111
+ // terser: { ecma: 2020, module: true },
112
+ };
113
+ ```
114
+
115
+ ### Options
116
+
117
+ | Option | Type | Default | Description |
118
+ | ------------------ | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
119
+ | `input` | `string` | `"src"` | Source directory to scan for `.js`, `.ts`, and `.css` files. |
120
+ | `output.dir` | `string` | `"dist"` | Output directory for compiled files. |
121
+ | `output.format` | `string` | `"esm"` | Rollup output format. |
122
+ | `output.sourcemap` | `boolean` | `true` | Whether to emit sourcemaps. |
123
+ | `bundle` | `string \| false` | `"src/index.js"` | Entry point for the single-file bundle. Auto-detects `src/index.ts` if no `.js` entry exists. Set to `false` to disable. |
124
+ | `plugins` | `Plugin[]` | `[]` | Additional Rollup plugins appended after the built-in set. |
125
+ | `analyze` | `object \| false` | `{ plugins: [] }` | CEM analysis options. Set to `false` to skip Custom Elements Manifest generation, TypeScript declarations, and JSX types entirely. |
126
+ | `analyze.plugins` | `Plugin[]` | `[]` | Additional CEM analyzer plugins. |
127
+ | `target` | `string \| string[] \| false` | `false` | Browserslist target(s) for transpilation. When set, enables syntax transforms (e.g. class fields, optional chaining) via `@babel/preset-env` to widen browser support. Example: `["chrome 71", "firefox 69", "safari 12.1"]`. |
128
+ | `terser` | `object` | `{ ecma: 2020, module: true }` | Custom Terser minifier options, merged with the defaults. See the [Terser API docs](https://terser.org/docs/api-reference/) for available options. |
129
+
130
+ ## Build output
131
+
132
+ Running `elena build` produces:
133
+
134
+ | File | Description |
135
+ | --------------------------- | ---------------------------------------------------------------------------------------------------- |
136
+ | `dist/*.js` | Individual ES modules for each source file. |
137
+ | `dist/*.css` | Minified individual CSS files. |
138
+ | `dist/bundle.js` | Single-file JavaScript bundle _(optional)_. |
139
+ | `dist/bundle.css` | Concatenated and minified CSS bundle. CSS files imported as CSS Module Scripts (`with { type: "css" }`) for Shadow DOM are excluded. |
140
+ | `dist/custom-elements.json` | [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) describing all components. |
141
+ | `dist/custom-elements.d.ts` | JSX integration types mapping tag names to prop types. |
142
+ | `dist/*.d.ts` | Per-component TypeScript declarations with typed props and events. |
143
+
144
+ > **Note:** CSS files that are imported as CSS Module Scripts for Shadow DOM use (`import styles from "./button.css" with { type: "css" }`) are inlined as `CSSStyleSheet` objects in the JavaScript output and excluded from `bundle.css`. Individual `.css` files are still emitted.
145
+
146
+ ## TypeScript support
147
+
148
+ The bundler supports both JavaScript and TypeScript source files. When `.ts` files are detected in the source directory, the bundler automatically transpiles them to JavaScript using `@rollup/plugin-typescript`. The output is identical to what you get from JavaScript sources.
149
+
150
+ To use TypeScript, write your components with inline type annotations instead of JSDoc:
151
+
152
+ ```ts
153
+ import { Elena, html } from "@elenajs/core";
154
+
155
+ export default class Button extends Elena(HTMLElement) {
156
+ static tagName = "elena-button";
157
+ static props = ["variant"];
158
+
159
+ /**
160
+ * The style variant of the component.
161
+ * @attribute
162
+ */
163
+ variant: "default" | "primary" | "danger" = "default";
164
+
165
+ render() {
166
+ return html`<button>${this.text}</button>`;
167
+ }
168
+ }
169
+ Button.define();
170
+ ```
171
+
172
+ A `tsconfig.json` is required in the project root. A minimal configuration:
173
+
174
+ ```json
175
+ {
176
+ "compilerOptions": {
177
+ "target": "ES2020",
178
+ "module": "ESNext",
179
+ "moduleResolution": "bundler",
180
+ "skipLibCheck": true
181
+ },
182
+ "include": ["src"]
183
+ }
184
+ ```
185
+
186
+ > **Note:** The bundler handles TypeScript declarations separately via the CEM analyzer, you do not need `declaration: true` in your `tsconfig.json`.
187
+
188
+ ## Programmatic API
189
+
190
+ The bundler exports its internals so you can integrate it into your own build scripts:
191
+
192
+ ```js
193
+ import {
194
+ createRollupConfig,
195
+ runRollupBuild,
196
+ watchRollupBuild,
197
+ createCemConfig,
198
+ runCemAnalyze,
199
+ } from "@elenajs/bundler";
200
+ ```
201
+
202
+ Sub-path imports are also available:
203
+
204
+ ```js
205
+ import { createRollupConfig, runRollupBuild, watchRollupBuild } from "@elenajs/bundler/rollup";
206
+ import { createCemConfig, runCemAnalyze } from "@elenajs/bundler/cem";
207
+ ```
208
+
209
+ ### `createRollupConfig(options?)`
210
+
211
+ Returns a Rollup configuration array. Useful if you want to wrap or extend the config in a custom `rollup.config.js`.
212
+
213
+ ### `runRollupBuild(config)`
214
+
215
+ Runs both build phases (individual modules + optional single-file bundle) programmatically.
216
+
217
+ ### `watchRollupBuild(config, opts?)`
218
+
219
+ Starts a Rollup watch session that rebuilds on file changes. Returns the Rollup watcher instance. Pass `opts.onRebuild` as an async callback to run after each successful rebuild (e.g. to re-run CEM analysis).
220
+
221
+ ### `createCemConfig(options?)`
222
+
223
+ Returns the Custom Elements Manifest analyzer configuration object.
224
+
225
+ ### `runCemAnalyze(config, cwd?)`
226
+
227
+ Runs the CEM analysis and writes `custom-elements.json`, `custom-elements.d.ts`, and per-component `.d.ts` files.
228
+
229
+ ## License
230
+
231
+ MIT
232
+
233
+ ## Copyright
234
+
235
+ Copyright © 2026 [Ariel Salminen](https://arielsalminen.com)
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@elenajs/bundler",
3
- "version": "0.9.0",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Bundler for Progressive Web Component libraries built with Elena.",
5
5
  "author": "Elena <hi@elenajs.com>",
6
6
  "homepage": "https://elenajs.com/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/getelena/elena.git",
10
+ "directory": "packages/bundler"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/getelena/elena/issues"
14
+ },
7
15
  "license": "MIT",
8
16
  "publishConfig": {
9
17
  "access": "public"
@@ -27,24 +35,27 @@
27
35
  "test": "vitest run"
28
36
  },
29
37
  "dependencies": {
38
+ "@babel/core": "7.29.0",
39
+ "@babel/preset-env": "7.29.0",
30
40
  "@custom-elements-manifest/analyzer": "0.11.0",
31
- "@elenajs/plugin-cem-define": "^0.4.0",
32
- "@elenajs/plugin-cem-tag": "^0.4.0",
33
- "@elenajs/plugin-cem-typescript": "^0.5.0",
34
- "@elenajs/plugin-rollup-css": "^0.6.0",
41
+ "@elenajs/plugin-cem-define": "^1.0.0-beta.2",
42
+ "@elenajs/plugin-cem-tag": "^1.0.0-beta.2",
43
+ "@elenajs/plugin-cem-typescript": "^1.0.0-beta.2",
44
+ "@elenajs/plugin-rollup-css": "^1.0.0-beta.2",
45
+ "@rollup/plugin-babel": "7.0.0",
35
46
  "@rollup/plugin-node-resolve": "16.0.3",
36
- "@rollup/plugin-terser": "0.4.4",
37
- "@rollup/plugin-typescript": "^12.1.0",
47
+ "@rollup/plugin-terser": "1.0.0",
48
+ "@rollup/plugin-typescript": "12.3.0",
38
49
  "custom-element-jsx-integration": "1.6.0",
39
50
  "globby": "16.1.1",
40
- "rollup": "4.58.0",
41
- "rollup-plugin-minify-html-literals-v3": "^1.3.4",
51
+ "rollup": "4.59.0",
52
+ "rollup-plugin-minify-html-literals-v3": "1.3.4",
42
53
  "rollup-plugin-summary": "3.0.1",
43
- "tslib": "^2.8.0",
44
- "typescript": "^5.8.0"
54
+ "tslib": "2.8.1",
55
+ "typescript": "5.9.3"
45
56
  },
46
57
  "devDependencies": {
47
- "vitest": "4.0.18"
58
+ "vitest": "4.1.0"
48
59
  },
49
- "gitHead": "b4c41483e5196b542a1b87361f7d37222737fccc"
60
+ "gitHead": "18e13f305c8823f7633c739f2ec61cec2420267b"
50
61
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ██████████ ████
3
3
  * ░░███░░░░░█░░███
4
- * ░███ █ ░ ███ ██████ ████████ ██████
5
- * ░██████ ███ ███░░███░░███░░███ ░░░░░███
6
- * ░███░░█ ███ ░███████ ░███ ░███ ███████
7
- * ░███ ░ █ ███ ░███░░░ ░███ ░███ ███░░███
4
+ * ░███ █ ░ ░███ ██████ ████████ ██████
5
+ * ░██████ ░███ ███░░███░░███░░███ ░░░░░███
6
+ * ░███░░█ ░███ ░███████ ░███ ░███ ███████
7
+ * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███
8
8
  * ██████████ █████░░██████ ████ █████░░████████
9
9
  * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░
10
10
  *
@@ -23,8 +23,7 @@ import { elenaTypeScriptPlugin } from "@elenajs/plugin-cem-typescript";
23
23
  import { color } from "./common/color.js";
24
24
 
25
25
  /**
26
- * Returns the CEM config object for the given Elena config. Useful for advanced
27
- * users who still call the CEM CLI with a thin `elena.config.js` wrapper.
26
+ * Return the CEM config object for the given Elena config.
28
27
  *
29
28
  * @param {import("./common/load-config.js").ElenaConfig} [options]
30
29
  * @returns {object} CEM config object
@@ -60,10 +59,21 @@ export function createCemConfig(options = {}) {
60
59
  * @returns {Promise<void>}
61
60
  */
62
61
  export async function runCemAnalyze(config, cwd = process.cwd()) {
62
+ if (config.analyze === false) {
63
+ return;
64
+ }
65
+
63
66
  const src = config.input ?? "src";
64
67
  const outdir = config.output?.dir ?? "dist";
65
68
  const extraPlugins = config.analyze?.plugins ?? [];
66
69
 
70
+ const srcPath = resolve(cwd, src);
71
+ if (!existsSync(srcPath)) {
72
+ throw new Error(
73
+ `░█ [ELENA]: Input directory "${src}" does not exist. Check your "input" config option.`
74
+ );
75
+ }
76
+
67
77
  console.log(` `);
68
78
  console.log(color(`░█ [ELENA]: Analyzing the build output...`));
69
79
  console.log(color(`░█ [ELENA]: Generating Custom Elements Manifest...`));
package/src/cli.js CHANGED
@@ -3,10 +3,10 @@
3
3
  /**
4
4
  * ██████████ ████
5
5
  * ░░███░░░░░█░░███
6
- * ░███ █ ░ ███ ██████ ████████ ██████
7
- * ░██████ ███ ███░░███░░███░░███ ░░░░░███
8
- * ░███░░█ ███ ░███████ ░███ ░███ ███████
9
- * ░███ ░ █ ███ ░███░░░ ░███ ░███ ███░░███
6
+ * ░███ █ ░ ░███ ██████ ████████ ██████
7
+ * ░██████ ░███ ███░░███░░███░░███ ░░░░░███
8
+ * ░███░░█ ░███ ░███████ ░███ ░███ ███████
9
+ * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███
10
10
  * ██████████ █████░░██████ ████ █████░░████████
11
11
  * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░
12
12
  *
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { loadConfig } from "./common/load-config.js";
18
- import { runRollupBuild } from "./rollup-build.js";
18
+ import { runRollupBuild, watchRollupBuild } from "./rollup-build.js";
19
19
  import { runCemAnalyze } from "./cem-analyze.js";
20
20
  import { color } from "./common/color.js";
21
21
 
@@ -30,21 +30,54 @@ const BANNER = `
30
30
  ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░
31
31
  `;
32
32
 
33
- const [, , command = "build"] = process.argv;
33
+ const args = process.argv.slice(2);
34
+ const command = args.find(a => !a.startsWith("--")) ?? "build";
34
35
 
35
- /** Loads the Elena config and runs the Rollup build + CEM analysis. */
36
+ const configIdx = args.indexOf("--config");
37
+ const configPath = configIdx !== -1 ? args[configIdx + 1] : undefined;
38
+
39
+ /** Loads the Elena config and runs the build or watch. */
36
40
  async function main() {
37
- if (command !== "build") {
38
- console.error(`Unknown command: ${command}. Usage: elena [build]`);
41
+ if (command !== "build" && command !== "watch") {
42
+ console.error(
43
+ `░█ [ELENA]: Unknown command: ${command}. Usage: elena [build|watch] [--config <path>]`
44
+ );
39
45
  process.exit(1);
40
46
  }
41
47
 
42
48
  console.log(color(BANNER));
43
49
 
44
- const config = await loadConfig(process.cwd());
50
+ const config = await loadConfig(process.cwd(), configPath);
51
+
52
+ if (command === "watch") {
53
+ const onRebuild =
54
+ config.analyze !== false
55
+ ? async cfg => {
56
+ try {
57
+ await runCemAnalyze(cfg);
58
+ } catch (err) {
59
+ console.error(err);
60
+ }
61
+ }
62
+ : undefined;
63
+
64
+ const watcher = watchRollupBuild(config, { onRebuild });
65
+
66
+ process.on("SIGINT", () => {
67
+ watcher.close();
68
+ process.exit(0);
69
+ });
70
+ process.on("SIGTERM", () => {
71
+ watcher.close();
72
+ process.exit(0);
73
+ });
74
+ return;
75
+ }
45
76
 
46
77
  await runRollupBuild(config);
47
- await runCemAnalyze(config);
78
+ if (config.analyze !== false) {
79
+ await runCemAnalyze(config);
80
+ }
48
81
  }
49
82
 
50
83
  main().catch(err => {
@@ -1,6 +1,8 @@
1
1
  import { pathToFileURL } from "url";
2
2
  import { resolve } from "path";
3
3
  import { existsSync } from "fs";
4
+ import { color } from "./color.js";
5
+ import { validateConfig } from "./validate-config.js";
4
6
 
5
7
  /**
6
8
  * @typedef {object} ElenaOutputConfig
@@ -21,7 +23,13 @@ import { existsSync } from "fs";
21
23
  * @property {ElenaOutputConfig} [output] Rollup output options.
22
24
  * @property {string|false} [bundle] Entry for the single-file bundle; `false` to disable.
23
25
  * @property {import("rollup").Plugin[]} [plugins] Additional Rollup plugins appended after built-ins.
24
- * @property {ElenaAnalyzeConfig} [analyze] CEM analysis options.
26
+ * @property {ElenaAnalyzeConfig|false} [analyze] CEM analysis options; `false` to skip analysis.
27
+ * @property {string | string[] | false} [target] Browserslist target(s) for transpilation via
28
+ * `@babel/preset-env`. When set, enables syntax transforms (e.g. class fields, optional
29
+ * chaining) to widen browser support. Set to `false` (default) to skip transpilation.
30
+ * Example: `["chrome 71", "firefox 69", "safari 12.1"]`
31
+ * @property {object} [terser] Custom Terser minifier options, merged with the defaults
32
+ * `{ ecma: 2020, module: true }`.
25
33
  */
26
34
 
27
35
  /** @type {Required<ElenaConfig>} */
@@ -31,29 +39,76 @@ const DEFAULTS = {
31
39
  bundle: "src/index.js",
32
40
  plugins: [],
33
41
  analyze: { plugins: [] },
42
+ target: false,
43
+ terser: { ecma: 2020, module: true },
34
44
  };
35
45
 
36
46
  /**
37
- * Loads the Elena config from `elena.config.mjs` or `elena.config.js` in `cwd`,
47
+ * Merges user config with defaults.
48
+ *
49
+ * @param {ElenaConfig} user
50
+ * @returns {Required<ElenaConfig>}
51
+ */
52
+ function mergeConfig(user) {
53
+ return {
54
+ ...DEFAULTS,
55
+ ...user,
56
+ output: { ...DEFAULTS.output, ...user.output },
57
+ analyze: user.analyze === false ? false : { ...DEFAULTS.analyze, ...user.analyze },
58
+ terser: { ...DEFAULTS.terser, ...user.terser },
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Loads and imports a config file, returning the merged config.
64
+ *
65
+ * @param {string} configPath
66
+ * @returns {Promise<Required<ElenaConfig>>}
67
+ */
68
+ async function importConfig(configPath) {
69
+ const mod = await import(pathToFileURL(configPath).href);
70
+ const user = mod.default ?? {};
71
+ validateConfig(user);
72
+ return mergeConfig(user);
73
+ }
74
+
75
+ /**
76
+ * Loads the Elena config from the specified path, or from
77
+ * `elena.config.mjs` / `elena.config.js` in `cwd`,
38
78
  * falling back to defaults if no config file is found.
39
79
  *
40
80
  * @param {string} [cwd]
81
+ * @param {string} [explicitPath]
41
82
  * @returns {Promise<Required<ElenaConfig>>}
42
83
  */
43
- export async function loadConfig(cwd = process.cwd()) {
84
+ export async function loadConfig(cwd = process.cwd(), explicitPath) {
85
+ if (explicitPath) {
86
+ const configPath = resolve(cwd, explicitPath);
87
+ if (!existsSync(configPath)) {
88
+ throw new Error(`Config file not found: ${configPath}`);
89
+ }
90
+ return importConfig(configPath);
91
+ }
92
+
44
93
  for (const name of ["elena.config.mjs", "elena.config.js"]) {
45
94
  const configPath = resolve(cwd, name);
46
95
  if (!existsSync(configPath)) {
47
96
  continue;
48
97
  }
49
- const mod = await import(pathToFileURL(configPath).href);
50
- const user = mod.default ?? {};
51
- return {
52
- ...DEFAULTS,
53
- ...user,
54
- output: { ...DEFAULTS.output, ...user.output },
55
- analyze: { ...DEFAULTS.analyze, ...user.analyze },
56
- };
98
+ return importConfig(configPath);
57
99
  }
100
+
101
+ const wrongExtensions = [".ts", ".json", ".yaml", ".yml", ".cjs"];
102
+ for (const ext of wrongExtensions) {
103
+ const wrongPath = resolve(cwd, `elena.config${ext}`);
104
+ if (existsSync(wrongPath)) {
105
+ console.warn(
106
+ color(`░█ [ELENA]: Found "elena.config${ext}" but only .mjs and .js are supported.`)
107
+ );
108
+ break;
109
+ }
110
+ }
111
+
112
+ console.log(color(`░█ [ELENA]: No config file found, using defaults.`));
58
113
  return { ...DEFAULTS };
59
114
  }
@@ -0,0 +1,70 @@
1
+ import { color } from "./color.js";
2
+
3
+ const KNOWN_KEYS = new Set(["input", "output", "bundle", "plugins", "analyze", "target", "terser"]);
4
+
5
+ /**
6
+ * Validates a raw user config and throws on invalid values.
7
+ * Logs warnings for unknown keys.
8
+ *
9
+ * @param {Record<string, unknown>} config
10
+ */
11
+ export function validateConfig(config) {
12
+ for (const key of Object.keys(config)) {
13
+ if (!KNOWN_KEYS.has(key)) {
14
+ console.warn(color(`░█ [ELENA]: Unknown config option "${key}".`));
15
+ }
16
+ }
17
+
18
+ if (config.input !== undefined && typeof config.input !== "string") {
19
+ throw new Error(
20
+ `░█ [ELENA]: Invalid config: "input" must be a string, got ${typeof config.input}.`
21
+ );
22
+ }
23
+
24
+ if (config.output !== undefined) {
25
+ if (typeof config.output !== "object" || config.output === null) {
26
+ throw new Error(`░█ [ELENA]: Invalid config: "output" must be an object.`);
27
+ }
28
+ if (config.output.dir !== undefined && typeof config.output.dir !== "string") {
29
+ throw new Error(`░█ [ELENA]: Invalid config: "output.dir" must be a string.`);
30
+ }
31
+ if (config.output.format !== undefined && typeof config.output.format !== "string") {
32
+ throw new Error(`░█ [ELENA]: Invalid config: "output.format" must be a string.`);
33
+ }
34
+ if (config.output.sourcemap !== undefined && typeof config.output.sourcemap !== "boolean") {
35
+ throw new Error(`░█ [ELENA]: Invalid config: "output.sourcemap" must be a boolean.`);
36
+ }
37
+ }
38
+
39
+ if (config.bundle !== undefined && typeof config.bundle !== "string" && config.bundle !== false) {
40
+ throw new Error(`░█ [ELENA]: Invalid config: "bundle" must be a string or false.`);
41
+ }
42
+
43
+ if (config.plugins !== undefined && !Array.isArray(config.plugins)) {
44
+ throw new Error(`░█ [ELENA]: Invalid config: "plugins" must be an array.`);
45
+ }
46
+
47
+ if (config.analyze !== undefined && config.analyze !== false) {
48
+ if (typeof config.analyze !== "object" || config.analyze === null) {
49
+ throw new Error(`░█ [ELENA]: Invalid config: "analyze" must be an object or false.`);
50
+ }
51
+ if (config.analyze.plugins !== undefined && !Array.isArray(config.analyze.plugins)) {
52
+ throw new Error(`░█ [ELENA]: Invalid config: "analyze.plugins" must be an array.`);
53
+ }
54
+ }
55
+
56
+ if (config.target !== undefined && config.target !== false) {
57
+ if (typeof config.target !== "string" && !Array.isArray(config.target)) {
58
+ throw new Error(
59
+ `░█ [ELENA]: Invalid config: "target" must be a string, array of strings, or false.`
60
+ );
61
+ }
62
+ }
63
+
64
+ if (
65
+ config.terser !== undefined &&
66
+ (typeof config.terser !== "object" || config.terser === null)
67
+ ) {
68
+ throw new Error(`░█ [ELENA]: Invalid config: "terser" must be an object.`);
69
+ }
70
+ }
package/src/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ██████████ ████
3
3
  * ░░███░░░░░█░░███
4
- * ░███ █ ░ ███ ██████ ████████ ██████
5
- * ░██████ ███ ███░░███░░███░░███ ░░░░░███
6
- * ░███░░█ ███ ░███████ ░███ ░███ ███████
7
- * ░███ ░ █ ███ ░███░░░ ░███ ░███ ███░░███
4
+ * ░███ █ ░ ░███ ██████ ████████ ██████
5
+ * ░██████ ░███ ███░░███░░███░░███ ░░░░░███
6
+ * ░███░░█ ░███ ░███████ ░███ ░███ ███████
7
+ * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███
8
8
  * ██████████ █████░░██████ ████ █████░░████████
9
9
  * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░
10
10
  *
@@ -12,5 +12,5 @@
12
12
  * https://elenajs.com
13
13
  */
14
14
 
15
- export { createRollupConfig, runRollupBuild } from "./rollup-build.js";
15
+ export { createRollupConfig, runRollupBuild, watchRollupBuild } from "./rollup-build.js";
16
16
  export { createCemConfig, runCemAnalyze } from "./cem-analyze.js";
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ██████████ ████
3
3
  * ░░███░░░░░█░░███
4
- * ░███ █ ░ ███ ██████ ████████ ██████
5
- * ░██████ ███ ███░░███░░███░░███ ░░░░░███
6
- * ░███░░█ ███ ░███████ ░███ ░███ ███████
7
- * ░███ ░ █ ███ ░███░░░ ░███ ░███ ███░░███
4
+ * ░███ █ ░ ░███ ██████ ████████ ██████
5
+ * ░██████ ░███ ███░░███░░███░░███ ░░░░░███
6
+ * ░███░░█ ░███ ░███████ ░███ ░███ ███████
7
+ * ░███ ░ █ ░███ ░███░░░ ░███ ░███ ███░░███
8
8
  * ██████████ █████░░██████ ████ █████░░████████
9
9
  * ░░░░░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░
10
10
  *
@@ -13,14 +13,20 @@
13
13
  */
14
14
 
15
15
  import { existsSync, readdirSync } from "fs";
16
- import { rollup } from "rollup";
16
+ import { rollup, watch } from "rollup";
17
17
  import resolve from "@rollup/plugin-node-resolve";
18
18
  import terser from "@rollup/plugin-terser";
19
19
  import typescript from "@rollup/plugin-typescript";
20
20
  import minifyHtmlLiterals from "rollup-plugin-minify-html-literals-v3";
21
21
  import summary from "rollup-plugin-summary";
22
- import { cssPlugin, cssBundlePlugin } from "@elenajs/plugin-rollup-css";
22
+ import {
23
+ cssPlugin,
24
+ cssBundlePlugin,
25
+ cssModuleScriptPlugin,
26
+ cssStaticStylesPlugin,
27
+ } from "@elenajs/plugin-rollup-css";
23
28
  import { color } from "./common/color.js";
29
+ import babel from "@rollup/plugin-babel";
24
30
 
25
31
  const TREESHAKE = {
26
32
  moduleSideEffects: false,
@@ -28,7 +34,7 @@ const TREESHAKE = {
28
34
  };
29
35
 
30
36
  /**
31
- * Suppresses noisy Rollup warnings.
37
+ * Suppress noisy Rollup warnings.
32
38
  *
33
39
  * @param {import("rollup").RollupWarning} warning
34
40
  * @param {function} warn
@@ -41,9 +47,9 @@ function onwarn(warning, warn) {
41
47
  }
42
48
 
43
49
  /**
44
- * Builds the plugin list for a single Rollup build target.
50
+ * Build the plugin list for a single Rollup build target.
45
51
  *
46
- * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean }} opts
52
+ * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean; target?: string | string[] | false }} opts
47
53
  * @returns {import("rollup").Plugin[]}
48
54
  */
49
55
  function buildPlugins({
@@ -53,8 +59,10 @@ function buildPlugins({
53
59
  includeCssBundle,
54
60
  extraPlugins = [],
55
61
  hasTs = false,
62
+ target = false,
63
+ terserOpts = { ecma: 2020, module: true },
56
64
  }) {
57
- const plugins = [resolve({ extensions: [".js", ".ts", ".css"] })];
65
+ const plugins = [cssModuleScriptPlugin(), resolve({ extensions: [".js", ".ts", ".css"] })];
58
66
 
59
67
  if (hasTs) {
60
68
  plugins.push(
@@ -69,17 +77,30 @@ function buildPlugins({
69
77
  );
70
78
  }
71
79
 
80
+ if (target) {
81
+ plugins.push(
82
+ babel({
83
+ babelHelpers: "bundled",
84
+ presets: [["@babel/preset-env", { targets: target, bugfixes: true, modules: false }]],
85
+ extensions: [".js", ".ts"],
86
+ })
87
+ );
88
+ }
89
+
72
90
  plugins.push(
91
+ cssStaticStylesPlugin(),
73
92
  minifyHtmlLiterals({
74
93
  options: {
75
- // Minify any template literal containing HTML, regardless of tag name
76
- shouldMinify: template => template.parts.some(({ text }) => /<[a-z]/i.test(text)),
94
+ shouldMinify: template => {
95
+ const tag = template.tag && template.tag.toLowerCase();
96
+ return (
97
+ (tag && (tag.includes("html") || tag.includes("svg"))) ||
98
+ template.parts.some(({ text }) => /<[a-z]/i.test(text))
99
+ );
100
+ },
77
101
  },
78
102
  }),
79
- terser({
80
- ecma: 2020,
81
- module: true,
82
- }),
103
+ terser(terserOpts),
83
104
  cssPlugin(src)
84
105
  );
85
106
 
@@ -87,7 +108,6 @@ function buildPlugins({
87
108
  plugins.push(cssBundlePlugin(src, "bundle.css"));
88
109
  }
89
110
 
90
- // User-provided plugins are appended after built-ins, before summary.
91
111
  plugins.push(...extraPlugins);
92
112
 
93
113
  if (hasSummary) {
@@ -98,10 +118,9 @@ function buildPlugins({
98
118
  }
99
119
 
100
120
  /**
101
- * Returns the Rollup config array for the given Elena config. Useful for
102
- * users who want to call `rollup -c` with a thin wrapper config file.
121
+ * Return the Rollup config array for the given Elena config.
103
122
  *
104
- * @param {import("./utils/load-config.js").ElenaConfig} [options]
123
+ * @param {import("./common/load-config.js").ElenaConfig} [options]
105
124
  * @returns {import("rollup").RollupOptions[]}
106
125
  */
107
126
  export function createRollupConfig(options = {}) {
@@ -111,6 +130,14 @@ export function createRollupConfig(options = {}) {
111
130
  const sourcemap = options.output?.sourcemap ?? true;
112
131
  let bundle = options.bundle !== undefined ? options.bundle : "src/index.js";
113
132
  const extraPlugins = options.plugins ?? [];
133
+ const target = options.target ?? false;
134
+ const terserOpts = options.terser ?? { ecma: 2020, module: true };
135
+
136
+ if (!existsSync(src)) {
137
+ throw new Error(
138
+ `░█ [ELENA]: Input directory "${src}" does not exist. Check your "input" config option.`
139
+ );
140
+ }
114
141
 
115
142
  const entries = readdirSync(src, { recursive: true })
116
143
  .filter(
@@ -127,6 +154,12 @@ export function createRollupConfig(options = {}) {
127
154
  bundle = "src/index.ts";
128
155
  }
129
156
 
157
+ if (bundle && !existsSync(bundle)) {
158
+ throw new Error(
159
+ `░█ [ELENA]: Bundle entry "${bundle}" does not exist. Check your "bundle" config option.`
160
+ );
161
+ }
162
+
130
163
  const configs = [
131
164
  {
132
165
  input: entries,
@@ -137,6 +170,8 @@ export function createRollupConfig(options = {}) {
137
170
  includeCssBundle: true,
138
171
  extraPlugins,
139
172
  hasTs,
173
+ target,
174
+ terserOpts,
140
175
  }),
141
176
  output: {
142
177
  format,
@@ -160,6 +195,8 @@ export function createRollupConfig(options = {}) {
160
195
  includeCssBundle: false,
161
196
  extraPlugins,
162
197
  hasTs,
198
+ target,
199
+ terserOpts,
163
200
  }),
164
201
  output: { format, sourcemap, file: `${outdir}/bundle.js` },
165
202
  preserveEntrySignatures: "strict",
@@ -172,10 +209,10 @@ export function createRollupConfig(options = {}) {
172
209
  }
173
210
 
174
211
  /**
175
- * Runs Rollup build targets programmatically using the Rollup Node.js API.
176
- * Reuses `createRollupConfig` to avoid duplicating config resolution logic.
212
+ * Run Rollup build targets programmatically using the Rollup Node.js API.
213
+ * Reuse `createRollupConfig` to avoid duplicating config resolution logic.
177
214
  *
178
- * @param {import("./utils/load-config.js").ElenaConfig} config
215
+ * @param {import("./common/load-config.js").ElenaConfig} config
179
216
  * @returns {Promise<void>}
180
217
  */
181
218
  export async function runRollupBuild(config) {
@@ -184,6 +221,8 @@ export async function runRollupBuild(config) {
184
221
  console.log(color(`░█ [ELENA]: Building Progressive Web Components...`));
185
222
  console.log(` `);
186
223
 
224
+ let cache;
225
+
187
226
  for (const { output, ...inputOpts } of configs) {
188
227
  if (Array.isArray(inputOpts.input)) {
189
228
  for (const entry of inputOpts.input) {
@@ -194,8 +233,61 @@ export async function runRollupBuild(config) {
194
233
  console.log(` `);
195
234
  }
196
235
 
197
- const build = await rollup(inputOpts);
236
+ const build = await rollup({ ...inputOpts, cache });
237
+ cache = build.cache;
198
238
  await build.write(output);
199
239
  await build.close();
200
240
  }
201
241
  }
242
+
243
+ /**
244
+ * Start a Rollup watch session using the Rollup Node.js watch API.
245
+ * Rebuild on changes and optionally re-run a callback after build.
246
+ *
247
+ * @param {import("./common/load-config.js").ElenaConfig} config
248
+ * @param {{ onRebuild?: (config: import("./common/load-config.js").ElenaConfig) => Promise<void> }} [opts]
249
+ * @returns {import("rollup").RollupWatcher}
250
+ */
251
+ export function watchRollupBuild(config, opts = {}) {
252
+ const configs = createRollupConfig(config);
253
+
254
+ console.log(color(`░█ [ELENA]: Watching for changes...`));
255
+ console.log(` `);
256
+
257
+ const watchConfigs = configs.map(({ output, ...inputOpts }) => ({
258
+ ...inputOpts,
259
+ output,
260
+ watch: { clearScreen: false },
261
+ }));
262
+
263
+ const watcher = watch(watchConfigs);
264
+
265
+ watcher.on("event", async event => {
266
+ if (event.code === "BUNDLE_START") {
267
+ console.log(color(`░█ [ELENA]: Rebuilding...`));
268
+ }
269
+ if (event.code === "BUNDLE_END") {
270
+ console.log(color(`░█ [ELENA]: Build completed in ${event.duration}ms.`));
271
+ await event.result.close();
272
+ if (opts.onRebuild) {
273
+ try {
274
+ await opts.onRebuild(config);
275
+ } catch (err) {
276
+ console.error(err);
277
+ }
278
+ }
279
+ }
280
+ if (event.code === "ERROR") {
281
+ console.error(color(`░█ [ELENA]: Build error:`));
282
+ console.error(event.error);
283
+ if (event.result) {
284
+ await event.result.close();
285
+ }
286
+ }
287
+ if (event.code === "END") {
288
+ console.log(color(`░█ [ELENA]: Waiting for changes...`));
289
+ }
290
+ });
291
+
292
+ return watcher;
293
+ }