vanillaforge 1.9.0 → 1.9.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/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.9.1] - 2026-06-23
11
+
12
+ ### Fixed
13
+
14
+ #### Font data now loads lazily (`src/plugins/fonts/`)
15
+ - `font-manifests.js` converted static imports of `inter.js` and `jetbrains-mono.js`
16
+ (~244 KB of base64 font data) to dynamic `import()` inside each `dataUri()` function.
17
+ The data is only fetched the first time `fontsPlugin` is actually installed.
18
+ Previously, every `import { createApp } from 'vanillaforge'` — including projects that
19
+ never use fonts — paid the 244 KB parsing cost up front, which broke the demo and
20
+ generated apps in VS Code's built-in browser (module-load timeout / stuck loading spinner).
21
+ - `FontsService._src()`, `_buildCSS()`, and `_reinject()` are now `async` to handle the
22
+ lazy data loading. A `_ready` Promise is exposed so callers can `await svc._ready`
23
+ after installation.
24
+ - `ESLint` config updated: `AbortController` added to browser globals.
25
+
26
+ #### `autoLoadCSS` no longer hangs in file:// environments (`src/components/base-component.js`)
27
+ - Added `AbortController` with a 500 ms timeout to the `fetch HEAD` call in `autoLoadCSS`.
28
+ In environments where `fetch` never rejects (VS Code webview, `file://` protocol),
29
+ the old code left `init()` suspended indefinitely. The abort ensures the method always
30
+ returns within half a second even when the network is unavailable.
31
+
32
+ #### Demo CSS always loads in VS Code's built-in browser (`index.html`)
33
+ - Added explicit `<link>` tags for `home-component.css` and `not-found-component.css`.
34
+ All visual styling for the demo lives in those files; relying solely on `autoLoadCSS`
35
+ meant a blank/unstyled page whenever `fetch` was unavailable.
36
+
10
37
  ## [1.9.0] - 2026-06-23
11
38
 
12
39
  This release completes the public API surface and prepares the package for npm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanillaforge",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "VanillaForge - A modern, lightweight vanilla JavaScript framework for forging Single Page Applications",
5
5
  "type": "module",
6
6
  "main": "src/framework.js",
@@ -549,31 +549,41 @@ export class BaseComponent {
549
549
  */
550
550
  async autoLoadCSS() {
551
551
  if (!this.name || typeof window === 'undefined') return;
552
-
553
- // Generate CSS filename from component name
552
+
554
553
  const cssFileName = this.name.toLowerCase()
555
- .replace(/component$/, '') // Remove 'component' suffix if present
556
- .replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with hyphens
557
- .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
558
- .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
559
-
560
- // Add 'component' suffix back for CSS file naming convention
554
+ .replace(/component$/, '')
555
+ .replace(/[^a-z0-9]/g, '-')
556
+ .replace(/-+/g, '-')
557
+ .replace(/^-|-$/g, '');
558
+
561
559
  const fullCssName = cssFileName ? `${cssFileName}-component` : this.name.toLowerCase();
562
-
560
+
563
561
  const cssPaths = [
564
- `styles/components/${fullCssName}.css`, // For built version (primary)
565
- `src/styles/components/${fullCssName}.css`, // For development
566
- `styles/components/${this.name.toLowerCase()}.css`, // Fallback with exact name
567
- `src/styles/components/${this.name.toLowerCase()}.css` // Fallback development
562
+ `styles/components/${fullCssName}.css`,
563
+ `src/styles/components/${fullCssName}.css`,
564
+ `styles/components/${this.name.toLowerCase()}.css`,
565
+ `src/styles/components/${this.name.toLowerCase()}.css`,
568
566
  ];
569
-
567
+
570
568
  for (const cssPath of cssPaths) {
571
569
  try {
570
+ // HEAD check avoids a visible 404 in the browser console when
571
+ // the file simply doesn't exist (common in generated projects).
572
+ // AbortController prevents the fetch from hanging indefinitely
573
+ // in file:// environments (e.g. VS Code's built-in browser).
574
+ const controller = new AbortController();
575
+ const timeout = setTimeout(() => controller.abort(), 500);
576
+ let resp;
577
+ try {
578
+ resp = await fetch(cssPath, { method: 'HEAD', signal: controller.signal });
579
+ } finally {
580
+ clearTimeout(timeout);
581
+ }
582
+ if (!resp.ok) continue;
572
583
  await this.loadCSS(cssPath);
573
584
  this.logger.debug(`Auto-loaded CSS: ${cssPath}`);
574
- break; // Stop after first successful load
585
+ break;
575
586
  } catch (_e) {
576
- // Continue to next path - this is expected behavior
577
587
  continue;
578
588
  }
579
589
  }
package/src/framework.js CHANGED
@@ -319,5 +319,5 @@ export function createApp(config = {}) {
319
319
  }
320
320
 
321
321
  // Framework metadata
322
- export const FRAMEWORK_VERSION = '1.9.0';
322
+ export const FRAMEWORK_VERSION = '1.9.1';
323
323
  export const FRAMEWORK_NAME = 'VanillaForge';
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Built-in font manifests for VanillaForge fontsPlugin.
3
3
  *
4
- * Each manifest describes a font family. The font data is bundled directly as
5
- * base64 data URIs so no external requests or file setup are needed — the
6
- * plugin works out of the box.
4
+ * Each manifest describes a font family. Font data is loaded lazily via
5
+ * dynamic import it is only fetched when fontsPlugin is actually installed
6
+ * with a given family. This prevents ~244 KB of base64 font data from being
7
+ * eagerly parsed by every app that imports framework.js.
7
8
  *
8
9
  * Bundled fonts are Latin-subset, variable-weight woff2 files sourced from
9
10
  * the @fontsource-variable packages (open-source, MIT-licensed subsets).
@@ -15,13 +16,11 @@
15
16
  * @property {boolean} variable - True when a single file covers all weights.
16
17
  * @property {number[]} weights - [min, max] for variable fonts; list for static.
17
18
  * @property {string[]} styles - Available styles ('normal', 'italic').
18
- * @property {function(string): string|null} dataUri - Returns the bundled data URI for a style,
19
- * or null if not bundled (triggers URL path lookup instead).
19
+ * @property {function(string): Promise<string|null>} dataUri - Async function returning the
20
+ * bundled data URI for a style, or null (triggers URL path
21
+ * lookup instead).
20
22
  */
21
23
 
22
- import { INTER_NORMAL, INTER_ITALIC } from './files/inter.js';
23
- import { JETBRAINS_MONO_NORMAL, JETBRAINS_MONO_ITALIC } from './files/jetbrains-mono.js';
24
-
25
24
  /** @type {Map<string, FontManifest>} */
26
25
  export const FONT_MANIFESTS = new Map([
27
26
  [
@@ -33,7 +32,10 @@ export const FONT_MANIFESTS = new Map([
33
32
  variable: true,
34
33
  weights: [100, 900],
35
34
  styles: ['normal', 'italic'],
36
- dataUri: (style) => style === 'italic' ? INTER_ITALIC : INTER_NORMAL,
35
+ dataUri: async (style) => {
36
+ const { INTER_NORMAL, INTER_ITALIC } = await import('./files/inter.js');
37
+ return style === 'italic' ? INTER_ITALIC : INTER_NORMAL;
38
+ },
37
39
  filename: () => 'Inter-Variable.woff2',
38
40
  },
39
41
  ],
@@ -46,7 +48,10 @@ export const FONT_MANIFESTS = new Map([
46
48
  variable: true,
47
49
  weights: [100, 800],
48
50
  styles: ['normal', 'italic'],
49
- dataUri: (style) => style === 'italic' ? JETBRAINS_MONO_ITALIC : JETBRAINS_MONO_NORMAL,
51
+ dataUri: async (style) => {
52
+ const { JETBRAINS_MONO_NORMAL, JETBRAINS_MONO_ITALIC } = await import('./files/jetbrains-mono.js');
53
+ return style === 'italic' ? JETBRAINS_MONO_ITALIC : JETBRAINS_MONO_NORMAL;
54
+ },
50
55
  filename: () => 'JetBrainsMono-Variable.woff2',
51
56
  },
52
57
  ],
@@ -38,6 +38,7 @@
38
38
  * Installing JetBrains Mono sets --vf-font-mono to 'JetBrains Mono', monospace, ...
39
39
  *
40
40
  * Bundled fonts are Latin-subset, variable-weight woff2 (open-source, MIT-licensed).
41
+ * Font data is loaded lazily — it is only fetched when the plugin is actually installed.
41
42
  */
42
43
 
43
44
  import { FONT_MANIFESTS } from './font-manifests.js';
@@ -64,10 +65,13 @@ export class FontsService {
64
65
  this._manifests = new Map(FONT_MANIFESTS);
65
66
  this._loadedFamilies = [];
66
67
  this._styleEl = null;
68
+ // Resolves once @font-face CSS has been injected (after any lazy font data loads).
69
+ this._ready = Promise.resolve();
67
70
 
68
71
  const requested = options.families || [];
69
72
  if (requested.length > 0) {
70
73
  this._load(requested);
74
+ this._ready = this._reinject();
71
75
  }
72
76
  }
73
77
 
@@ -92,7 +96,7 @@ export class FontsService {
92
96
  addFamily(name, manifest) {
93
97
  this._manifests.set(name, manifest);
94
98
  this._loadOne(name, manifest, null, null);
95
- this._reinject();
99
+ this._ready = this._reinject();
96
100
  return this;
97
101
  }
98
102
 
@@ -115,8 +119,7 @@ export class FontsService {
115
119
 
116
120
  this._loadOne(name, manifest, weightFilter, styleFilter);
117
121
  }
118
-
119
- this._reinject();
122
+ // _reinject() is called by the constructor after _load()
120
123
  }
121
124
 
122
125
  _loadOne(name, manifest, weightFilter, styleFilter) {
@@ -125,10 +128,12 @@ export class FontsService {
125
128
  this._loadedFamilies.push({ name, manifest, weights, styles, cssFamily: manifest.cssFamily });
126
129
  }
127
130
 
128
- _reinject() {
131
+ async _reinject() {
129
132
  if (typeof document === 'undefined') return;
130
133
 
131
134
  if (!this._styleEl) {
135
+ // Create / reuse the style element synchronously before any await so that
136
+ // the element exists in the DOM as soon as _reinject() is called.
132
137
  this._styleEl = document.getElementById('vf-fonts');
133
138
  if (!this._styleEl) {
134
139
  this._styleEl = document.createElement('style');
@@ -137,23 +142,24 @@ export class FontsService {
137
142
  }
138
143
  }
139
144
 
140
- this._styleEl.textContent = this._buildCSS();
145
+ // Build CSS — may trigger lazy font data imports (async).
146
+ this._styleEl.textContent = await this._buildCSS();
141
147
  }
142
148
 
143
- _buildCSS() {
149
+ async _buildCSS() {
144
150
  const blocks = [];
145
151
 
146
152
  for (const { manifest, weights, styles, cssFamily } of this._loadedFamilies) {
147
153
  if (manifest.variable) {
148
154
  const [minW, maxW] = [Math.min(...weights), Math.max(...weights)];
149
155
  for (const style of styles) {
150
- const src = this._src(manifest, null, style);
156
+ const src = await this._src(manifest, null, style);
151
157
  blocks.push(this._block(cssFamily, style, `${minW} ${maxW}`, src));
152
158
  }
153
159
  } else {
154
160
  for (const weight of weights) {
155
161
  for (const style of styles) {
156
- const src = this._src(manifest, weight, style);
162
+ const src = await this._src(manifest, weight, style);
157
163
  blocks.push(this._block(cssFamily, style, String(weight), src));
158
164
  }
159
165
  }
@@ -169,9 +175,9 @@ export class FontsService {
169
175
  * - a path was explicitly provided, OR
170
176
  * - the manifest has no dataUri function (custom family).
171
177
  */
172
- _src(manifest, weight, style) {
178
+ async _src(manifest, weight, style) {
173
179
  if (!this._path && typeof manifest.dataUri === 'function') {
174
- const uri = manifest.dataUri(style);
180
+ const uri = await manifest.dataUri(style);
175
181
  if (uri) return `url('${uri}') format('woff2')`;
176
182
  }
177
183
 
@@ -231,7 +237,7 @@ export class FontsService {
231
237
  * display {string} - CSS font-display value (default 'swap').
232
238
  *
233
239
  * Side effects on install:
234
- * - Injects <style id="vf-fonts"> with @font-face declarations.
240
+ * - Injects <style id="vf-fonts"> with @font-face declarations (async, after dynamic import).
235
241
  * - Registers FontsService under 'fonts' (app.get('fonts')).
236
242
  * - If themePlugin is installed, updates --vf-font-sans / --vf-font-mono.
237
243
  */