diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e547837..42c3d972 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## v1.45.0
+
+_29 dec 2025_
+
+- Added support for defining types for custom plugins on the Blits component definitions
+- Added importable type definitions for built-in plugins
+- Added support for JS sourcemaps
+- Added reactivity for shader props (individual shaders, not effects via dynamic shader)
+- Fixed issue with usage of Blits plugins not working in for loops
+- Improved test coverage
+
## v1.44.0
_20 Nov 2025_
diff --git a/docs/plugins/global_app_state.md b/docs/plugins/global_app_state.md
index f3c30b04..f7008c18 100644
--- a/docs/plugins/global_app_state.md
+++ b/docs/plugins/global_app_state.md
@@ -48,6 +48,20 @@ When registering the Global App State plugin, you pass it a state object as the
The newly created Global App state works exactly the same as an internal Component state. You can read values and you can directly change values. And when you do change a value, it automatically triggers reactive updates. Either via reactive attributes in the template, or in the form of watchers / computed values in the Component logic.
+## TypeScript Support
+
+Enable autocomplete and type inference for the App State plugin by adding a `blits.d.ts` file in the root folder of your app project:
+
+```typescript
+import type { AppStatePlugin } from '@lightningjs/blits/plugins/appstate'
+
+declare module '@lightningjs/blits' {
+ interface CustomComponentProperties {
+ $appState?: AppStatePlugin
+ }
+}
+```
+
### Using global app state in a Component
Any variable in the Global App state can be used directly in the template, much like a local Component state. Changing values in the global app state also works exactly the same as updating the internal component state.
diff --git a/docs/plugins/language.md b/docs/plugins/language.md
index 538cab95..e60845a7 100644
--- a/docs/plugins/language.md
+++ b/docs/plugins/language.md
@@ -41,6 +41,20 @@ The Language plugin accepts an optional configuration object with 2 keys:
After registration of the Language plugin, it will be available in each Blits Component as `this.$language`.
+## TypeScript Support
+
+Enable autocomplete and type inference for the Language plugin by adding a `blits.d.ts` file in the root folder of your app project:
+
+```typescript
+import type { LanguagePlugin } from '@lightningjs/blits/plugins/language'
+
+declare module '@lightningjs/blits' {
+ interface CustomComponentProperties {
+ $language?: LanguagePlugin
+ }
+}
+```
+
## Translations file
The most common way of defining a set of translations, is to use a dedicated JSON file, modeled after
diff --git a/docs/plugins/storage.md b/docs/plugins/storage.md
index a147b29b..e2517a7d 100644
--- a/docs/plugins/storage.md
+++ b/docs/plugins/storage.md
@@ -35,6 +35,20 @@ Within the application we can call the storage plugin methods as below
this.$storage.get(key, value)
```
+## TypeScript Support
+
+Enable autocomplete and type inference for the Storage plugin by adding a `blits.d.ts` file in the root folder of your app project:
+
+```typescript
+import type { StoragePlugin } from '@lightningjs/blits/plugins/storage'
+
+declare module '@lightningjs/blits' {
+ interface CustomComponentProperties {
+ $storage?: StoragePlugin
+ }
+}
+```
+
## Available methods
### set
diff --git a/docs/plugins/theme.md b/docs/plugins/theme.md
index ff7a073d..dd06623e 100644
--- a/docs/plugins/theme.md
+++ b/docs/plugins/theme.md
@@ -114,6 +114,20 @@ Blits.Plugin(theme, {
In the definition above we've specified 3 different themes: `base`, `dark` and `large`. The dark and large theme are not complete definitions,
which means that they will inherit missing values from the base theme.
+## TypeScript Support
+
+Enable autocomplete and type inference for the Theme plugin by adding a `blits.d.ts` file in the root folder of your app project:
+
+```typescript
+import type { ThemePlugin } from '@lightningjs/blits/plugins/theme'
+
+declare module '@lightningjs/blits' {
+ interface CustomComponentProperties {
+ $theme?: ThemePlugin
+ }
+}
+```
+
## Getting theme values
diff --git a/index.d.ts b/index.d.ts
index 0bd00395..e56935ff 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -18,10 +18,10 @@
// blits file type reference
///
-import {type ShaderEffect as RendererShaderEffect, type WebGlCoreShader, type RendererMainSettings} from '@lightningjs/renderer'
-
declare module '@lightningjs/blits' {
-
+ type RendererShaderEffect = import('@lightningjs/renderer').ShaderEffect
+ type WebGlCoreShader = import('@lightningjs/renderer').WebGlCoreShader
+ type RendererMainSettings = import('@lightningjs/renderer').RendererMainSettings
export interface AnnouncerUtteranceOptions {
/**
@@ -209,7 +209,7 @@ declare module '@lightningjs/blits' {
}
export interface Input {
- [key: string]: (event: KeyboardEvent) => void | undefined | unknown,
+ [key: string]: ((event: KeyboardEvent) => unknown) | undefined,
/**
* Catch all input function
*
@@ -362,7 +362,39 @@ declare module '@lightningjs/blits' {
}
}
- export type ComponentBase = {
+ // Extension point for app- and plugin-specific fields on the component `this`.
+ // Add your own properties (e.g., `$telemetry`, `componentName`) via TypeScript
+ // module augmentation in your app, without changing core types.
+ // Note: `ComponentBase` extends this interface, so augmented fields appear in all
+ // hooks, methods, input, computed, and watch.
+ export interface CustomComponentProperties {
+ // Empty by design: extend in your app via TypeScript module augmentation.
+ }
+
+ export interface LanguagePlugin {
+ translate(key: string, ...replacements: any[]): string
+ readonly language: string
+ set(language: string): void
+ translations(translationsObject: Record): void
+ load(file: string): Promise
+ }
+
+ export interface ThemePlugin {
+ get(key: string): T | undefined
+ get(key: string, fallback: T): T
+ set(theme: string): void
+ }
+
+ export interface StoragePlugin {
+ get(key: string): T | null
+ set(key: string, value: unknown): boolean
+ remove(key: string): void
+ clear(): void
+ }
+
+ export type AppStatePlugin = Record> = TState
+
+ export interface ComponentBase extends CustomComponentProperties {
/**
* Indicates whether the component currently has focus
*
@@ -624,7 +656,7 @@ declare module '@lightningjs/blits' {
}
export interface RouterHooks {
- init?: () => Promise<> | void;
+ init?: () => Promise | void;
beforeEach?: (to: Route, from: Route) => string | Route | Promise | void;
error?: (err: string) => string | Route | Promise | void;
}
diff --git a/package-lock.json b/package-lock.json
index 11e7820d..585c9145 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@lightningjs/blits",
- "version": "1.44.0",
+ "version": "1.45.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@lightningjs/blits",
- "version": "1.44.0",
+ "version": "1.45.0",
"license": "Apache-2.0",
"dependencies": {
"@lightningjs/msdf-generator": "^1.2.0",
@@ -30,6 +30,7 @@
"husky": "^9.1.7",
"jsdom": "24.0.0",
"lint-staged": "^15.5.0",
+ "magic-string": "^0.30.21",
"prettier": "^3.6.2",
"sinon": "^21.0.0",
"tap-diff": "^0.1.1",
@@ -1147,9 +1148,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
@@ -4930,6 +4931,16 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
diff --git a/package.json b/package.json
index cceb4663..9de6f537 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@lightningjs/blits",
- "version": "1.44.0",
+ "version": "1.45.0",
"description": "Blits: The Lightning 3 App Development Framework",
"bin": "bin/index.js",
"exports": {
@@ -8,7 +8,26 @@
"./vite": "./vite/index.js",
"./transitions": "./src/router/transitions/index.js",
"./precompiler": "./src/lib/precompiler/precompiler.js",
- "./plugins": "./src/plugins/index.js",
+ "./plugins": {
+ "types": "./src/plugins/index.d.ts",
+ "default": "./src/plugins/index.js"
+ },
+ "./plugins/language": {
+ "types": "./src/plugins/language.d.ts",
+ "default": "./src/plugins/language.js"
+ },
+ "./plugins/theme": {
+ "types": "./src/plugins/theme.d.ts",
+ "default": "./src/plugins/theme.js"
+ },
+ "./plugins/appstate": {
+ "types": "./src/plugins/appstate.d.ts",
+ "default": "./src/plugins/appstate.js"
+ },
+ "./plugins/storage": {
+ "types": "./src/plugins/storage/storage.d.ts",
+ "default": "./src/plugins/storage/storage.js"
+ },
"./symbols": "./src/lib/symbols.js",
"./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
},
@@ -48,6 +67,7 @@
"husky": "^9.1.7",
"jsdom": "24.0.0",
"lint-staged": "^15.5.0",
+ "magic-string": "^0.30.21",
"prettier": "^3.6.2",
"sinon": "^21.0.0",
"tap-diff": "^0.1.1",
diff --git a/src/components/Sprite.js b/src/components/Sprite.js
index 8014c1cd..8801ba35 100644
--- a/src/components/Sprite.js
+++ b/src/components/Sprite.js
@@ -61,15 +61,20 @@ export default () =>
// Resolve frame data from sprite map
let options = null
if (
- this.map !== undefined &&
this.map !== null &&
- this.frame !== undefined &&
- this.frame !== null
+ this.map !== undefined &&
+ this.frame !== null &&
+ this.frame !== undefined
) {
- options =
- 'frames' in this.map
- ? Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
- : this.map[this.frame]
+ if (
+ this.map.frames !== null &&
+ this.map.frames !== undefined &&
+ this.frame in this.map.frames
+ ) {
+ options = Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
+ } else if (this.frame in this.map) {
+ options = this.map[this.frame]
+ }
}
// If no map but frame is object (manual subtexture)
diff --git a/src/components/Sprite.test.js b/src/components/Sprite.test.js
new file mode 100644
index 00000000..3e9c5319
--- /dev/null
+++ b/src/components/Sprite.test.js
@@ -0,0 +1,182 @@
+import test from 'tape'
+import Sprite from './Sprite.js'
+import symbols from '../lib/symbols.js'
+import { initLog } from '../lib/log.js'
+import { stage, renderer } from '../launch.js'
+import { renderer as engineRenderer } from '../engines/L3/launch.js'
+import element from '../engines/L3/element.js'
+import { EventEmitter } from 'node:events'
+
+initLog()
+
+stage.element = element
+Object.assign(renderer, engineRenderer)
+engineRenderer.createNode = () => new EventEmitter()
+
+function createSprite() {
+ return Sprite()({}, { node: { width: 1920, height: 1080 } })
+}
+
+test('Sprite - Type', (assert) => {
+ assert.equal(typeof Sprite, 'function', 'Sprite should be a function')
+ assert.equal(typeof Sprite(), 'function', 'Sprite() should return a function')
+ assert.end()
+})
+
+test('Sprite - Initialization', (assert) => {
+ const sprite = createSprite()
+ const props = sprite[symbols.props]
+ const state = sprite[symbols.state]
+
+ assert.equal(props.image, undefined, 'image prop should be undefined initially')
+ assert.equal(state.spriteTexture, null, 'spriteTexture should be null initially')
+ assert.equal(state.currentSrc, null, 'currentSrc should be null initially')
+ assert.end()
+})
+
+test('Sprite - Texture returns null for invalid inputs', (assert) => {
+ const sprite = createSprite()
+
+ sprite[symbols.props].image = undefined
+ assert.equal(sprite.texture, null, 'texture should be null when image is undefined')
+
+ sprite[symbols.props].image = null
+ assert.equal(sprite.texture, null, 'texture should be null when image is null')
+
+ sprite[symbols.props].image = 'test.png'
+ const originalCreateTexture = renderer.createTexture
+ delete renderer.createTexture
+ assert.equal(
+ sprite.texture,
+ null,
+ 'texture should be null when renderer.createTexture is missing'
+ )
+
+ renderer.createTexture = originalCreateTexture
+ assert.end()
+})
+
+test('Sprite - Texture creates and reuses ImageTexture', (assert) => {
+ const mockTex = {}
+ let calls = 0
+ const original = renderer.createTexture
+ try {
+ renderer.createTexture = () => (calls++, mockTex)
+
+ const sprite = createSprite()
+ sprite[symbols.props].image = 'test.png'
+ assert.ok(sprite.texture !== null, 'texture should not be null when image is set')
+ assert.equal(
+ sprite[symbols.state].currentSrc,
+ 'test.png',
+ 'currentSrc should be set to image path'
+ )
+ assert.equal(calls, 1, 'createTexture should be called once for first image')
+
+ sprite[symbols.props].image = 'test2.png'
+ sprite.texture
+ assert.equal(calls, 2, 'createTexture should be called again when image changes')
+ } finally {
+ renderer.createTexture = original
+ }
+ assert.end()
+})
+
+test('Sprite - Texture creates SubTexture with map and frame', (assert) => {
+ const imgTex = {}
+ const subTex = {}
+ let imgCalls = 0
+ const original = renderer.createTexture
+ try {
+ renderer.createTexture = (type) => (type === 'ImageTexture' ? (imgCalls++, imgTex) : subTex)
+
+ const sprite = createSprite()
+ sprite[symbols.props].image = 'sheet.png'
+ sprite.texture
+ const baseTex = sprite[symbols.state].spriteTexture
+
+ // map.frames, map.frame1, missing w/h, manual frame object
+ const cases = [
+ {
+ map: { frames: { f1: { x: 10, y: 20, w: 50, h: 60 } }, defaults: { w: 100, h: 100 } },
+ frame: 'f1',
+ },
+ { map: { f1: { x: 5, y: 10, w: 30, h: 40 } }, frame: 'f1' },
+ {
+ map: { frames: { f1: { x: 10, y: 20, w: 50 } }, defaults: { w: 100, h: 100 } },
+ frame: 'f1',
+ },
+ { map: null, frame: { x: 15, y: 25, w: 35, h: 45 } },
+ ]
+
+ cases.forEach(({ map, frame }) => {
+ sprite[symbols.props].map = map
+ sprite[symbols.props].frame = frame
+ assert.equal(
+ sprite.texture,
+ subTex,
+ 'texture should be SubTexture when map and frame are set'
+ )
+ assert.equal(
+ sprite[symbols.state].currentSrc,
+ 'sheet.png',
+ 'currentSrc should remain unchanged'
+ )
+ assert.equal(imgCalls, 1, 'ImageTexture should be created only once')
+ assert.equal(sprite[symbols.state].spriteTexture, baseTex, 'spriteTexture should be reused')
+ })
+ } finally {
+ renderer.createTexture = original
+ }
+ assert.end()
+})
+
+test('Sprite - Texture returns spriteTexture when no frame', (assert) => {
+ const mockTex = {}
+ const mockSubTex = {}
+ let subTexCalls = 0
+ const original = renderer.createTexture
+ try {
+ renderer.createTexture = (type) => {
+ if (type === 'ImageTexture') return mockTex
+ if (type === 'SubTexture') {
+ subTexCalls++
+ return mockSubTex
+ }
+ return null
+ }
+
+ const sprite = createSprite()
+ sprite[symbols.props].image = 'test.png'
+
+ // Case 1: No frame used → must NOT create SubTexture
+ sprite[symbols.props].map = null
+ sprite[symbols.props].frame = null
+ assert.ok(sprite.texture !== null, 'texture should not be null when no frame is set')
+ assert.equal(subTexCalls, 0, 'should not create SubTexture when no frame')
+
+ // Case 2: Invalid frame in map.frames → should NOT create SubTexture
+ sprite[symbols.props].map = { frames: { f1: { x: 10, y: 20, w: 50, h: 60 } } }
+ sprite[symbols.props].frame = 'nonexistent'
+ sprite.texture // trigger computation
+ assert.equal(
+ subTexCalls,
+ 0,
+ 'should NOT create SubTexture for nonexistent frame in map.frames structure'
+ )
+
+ // Case 3: Invalid frame in direct map → should NOT create additional SubTexture
+ sprite[symbols.props].map = { f1: { x: 10, y: 20, w: 50, h: 60 } }
+ sprite[symbols.props].frame = 'nonexistent'
+ const finalTexture = sprite.texture
+ assert.ok(finalTexture !== null, 'texture should not be null')
+ assert.equal(
+ subTexCalls,
+ 0,
+ 'should NOT create additional SubTexture for nonexistent frame in direct map'
+ )
+ } finally {
+ renderer.createTexture = original
+ }
+ assert.end()
+})
diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js
index 1f400c7b..1908af51 100644
--- a/src/engines/L3/element.js
+++ b/src/engines/L3/element.js
@@ -380,10 +380,21 @@ const propsTransformer = {
}
},
set shader(v) {
- if (v !== null) {
- this.props['shader'] = renderer.createShader(v.type, v.props)
- } else {
- this.props['shader'] = renderer.createShader('DefaultShader')
+ const target = this.element.node !== undefined ? this.element.node : this.props
+
+ if (v === null) {
+ target['shader'] = null
+ return
+ }
+
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
+ if (target.shader !== undefined && target.shader.type === v.type) {
+ for (const prop in v.props) {
+ target.shader.props[prop] = v.props[prop]
+ }
+ return
+ }
+ target['shader'] = renderer.createShader(v.type, v.props)
}
},
set effects(v) {
diff --git a/src/lib/codegenerator/generator.js b/src/lib/codegenerator/generator.js
index 2390137f..d4c13a7b 100644
--- a/src/lib/codegenerator/generator.js
+++ b/src/lib/codegenerator/generator.js
@@ -409,7 +409,7 @@ const generateForLoopCode = function (templateObject, parent) {
.replace(')', '')
.split(/\s*,\s*/)
- const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)(\\w+))`, 'gi')
+ const scopeRegex = new RegExp(`(scope\\.(?!${item}\\.|${index}|key)([\\w$]+))`, 'gi')
// local context
const ctx = {
diff --git a/src/lib/precompiler/precompiler.js b/src/lib/precompiler/precompiler.js
index c29e05ad..7552b20f 100644
--- a/src/lib/precompiler/precompiler.js
+++ b/src/lib/precompiler/precompiler.js
@@ -17,6 +17,7 @@
import parser from '../templateparser/parser.js'
import generator from '../codegenerator/generator.js'
+import MagicString from 'magic-string'
export default (source, filePath, mode) => {
if (
@@ -26,12 +27,9 @@ export default (source, filePath, mode) => {
/\{.*?template\s*:\s*(['"`])((?:\\?.)*?)\1.*?\}/s.test(source) // object with template key
) {
const templates = source.matchAll(/(? {
// Only process if it looks like a Blits template
if (templateContent.match(/^\s*(|<[A-Za-z][^>]*>)/s)) {
- const templateStartIndex = template.index + offset
+ // Use original indices - MagicString handles position tracking automatically
+ const templateStartIndex = template.index
const templateEndIndex = templateStartIndex + template[0].length
// Parse the template
@@ -58,16 +57,26 @@ export default (source, filePath, mode) => {
(fn) => fn.toString()
)}], context: {}}`
- offset += replacement.length - template[0].length
-
- newSource =
- newSource.substring(0, templateStartIndex) +
- replacement +
- newSource.substring(templateEndIndex)
+ // MagicString tracks all changes automatically, no offset needed
+ s.overwrite(templateStartIndex, templateEndIndex, replacement)
}
}
}
- return newSource
+
+ const newSource = s.toString()
+
+ if (newSource !== source) {
+ const fileName = filePath.split(/[\\/]/).pop()
+ return {
+ code: newSource,
+ map: s.generateMap({
+ hires: true,
+ source: fileName,
+ includeContent: true,
+ file: fileName,
+ }),
+ }
+ }
}
return source
}
diff --git a/src/lib/reactivityguard/computedprops.js b/src/lib/reactivityguard/computedprops.js
index 8f3248f1..90ced535 100644
--- a/src/lib/reactivityguard/computedprops.js
+++ b/src/lib/reactivityguard/computedprops.js
@@ -213,7 +213,12 @@ export default (code) => {
modifiedCode = modifiedCode.replace(mod.original, mod.replacement)
}
- return { code: modifiedCode }
+ // Returning code without a source map
+ // Vite will automatically chain this with the next plugin's source map (preCompiler)
+ return {
+ code: modifiedCode,
+ map: { mappings: '' },
+ }
}
return null
diff --git a/src/plugins/appstate.d.ts b/src/plugins/appstate.d.ts
new file mode 100644
index 00000000..9145c943
--- /dev/null
+++ b/src/plugins/appstate.d.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Comcast Cable Communications Management, LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { AppStatePlugin } from '@lightningjs/blits'
+
+// Re-export AppStatePlugin for direct imports
+export type { AppStatePlugin }
+
+declare const appState: {
+ readonly name: 'appState'
+ plugin: >(state?: TState) => AppStatePlugin
+}
+
+export default appState
+
diff --git a/src/plugins/index.d.ts b/src/plugins/index.d.ts
new file mode 100644
index 00000000..c49975e8
--- /dev/null
+++ b/src/plugins/index.d.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Comcast Cable Communications Management, LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { default as language } from './language.js'
+export { default as theme } from './theme.js'
+export { default as appState } from './appstate.js'
+export { default as storage } from './storage/storage.js'
+
+// Re-export plugin interfaces for convenience
+export type { LanguagePlugin, ThemePlugin, StoragePlugin, AppStatePlugin } from '@lightningjs/blits'
+
+// Re-export option types
+export type { LanguagePluginOptions } from './language.js'
+export type { ThemePluginConfig } from './theme.js'
+
diff --git a/src/plugins/language.d.ts b/src/plugins/language.d.ts
new file mode 100644
index 00000000..70ba2fa3
--- /dev/null
+++ b/src/plugins/language.d.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 Comcast Cable Communications Management, LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { LanguagePlugin } from '@lightningjs/blits'
+
+export interface LanguagePluginOptions {
+ file?: string
+ language?: string
+}
+
+// Re-export LanguagePlugin for direct imports
+export type { LanguagePlugin }
+
+declare const language: {
+ readonly name: 'language'
+ plugin: (options?: LanguagePluginOptions) => LanguagePlugin
+}
+
+export default language
+
diff --git a/src/plugins/storage/storage.d.ts b/src/plugins/storage/storage.d.ts
new file mode 100644
index 00000000..b29d20e0
--- /dev/null
+++ b/src/plugins/storage/storage.d.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Comcast Cable Communications Management, LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { StoragePlugin } from '@lightningjs/blits'
+
+// Re-export StoragePlugin for direct imports
+export type { StoragePlugin }
+
+declare const storage: {
+ readonly name: 'storage'
+ plugin: () => StoragePlugin
+}
+
+export default storage
+
diff --git a/src/plugins/theme.d.ts b/src/plugins/theme.d.ts
new file mode 100644
index 00000000..6e5a850f
--- /dev/null
+++ b/src/plugins/theme.d.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 Comcast Cable Communications Management, LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { ThemePlugin } from '@lightningjs/blits'
+
+export interface ThemePluginConfig {
+ themes?: Record>
+ current?: string
+ base?: string
+}
+
+// Re-export ThemePlugin for direct imports
+export type { ThemePlugin }
+
+declare const theme: {
+ readonly name: 'theme'
+ plugin: (config?: ThemePluginConfig | Record) => ThemePlugin
+}
+
+export default theme
+
diff --git a/vite/preCompiler.js b/vite/preCompiler.js
index 176bb96d..e4d59828 100644
--- a/vite/preCompiler.js
+++ b/vite/preCompiler.js
@@ -32,12 +32,22 @@ export default function () {
// we should only precompile .blits, .js and .ts files
if (
- fileExtension === '.js' ||
- fileExtension === '.ts' ||
- fileExtension === '.blits' ||
- fileExtension === '.mjs'
+ filePath.indexOf('node_modules') === -1 &&
+ (fileExtension === '.js' ||
+ fileExtension === '.ts' ||
+ fileExtension === '.blits' ||
+ fileExtension === '.mjs')
) {
- return compiler(source, filePath, config.mode)
+ const result = compiler(source, filePath, config.mode)
+
+ if (typeof result === 'object') {
+ return {
+ code: result.code,
+ map: result.map,
+ }
+ }
+ // No transformation needed - return null so Vite knows no changes were made
+ return null
}
// vite expects null if there is no modification