Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion litmus/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
manifest.scss
manifest.scss
dist-vite
20 changes: 20 additions & 0 deletions litmus/index.vite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no"
/>
<title>Cx - Litmus (Vite)</title>
<link
href="https://fonts.googleapis.com/css?family=Open+Sans"
rel="stylesheet"
type="text/css"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/index.vite.js"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions litmus/index.vite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Widget, startAppLoop, History } from "cx/ui";
import { Timing, Debug } from "cx/util";
import { Store } from "cx/data";
import "./error";

import "./index.scss";

// Vite entry point. Change the import below to test a different demo,
// or keep it in sync with index.js (the webpack entry).
import Demo from "./features/charts/line-graph/SmoothingOvershoot";

let store = (window.store = new Store());

Widget.resetCounter();
Timing.enable("app-loop");
Debug.enable("app-data");

History.connect(store, "url");

let stop;

if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose((data) => {
data.state = store.getData();
if (stop) stop();
});
if (import.meta.hot.data?.state) store.load(import.meta.hot.data.state);
}

stop = startAppLoop(document.getElementById("app"), store, Demo);
6 changes: 5 additions & 1 deletion litmus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"version": "1.0.0",
"scripts": {
"start": "webpack-dev-server --open",
"start:prod": "webpack serve --mode production --open"
"start:prod": "webpack serve --mode production --open",
"start:vite": "vite",
"build:vite": "vite build"
},
"dependencies": {
"casual": "^1.6.2",
Expand All @@ -30,6 +32,7 @@
"babel-preset-cx-env": "workspace:*",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"cx-scss-manifest-vite-plugin": "workspace:*",
"cx-scss-manifest-webpack-plugin": "workspace:*",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.5",
Expand All @@ -46,6 +49,7 @@
"ts-loader": "^9.5.1",
"typescript": "^5.9.3",
"url-loader": "^4.1.1",
"vite": "^8.0.0",
"webpack": "^5.103.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cleanup-plugin": "^0.5.1",
Expand Down
67 changes: 67 additions & 0 deletions litmus/vite.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { defineConfig } from "vite";
import { transform } from "esbuild";
import path from "path";
import { fileURLToPath } from "url";
import cxScssManifest from "cx-scss-manifest-vite-plugin";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// run against live CxJS sources instead of the compiled build output
const cxSrc = path.resolve(__dirname, "../packages/cx/src").replace(/\\/g, "/");

export default defineConfig(({ command }) => ({
plugins: [
cxScssManifest({
outputPath: path.join(__dirname, "manifest.scss"),
}),
{
// litmus examples use JSX inside .js files, which rolldown's built-in
// transform doesn't support, so those files go through esbuild instead
name: "litmus-js-jsx",
enforce: "pre",
async transform(code, id) {
const file = id.split("?")[0];
if (!file.endsWith(".js") || file.includes("node_modules")) return null;
const result = await transform(code, {
loader: "jsx",
jsx: "automatic",
jsxImportSource: "cx",
jsxDev: command === "serve",
sourcemap: true,
});
return { code: result.code, map: result.map };
},
},
],
oxc: {
jsx: {
runtime: "automatic",
importSource: "cx",
},
},
resolve: {
alias: [
{ find: /^cx\/src\/(.*)$/, replacement: `${cxSrc}/$1` },
{ find: /^cx\/(jsx-dev-runtime|jsx-runtime)$/, replacement: `${cxSrc}/$1.ts` },
{ find: /^cx\/([\w-]+)$/, replacement: `${cxSrc}/$1/index.ts` },
{ find: /^cx\/([\w-]+)\/(.*)$/, replacement: `${cxSrc}/$1/$2` },
],
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api", "import", "global-builtin", "slash-div"],
},
},
},
server: {
port: 8090,
open: "/index.vite.html",
},
build: {
outDir: "dist-vite",
rollupOptions: {
input: path.join(__dirname, "index.vite.html"),
},
},
}));
7 changes: 7 additions & 0 deletions packages/cx-scss-manifest-vite-plugin/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2026 Codaxy d.o.o. Banja Luka

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
91 changes: 91 additions & 0 deletions packages/cx-scss-manifest-vite-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# cx-scss-manifest-vite-plugin

A Vite plugin that analyzes your application's source code and generates an SCSS manifest file
to include only the CxJS widget styles that are actually used. For smaller apps, this can reduce
the generated CSS by 70-90%.

This is the Vite counterpart of [cx-scss-manifest-webpack-plugin](../cx-scss-manifest-webpack-plugin).

## How It Works

CxJS ships SCSS for every widget (Button, Grid, Window, etc.). By default, all widget styles are
included during SCSS compilation. This plugin scans the import statements of your application's
modules to determine which CxJS modules your app actually uses, then generates a manifest that
tells the SCSS compiler to include only the styles for those modules.

The generated `manifest.scss` looks like:

```scss
@use "cx/src/util/scss/include.scss" as * with ($cx-include-all: false);

@include cx-widgets(
"cx/widgets/Button",
"cx/widgets/Grid",
"cx/widgets/HtmlElement"
);
```

## Installation

```bash
npm install cx-scss-manifest-vite-plugin --save-dev
```

## Usage

Add the plugin to your `vite.config.js`:

```js
import { defineConfig } from "vite";
import cxScssManifest from "cx-scss-manifest-vite-plugin";
import path from "path";

export default defineConfig({
plugins: [
cxScssManifest({
outputPath: path.join(__dirname, "manifest.scss"),
}),
],
});
```

Then import the manifest **before** the CxJS SCSS in your main stylesheet:

```scss
@use "manifest";
@use "cx/src/index";
```

The import order matters. The manifest must be loaded first so it configures the SCSS
module system before any widget styles are compiled.

### With a Theme

When using a SCSS theme, import the manifest before the theme:

```scss
@use "manifest";
@use "cx-theme-aquamarine/src/index";
```

The theme internally loads `cx/src/index`, so you don't need to import it separately.

## Important

The generated `manifest.scss` should be checked into version control for apps that use
`vite build` — SCSS is compiled in the same pass, so the manifest from the _previous_
build is what actually takes effect. Starting from an empty manifest would ship a build
with incomplete styles. For dev-only environments, gitignoring the file is fine; the
worst case is an unstyled first paint in a fresh clone until the manifest fills up.

- **Dev server:** modules are transformed on demand, so manifest entries are collected as
pages are visited. Whenever a new CxJS widget is detected, the manifest is updated and
Vite hot-reloads the stylesheet automatically.
- **Production build:** the full module graph is scanned, so all imports (including lazy
routes) are detected. If the manifest changes during a build, the plugin emits a
warning; run the build again to apply the changes.

Entries are only ever added, never removed — existing entries are re-read from the file
on startup, so a dev session that visits only a few pages never shrinks the manifest.
To reset it (e.g. after removing widgets from the app), delete the entries from
`manifest.scss` and run a production build.
24 changes: 24 additions & 0 deletions packages/cx-scss-manifest-vite-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "cx-scss-manifest-vite-plugin",
"version": "26.8.0",
"description": "Vite plugin that inspects app's source code and generates a manifest file for importing only required elements of CxJS SCSS",
"main": "src/index.js",
"types": "src/index.d.ts",
"author": "Codaxy",
"license": "MIT",
"bugs": {
"url": "https://github.com/codaxy/cxjs"
},
"homepage": "https://github.com/codaxy/cxjs",
"dependencies": {
"es-module-lexer": "^1.5.0"
},
"peerDependencies": {
"cx": "*",
"vite": ">=4.0.0"
},
"repository": {
"type": "git",
"url": "git@github.com:codaxy/cx.git"
}
}
12 changes: 12 additions & 0 deletions packages/cx-scss-manifest-vite-plugin/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Plugin } from "vite";

declare namespace cxScssManifestPlugin {
interface Options {
/** Path of the manifest.scss file to generate. */
outputPath: string;
}
}

declare function cxScssManifestPlugin(options: cxScssManifestPlugin.Options): Plugin;

export = cxScssManifestPlugin;
Loading
Loading