@elenajs/bundler 0.9.0 → 1.0.0-rc.10

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,240 @@
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
+ // Banner comment prepended to bundle output files.
114
+ // Use a @license tag so minifiers preserve it.
115
+ // banner: `/** @license MIT */`,
116
+ };
117
+ ```
118
+
119
+ ### Options
120
+
121
+ | Option | Type | Default | Description |
122
+ | ------------------ | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
123
+ | `input` | `string` | `"src"` | Source directory to scan for `.js`, `.ts`, and `.css` files. |
124
+ | `output.dir` | `string` | `"dist"` | Output directory for compiled files. |
125
+ | `output.format` | `string` | `"esm"` | Rollup output format. |
126
+ | `output.sourcemap` | `boolean` | `true` | Whether to emit sourcemaps. |
127
+ | `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. |
128
+ | `plugins` | `Plugin[]` | `[]` | Additional Rollup plugins appended after the built-in set. |
129
+ | `analyze` | `object \| false` | `{ plugins: [] }` | CEM analysis options. Set to `false` to skip Custom Elements Manifest generation, TypeScript declarations, and JSX types entirely. |
130
+ | `analyze.plugins` | `Plugin[]` | `[]` | Additional CEM analyzer plugins. |
131
+ | `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"]`. |
132
+ | `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. |
133
+ | `banner` | `string \| false` | `false` | Banner comment prepended to `index.js` and `bundle.js` output files. Use a `@license` JSDoc tag so minifiers preserve it. |
134
+
135
+ ## Build output
136
+
137
+ Running `elena build` produces:
138
+
139
+ | File | Description |
140
+ | --------------------------- | ---------------------------------------------------------------------------------------------------- |
141
+ | `dist/*.js` | Individual ES modules for each source file. |
142
+ | `dist/*.css` | Minified individual CSS files. |
143
+ | `dist/bundle.js` | Single-file JavaScript bundle _(optional)_. |
144
+ | `dist/bundle.css` | Concatenated and minified CSS bundle. CSS files imported as CSS Module Scripts (`with { type: "css" }`) for Shadow DOM are excluded. |
145
+ | `dist/custom-elements.json` | [Custom Elements Manifest](https://custom-elements-manifest.open-wc.org/) describing all components. |
146
+ | `dist/custom-elements.d.ts` | JSX integration types mapping tag names to prop types. |
147
+ | `dist/*.d.ts` | Per-component TypeScript declarations with typed props and events. |
148
+
149
+ > **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.
150
+
151
+ ## TypeScript support
152
+
153
+ 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.
154
+
155
+ To use TypeScript, write your components with inline type annotations instead of JSDoc:
156
+
157
+ ```ts
158
+ import { Elena, html } from "@elenajs/core";
159
+
160
+ export default class Button extends Elena(HTMLElement) {
161
+ static tagName = "elena-button";
162
+ static props = ["variant"];
163
+
164
+ /**
165
+ * The style variant of the component.
166
+ * @property
167
+ */
168
+ variant: "default" | "primary" | "danger" = "default";
169
+
170
+ render() {
171
+ return html`<button>${this.text}</button>`;
172
+ }
173
+ }
174
+ Button.define();
175
+ ```
176
+
177
+ A `tsconfig.json` is required in the project root. A minimal configuration:
178
+
179
+ ```json
180
+ {
181
+ "compilerOptions": {
182
+ "target": "ES2020",
183
+ "module": "ESNext",
184
+ "moduleResolution": "bundler",
185
+ "skipLibCheck": true
186
+ },
187
+ "include": ["src"]
188
+ }
189
+ ```
190
+
191
+ > **Note:** The bundler handles TypeScript declarations separately via the CEM analyzer, you do not need `declaration: true` in your `tsconfig.json`.
192
+
193
+ ## Programmatic API
194
+
195
+ The bundler exports its internals so you can integrate it into your own build scripts:
196
+
197
+ ```js
198
+ import {
199
+ createRollupConfig,
200
+ runRollupBuild,
201
+ watchRollupBuild,
202
+ createCemConfig,
203
+ runCemAnalyze,
204
+ } from "@elenajs/bundler";
205
+ ```
206
+
207
+ Sub-path imports are also available:
208
+
209
+ ```js
210
+ import { createRollupConfig, runRollupBuild, watchRollupBuild } from "@elenajs/bundler/rollup";
211
+ import { createCemConfig, runCemAnalyze } from "@elenajs/bundler/cem";
212
+ ```
213
+
214
+ ### `createRollupConfig(options?)`
215
+
216
+ Returns a Rollup configuration array. Useful if you want to wrap or extend the config in a custom `rollup.config.js`.
217
+
218
+ ### `runRollupBuild(config)`
219
+
220
+ Runs both build phases (individual modules + optional single-file bundle) programmatically.
221
+
222
+ ### `watchRollupBuild(config, opts?)`
223
+
224
+ 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).
225
+
226
+ ### `createCemConfig(options?)`
227
+
228
+ Returns the Custom Elements Manifest analyzer configuration object.
229
+
230
+ ### `runCemAnalyze(config, cwd?)`
231
+
232
+ Runs the CEM analysis and writes `custom-elements.json`, `custom-elements.d.ts`, and per-component `.d.ts` files.
233
+
234
+ ## License
235
+
236
+ MIT
237
+
238
+ ## Copyright
239
+
240
+ 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.10",
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.2",
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-rc.5",
42
+ "@elenajs/plugin-cem-prop": "^1.0.0-rc.5",
43
+ "@elenajs/plugin-cem-tag": "^1.0.0-rc.5",
44
+ "@elenajs/plugin-cem-typescript": "^1.0.0-rc.6",
45
+ "@elenajs/plugin-rollup-css": "^1.0.0-rc.5",
46
+ "@rollup/plugin-babel": "7.0.0",
35
47
  "@rollup/plugin-node-resolve": "16.0.3",
36
- "@rollup/plugin-terser": "0.4.4",
37
- "@rollup/plugin-typescript": "^12.1.0",
48
+ "@rollup/plugin-terser": "1.0.0",
49
+ "@rollup/plugin-typescript": "12.3.0",
38
50
  "custom-element-jsx-integration": "1.6.0",
39
51
  "globby": "16.1.1",
40
- "rollup": "4.58.0",
41
- "rollup-plugin-minify-html-literals-v3": "^1.3.4",
52
+ "rollup": "4.60.0",
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": "6.0.2"
45
56
  },
46
57
  "devDependencies": {
47
- "vitest": "4.0.18"
58
+ "vitest": "4.1.1"
48
59
  },
49
- "gitHead": "b4c41483e5196b542a1b87361f7d37222737fccc"
60
+ "gitHead": "89275a58542d7a86e64b6052416a935c89d90d52"
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
  *
@@ -18,13 +18,13 @@ import { create, ts } from "@custom-elements-manifest/analyzer";
18
18
  import { globby } from "globby";
19
19
  import { customElementJsxPlugin as elenaJsxPlugin } from "custom-element-jsx-integration";
20
20
  import { elenaDefinePlugin } from "@elenajs/plugin-cem-define";
21
+ import { elenaPropPlugin } from "@elenajs/plugin-cem-prop";
21
22
  import { elenaTagPlugin } from "@elenajs/plugin-cem-tag";
22
23
  import { elenaTypeScriptPlugin } from "@elenajs/plugin-cem-typescript";
23
24
  import { color } from "./common/color.js";
24
25
 
25
26
  /**
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.
27
+ * Return the CEM config object for the given Elena config.
28
28
  *
29
29
  * @param {import("./common/load-config.js").ElenaConfig} [options]
30
30
  * @returns {object} CEM config object
@@ -42,6 +42,7 @@ export function createCemConfig(options = {}) {
42
42
  exclude: ["**/*.test.js", "**/*.test.ts", "node_modules"],
43
43
  plugins: [
44
44
  elenaDefinePlugin(),
45
+ elenaPropPlugin(),
45
46
  elenaTagPlugin("status"),
46
47
  elenaTagPlugin("displayName"),
47
48
  elenaJsxPlugin({ outdir, fileName: "custom-elements.d.ts" }),
@@ -60,10 +61,21 @@ export function createCemConfig(options = {}) {
60
61
  * @returns {Promise<void>}
61
62
  */
62
63
  export async function runCemAnalyze(config, cwd = process.cwd()) {
64
+ if (config.analyze === false) {
65
+ return;
66
+ }
67
+
63
68
  const src = config.input ?? "src";
64
69
  const outdir = config.output?.dir ?? "dist";
65
70
  const extraPlugins = config.analyze?.plugins ?? [];
66
71
 
72
+ const srcPath = resolve(cwd, src);
73
+ if (!existsSync(srcPath)) {
74
+ throw new Error(
75
+ `░█ [ELENA]: Input directory "${src}" does not exist. Check your "input" config option.`
76
+ );
77
+ }
78
+
67
79
  console.log(` `);
68
80
  console.log(color(`░█ [ELENA]: Analyzing the build output...`));
69
81
  console.log(color(`░█ [ELENA]: Generating Custom Elements Manifest...`));
@@ -84,6 +96,7 @@ export async function runCemAnalyze(config, cwd = process.cwd()) {
84
96
 
85
97
  const plugins = [
86
98
  elenaDefinePlugin(),
99
+ elenaPropPlugin(),
87
100
  elenaTagPlugin("status"),
88
101
  elenaTagPlugin("displayName"),
89
102
  elenaJsxPlugin({ outdir, fileName: "custom-elements.d.ts" }),
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,15 @@ 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 }`.
33
+ * @property {string|false} [banner] Banner comment prepended to every output file. Use a
34
+ * `@license` JSDoc tag so minifiers preserve it. Set to `false` (default) to omit.
25
35
  */
26
36
 
27
37
  /** @type {Required<ElenaConfig>} */
@@ -31,29 +41,77 @@ const DEFAULTS = {
31
41
  bundle: "src/index.js",
32
42
  plugins: [],
33
43
  analyze: { plugins: [] },
44
+ target: false,
45
+ terser: { ecma: 2020, module: true },
46
+ banner: false,
34
47
  };
35
48
 
36
49
  /**
37
- * Loads the Elena config from `elena.config.mjs` or `elena.config.js` in `cwd`,
50
+ * Merges user config with defaults.
51
+ *
52
+ * @param {ElenaConfig} user
53
+ * @returns {Required<ElenaConfig>}
54
+ */
55
+ function mergeConfig(user) {
56
+ return {
57
+ ...DEFAULTS,
58
+ ...user,
59
+ output: { ...DEFAULTS.output, ...user.output },
60
+ analyze: user.analyze === false ? false : { ...DEFAULTS.analyze, ...user.analyze },
61
+ terser: { ...DEFAULTS.terser, ...user.terser },
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Loads and imports a config file, returning the merged config.
67
+ *
68
+ * @param {string} configPath
69
+ * @returns {Promise<Required<ElenaConfig>>}
70
+ */
71
+ async function importConfig(configPath) {
72
+ const mod = await import(pathToFileURL(configPath).href);
73
+ const user = mod.default ?? {};
74
+ validateConfig(user);
75
+ return mergeConfig(user);
76
+ }
77
+
78
+ /**
79
+ * Loads the Elena config from the specified path, or from
80
+ * `elena.config.mjs` / `elena.config.js` in `cwd`,
38
81
  * falling back to defaults if no config file is found.
39
82
  *
40
83
  * @param {string} [cwd]
84
+ * @param {string} [explicitPath]
41
85
  * @returns {Promise<Required<ElenaConfig>>}
42
86
  */
43
- export async function loadConfig(cwd = process.cwd()) {
87
+ export async function loadConfig(cwd = process.cwd(), explicitPath) {
88
+ if (explicitPath) {
89
+ const configPath = resolve(cwd, explicitPath);
90
+ if (!existsSync(configPath)) {
91
+ throw new Error(`Config file not found: ${configPath}`);
92
+ }
93
+ return importConfig(configPath);
94
+ }
95
+
44
96
  for (const name of ["elena.config.mjs", "elena.config.js"]) {
45
97
  const configPath = resolve(cwd, name);
46
98
  if (!existsSync(configPath)) {
47
99
  continue;
48
100
  }
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
- };
101
+ return importConfig(configPath);
57
102
  }
103
+
104
+ const wrongExtensions = [".ts", ".json", ".yaml", ".yml", ".cjs"];
105
+ for (const ext of wrongExtensions) {
106
+ const wrongPath = resolve(cwd, `elena.config${ext}`);
107
+ if (existsSync(wrongPath)) {
108
+ console.warn(
109
+ color(`░█ [ELENA]: Found "elena.config${ext}" but only .mjs and .js are supported.`)
110
+ );
111
+ break;
112
+ }
113
+ }
114
+
115
+ console.log(color(`░█ [ELENA]: No config file found, using defaults.`));
58
116
  return { ...DEFAULTS };
59
117
  }
@@ -0,0 +1,83 @@
1
+ import { color } from "./color.js";
2
+
3
+ const KNOWN_KEYS = new Set([
4
+ "input",
5
+ "output",
6
+ "bundle",
7
+ "plugins",
8
+ "analyze",
9
+ "target",
10
+ "terser",
11
+ "banner",
12
+ ]);
13
+
14
+ /**
15
+ * Validates a raw user config and throws on invalid values.
16
+ * Logs warnings for unknown keys.
17
+ *
18
+ * @param {Record<string, unknown>} config
19
+ */
20
+ export function validateConfig(config) {
21
+ for (const key of Object.keys(config)) {
22
+ if (!KNOWN_KEYS.has(key)) {
23
+ console.warn(color(`░█ [ELENA]: Unknown config option "${key}".`));
24
+ }
25
+ }
26
+
27
+ if (config.input !== undefined && typeof config.input !== "string") {
28
+ throw new Error(
29
+ `░█ [ELENA]: Invalid config: "input" must be a string, got ${typeof config.input}.`
30
+ );
31
+ }
32
+
33
+ if (config.output !== undefined) {
34
+ if (typeof config.output !== "object" || config.output === null) {
35
+ throw new Error(`░█ [ELENA]: Invalid config: "output" must be an object.`);
36
+ }
37
+ if (config.output.dir !== undefined && typeof config.output.dir !== "string") {
38
+ throw new Error(`░█ [ELENA]: Invalid config: "output.dir" must be a string.`);
39
+ }
40
+ if (config.output.format !== undefined && typeof config.output.format !== "string") {
41
+ throw new Error(`░█ [ELENA]: Invalid config: "output.format" must be a string.`);
42
+ }
43
+ if (config.output.sourcemap !== undefined && typeof config.output.sourcemap !== "boolean") {
44
+ throw new Error(`░█ [ELENA]: Invalid config: "output.sourcemap" must be a boolean.`);
45
+ }
46
+ }
47
+
48
+ if (config.bundle !== undefined && typeof config.bundle !== "string" && config.bundle !== false) {
49
+ throw new Error(`░█ [ELENA]: Invalid config: "bundle" must be a string or false.`);
50
+ }
51
+
52
+ if (config.plugins !== undefined && !Array.isArray(config.plugins)) {
53
+ throw new Error(`░█ [ELENA]: Invalid config: "plugins" must be an array.`);
54
+ }
55
+
56
+ if (config.analyze !== undefined && config.analyze !== false) {
57
+ if (typeof config.analyze !== "object" || config.analyze === null) {
58
+ throw new Error(`░█ [ELENA]: Invalid config: "analyze" must be an object or false.`);
59
+ }
60
+ if (config.analyze.plugins !== undefined && !Array.isArray(config.analyze.plugins)) {
61
+ throw new Error(`░█ [ELENA]: Invalid config: "analyze.plugins" must be an array.`);
62
+ }
63
+ }
64
+
65
+ if (config.target !== undefined && config.target !== false) {
66
+ if (typeof config.target !== "string" && !Array.isArray(config.target)) {
67
+ throw new Error(
68
+ `░█ [ELENA]: Invalid config: "target" must be a string, array of strings, or false.`
69
+ );
70
+ }
71
+ }
72
+
73
+ if (
74
+ config.terser !== undefined &&
75
+ (typeof config.terser !== "object" || config.terser === null)
76
+ ) {
77
+ throw new Error(`░█ [ELENA]: Invalid config: "terser" must be an object.`);
78
+ }
79
+
80
+ if (config.banner !== undefined && typeof config.banner !== "string" && config.banner !== false) {
81
+ throw new Error(`░█ [ELENA]: Invalid config: "banner" must be a string or false.`);
82
+ }
83
+ }
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,19 @@
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
- import minifyHtmlLiterals from "rollup-plugin-minify-html-literals-v3";
21
20
  import summary from "rollup-plugin-summary";
22
- import { cssPlugin, cssBundlePlugin } from "@elenajs/plugin-rollup-css";
21
+ import {
22
+ cssPlugin,
23
+ cssBundlePlugin,
24
+ cssModuleScriptPlugin,
25
+ cssStaticStylesPlugin,
26
+ } from "@elenajs/plugin-rollup-css";
23
27
  import { color } from "./common/color.js";
28
+ import babel from "@rollup/plugin-babel";
24
29
 
25
30
  const TREESHAKE = {
26
31
  moduleSideEffects: false,
@@ -28,7 +33,7 @@ const TREESHAKE = {
28
33
  };
29
34
 
30
35
  /**
31
- * Suppresses noisy Rollup warnings.
36
+ * Suppress noisy Rollup warnings.
32
37
  *
33
38
  * @param {import("rollup").RollupWarning} warning
34
39
  * @param {function} warn
@@ -41,9 +46,9 @@ function onwarn(warning, warn) {
41
46
  }
42
47
 
43
48
  /**
44
- * Builds the plugin list for a single Rollup build target.
49
+ * Build the plugin list for a single Rollup build target.
45
50
  *
46
- * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean }} opts
51
+ * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean; target?: string | string[] | false }} opts
47
52
  * @returns {import("rollup").Plugin[]}
48
53
  */
49
54
  function buildPlugins({
@@ -53,8 +58,10 @@ function buildPlugins({
53
58
  includeCssBundle,
54
59
  extraPlugins = [],
55
60
  hasTs = false,
61
+ target = false,
62
+ terserOpts = { ecma: 2020, module: true },
56
63
  }) {
57
- const plugins = [resolve({ extensions: [".js", ".ts", ".css"] })];
64
+ const plugins = [cssModuleScriptPlugin(), resolve({ extensions: [".js", ".ts", ".css"] })];
58
65
 
59
66
  if (hasTs) {
60
67
  plugins.push(
@@ -69,25 +76,22 @@ function buildPlugins({
69
76
  );
70
77
  }
71
78
 
72
- plugins.push(
73
- minifyHtmlLiterals({
74
- options: {
75
- // Minify any template literal containing HTML, regardless of tag name
76
- shouldMinify: template => template.parts.some(({ text }) => /<[a-z]/i.test(text)),
77
- },
78
- }),
79
- terser({
80
- ecma: 2020,
81
- module: true,
82
- }),
83
- cssPlugin(src)
84
- );
79
+ if (target) {
80
+ plugins.push(
81
+ babel({
82
+ babelHelpers: "bundled",
83
+ presets: [["@babel/preset-env", { targets: target, bugfixes: true, modules: false }]],
84
+ extensions: [".js", ".ts"],
85
+ })
86
+ );
87
+ }
88
+
89
+ plugins.push(cssStaticStylesPlugin(), terser(terserOpts), cssPlugin(src));
85
90
 
86
91
  if (includeCssBundle) {
87
92
  plugins.push(cssBundlePlugin(src, "bundle.css"));
88
93
  }
89
94
 
90
- // User-provided plugins are appended after built-ins, before summary.
91
95
  plugins.push(...extraPlugins);
92
96
 
93
97
  if (hasSummary) {
@@ -98,10 +102,9 @@ function buildPlugins({
98
102
  }
99
103
 
100
104
  /**
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.
105
+ * Return the Rollup config array for the given Elena config.
103
106
  *
104
- * @param {import("./utils/load-config.js").ElenaConfig} [options]
107
+ * @param {import("./common/load-config.js").ElenaConfig} [options]
105
108
  * @returns {import("rollup").RollupOptions[]}
106
109
  */
107
110
  export function createRollupConfig(options = {}) {
@@ -111,6 +114,15 @@ export function createRollupConfig(options = {}) {
111
114
  const sourcemap = options.output?.sourcemap ?? true;
112
115
  let bundle = options.bundle !== undefined ? options.bundle : "src/index.js";
113
116
  const extraPlugins = options.plugins ?? [];
117
+ const target = options.target ?? false;
118
+ const terserOpts = options.terser ?? { ecma: 2020, module: true };
119
+ const banner = options.banner || undefined;
120
+
121
+ if (!existsSync(src)) {
122
+ throw new Error(
123
+ `░█ [ELENA]: Input directory "${src}" does not exist. Check your "input" config option.`
124
+ );
125
+ }
114
126
 
115
127
  const entries = readdirSync(src, { recursive: true })
116
128
  .filter(
@@ -127,6 +139,12 @@ export function createRollupConfig(options = {}) {
127
139
  bundle = "src/index.ts";
128
140
  }
129
141
 
142
+ if (bundle && !existsSync(bundle)) {
143
+ throw new Error(
144
+ `░█ [ELENA]: Bundle entry "${bundle}" does not exist. Check your "bundle" config option.`
145
+ );
146
+ }
147
+
130
148
  const configs = [
131
149
  {
132
150
  input: entries,
@@ -137,8 +155,13 @@ export function createRollupConfig(options = {}) {
137
155
  includeCssBundle: true,
138
156
  extraPlugins,
139
157
  hasTs,
158
+ target,
159
+ terserOpts,
140
160
  }),
141
161
  output: {
162
+ ...(banner && {
163
+ banner: chunk => (chunk.fileName === "index.js" ? banner : ""),
164
+ }),
142
165
  format,
143
166
  sourcemap,
144
167
  dir: outdir,
@@ -160,8 +183,10 @@ export function createRollupConfig(options = {}) {
160
183
  includeCssBundle: false,
161
184
  extraPlugins,
162
185
  hasTs,
186
+ target,
187
+ terserOpts,
163
188
  }),
164
- output: { format, sourcemap, file: `${outdir}/bundle.js` },
189
+ output: { banner, format, sourcemap, file: `${outdir}/bundle.js` },
165
190
  preserveEntrySignatures: "strict",
166
191
  treeshake: TREESHAKE,
167
192
  onwarn,
@@ -172,10 +197,10 @@ export function createRollupConfig(options = {}) {
172
197
  }
173
198
 
174
199
  /**
175
- * Runs Rollup build targets programmatically using the Rollup Node.js API.
176
- * Reuses `createRollupConfig` to avoid duplicating config resolution logic.
200
+ * Run Rollup build targets programmatically using the Rollup Node.js API.
201
+ * Reuse `createRollupConfig` to avoid duplicating config resolution logic.
177
202
  *
178
- * @param {import("./utils/load-config.js").ElenaConfig} config
203
+ * @param {import("./common/load-config.js").ElenaConfig} config
179
204
  * @returns {Promise<void>}
180
205
  */
181
206
  export async function runRollupBuild(config) {
@@ -184,6 +209,8 @@ export async function runRollupBuild(config) {
184
209
  console.log(color(`░█ [ELENA]: Building Progressive Web Components...`));
185
210
  console.log(` `);
186
211
 
212
+ let cache;
213
+
187
214
  for (const { output, ...inputOpts } of configs) {
188
215
  if (Array.isArray(inputOpts.input)) {
189
216
  for (const entry of inputOpts.input) {
@@ -194,8 +221,61 @@ export async function runRollupBuild(config) {
194
221
  console.log(` `);
195
222
  }
196
223
 
197
- const build = await rollup(inputOpts);
224
+ const build = await rollup({ ...inputOpts, cache });
225
+ cache = build.cache;
198
226
  await build.write(output);
199
227
  await build.close();
200
228
  }
201
229
  }
230
+
231
+ /**
232
+ * Start a Rollup watch session using the Rollup Node.js watch API.
233
+ * Rebuild on changes and optionally re-run a callback after build.
234
+ *
235
+ * @param {import("./common/load-config.js").ElenaConfig} config
236
+ * @param {{ onRebuild?: (config: import("./common/load-config.js").ElenaConfig) => Promise<void> }} [opts]
237
+ * @returns {import("rollup").RollupWatcher}
238
+ */
239
+ export function watchRollupBuild(config, opts = {}) {
240
+ const configs = createRollupConfig(config);
241
+
242
+ console.log(color(`░█ [ELENA]: Watching for changes...`));
243
+ console.log(` `);
244
+
245
+ const watchConfigs = configs.map(({ output, ...inputOpts }) => ({
246
+ ...inputOpts,
247
+ output,
248
+ watch: { clearScreen: false },
249
+ }));
250
+
251
+ const watcher = watch(watchConfigs);
252
+
253
+ watcher.on("event", async event => {
254
+ if (event.code === "BUNDLE_START") {
255
+ console.log(color(`░█ [ELENA]: Rebuilding...`));
256
+ }
257
+ if (event.code === "BUNDLE_END") {
258
+ console.log(color(`░█ [ELENA]: Build completed in ${event.duration}ms.`));
259
+ await event.result.close();
260
+ if (opts.onRebuild) {
261
+ try {
262
+ await opts.onRebuild(config);
263
+ } catch (err) {
264
+ console.error(err);
265
+ }
266
+ }
267
+ }
268
+ if (event.code === "ERROR") {
269
+ console.error(color(`░█ [ELENA]: Build error:`));
270
+ console.error(event.error);
271
+ if (event.result) {
272
+ await event.result.close();
273
+ }
274
+ }
275
+ if (event.code === "END") {
276
+ console.log(color(`░█ [ELENA]: Waiting for changes...`));
277
+ }
278
+ });
279
+
280
+ return watcher;
281
+ }