@elenajs/bundler 1.0.0-rc.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
+ <br/>
1
2
  <div align="center">
2
3
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/img/elena-dark.png" alt="Elena" width="558" height="220">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://elenajs.com/elena-v2-dark.png" alt="Elena" width="127" height="156">
4
5
  </source>
5
- <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
6
+ <source media="(prefers-color-scheme: light)" srcset="https://elenajs.com/elena-v2.png" alt="Elena" width="127" height="156">
6
7
  </source>
7
- <img src="https://elenajs.com/img/elena-light.png" alt="Elena" width="558" height="220">
8
+ <img src="https://elenajs.com/elena-v2.png" alt="Elena" width="127" height="156">
8
9
  </picture>
9
10
 
10
- ### Bundler for Progressive Web Component libraries built with Elena.
11
+ ### Bundler for Progressive Web Component libraries built with Elena
11
12
 
12
13
  <br/>
13
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elenajs/bundler",
3
- "version": "1.0.0-rc.7",
3
+ "version": "1.0.0",
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/",
@@ -36,27 +36,26 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/core": "7.29.0",
39
- "@babel/preset-env": "7.29.0",
39
+ "@babel/preset-env": "7.29.2",
40
40
  "@custom-elements-manifest/analyzer": "0.11.0",
41
- "@elenajs/plugin-cem-define": "^1.0.0-rc.3",
42
- "@elenajs/plugin-cem-prop": "^1.0.0-rc.3",
43
- "@elenajs/plugin-cem-tag": "^1.0.0-rc.3",
44
- "@elenajs/plugin-cem-typescript": "^1.0.0-rc.4",
45
- "@elenajs/plugin-rollup-css": "^1.0.0-rc.3",
41
+ "@elenajs/plugin-cem-define": "^1.0.0",
42
+ "@elenajs/plugin-cem-prop": "^1.0.0",
43
+ "@elenajs/plugin-cem-tag": "^1.0.0",
44
+ "@elenajs/plugin-cem-typescript": "^1.0.0",
45
+ "@elenajs/plugin-rollup-css": "^1.0.0",
46
46
  "@rollup/plugin-babel": "7.0.0",
47
47
  "@rollup/plugin-node-resolve": "16.0.3",
48
48
  "@rollup/plugin-terser": "1.0.0",
49
49
  "@rollup/plugin-typescript": "12.3.0",
50
50
  "custom-element-jsx-integration": "1.6.0",
51
- "globby": "16.1.1",
52
- "rollup": "4.59.0",
53
- "rollup-plugin-minify-html-literals-v3": "1.3.4",
51
+ "globby": "16.2.0",
52
+ "rollup": "4.60.1",
54
53
  "rollup-plugin-summary": "3.0.1",
55
54
  "tslib": "2.8.1",
56
- "typescript": "5.9.3"
55
+ "typescript": "6.0.2"
57
56
  },
58
57
  "devDependencies": {
59
- "vitest": "4.1.0"
58
+ "vitest": "4.1.2"
60
59
  },
61
- "gitHead": "13a0e3064f943d6ffceb465a59103e034950c28e"
60
+ "gitHead": "ef466853f84deb6559ea66b54cd85b7e043e9f36"
62
61
  }
@@ -9,6 +9,8 @@ import { validateConfig } from "./validate-config.js";
9
9
  * @property {string} [dir] Output directory for individual modules, CSS, and CEM artifacts.
10
10
  * @property {string} [format] Rollup output format (default: `"esm"`).
11
11
  * @property {boolean} [sourcemap] Whether to emit sourcemaps (default: `true`).
12
+ * @property {string} [filename] Output filename for the bundle (default: `"bundle.js"`).
13
+ * The CSS bundle filename is derived by replacing the `.js` extension with `.css`.
12
14
  */
13
15
 
14
16
  /**
@@ -32,6 +34,10 @@ import { validateConfig } from "./validate-config.js";
32
34
  * `{ ecma: 2020, module: true }`.
33
35
  * @property {string|false} [banner] Banner comment prepended to every output file. Use a
34
36
  * `@license` JSDoc tag so minifiers preserve it. Set to `false` (default) to omit.
37
+ * @property {"auto"|"scoped"} [registration] Controls how components are registered.
38
+ * `"auto"` (default) preserves `.define()` calls in the output.
39
+ * `"scoped"` strips `.define()` calls and generates a `register.js` helper
40
+ * with a `defineAll(registry?)` function for scoped registry usage.
35
41
  */
36
42
 
37
43
  /** @type {Required<ElenaConfig>} */
@@ -44,6 +50,7 @@ const DEFAULTS = {
44
50
  target: false,
45
51
  terser: { ecma: 2020, module: true },
46
52
  banner: false,
53
+ registration: "auto",
47
54
  };
48
55
 
49
56
  /**
@@ -9,6 +9,7 @@ const KNOWN_KEYS = new Set([
9
9
  "target",
10
10
  "terser",
11
11
  "banner",
12
+ "registration",
12
13
  ]);
13
14
 
14
15
  /**
@@ -43,6 +44,9 @@ export function validateConfig(config) {
43
44
  if (config.output.sourcemap !== undefined && typeof config.output.sourcemap !== "boolean") {
44
45
  throw new Error(`░█ [ELENA]: Invalid config: "output.sourcemap" must be a boolean.`);
45
46
  }
47
+ if (config.output.filename !== undefined && typeof config.output.filename !== "string") {
48
+ throw new Error(`░█ [ELENA]: Invalid config: "output.filename" must be a string.`);
49
+ }
46
50
  }
47
51
 
48
52
  if (config.bundle !== undefined && typeof config.bundle !== "string" && config.bundle !== false) {
@@ -80,4 +84,12 @@ export function validateConfig(config) {
80
84
  if (config.banner !== undefined && typeof config.banner !== "string" && config.banner !== false) {
81
85
  throw new Error(`░█ [ELENA]: Invalid config: "banner" must be a string or false.`);
82
86
  }
87
+
88
+ if (
89
+ config.registration !== undefined &&
90
+ config.registration !== "auto" &&
91
+ config.registration !== "scoped"
92
+ ) {
93
+ throw new Error(`░█ [ELENA]: Invalid config: "registration" must be "auto" or "scoped".`);
94
+ }
83
95
  }
@@ -17,7 +17,6 @@ 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
21
  import {
23
22
  cssPlugin,
@@ -29,10 +28,118 @@ import { color } from "./common/color.js";
29
28
  import babel from "@rollup/plugin-babel";
30
29
 
31
30
  const TREESHAKE = {
32
- moduleSideEffects: false,
33
31
  propertyReadSideEffects: false,
34
32
  };
35
33
 
34
+ const DEFINE_CALL = /^\s*\w+\.define\(\);\s*$/gm;
35
+ const SIDE_EFFECT_IMPORT = /^\s*import\s+["']([^"']+)["']\s*;\s*$/gm;
36
+
37
+ /**
38
+ * Rollup plugin that strips `.define()` calls and side-effect-only
39
+ * imports that resolve to Elena component modules (files containing
40
+ * `.define()` calls). Non-component side-effect imports (polyfills,
41
+ * CSS, setup scripts) are preserved.
42
+ *
43
+ * Used with `registration: "scoped"`.
44
+ *
45
+ * @returns {import("rollup").Plugin}
46
+ */
47
+ function stripRegistrationPlugin() {
48
+ return {
49
+ name: "elena-strip-registration",
50
+ async transform(code, id) {
51
+ if (!id.endsWith(".js") && !id.endsWith(".ts")) {
52
+ return null;
53
+ }
54
+
55
+ let stripped = code.replace(DEFINE_CALL, "");
56
+
57
+ // Check each side-effect import: only strip if the resolved
58
+ // module contains a .define() call (i.e. it is a component).
59
+ const importMatches = [...stripped.matchAll(SIDE_EFFECT_IMPORT)];
60
+ for (const match of importMatches) {
61
+ const specifier = match[1];
62
+ const resolved = await this.resolve(specifier, id);
63
+ if (!resolved) {
64
+ continue;
65
+ }
66
+ const module = await this.load({ id: resolved.id });
67
+ if (module.code && /^\s*\w+\.define\(\);\s*$/m.test(module.code)) {
68
+ stripped = stripped.replace(match[0], "");
69
+ }
70
+ }
71
+
72
+ if (stripped === code) {
73
+ return null;
74
+ }
75
+ return { code: stripped, map: null };
76
+ },
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Rollup plugin that emits a `register.js` module exporting a
82
+ * `defineAll(registry?)` helper and re-exporting all component classes.
83
+ * Used with `registration: "scoped"`.
84
+ *
85
+ * @param {string} src - Source directory (e.g. `"src"`).
86
+ * @returns {import("rollup").Plugin}
87
+ */
88
+ function emitRegisterPlugin(src) {
89
+ return {
90
+ name: "elena-emit-register",
91
+ generateBundle(_, bundle) {
92
+ // Collect component modules from the emitted chunks.
93
+ const components = [];
94
+ for (const [fileName, chunk] of Object.entries(bundle)) {
95
+ if (chunk.type !== "chunk") {
96
+ continue;
97
+ }
98
+ // Check the original module source for static tagName.
99
+ for (const moduleId of Object.keys(chunk.modules)) {
100
+ const info = this.getModuleInfo(moduleId);
101
+ if (!info || !info.code) {
102
+ continue;
103
+ }
104
+ if (/static\s+tagName\s*=/.test(info.code)) {
105
+ const match = info.code.match(/(?:export\s+default\s+)?class\s+(\w+)/);
106
+ if (match) {
107
+ components.push({ className: match[1], importPath: `./${fileName}` });
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (components.length === 0) {
114
+ return;
115
+ }
116
+
117
+ const imports = components
118
+ .map(c => `import { default as ${c.className} } from "${c.importPath}";`)
119
+ .join("\n");
120
+ const definealls = components.map(c => ` ${c.className}.define(registry);`).join("\n");
121
+ const exports = components.map(c => c.className).join(", ");
122
+
123
+ const source = [
124
+ imports,
125
+ "",
126
+ `export function defineAll(registry) {`,
127
+ definealls,
128
+ `}`,
129
+ "",
130
+ `export { ${exports} };`,
131
+ "",
132
+ ].join("\n");
133
+
134
+ this.emitFile({
135
+ type: "asset",
136
+ fileName: "register.js",
137
+ source,
138
+ });
139
+ },
140
+ };
141
+ }
142
+
36
143
  /**
37
144
  * Suppress noisy Rollup warnings.
38
145
  *
@@ -49,7 +156,7 @@ function onwarn(warning, warn) {
49
156
  /**
50
157
  * Build the plugin list for a single Rollup build target.
51
158
  *
52
- * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean; target?: string | string[] | false }} opts
159
+ * @param {{ src: string; outdir: string; hasSummary: boolean; includeCssBundle: boolean; cssBundleFilename?: string; extraPlugins?: import("rollup").Plugin[]; hasTs?: boolean; target?: string | string[] | false }} opts
53
160
  * @returns {import("rollup").Plugin[]}
54
161
  */
55
162
  function buildPlugins({
@@ -57,6 +164,7 @@ function buildPlugins({
57
164
  outdir,
58
165
  hasSummary,
59
166
  includeCssBundle,
167
+ cssBundleFilename = "bundle.css",
60
168
  extraPlugins = [],
61
169
  hasTs = false,
62
170
  target = false,
@@ -87,25 +195,10 @@ function buildPlugins({
87
195
  );
88
196
  }
89
197
 
90
- plugins.push(
91
- cssStaticStylesPlugin(),
92
- minifyHtmlLiterals({
93
- options: {
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
- },
101
- },
102
- }),
103
- terser(terserOpts),
104
- cssPlugin(src)
105
- );
198
+ plugins.push(cssStaticStylesPlugin(), terser(terserOpts), cssPlugin(src));
106
199
 
107
200
  if (includeCssBundle) {
108
- plugins.push(cssBundlePlugin(src, "bundle.css"));
201
+ plugins.push(cssBundlePlugin(src, cssBundleFilename));
109
202
  }
110
203
 
111
204
  plugins.push(...extraPlugins);
@@ -129,10 +222,13 @@ export function createRollupConfig(options = {}) {
129
222
  const format = options.output?.format ?? "esm";
130
223
  const sourcemap = options.output?.sourcemap ?? true;
131
224
  let bundle = options.bundle !== undefined ? options.bundle : "src/index.js";
225
+ const bundleFilename = options.output?.filename ?? "bundle.js";
226
+ const cssBundleFilename = bundleFilename.replace(/\.js$/, ".css");
132
227
  const extraPlugins = options.plugins ?? [];
133
228
  const target = options.target ?? false;
134
229
  const terserOpts = options.terser ?? { ecma: 2020, module: true };
135
230
  const banner = options.banner || undefined;
231
+ const scoped = options.registration === "scoped";
136
232
 
137
233
  if (!existsSync(src)) {
138
234
  throw new Error(
@@ -161,19 +257,25 @@ export function createRollupConfig(options = {}) {
161
257
  );
162
258
  }
163
259
 
260
+ const scopedPlugins = scoped ? [stripRegistrationPlugin(), emitRegisterPlugin(src)] : [];
261
+
164
262
  const configs = [
165
263
  {
166
264
  input: entries,
167
- plugins: buildPlugins({
168
- src,
169
- outdir,
170
- hasSummary: false,
171
- includeCssBundle: true,
172
- extraPlugins,
173
- hasTs,
174
- target,
175
- terserOpts,
176
- }),
265
+ plugins: [
266
+ ...scopedPlugins,
267
+ ...buildPlugins({
268
+ src,
269
+ outdir,
270
+ hasSummary: false,
271
+ includeCssBundle: true,
272
+ cssBundleFilename,
273
+ extraPlugins,
274
+ hasTs,
275
+ target,
276
+ terserOpts,
277
+ }),
278
+ ],
177
279
  output: {
178
280
  ...(banner && {
179
281
  banner: chunk => (chunk.fileName === "index.js" ? banner : ""),
@@ -190,19 +292,23 @@ export function createRollupConfig(options = {}) {
190
292
  ];
191
293
 
192
294
  if (bundle) {
295
+ const bundleScoped = scoped ? [stripRegistrationPlugin()] : [];
193
296
  configs.push({
194
297
  input: bundle,
195
- plugins: buildPlugins({
196
- src,
197
- outdir,
198
- hasSummary: true,
199
- includeCssBundle: false,
200
- extraPlugins,
201
- hasTs,
202
- target,
203
- terserOpts,
204
- }),
205
- output: { banner, format, sourcemap, file: `${outdir}/bundle.js` },
298
+ plugins: [
299
+ ...bundleScoped,
300
+ ...buildPlugins({
301
+ src,
302
+ outdir,
303
+ hasSummary: true,
304
+ includeCssBundle: false,
305
+ extraPlugins,
306
+ hasTs,
307
+ target,
308
+ terserOpts,
309
+ }),
310
+ ],
311
+ output: { banner, format, sourcemap, file: `${outdir}/${bundleFilename}` },
206
312
  preserveEntrySignatures: "strict",
207
313
  treeshake: TREESHAKE,
208
314
  onwarn,