├── packages
├── forgecss
│ ├── .gitignore
│ ├── .npmignore
│ ├── fx.d.ts
│ ├── tsconfig.json
│ ├── fx.js
│ ├── lib
│ │ ├── constants.js
│ │ ├── forge-lang
│ │ │ ├── utils.js
│ │ │ ├── constants.js
│ │ │ ├── Compiler.js
│ │ │ └── Parser.js
│ │ ├── usages.js
│ │ ├── fx.js
│ │ ├── inventory.js
│ │ └── helpers.js
│ ├── tests
│ │ ├── cases
│ │ │ ├── 03-media-query
│ │ │ │ ├── src
│ │ │ │ │ ├── styles.css
│ │ │ │ │ ├── page.html
│ │ │ │ │ └── another.html
│ │ │ │ ├── expected.css
│ │ │ │ └── test.js
│ │ │ ├── 01-usages
│ │ │ │ ├── src
│ │ │ │ │ ├── page.html
│ │ │ │ │ └── page.tsx
│ │ │ │ └── test.js
│ │ │ ├── 06-apply
│ │ │ │ └── test.js
│ │ │ ├── 02-fx
│ │ │ │ └── test.js
│ │ │ ├── 04-pseudo
│ │ │ │ └── test.js
│ │ │ ├── 07-bundleAll
│ │ │ │ └── test.js
│ │ │ ├── 05-arbitrary
│ │ │ │ └── test.js
│ │ │ └── 00-lang
│ │ │ │ └── test.js
│ │ ├── run.js
│ │ └── helpers.js
│ ├── index.d.ts
│ ├── scripts
│ │ └── build.js
│ ├── index.fx.js
│ ├── package.json
│ ├── dist
│ │ └── client.min.js
│ ├── index.cli.js
│ ├── index.browser.js
│ └── index.js
└── site
│ ├── tsconfig.json
│ ├── src
│ ├── main.tsx
│ ├── utils
│ │ └── transformHtmlClassAttributes.ts
│ ├── constants.ts
│ ├── Editor.tsx
│ ├── Syntax.tsx
│ ├── index.css
│ └── App.tsx
│ ├── vite.config.ts
│ ├── forgecss.config.json
│ ├── public
│ ├── code.svg
│ ├── align-left.svg
│ ├── github.svg
│ ├── sliders.svg
│ ├── prism.css
│ ├── forgecss.svg
│ ├── styles.css
│ ├── input.svg
│ ├── output.svg
│ └── prism.js
│ ├── .gitignore
│ ├── eslint.config.js
│ ├── tsconfig.node.json
│ ├── tsconfig.app.json
│ ├── index.html
│ ├── package.json
│ └── README.md
├── examples
├── react
│ ├── tsconfig.json
│ ├── forgecss.config.js
│ ├── vite.config.ts
│ ├── src
│ │ ├── main.tsx
│ │ ├── forgecss.css
│ │ ├── index.css
│ │ └── App.tsx
│ ├── .gitignore
│ ├── index.html
│ ├── eslint.config.js
│ ├── tsconfig.node.json
│ ├── tsconfig.app.json
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── README.md
│ └── ast.json
└── vanilla
│ ├── package.json
│ ├── public
│ ├── forgecss.css
│ ├── index.html
│ ├── styles.css
│ └── about.html
│ ├── dev.js
│ └── package-lock.json
├── README.md
├── LICENSE
└── .gitignore
/packages/forgecss/.gitignore:
--------------------------------------------------------------------------------
1 | ast.json
--------------------------------------------------------------------------------
/packages/forgecss/.npmignore:
--------------------------------------------------------------------------------
1 | tests
2 | node_modules
--------------------------------------------------------------------------------
/packages/forgecss/fx.d.ts:
--------------------------------------------------------------------------------
1 | export default function fx(classes: string): string;
2 |
--------------------------------------------------------------------------------
/packages/forgecss/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["lib/**/*", "index.d.ts", "**/*.js"],
3 | }
--------------------------------------------------------------------------------
/packages/forgecss/fx.js:
--------------------------------------------------------------------------------
1 | import forgeCSSExpressionTransformer from "./lib/fx.js";
2 |
3 | export default forgeCSSExpressionTransformer;
4 |
--------------------------------------------------------------------------------
/examples/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/site/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/site/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import App from './App.tsx'
3 |
4 | createRoot(document.getElementById('root')!).render(
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/examples/react/forgecss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "dir": "./src",
3 | "output": "./src/forgecss.css",
4 | "breakpoints": {
5 | "desktop": "all and (min-width: 768px)"
6 | },
7 | "minify": false
8 | }
--------------------------------------------------------------------------------
/examples/react/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/examples/vanilla/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "dependencies": {
5 | "express": "^5.2.1"
6 | },
7 | "scripts": {
8 | "dev": "node ./dev.js"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/site/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/packages/site/forgecss.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "dir": "./src",
3 | "output": "./public/styles.css",
4 | "breakpoints": {
5 | "desktop": "all and (min-width: 780px)",
6 | "mobile": "all and (max-width: 779px)"
7 | },
8 | "bundleAll": true,
9 | "minify": true
10 | }
--------------------------------------------------------------------------------
/examples/react/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/packages/forgecss/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_OPTIONS = {
2 | inventoryFiles: ["css", "less", "scss"],
3 | usageFiles: ["html", "jsx", "tsx"],
4 | usageAttributes: ["class", "className"],
5 | breakpoints: {},
6 | verbose: true,
7 | minify: true,
8 | bundleAll: false
9 | };
10 |
--------------------------------------------------------------------------------
/packages/site/public/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/03-media-query/src/styles.css:
--------------------------------------------------------------------------------
1 | .inline {
2 | display: inline;
3 | }
4 | .mt1 {
5 | margin-top: 0.25rem;
6 | }
7 | .mt2 {
8 | margin-top: 0.5rem;
9 | }
10 | .pt2 {
11 | padding-top: 0.5rem;
12 | }
13 | .fz2 {
14 | font-size: 2rem;
15 | }
16 | .no-used {
17 | color: yellow;
18 | }
19 | .red {
20 | color: red;
21 | }
--------------------------------------------------------------------------------
/packages/site/src/utils/transformHtmlClassAttributes.ts:
--------------------------------------------------------------------------------
1 | export default function transformHtmlClassAttributes(html: string, mapClass: Function) {
2 | // @ts-ignore
3 | return html.replace(/\bclass\s*=\s*(["'])(.*?)\1/gs, (m, quote, value) => {
4 | const next = mapClass(value);
5 | if (next == null) return "";
6 | return `class=${quote}${next}${quote}`;
7 | });
8 | }
--------------------------------------------------------------------------------
/packages/site/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/react/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/03-media-query/src/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/site/public/align-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/03-media-query/src/another.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ForgeCSS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ForgeCSS
2 |
3 | ForgeCSS turns strings into fully generated responsive CSS using a custom DSL.
4 |
5 | ## Client ForgeCSS Expression transformer for vanilla apps
6 |
7 | ```html
8 |
9 | ```
10 |
11 | ## Tests
12 |
13 | To run the tests:
14 |
15 | ```
16 | > npm run tests
17 | > npm run tests -- --spec=02
18 | ```
19 |
20 | in `/packages/forgetcss`.
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/01-usages/src/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 | Hello World
11 |
12 |
13 |
14 | This is a paragraph .
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/03-media-query/expected.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 1024px) {
2 | .desktop_mt2 {
3 | margin-top: 0.5rem
4 | }
5 | .desktop_mt1 {
6 | margin-top: 0.25rem
7 | }
8 | }
9 | @media all and (max-width: 1023px) {
10 | .mobile_fz2 {
11 | font-size: 2rem
12 | }
13 | .mobile_red {
14 | color: red
15 | }
16 | }
17 | @media all and (orientation: portrait) {
18 | .portrait_pt2 {
19 | padding-top: 0.5rem
20 | }
21 | }
--------------------------------------------------------------------------------
/packages/site/public/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vanilla/public/forgecss.css:
--------------------------------------------------------------------------------
1 | /* ForgeCSS auto-generated file */
2 | @media all and (min-width: 768px) {
3 | .desktop_maxw600 {
4 | max-width: 600px
5 | }
6 | .desktop_p2 {
7 | padding: 2rem
8 | }
9 | .desktop_green {
10 | color: #009f00
11 | }
12 | .desktop_white-bg {
13 | background-color: #fff
14 | }
15 | }
16 | .hover_blue:hover {
17 | color: #0c5bb0;
18 | text-decoration: underline
19 | }
20 | @media all and (max-width: 768px) {
21 | .mobile_fz-sm {
22 | font-size: 0.8em
23 | }
24 | }
--------------------------------------------------------------------------------
/packages/forgecss/lib/forge-lang/utils.js:
--------------------------------------------------------------------------------
1 | export function minifyCSS(css) {
2 | return (
3 | css
4 | // remove comments
5 | .replace(/\/\*[\s\S]*?\*\//g, "")
6 | // remove whitespace around symbols
7 | .replace(/\s*([{}:;,])\s*/g, "$1")
8 | // remove trailing semicolons
9 | .replace(/;}/g, "}")
10 | // collapse multiple spaces
11 | .replace(/\s+/g, " ")
12 | // remove spaces before/after braces
13 | .replace(/\s*{\s*/g, "{")
14 | .replace(/\s*}\s*/g, "}")
15 | // trim
16 | .trim()
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/forgecss/lib/forge-lang/constants.js:
--------------------------------------------------------------------------------
1 | export const NODE_TYPE = {
2 | VARIANT: "variant",
3 | CALL: "call",
4 | TOKEN: "token"
5 | };
6 | export const ALLOWED_PSEUDO_CLASSES = [
7 | "hover",
8 | "active",
9 | "focus",
10 | "focus-visible",
11 | "focus-within",
12 | "disabled",
13 | "enabled",
14 | "read-only",
15 | "read-write",
16 | "checked",
17 | "indeterminate",
18 | "valid",
19 | "invalid",
20 | "required",
21 | "optional",
22 | "in-range",
23 | "out-of-range",
24 | "placeholder-shown",
25 | "autofill",
26 | "user-invalid"
27 | ];
28 |
--------------------------------------------------------------------------------
/packages/site/public/sliders.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/03-media-query/test.js:
--------------------------------------------------------------------------------
1 | import ForgeCSS from '../../../index.js';
2 | import { getPath, expect } from '../../helpers.js';
3 |
4 | const __dirname = '/cases/03-media-query';
5 |
6 | export default async function test() {
7 | const forgecss = ForgeCSS({
8 | minify: false,
9 | breakpoints: {
10 | desktop: "all and (min-width: 1024px)",
11 | mobile: "all and (max-width: 1023px)",
12 | portrait: "all and (orientation: portrait)"
13 | }
14 | });
15 | const result = await forgecss.parseDirectory({ dir: getPath(__dirname + "/src") });
16 | return expect.toEqualFile(result, __dirname + "/expected.css");
17 | }
--------------------------------------------------------------------------------
/examples/react/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 | import { defineConfig, globalIgnores } from 'eslint/config'
7 |
8 | export default defineConfig([
9 | globalIgnores(['dist']),
10 | {
11 | files: ['**/*.{ts,tsx}'],
12 | extends: [
13 | js.configs.recommended,
14 | tseslint.configs.recommended,
15 | reactHooks.configs.flat.recommended,
16 | reactRefresh.configs.vite,
17 | ],
18 | languageOptions: {
19 | ecmaVersion: 2020,
20 | globals: globals.browser,
21 | },
22 | },
23 | ])
24 |
--------------------------------------------------------------------------------
/packages/site/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 | import { defineConfig, globalIgnores } from 'eslint/config'
7 |
8 | export default defineConfig([
9 | globalIgnores(['dist']),
10 | {
11 | files: ['**/*.{ts,tsx}'],
12 | extends: [
13 | js.configs.recommended,
14 | tseslint.configs.recommended,
15 | reactHooks.configs.flat.recommended,
16 | reactRefresh.configs.vite,
17 | ],
18 | languageOptions: {
19 | ecmaVersion: 2020,
20 | globals: globals.browser,
21 | },
22 | },
23 | ])
24 |
--------------------------------------------------------------------------------
/packages/forgecss/index.d.ts:
--------------------------------------------------------------------------------
1 | export type ForgeCSSOptions = {
2 | inventoryFiles?: string[];
3 | usageFiles?: string[];
4 | usageAttributes?: string[];
5 | breakpoints?: {
6 | [key: string]: string;
7 | };
8 | verbose?: boolean;
9 | minify?: boolean;
10 | bundleAll?: boolean;
11 | };
12 |
13 | export type ForgeInstance = {
14 | parseDirectory: (options: { dir: string; output?: string; watch?: boolean }) => Promise;
15 | parseFile: (options: { file: string; output?: string; watch?: boolean }) => Promise;
16 | parse: (options: { css: string; html?: string; jsx?: string; output?: string; }) => Promise;
17 | };
18 |
19 | declare function ForgeCSS(options?: ForgeCSSOptions): ForgeInstance;
20 |
21 | export default ForgeCSS;
--------------------------------------------------------------------------------
/examples/react/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2023",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "types": ["node"],
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "erasableSyntaxOnly": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["vite.config.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/site/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2023",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "types": ["node"],
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "erasableSyntaxOnly": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["vite.config.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/06-apply/test.js:
--------------------------------------------------------------------------------
1 | import { minifyCSS, expect } from "../../helpers.js";
2 | import ForgeCSS from "../../../index.js";
3 |
4 | const CASES = [
5 | {
6 | styles: `
7 | .red { color: red }
8 | .mt1 { margin-top: 1rem }
9 | .box > div, p + p {
10 | padding: 1rem;
11 | --apply: red mt1;
12 | }
13 | `,
14 | usage: "box",
15 | expectedCSS: ".box > div,p + p{color:red;margin-top:1rem}"
16 | }
17 | ];
18 |
19 | export default async function test() {
20 | for (let testCase of CASES) {
21 | const css = await ForgeCSS().parse({
22 | css: testCase.styles,
23 | html: `
`
24 | });
25 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) {
26 | return false;
27 | }
28 | }
29 | return true;
30 | }
31 |
--------------------------------------------------------------------------------
/examples/react/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2022",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "types": ["vite/client"],
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "moduleDetection": "force",
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "erasableSyntaxOnly": true,
24 | "noFallthroughCasesInSwitch": true,
25 | "noUncheckedSideEffectImports": true
26 | },
27 | "include": ["src"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/site/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2022",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "types": ["vite/client"],
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "moduleDetection": "force",
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "erasableSyntaxOnly": true,
24 | "noFallthroughCasesInSwitch": true,
25 | "noUncheckedSideEffectImports": true
26 | },
27 | "include": ["src"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/02-fx/test.js:
--------------------------------------------------------------------------------
1 | import fx from '../../../lib/fx.js';
2 | import { expect } from '../../helpers.js';
3 |
4 | const transformations = [
5 | ["", ""],
6 | ["a b", "a b"],
7 | ["a d:x y", "a d_x y"],
8 | ["a d:x,z m:y", "a d_x d_z m_y"],
9 | ["hover:red a", "hover_red a"],
10 | ["[&:hover]:a b", "I-hover_a b"],
11 | ["[&:required:disabled]:red", "I-required-disabled_red"],
12 | ["[true]:my1 red", "my1 red"],
13 | ["[false]:my1 red", "red"],
14 | ["[.dark &]:black mt2", "dark-I_black mt2"],
15 | ["mt1 [.dark &[type='password']]:mt2", "mt1 dark-Itype-password_mt2"],
16 | ["desktop:[.dark &]:b", "desktop-dark-I_b"]
17 | ];
18 |
19 | export default async function test() {
20 | for(let [input, expected] of transformations) {
21 | if (!expect.toBe(fx(input), expected)) {
22 | return false;
23 | }
24 | }
25 | return true;
26 | }
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/04-pseudo/test.js:
--------------------------------------------------------------------------------
1 | import { minifyCSS, expect } from "../../helpers.js";
2 | import ForgeCSS from '../../../index.js'
3 |
4 | const CASES = [
5 | {
6 | styles: ".red { color: red }",
7 | usage: '
',
8 | expected: ".hover_red:hover{color:red}"
9 | },
10 | {
11 | styles: ".red { color: red }.mt2 { margin-top: 2rem }",
12 | usage: '
',
13 | expected: ".focus_red:focus{color:red}.focus_mt2:focus{margin-top:2rem}.active_mt2:active{margin-top:2rem}"
14 | }
15 | ];
16 |
17 | export default async function test() {
18 | for (let testCase of CASES) {
19 | const css = await ForgeCSS().parse({ css: testCase.styles, html: testCase.usage });
20 | if (!expect.toBe(minifyCSS(css), testCase.expected)) {
21 | return false;
22 | }
23 | }
24 | return true;
25 | }
--------------------------------------------------------------------------------
/packages/forgecss/scripts/build.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import esbuild from "esbuild";
3 | import { fileURLToPath } from "url";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = path.dirname(__filename);
7 | const minify = true;
8 |
9 | (async function () {
10 | await esbuild.build({
11 | entryPoints: [path.join(__dirname, "..", "index.fx.js")],
12 | bundle: true,
13 | minify,
14 | outfile: path.join(__dirname, "..", "dist", "client.min.js"),
15 | platform: "browser",
16 | sourcemap: false,
17 | plugins: []
18 | });
19 | await esbuild.build({
20 | entryPoints: [path.join(__dirname, "..", "index.browser.js")],
21 | bundle: true,
22 | minify,
23 | outfile: path.join(__dirname, "..", "dist", "standalone.min.js"),
24 | platform: "browser",
25 | sourcemap: false,
26 | plugins: []
27 | });
28 | })();
29 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/01-usages/src/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fx } from 'forgecss/fx';
3 |
4 | export default function App({ className }: { className?: string }) {
5 | const foo = 'bar';
6 | const flagA = true;
7 | const flagB = false;
8 |
9 | return (
10 |
11 | Hello world!
12 | Something else
13 | No usage of fx so no pick up
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
--------------------------------------------------------------------------------
/packages/forgecss/index.fx.js:
--------------------------------------------------------------------------------
1 | import fxFn from "./lib/fx.js";
2 |
3 | function fxAll(root) {
4 | var rootNode = root || document;
5 | var nodes = rootNode.querySelectorAll('[class]');
6 |
7 | for (var i = 0; i < nodes.length; i++) {
8 | var el = nodes[i];
9 | var original = el.getAttribute('class');
10 | if (!original) continue;
11 |
12 | var transformed = fxFn(original);
13 |
14 | if (typeof transformed === 'string' && transformed !== original) {
15 | el.setAttribute('class', transformed);
16 | }
17 | }
18 | }
19 |
20 | window.fx = function(str) {
21 | if (str) {
22 | return fxFn(str);
23 | }
24 | return fxAll();
25 | };
26 |
27 | if (document.readyState !== 'loading') {
28 | fxAll();
29 | } else {
30 | document.addEventListener('DOMContentLoaded', function () {
31 | fxAll();
32 | });
33 | }
34 | window.addEventListener('load', function () {
35 | fxAll();
36 | });
--------------------------------------------------------------------------------
/examples/react/src/forgecss.css:
--------------------------------------------------------------------------------
1 | /* ForgeCSS auto-generated file */
2 | @media all and (min-width: 768px) {
3 | .desktop_w400 {
4 | width: 400px
5 | }
6 | .desktop_p2 {
7 | padding: 2rem
8 | }
9 | .desktop_flex-row {
10 | display: flex;
11 | flex-direction: row
12 | }
13 | .desktop_align-center {
14 | align-items: center
15 | }
16 | .desktop_autow {
17 | width: auto
18 | }
19 | }
20 | .light .light-I_surface-light {
21 | background-color: var(--surface-light)
22 | }
23 | .light .light-I_text-light {
24 | color: var(--text-light)
25 | }
26 | .disabled_op05:disabled {
27 | opacity: 0.5
28 | }
29 | .hover_primary2-bg:hover {
30 | background-color: var(--primary-color2)
31 | }
32 | .I-disabled_op05:disabled {
33 | opacity: 0.5
34 | }
35 | .I-disabled-hover_primary-bg:disabled:hover {
36 | background-color: var(--primary-color)
37 | }
--------------------------------------------------------------------------------
/examples/vanilla/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ForgeCSS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
About the project
14 |
15 |
Hey
16 |
ForgeCSS parses className strings as a small DSL and generates responsive CSS based on your own utility classes . It gives you Tailwind-like breakpoint expressiveness without adopting a new styling framework.
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/07-bundleAll/test.js:
--------------------------------------------------------------------------------
1 | import { minifyCSS, expect } from "../../helpers.js";
2 | import ForgeCSS from "../../../index.js";
3 |
4 | const CASES = [
5 | {
6 | styles: `
7 | .red { color: red }
8 | .mt1 { margin-top: 1rem }
9 | `,
10 | usage: "red desktop:mt1",
11 | expectedCSS: ".red{color:red}.mt1{margin-top:1rem}@media @media (min-width:750px){.desktop_mt1{margin-top:1rem}}"
12 | }
13 | ];
14 |
15 | export default async function test() {
16 | for (let testCase of CASES) {
17 | const css = await ForgeCSS({
18 | breakpoints: {
19 | desktop: "@media (min-width: 750px)"
20 | },
21 | bundleAll: true
22 | }).parse({
23 | css: testCase.styles,
24 | html: `
`
25 | });
26 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) {
27 | return false;
28 | }
29 | }
30 | return true;
31 | }
32 |
--------------------------------------------------------------------------------
/examples/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "concurrently \"vite\" \"node ../../packages/forgecss/index.cli.js -w -v\"",
8 | "build": "forgecss --verbose && tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "concurrently": "^9.2.1",
14 | "forgecss": "^0.1.7",
15 | "react": "^19.2.0",
16 | "react-dom": "^19.2.0"
17 | },
18 | "devDependencies": {
19 | "@eslint/js": "^9.39.1",
20 | "@types/node": "^24.10.1",
21 | "@types/react": "^19.2.5",
22 | "@types/react-dom": "^19.2.3",
23 | "@vitejs/plugin-react": "^5.1.1",
24 | "eslint": "^9.39.1",
25 | "eslint-plugin-react-hooks": "^7.0.1",
26 | "eslint-plugin-react-refresh": "^0.4.24",
27 | "globals": "^16.5.0",
28 | "typescript": "~5.9.3",
29 | "typescript-eslint": "^8.46.4",
30 | "vite": "^7.2.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/vanilla/public/styles.css:
--------------------------------------------------------------------------------
1 | *, :after, :before {
2 | box-sizing: border-box;
3 | }
4 | html, body {
5 | margin: 0;
6 | padding: 0;
7 | font-family: Arial, sans-serif;
8 | font-size: 20px;
9 | width: 100%;
10 | height: 100%;
11 | }
12 | main {
13 | height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | }
19 | .grey-bg {
20 | background-color: #d1d1d1;
21 | }
22 | .white-bg {
23 | background-color: #fff;
24 | }
25 | .maxw300 {
26 | max-width: 300px;
27 | }
28 | .maxw600 {
29 | max-width: 600px;
30 | }
31 | .maxw900 {
32 | max-width: 900px;
33 | }
34 | .p1 {
35 | padding: 1rem;
36 | }
37 | .p2 {
38 | padding: 2rem;
39 | }
40 | .br1 {
41 | border-radius: 1rem;
42 | }
43 | .fz-sm {
44 | font-size: 0.8em;
45 | }
46 | .blue {
47 | color: #0c5bb0;
48 | text-decoration: underline;
49 | }
50 | .green {
51 | color: #009f00;
52 | }
53 | .transition-bg-color {
54 | transition: background-color 800ms;
55 | }
--------------------------------------------------------------------------------
/packages/forgecss/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "forgecss",
3 | "version": "0.14.0",
4 | "type": "module",
5 | "description": "ForgeCSS turns strings into fully generated responsive CSS using a custom DSL.",
6 | "author": "Krasimir Tsonev",
7 | "main": "index.js",
8 | "scripts": {
9 | "build": "node ./scripts/build.js",
10 | "test": "node ./tests/run.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@github.com:krasimir/ForgeCSS.git"
15 | },
16 | "license": "MIT",
17 | "keywords": [
18 | "css",
19 | "dsl",
20 | "generator",
21 | "processor",
22 | "responsive",
23 | "media queries"
24 | ],
25 | "dependencies": {
26 | "@swc/cli": "^0.7.7",
27 | "@swc/core": "1.12.1",
28 | "chokidar": "^4.0.3",
29 | "commander": "^14.0.2",
30 | "postcss": "^8.5.6",
31 | "postcss-safe-parser": "^7.0.1"
32 | },
33 | "devDependencies": {
34 | "esbuild": "^0.27.1"
35 | },
36 | "bin": {
37 | "forgecss": "./index.cli.js"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/site/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ForgeCSS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/vanilla/dev.js:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "path";
3 | import express from "express";
4 | import { fileURLToPath } from "url";
5 | import ForgeCSS from "../../packages/forgecss/index.js";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | const fxCode = fs.readFileSync(path.join(__dirname, "../../packages/forgecss/dist/client.min.js"), "utf-8");
11 |
12 | const PORT = 5173;
13 | const app = express();
14 |
15 | ForgeCSS({
16 | breakpoints: {
17 | desktop: "all and (min-width: 768px)",
18 | mobile: "all and (max-width: 768px)"
19 | },
20 | minify: false,
21 | }).parseDirectory({
22 | dir: path.join(__dirname, "public"),
23 | output: path.join(__dirname, "public/forgecss.css"),
24 | watch: true
25 | });
26 |
27 | app.get("/fx.min.js", (req, res) => {
28 | res.type("application/javascript");
29 | res.send(fxCode);
30 | });
31 | app.use(express.static("public"));
32 |
33 | app.listen(PORT, () => {
34 | console.log("Server is running on http://localhost:" + PORT);
35 | });
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Krasimir Tsonev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "get-standalone": "cp ../forgecss/dist/standalone.min.js ./public/standalone.min.js",
8 | "dev": "npm run get-standalone && concurrently \"vite\" \"forgecss -w -v\"",
9 | "build": "npm run get-standalone && forgecss --verbose && tsc -b && vite build",
10 | "lint": "eslint .",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "concurrently": "^9.2.1",
15 | "forgecss": "^0.14.0",
16 | "monaco-editor": "^0.32.1",
17 | "react": "^19.2.0",
18 | "react-dom": "^19.2.0"
19 | },
20 | "devDependencies": {
21 | "@eslint/js": "^9.39.1",
22 | "@types/node": "^24.10.1",
23 | "@types/react": "^19.2.5",
24 | "@types/react-dom": "^19.2.3",
25 | "@vitejs/plugin-react": "^5.1.1",
26 | "eslint": "^9.39.1",
27 | "eslint-plugin-react-hooks": "^7.0.1",
28 | "eslint-plugin-react-refresh": "^0.4.24",
29 | "globals": "^16.5.0",
30 | "typescript": "~5.9.3",
31 | "typescript-eslint": "^8.46.4",
32 | "vite": "^7.2.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/forgecss/dist/client.min.js:
--------------------------------------------------------------------------------
1 | (()=>{function c(t){let n=t.lastIndexOf(":");if(n===-1)return[null,t];let e=t.slice(0,n),o=t.slice(n+1);return[e,o]}function s(t){let n=[],e="",o=0,l=null;for(let r=0;r0){if(l){e+=i,i===l&&t[r-1]!=="\\"&&(l=null);continue}else if(i==="'"||i==='"'){l=i,e+=i;continue}}if(i==="["){o++,e+=i;continue}if(i==="]"&&o>0){o--,e+=i;continue}if(o===0&&/\s/.test(i)){for(e&&n.push(e),e="";r+1{let[e,o]=c(n);return!e||e==="[true]"?o:e==="[false]"?!1:(e=a(e),o.split(",").map(l=>`${e}_${l}`).join(" "))}).filter(Boolean).join(" ")}function a(t){let n=t.trim();return n=n.replace(/[&]/g,"I"),n=n.replace(/[:| =]/g,"-"),n=n.replace(/[^a-zA-Z0-9_-]/g,""),n}function f(t){for(var n=t||document,e=n.querySelectorAll("[class]"),o=0;o
5 | Hey world!
6 |
7 | I'm ForgeCSS .
8 |
9 | `,
10 | selected: true,
11 | type: "html"
12 | },
13 | {
14 | filename: "styles.css",
15 | content: `.mt1 { margin-top: 1rem; }
16 | .mt2 { margin-top: 2rem; }
17 | .white { color: #fff; }
18 | .black-bg { background-color: #000; }
19 | .red { color: #9f0000; }`,
20 | selected: false,
21 | type: "css"
22 | },
23 | {
24 | filename: "forgecss.config.json",
25 | content: `{
26 | "breakpoints": {
27 | "d": "all and (min-width: 768px)"
28 | },
29 | "bundleAll": true
30 | }`,
31 | selected: false,
32 | type: "javascript"
33 | }
34 | ];
35 | export const ACTUAL_HTML_FILE = {
36 | filename: "page.html",
37 | content: "",
38 | selected: true,
39 | type: "html"
40 | };
41 | export const TOTAL_CSS_FILE = {
42 | filename: "styles.css",
43 | content: "",
44 | selected: false,
45 | type: "css"
46 | };
47 | export const DEFAULT_OUTPUT_FILES = [ACTUAL_HTML_FILE, TOTAL_CSS_FILE];
--------------------------------------------------------------------------------
/packages/forgecss/lib/usages.js:
--------------------------------------------------------------------------------
1 | let USAGES = {};
2 |
3 | export async function findUsages(filePath, content, JSXParser) {
4 | try {
5 | if (USAGES[filePath]) {
6 | // already processed
7 | return;
8 | }
9 | USAGES[filePath] = [];
10 | const extension = filePath.split(".").pop().toLowerCase();
11 |
12 | // HTML
13 | if (extension === "html") {
14 | extractClassNamesFromHTML(content).forEach((cls) => {
15 | USAGES[filePath].push(cls);
16 | });
17 | return;
18 | }
19 | if (JSXParser && (extension === "jsx" || extension === "tsx")) {
20 | await JSXParser(content, USAGES, filePath);
21 | return;
22 | }
23 | } catch (err) {
24 | console.error(`forgecss: error processing file ${filePath.replace(process.cwd(), "")}`, err);
25 | }
26 | }
27 | export function invalidateUsageCache(filePath) {
28 | if (!filePath) {
29 | USAGES = {};
30 | return;
31 | }
32 | if (USAGES[filePath]) {
33 | delete USAGES[filePath];
34 | }
35 | }
36 | export function getUsages() {
37 | return USAGES;
38 | }
39 |
40 | function extractClassNamesFromHTML(html) {
41 | const result = [];
42 | const classAttrRE = /\bclass\s*=\s*(["'])(.*?)\1/gis;
43 |
44 | let match;
45 | while ((match = classAttrRE.exec(html))) {
46 | result.push(match[2].trim());
47 | }
48 |
49 | return result;
50 | }
--------------------------------------------------------------------------------
/examples/react/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/site/public/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.30.0
2 | https://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+jsx+tsx+typescript */
3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
4 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/01-usages/test.js:
--------------------------------------------------------------------------------
1 | import { JSXParser, readFileContent } from "../../../lib/helpers.js";
2 | import { invalidateInventory } from "../../../lib/inventory.js";
3 | import { findUsages, getUsages, invalidateUsageCache } from "../../../lib/usages.js";
4 | import { getPath, expect } from "../../helpers.js";
5 |
6 | export default async function test() {
7 | const cases = [
8 | {
9 | file: getPath("/cases/01-usages/src/page.html"),
10 | expected: {
11 | [getPath("/cases/01-usages/src/page.html")]: [
12 | "mt1 desktop:red",
13 | "fz3 mx-l [.dark &:hover]:red",
14 | "vibe"
15 | ]
16 | }
17 | },
18 | {
19 | file: getPath("/cases/01-usages/src/page.tsx"),
20 | expected: {
21 | [getPath("/cases/01-usages/src/page.tsx")]: [
22 | "a desktop:b",
23 | "c mobile:d desktop:b2 e",
24 | "a []:b []:c",
25 | "[&:hover]:a",
26 | "a [.dark &]:b c",
27 | "a [.dark desktop:b]:c d",
28 | "a [.dark &:has(.desc)]:c d",
29 | "a [.dark &[type='password']]:c d"
30 | ]
31 | }
32 | }
33 | ];
34 | for (let i=0; i {
17 | const filePath = path.join(dir, file);
18 | const stat = fs.statSync(filePath);
19 |
20 | if (stat.isDirectory()) {
21 | cases.push(...getCases(filePath));
22 | } else if (file.endsWith("test.js")) {
23 | cases.push(filePath);
24 | }
25 | });
26 |
27 | return cases;
28 | }
29 | async function importTest(configPath) {
30 | const abs = path.resolve(configPath);
31 | const fileUrl = pathToFileURL(abs).href;
32 |
33 | const mod = await import(fileUrl);
34 | return mod.default ?? mod;
35 | }
36 |
37 | (async () => {
38 | const cases = getCases();
39 | for (let testFile of cases) {
40 | if (spec && !testFile.match(new RegExp(spec, 'i'))) {
41 | continue;
42 | }
43 | invalidateUsageCache();
44 | invalidateInventory();
45 | const test = await importTest(testFile);
46 | const testName = testFile.replace(process.cwd(), "");
47 | console.log("----- " + testName + " -----");
48 | try {
49 | if (await test()) {
50 | console.log(`✅ ${testName}`);
51 | } else {
52 | console.error(`❌ ${testName}`);
53 | }
54 | } catch (err) {
55 | console.error(`❌ ${testName}`, err);
56 | }
57 | }
58 | })();
59 |
--------------------------------------------------------------------------------
/examples/vanilla/public/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ForgeCSS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Back home
14 |
15 |
ForgeCSS introduces ForgeLang, a lightweight DSL embedded directly into your React className strings. It lets you express responsive design using intuitive prefixes like desktop:mt-4 or mobile:text-sm, then compiles those tokens into real CSS. Instead of reinventing a utility framework, ForgeCSS builds on top of the styles you already have—reading your existing CSS utilities, extracting their declarations, and generating fully scoped media-query variants automatically. This means your design system stays in control while ForgeCSS handles the heavy lifting of responsive expansion.
16 |
The project was created for teams who love utility classes but want more expressive breakpoint logic without adopting a full Tailwind pipeline. By treating className as a parseable language rather than a plain string, ForgeCSS enables custom syntax, prefix extensions, and future transformations that can evolve with your codebase. It slots into any React project with minimal setup and produces predictable, deduplicated CSS output that mirrors your real styles. Think of it as a tiny compiler for responsive utilities—simple to adopt, endlessly flexible, and built for modern UI development.
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/forgecss/lib/fx.js:
--------------------------------------------------------------------------------
1 | function splitClassName(label) {
2 | const lastColonIndex = label.lastIndexOf(":");
3 | if (lastColonIndex === -1) {
4 | return [null, label];
5 | }
6 | const prefix = label.slice(0, lastColonIndex);
7 | const rest = label.slice(lastColonIndex + 1);
8 | return [prefix, rest];
9 | }
10 | function parseClass(str) {
11 | const out = [];
12 | let buf = "";
13 |
14 | let depth = 0;
15 | let quote = null;
16 | for (let i = 0; i < str.length; i++) {
17 | const ch = str[i];
18 | if (depth > 0) {
19 | if (quote) {
20 | buf += ch;
21 | if (ch === quote && str[i - 1] !== "\\") quote = null;
22 | continue;
23 | } else if (ch === "'" || ch === '"') {
24 | quote = ch;
25 | buf += ch;
26 | continue;
27 | }
28 | }
29 | if (ch === "[") {
30 | depth++;
31 | buf += ch;
32 | continue;
33 | }
34 | if (ch === "]" && depth > 0) {
35 | depth--;
36 | buf += ch;
37 | continue;
38 | }
39 | if (depth === 0 && /\s/.test(ch)) {
40 | if (buf) out.push(buf);
41 | buf = "";
42 | while (i + 1 < str.length && /\s/.test(str[i + 1])) i++;
43 | continue;
44 | }
45 | buf += ch;
46 | }
47 |
48 | if (buf) out.push(buf);
49 | return out;
50 | }
51 | export default function fx(classes) {
52 | return parseClass(classes)
53 | .map((className) => {
54 | let [label, rest] = splitClassName(className);
55 | if (!label || label === "[true]") return rest;
56 | if (label === "[false]") return false;
57 | label = normalizeLabel(label);
58 | return rest
59 | .split(",")
60 | .map((cls) => `${label}_${cls}`)
61 | .join(" ");
62 | })
63 | .filter(Boolean)
64 | .join(" ");
65 | }
66 | export function normalizeLabel(label) {
67 | let normalized = label.trim();
68 | normalized = normalized.replace(/[&]/g, "I");
69 | normalized = normalized.replace(/[:| =]/g, "-");
70 | normalized = normalized.replace(/[^a-zA-Z0-9_-]/g, "");
71 | return normalized;
72 | }
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/05-arbitrary/test.js:
--------------------------------------------------------------------------------
1 | import { minifyCSS, expect } from "../../helpers.js";
2 | import fx from "../../../lib/fx.js";
3 | import ForgeCSS from "../../../index.js";
4 |
5 | const CASES = [
6 | {
7 | styles: `
8 | .red { color: red }
9 | `,
10 | usage: "[&:hover]:red",
11 | expectedClass: "I-hover_red",
12 | expectedCSS: ".I-hover_red:hover{color:red}",
13 | type: "html"
14 | },
15 | {
16 | styles: `
17 | .red { color: red }
18 | .fz2 { font-size: 2rem }
19 | .mt2 { margin-top: 2rem }
20 | `,
21 | usage: `[&:hover]:red,fz2 [&:hover]:mt2`,
22 | expectedClass: "I-hover_red I-hover_fz2 I-hover_mt2",
23 | expectedCSS:
24 | ".I-hover_red:hover{color:red}.I-hover_fz2:hover{font-size:2rem}.I-hover_mt2:hover{margin-top:2rem}",
25 | type: "html"
26 | },
27 | {
28 | styles: `
29 | .red { color: red }
30 | .fz2 { font-size: 2rem }
31 | `,
32 | usage: `[&:required:disabled]:red,fz2`,
33 | expectedClass: "I-required-disabled_red I-required-disabled_fz2",
34 | expectedCSS:
35 | ".I-required-disabled_red:required:disabled{color:red}.I-required-disabled_fz2:required:disabled{font-size:2rem}",
36 | type: "html"
37 | },
38 | {
39 | styles: `
40 | .red { color: red }
41 | `,
42 | usage: `[.dark &]:red`,
43 | expectedClass: "dark-I_red",
44 | expectedCSS: ".dark .dark-I_red{color:red}",
45 | type: "html"
46 | },
47 | {
48 | styles: `
49 | .red { color: red }
50 | `,
51 | usage: "[true]:red",
52 | expectedClass: "red",
53 | expectedCSS: "",
54 | type: "jsx"
55 | }
56 | ];
57 |
58 | export default async function test() {
59 | for (let testCase of CASES) {
60 | const css = await ForgeCSS().parse({
61 | css: testCase.styles,
62 | [testCase.type || "html"]:
63 | testCase.type === "html"
64 | ? `
`
65 | : `function Component() { return
}`
66 | });
67 | if (!expect.toBe(fx(testCase.usage), testCase.expectedClass)) {
68 | return false;
69 | }
70 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) {
71 | return false;
72 | }
73 | }
74 | return true;
75 | }
--------------------------------------------------------------------------------
/packages/forgecss/lib/inventory.js:
--------------------------------------------------------------------------------
1 | import postcss from "postcss";
2 | import safeParser from "postcss-safe-parser";
3 |
4 | let INVENTORY = {};
5 |
6 | export function extractStyles(filePath, content) {
7 | INVENTORY[filePath] = postcss.parse(content, { parser: safeParser });
8 | }
9 | export function getStylesByClassName(selector) {
10 | const decls = [];
11 | Object.keys(INVENTORY).forEach((filePath) => {
12 | INVENTORY[filePath].walkRules((rule) => {
13 | if (rule.selectors && rule.selectors.includes(`.${selector}`)) {
14 | rule.walkDecls((d) => {
15 | decls.push({ prop: d.prop, value: d.value, important: d.important });
16 | });
17 | }
18 | });
19 | });
20 | if (decls.length === 0) {
21 | console.warn(`forgecss: no styles found for class "${selector}".`);
22 | }
23 | return decls;
24 | }
25 | export function invalidateInventory(filePath) {
26 | if (!filePath) {
27 | INVENTORY = {};
28 | return;
29 | }
30 | if (INVENTORY[filePath]) {
31 | delete INVENTORY[filePath];
32 | }
33 | }
34 | export function resolveApplys() {
35 | let resolvedApplies;
36 | Object.keys(INVENTORY).forEach((filePath) => {
37 | INVENTORY[filePath].walkRules((rule) => {
38 | rule.walkDecls((d) => {
39 | if (d.prop === "--apply") {
40 | const classesToApply = d.value
41 | .split(" ")
42 | .map((c) => c.trim())
43 | .filter(Boolean);
44 | const newRule = postcss.rule({ selector: rule.selector });
45 | classesToApply.forEach((className) => {
46 | const styles = getStylesByClassName(className);
47 | styles.forEach((style) => {
48 | newRule.append({
49 | prop: style.prop,
50 | value: style.value,
51 | important: style.important
52 | });
53 | });
54 | });
55 | if (!resolvedApplies) {
56 | resolvedApplies = postcss.root();
57 | }
58 | resolvedApplies.append(newRule);
59 | }
60 | });
61 | });
62 | });
63 | return resolvedApplies;
64 | }
65 | export function getInventory() {
66 | return INVENTORY;
67 | }
68 | export function getAllCSS() {
69 | let combined = "";
70 | Object.keys(INVENTORY).forEach((filePath) => {
71 | combined += INVENTORY[filePath].toString() + "\n";
72 | });
73 | return combined;
74 | }
--------------------------------------------------------------------------------
/packages/forgecss/tests/helpers.js:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = path.dirname(__filename);
7 |
8 | export function root() {
9 | return __dirname;
10 | }
11 | export function getPath(p) {
12 | return path.join(__dirname, p);
13 | }
14 | export const expect = {
15 | toBe(value, expected) {
16 | const result = value === expected;
17 | if (!result) {
18 | console.error("Expected:\n", expected);
19 | console.error("Received:\n", value);
20 | }
21 | return result;
22 | },
23 | toEqualFile(str, file) {
24 | const expected = fs.readFileSync(getPath(file), "utf-8");
25 | const result = str === expected;
26 | if (!result) {
27 | console.error("\nExpected:\n", expected);
28 | console.error("\nReceived:\n", str);
29 | }
30 | return result;
31 | },
32 | deepEqual(actual, expected) {
33 | function deepEqual(a, b) {
34 | if (a === b) return true;
35 |
36 | if (a === null || b === null) return a === b;
37 |
38 | if (Array.isArray(a) && Array.isArray(b)) {
39 | if (a.length !== b.length) return false;
40 | for (let i = 0; i < a.length; i++) {
41 | if (!deepEqual(a[i], b[i])) return false;
42 | }
43 | return true;
44 | }
45 |
46 | if (Array.isArray(a) || Array.isArray(b)) return false;
47 |
48 | if (typeof a === "object" && typeof b === "object") {
49 | const keysA = Object.keys(a);
50 | const keysB = Object.keys(b);
51 | if (keysA.length !== keysB.length) return false;
52 |
53 | for (const key of keysA) {
54 | if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
55 | if (!deepEqual(a[key], b[key])) return false;
56 | }
57 | return true;
58 | }
59 |
60 | return false;
61 | }
62 | const result = deepEqual(actual, expected);
63 | if (!result) {
64 | console.error("\nExpected:\n", JSON.stringify(expected, null, 2));
65 | console.error("\nActual:\n", JSON.stringify(actual, null, 2));
66 | }
67 | return result;
68 | }
69 | };
70 | export function minifyCSS(css = '') {
71 | return css
72 | .replace(/\/\*[^]*?\*\//g, "") // remove comments
73 | .replace(/\s+/g, " ") // collapse spaces
74 | .replace(/\s*([{}:;,])\s*/g, "$1") // trim syntax whitespace
75 | .trim();
76 |
77 | }
--------------------------------------------------------------------------------
/examples/react/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: #4CAF50;
3 | --primary-color2: #39e63f;
4 | --secondary-color: #8bcbe9;
5 | --background: #292929;
6 | --surface: #525252;
7 | --surface-light: #b9b9b9;
8 | --text: #f0f0f0;
9 | --text-light: #000;
10 | --error: #dd6c6c;
11 | }
12 | * {
13 | box-sizing: border-box;
14 | }
15 | body {
16 | background: var(--background);
17 | color: var(--text);
18 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
19 | font-size: 20px;
20 | margin: 0;
21 | padding: 0;
22 | width: 100%;
23 | height: 100%;
24 | }
25 | p {
26 | margin: 0;
27 | padding: 0;
28 | }
29 | main {
30 | justify-content: center;
31 | align-items: center;
32 | height: 100vh;
33 | }
34 | fieldset {
35 | min-width: 100%;
36 | filter: drop-shadow(0 0 0.75rem black);
37 | background: #393939
38 | }
39 | input {
40 | padding: 0.5rem;
41 | border-radius: 6px;
42 | border: none;
43 | font-size: 1rem;
44 | }
45 | button {
46 | padding: 0.5rem 1rem;
47 | border-radius: 6px;
48 | border: none;
49 | background-color: var(--primary-color);
50 | color: white;
51 | cursor: pointer;
52 | font-size: 1rem;
53 | }
54 | fieldset {
55 | border-radius: 6px;
56 | display: flex;
57 | flex-direction: column;
58 | gap: 1rem;
59 | }
60 | .no-border { border: none; }
61 | .primary { color: var(--primary-color); }
62 | .primary-bg { background-color: var(--primary-color); }
63 | .primary2 { color: var(--primary-color2); }
64 | .primary2-bg { background-color: var(--primary-color2); }
65 | .secondary { color: var(--secondary-color); }
66 | .secondary-bg { background-color: var(--secondary-color); }
67 | .surface { background-color: var(--surface); }
68 | .surface-light { background-color: var(--surface-light); }
69 | .text-light { color: var(--text-light); }
70 | .error { color: var(--error); }
71 | .error-bg { background-color: var(--error); }
72 | .error-border { border: 2px solid var(--error); }
73 | .success-border { border: 2px solid var(--primary-color); }
74 |
75 | .block { display: block; }
76 | .p1 { padding: 1rem; }
77 | .p2 { padding: 2rem; }
78 | .mt1 { margin-top: 1rem; }
79 | .mt05 { margin-top: 0.5rem; }
80 | .flex { display: flex; }
81 | .grid { display: grid; }
82 | .flex-col { display: flex; flex-direction: column; }
83 | .flex-row { display: flex; flex-direction: row; }
84 | .space-between { justify-content: space-between; }
85 | .align-center { align-items: center; }
86 | .align-start { align-items: start; }
87 | .gap1 { gap: 1rem; }
88 | .fullw { width: 100%; }
89 | .autow { width: auto; }
90 | .w400 { width: 400px; }
91 | .op05 { opacity: 0.5; }
--------------------------------------------------------------------------------
/examples/react/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## React Compiler
11 |
12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13 |
14 | ## Expanding the ESLint configuration
15 |
16 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17 |
18 | ```js
19 | export default defineConfig([
20 | globalIgnores(['dist']),
21 | {
22 | files: ['**/*.{ts,tsx}'],
23 | extends: [
24 | // Other configs...
25 |
26 | // Remove tseslint.configs.recommended and replace with this
27 | tseslint.configs.recommendedTypeChecked,
28 | // Alternatively, use this for stricter rules
29 | tseslint.configs.strictTypeChecked,
30 | // Optionally, add this for stylistic rules
31 | tseslint.configs.stylisticTypeChecked,
32 |
33 | // Other configs...
34 | ],
35 | languageOptions: {
36 | parserOptions: {
37 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
38 | tsconfigRootDir: import.meta.dirname,
39 | },
40 | // other options...
41 | },
42 | },
43 | ])
44 | ```
45 |
46 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47 |
48 | ```js
49 | // eslint.config.js
50 | import reactX from 'eslint-plugin-react-x'
51 | import reactDom from 'eslint-plugin-react-dom'
52 |
53 | export default defineConfig([
54 | globalIgnores(['dist']),
55 | {
56 | files: ['**/*.{ts,tsx}'],
57 | extends: [
58 | // Other configs...
59 | // Enable lint rules for React
60 | reactX.configs['recommended-typescript'],
61 | // Enable lint rules for React DOM
62 | reactDom.configs.recommended,
63 | ],
64 | languageOptions: {
65 | parserOptions: {
66 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
67 | tsconfigRootDir: import.meta.dirname,
68 | },
69 | // other options...
70 | },
71 | },
72 | ])
73 | ```
74 |
--------------------------------------------------------------------------------
/packages/site/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## React Compiler
11 |
12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13 |
14 | ## Expanding the ESLint configuration
15 |
16 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17 |
18 | ```js
19 | export default defineConfig([
20 | globalIgnores(['dist']),
21 | {
22 | files: ['**/*.{ts,tsx}'],
23 | extends: [
24 | // Other configs...
25 |
26 | // Remove tseslint.configs.recommended and replace with this
27 | tseslint.configs.recommendedTypeChecked,
28 | // Alternatively, use this for stricter rules
29 | tseslint.configs.strictTypeChecked,
30 | // Optionally, add this for stylistic rules
31 | tseslint.configs.stylisticTypeChecked,
32 |
33 | // Other configs...
34 | ],
35 | languageOptions: {
36 | parserOptions: {
37 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
38 | tsconfigRootDir: import.meta.dirname,
39 | },
40 | // other options...
41 | },
42 | },
43 | ])
44 | ```
45 |
46 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47 |
48 | ```js
49 | // eslint.config.js
50 | import reactX from 'eslint-plugin-react-x'
51 | import reactDom from 'eslint-plugin-react-dom'
52 |
53 | export default defineConfig([
54 | globalIgnores(['dist']),
55 | {
56 | files: ['**/*.{ts,tsx}'],
57 | extends: [
58 | // Other configs...
59 | // Enable lint rules for React
60 | reactX.configs['recommended-typescript'],
61 | // Enable lint rules for React DOM
62 | reactDom.configs.recommended,
63 | ],
64 | languageOptions: {
65 | parserOptions: {
66 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
67 | tsconfigRootDir: import.meta.dirname,
68 | },
69 | // other options...
70 | },
71 | },
72 | ])
73 | ```
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional stylelint cache
57 | .stylelintcache
58 |
59 | # Optional REPL history
60 | .node_repl_history
61 |
62 | # Output of 'npm pack'
63 | *.tgz
64 |
65 | # Yarn Integrity file
66 | .yarn-integrity
67 |
68 | # dotenv environment variable files
69 | .env
70 | .env.*
71 | !.env.example
72 |
73 | # parcel-bundler cache (https://parceljs.org/)
74 | .cache
75 | .parcel-cache
76 |
77 | # Next.js build output
78 | .next
79 | out
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 |
84 | # Gatsby files
85 | .cache/
86 | # Comment in the public line in if your project uses Gatsby and not Next.js
87 | # https://nextjs.org/blog/next-9-1#public-directory-support
88 | # public
89 |
90 | # vuepress build output
91 | .vuepress/dist
92 |
93 | # vuepress v2.x temp and cache directory
94 | .temp
95 | .cache
96 |
97 | # Sveltekit cache directory
98 | .svelte-kit/
99 |
100 | # vitepress build output
101 | **/.vitepress/dist
102 |
103 | # vitepress cache directory
104 | **/.vitepress/cache
105 |
106 | # Docusaurus cache and generated files
107 | .docusaurus
108 |
109 | # Serverless directories
110 | .serverless/
111 |
112 | # FuseBox cache
113 | .fusebox/
114 |
115 | # DynamoDB Local files
116 | .dynamodb/
117 |
118 | # Firebase cache directory
119 | .firebase/
120 |
121 | # TernJS port file
122 | .tern-port
123 |
124 | # Stores VSCode versions used for testing VSCode extensions
125 | .vscode-test
126 |
127 | # yarn v3
128 | .pnp.*
129 | .yarn/*
130 | !.yarn/patches
131 | !.yarn/plugins
132 | !.yarn/releases
133 | !.yarn/sdks
134 | !.yarn/versions
135 |
136 | # Vite logs files
137 | vite.config.js.timestamp-*
138 | vite.config.ts.timestamp-*
139 |
--------------------------------------------------------------------------------
/packages/forgecss/lib/helpers.js:
--------------------------------------------------------------------------------
1 | import swc from "@swc/core";
2 | import fs from "fs/promises";
3 | import path from "path";
4 |
5 | const FUNC_NAME = "fx";
6 | const { parse } = swc;
7 |
8 | export async function getAllFiles(dir, matchFiles) {
9 | const result = [];
10 | const stack = [dir];
11 |
12 | while (stack.length > 0) {
13 | const currentDir = stack.pop();
14 |
15 | let dirHandle;
16 | try {
17 | dirHandle = await fs.opendir(currentDir);
18 | } catch (err) {
19 | throw err;
20 | }
21 |
22 | for await (const entry of dirHandle) {
23 | const fullPath = path.join(currentDir, entry.name);
24 |
25 | if (entry.isDirectory()) {
26 | stack.push(fullPath);
27 | } else if (matchFiles.includes(fullPath.split(".").pop()?.toLowerCase())) {
28 | result.push(fullPath);
29 | }
30 | }
31 | }
32 |
33 | return result;
34 | }
35 | export function readFileContent(filePath) {
36 | return fs.readFile(filePath, "utf-8");
37 | }
38 | export async function JSXParser(content, USAGES, filePath) {
39 | const ast = await parse(content, {
40 | syntax: "typescript",
41 | tsx: true,
42 | decorators: false
43 | });
44 | function traverseASTNode(node, visitors, stack = []) {
45 | if (!node || typeof node.type !== "string") {
46 | return;
47 | }
48 |
49 | const visitor = visitors[node.type];
50 | if (visitor) {
51 | visitor(node, stack);
52 | }
53 |
54 | for (const key in node) {
55 | if (!node.hasOwnProperty(key)) continue;
56 |
57 | const child = node[key];
58 |
59 | if (Array.isArray(child)) {
60 | child.forEach((c) => {
61 | if (c) {
62 | if (typeof c.type === "string") {
63 | traverseASTNode(c, visitors, [node].concat(stack));
64 | } else if (c?.expression && typeof c.expression.type === "string") {
65 | traverseASTNode(c.expression, visitors, [node].concat(stack));
66 | } else if (c?.callee && typeof c.callee.type === "string") {
67 | traverseASTNode(c.callee, visitors, [node].concat(stack));
68 | } else if (c?.left && typeof c.left.type === "string") {
69 | traverseASTNode(c.left, visitors, [node].concat(stack));
70 | } else if (c?.right && typeof c.right.type === "string") {
71 | traverseASTNode(c.right, visitors, [node].concat(stack));
72 | }
73 | }
74 | });
75 | } else if (child && typeof child.type === "string") {
76 | traverseASTNode(child, visitors, [node].concat(stack));
77 | }
78 | }
79 | }
80 |
81 | traverseASTNode(ast, {
82 | JSXExpressionContainer(node) {
83 | if (node?.expression?.callee?.value === FUNC_NAME && node?.expression?.arguments) {
84 | if (node?.expression?.arguments[0]) {
85 | const arg = node.expression.arguments[0];
86 | let value = arg?.expression.value;
87 | if (arg.expression.type === "TemplateLiteral") {
88 | let quasis = arg.expression.quasis.map((elem) => elem?.cooked || "");
89 | value = quasis.join("");
90 | }
91 | USAGES[filePath].push(value);
92 | }
93 | }
94 | }
95 | });
96 | }
97 |
--------------------------------------------------------------------------------
/packages/forgecss/index.cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import fs from 'fs';
4 | import path from "node:path";
5 | import { pathToFileURL } from "node:url";
6 | import { program } from "commander";
7 | import chokidar from "chokidar";
8 |
9 | import ForgeCSS from './index.js';
10 |
11 | const POSSIBLE_CONFIG_FILES = [
12 | process.cwd() + "/forgecss.config.json",
13 | process.cwd() + "/forgecss.config.js",
14 | process.cwd() + "/forgecss.config.mjs"
15 | ];
16 |
17 | program.option("-c, --config ,", "Path to forgecss config file", POSSIBLE_CONFIG_FILES[0]);
18 | program.option("-w, --watch", "Enable watch mode", false);
19 | program.option("-v, --verbose", "Enable watch mode", false);
20 | program.parse();
21 |
22 | const options = program.opts();
23 | let config = null, instance = null;
24 |
25 | if (!fs.existsSync(options.config)) {
26 | let found = false;
27 | for (let possibleConfigFile of POSSIBLE_CONFIG_FILES) {
28 | if (fs.existsSync(possibleConfigFile)) {
29 | options.config = possibleConfigFile;
30 | found = true;
31 | break;
32 | }
33 | }
34 | if (!found) {
35 | throw new Error(`forgecss: config file not found at ${options.config} or any of the default locations.`);
36 | }
37 | }
38 |
39 | async function loadConfig(configPath) {
40 | const abs = path.resolve(configPath);
41 | if (abs.toLowerCase().endsWith('.json')) {
42 | const jsonStr = fs.readFileSync(abs, "utf-8");
43 | if (options.verbose) {
44 | console.log(`forgecss: Loaded config file from ${abs.replace(process.cwd(), '')}`);
45 | }
46 | try {
47 | return JSON.parse(jsonStr);
48 | } catch(err) {
49 | throw new Error(`forgecss: error parsing config file at ${configPath}: ${err}`);
50 | }
51 | } else {
52 | const module = await import(pathToFileURL(abs).href);
53 | if (options.verbose) {
54 | console.log(`forgecss: Loaded config file from ${abs.replace(process.cwd(), '')}`);
55 | }
56 | return module.default || module;
57 | }
58 | }
59 | async function runForgeCSS(lookAtPath = null) {
60 | if (!config) {
61 | // The very first run
62 | config = await loadConfig(options.config);
63 | if (!config.dir) {
64 | throw new Error('forgecss: missing "dir" in configuration.');
65 | }
66 | if (!config.output) {
67 | throw new Error('forgecss: missing "output" in configuration.');
68 | }
69 | instance = ForgeCSS(config);
70 | if (options.watch) {
71 | const watcher = chokidar.watch(config.dir, {
72 | persistent: true,
73 | ignoreInitial: true,
74 | ignored: (p, stats) => path.resolve(p) === path.resolve(config.output)
75 | });
76 | watcher.on("change", async (filePath) => {
77 | if (options.verbose) {
78 | console.log(`forgecss: Detected change in ${filePath}`);
79 | }
80 | runForgeCSS(filePath);
81 | });
82 | if (options.verbose) {
83 | console.log("forgecss: Watch mode enabled. Listening for file changes...");
84 | }
85 | }
86 | }
87 | if (lookAtPath) {
88 | instance.parseFile({ file: lookAtPath, output: config.output });
89 | } else {
90 | instance.parseDirectory({ dir: config.dir, output: config.output });
91 | }
92 | }
93 |
94 | runForgeCSS();
95 |
96 |
--------------------------------------------------------------------------------
/packages/site/src/Editor.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState, useEffect } from "react";
2 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
3 | import "monaco-editor/esm/vs/basic-languages/html/html.contribution";
4 | import "monaco-editor/esm/vs/basic-languages/css/css.contribution";
5 | import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution";
6 |
7 | monaco.editor.defineTheme("fcss", {
8 | base: "vs-dark", // or 'vs'
9 | inherit: true,
10 | rules: [
11 | { token: "comment", foreground: "6A9955" },
12 | { token: "string", foreground: "CE9178" },
13 | { token: "keyword", foreground: "C586C0" }
14 | ],
15 | colors: {
16 | "editor.background": "#292929",
17 | "editor.foreground": "#d4d4d4",
18 | "editorCursor.foreground": "#ffffff",
19 | "editor.lineHighlightBackground": "#1e1e1e",
20 | "editorLineNumber.foreground": "#5a5a5a",
21 | "editor.selectionBackground": "#264f78",
22 | "editor.inactiveSelectionBackground": "#3a3d41"
23 | }
24 | });
25 | type EditorProps = {
26 | language: string;
27 | code: string;
28 | className?: string,
29 | onChange?: (code: string) => void,
30 | readonly?: boolean
31 | };
32 |
33 | export function Editor({ language, code, className, onChange, readonly }: EditorProps) {
34 | const [editor, setEditor] = useState(null);
35 | const monacoEl = useRef(null);
36 |
37 | useEffect(() => {
38 | if (monacoEl) {
39 | setEditor((editor) => {
40 | if (editor) return editor;
41 |
42 | monaco.editor.setTheme("fcss");
43 |
44 | const editorInstance = monaco.editor.create(monacoEl.current!, {
45 | value: code,
46 | language: language || "javascript",
47 | minimap: { enabled: false },
48 | lineNumbers: "off",
49 | glyphMargin: false,
50 | folding: false,
51 | lineDecorationsWidth: 0,
52 | lineNumbersMinChars: 0,
53 | scrollbar: {
54 | vertical: "auto",
55 | horizontal: "hidden"
56 | },
57 | overviewRulerLanes: 0,
58 | fontSize: 14,
59 | lineHeight: 24,
60 | letterSpacing: 0.4,
61 | contextmenu: false,
62 | renderLineHighlight: "none",
63 | cursorBlinking: "smooth",
64 | automaticLayout: true,
65 | tabSize: 2,
66 | insertSpaces: true,
67 | readOnly: readonly,
68 | wordWrap: "on"
69 | });
70 | editorInstance.onDidChangeModelContent(() => {
71 | onChange && onChange(editorInstance.getValue());
72 | });
73 | return editorInstance;
74 | });
75 | }
76 |
77 | return () => {
78 | editor?.dispose();
79 | }
80 | }, [monacoEl.current]);
81 |
82 | useEffect(() => {
83 | if (!editor) return;
84 | if (editor.getValue() !== code) {
85 | editor.setValue(code);
86 | }
87 | const model = editor.getModel();
88 | if (model) {
89 | monaco.editor.setModelLanguage(model, language);
90 | }
91 | }, [code, language])
92 |
93 | return (
94 |
97 | );
98 | };
99 |
--------------------------------------------------------------------------------
/packages/site/src/Syntax.tsx:
--------------------------------------------------------------------------------
1 | import fx from "forgecss/fx";
2 |
3 | const EXAMPLES = [
4 | {
5 | input: (
6 |
7 | foo bar
8 |
9 | ),
10 | outputHTML: foo bar ,
11 | outputCSS: - ,
12 | text: (
13 |
14 | foo and bar are just tokens here. No CSS is
15 | generated.
16 |
17 | )
18 | },
19 | {
20 | input: (
21 |
22 | hover :bar
23 |
24 | ),
25 | outputHTML: hover_bar ,
26 | outputCSS: (
27 |
28 | hover_bar:hover {"{"} ... {"}"}
29 |
30 | ),
31 | text: (
32 |
33 | If hover is a valid pseudo class and bar is a
34 | valid utility, CSS will be generated for the hover state applying the styles set in{" "}
35 | bar class.
36 |
37 | )
38 | },
39 | {
40 | input: (
41 |
42 | desktop :bar
43 |
44 | ),
45 | outputHTML: desktop_bar ,
46 | outputCSS: (
47 |
48 | @media (min-width: 780px) {"{"}
49 |
50 |
51 | desktop_bar {"{"} ... {"}"}
52 |
53 |
54 | {"}"}
55 |
56 | ),
57 | text: (
58 |
59 | If desktop is a valid breakpoint (defined into the ForgeCSS configuration) and bar is a
60 | valid utility, media query will be generated for the created class desktop_bar with the styles set in bar class.
61 |
62 | )
63 | }
64 | ];
65 |
66 | export default function Syntax() {
67 | return (
68 |
69 |
syntax
70 | {EXAMPLES.map((example, i) => {
71 | return (
72 |
73 |
74 |
75 | {example.input}
76 |
77 |
78 |
79 | {example.outputHTML}
80 |
81 |
82 | {example.outputCSS}
83 |
84 |
85 | {example.text}
86 |
87 |
88 |
89 |
90 | );
91 | })}
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/packages/forgecss/index.browser.js:
--------------------------------------------------------------------------------
1 | import {
2 | extractStyles,
3 | getStylesByClassName,
4 | invalidateInventory,
5 | resolveApplys,
6 | getInventory,
7 | getAllCSS
8 | } from "./lib/inventory.js";
9 | import { invalidateUsageCache, findUsages, getUsages } from "./lib/usages.js";
10 | import fx from './lib/fx.js'
11 | import { astToRules, rulesToCSS } from "./lib/forge-lang/Compiler.js";
12 | import { toAST } from "./lib/forge-lang/Parser.js";
13 | import { DEFAULT_OPTIONS } from "./lib/constants.js";
14 |
15 | function ForgeCSS(options) {
16 | const config = { ...DEFAULT_OPTIONS };
17 |
18 | config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {});
19 | config.usageAttributes = options?.usageAttributes ?? DEFAULT_OPTIONS.usageAttributes;
20 | config.verbose = options?.verbose ?? DEFAULT_OPTIONS.verbose;
21 | config.minify = options?.minify ?? DEFAULT_OPTIONS.minify;
22 | config.bundleAll = options?.bundleAll ?? DEFAULT_OPTIONS.bundleAll;
23 |
24 | async function result() {
25 | try {
26 | const cache = {};
27 | const usages = getUsages();
28 | const ast = toAST(
29 | Object.values(usages).reduce((acc, i) => {
30 | return acc.concat(i);
31 | }, [])
32 | );
33 | let rules = astToRules(ast, {
34 | getStylesByClassName,
35 | cache,
36 | config
37 | });
38 | rules.push(resolveApplys());
39 | let css = rulesToCSS(rules.filter(Boolean), config);
40 | if (config.bundleAll) {
41 | css = getAllCSS() + "\n" + css;
42 | }
43 | if (config.minify) {
44 | css = minifyCSS(css);
45 | }
46 | if (config.verbose) {
47 | console.log("forgecss: output CSS generated successfully.");
48 | }
49 | return css;
50 | } catch (err) {
51 | console.error(`forgecss: error generating output CSS: ${err}`);
52 | }
53 | return null;
54 | }
55 |
56 | return {
57 | async parse({ css, html, jsx }) {
58 | if (!css) {
59 | throw new Error('forgecss: parse requires "css".');
60 | }
61 | if (!html && !jsx) {
62 | throw new Error('forgecss: parse requires "html" or "jsx".');
63 | }
64 | invalidateInventory();
65 | invalidateUsageCache();
66 | // filling the inventory
67 | try {
68 | extractStyles("styles.css", css);
69 | } catch (err) {
70 | console.error(`forgecss: error extracting styles.`, err);
71 | }
72 | // finding the usages
73 | try {
74 | if (html) {
75 | await findUsages("usage.html", html);
76 | } else if (jsx) {
77 | await findUsages("usage.jsx", jsx);
78 | }
79 | } catch (err) {
80 | console.error(`forgecss: error extracting usages.`, err);
81 | }
82 | return result();
83 | },
84 | fxAll: function (root) {
85 | const rootNode = root || document;
86 | const nodes = rootNode.querySelectorAll("[class]");
87 |
88 | for (let i = 0; i < nodes.length; i++) {
89 | let el = nodes[i];
90 | let original = el.getAttribute("class");
91 | if (!original) continue;
92 |
93 | let transformed = fx(original);
94 |
95 | if (typeof transformed === "string" && transformed !== original) {
96 | el.setAttribute("class", transformed);
97 | }
98 | }
99 | },
100 | fx,
101 | getUsages,
102 | getStylesByClassName,
103 | getInventory
104 | };
105 | }
106 |
107 | window.ForgeCSS = ForgeCSS;
--------------------------------------------------------------------------------
/packages/site/public/forgecss.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/react/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import './forgecss.css';
3 | import fx from '../../../packages/forgecss/fx'
4 |
5 | function App() {
6 | const [ progress, setProgress ] = useState(false);
7 | const [ success, setSuccess ] = useState(false);
8 | const [theme, setTheme ] = useState<'light' | 'dark'>('dark');
9 | const [errors, setErrors ] = useState<{ username: boolean | string, password: boolean | string }>({ username: false, password: false });
10 |
11 | async function submit(e) {
12 | e.preventDefault();
13 | setProgress(true);
14 | if (e.target.username.value === '') {
15 | setErrors(errors => ({ ...errors, username: 'Username is required' }));
16 | setProgress(false);
17 | return;
18 | }
19 | if (e.target.password.value === "") {
20 | setErrors((errors) => ({ ...errors, password: "Password is required" }));
21 | setProgress(false);
22 | return;
23 | }
24 | await new Promise((resolve) => setTimeout(resolve, 2000));
25 | setSuccess(true);
26 | setProgress(false);
27 | }
28 | function clearErrors(field: string) {
29 | setErrors((errors) => ({ ...errors, [field]: false }));
30 | }
31 |
32 | const isErrored = Boolean(errors.username || errors.password);
33 |
34 | return (
35 |
36 |
91 |
92 |
93 | setTheme("dark")} />
94 | Dark theme
95 |
96 |
97 | setTheme("light")} />
98 | Light theme
99 |
100 |
101 |
102 | );
103 | }
104 |
105 | export default App
106 |
--------------------------------------------------------------------------------
/packages/forgecss/lib/forge-lang/Compiler.js:
--------------------------------------------------------------------------------
1 | import postcss from "postcss";
2 | import { NODE_TYPE, ALLOWED_PSEUDO_CLASSES } from "./constants.js";
3 | import { minifyCSS } from './utils.js'
4 | import { normalizeLabel } from "../fx.js";
5 |
6 | export function astToRules(ast, options) {
7 | let rules = [];
8 | const { getStylesByClassName, cache = {}, config } = options
9 | // console.log(
10 | // "\n====================================================================== ^\n",
11 | // JSON.stringify(ast, null, 2),
12 | // "\n====================================================================== $\n"
13 | // );
14 |
15 | for(let node of ast) {
16 | switch (node.type) {
17 | case NODE_TYPE.TOKEN:
18 | // ignoring ... just tokens
19 | break;
20 | case NODE_TYPE.VARIANT:
21 | let variantSelector = node.selector;
22 | let classes = (node?.payload?.value ?? "").split(",").map((c) => c.trim()).filter(Boolean);
23 | let childRules;
24 | if (!node.payload.value && typeof node.payload === 'object') {
25 | childRules = astToRules([node.payload], options);
26 | }
27 |
28 | // -------------------------------------------------------- pseudo
29 | if (ALLOWED_PSEUDO_CLASSES.includes(variantSelector)) {
30 | classes.forEach(cls => {
31 | let selector = `.${variantSelector}_${cls}`;
32 | const rule = createRule(`${selector}:${variantSelector}`, cls, cache);
33 | if (rule) {
34 | rules.push(rule);
35 | }
36 | });
37 | // -------------------------------------------------------- media queries
38 | } else if (config.breakpoints[variantSelector]) {
39 | let mediaRule;
40 | if (cache[config.breakpoints[variantSelector]]) {
41 | mediaRule = cache[config.breakpoints[variantSelector]];
42 | } else {
43 | mediaRule = cache[config.breakpoints[variantSelector]] = postcss.atRule({
44 | name: "media",
45 | params: config.breakpoints[variantSelector]
46 | });
47 | rules.push(mediaRule);
48 | }
49 | if (childRules) {
50 | childRules.forEach(r => {
51 | mediaRule.append(r);
52 | })
53 | } else {
54 | classes.forEach((cls) => {
55 | let selector = `.${variantSelector}_${cls}`;
56 | const rule = createRule(selector, cls, cache);
57 | if (rule) {
58 | mediaRule.append(rule);
59 | }
60 | });
61 | }
62 | } else if (node.payload?.type === NODE_TYPE.TOKEN && node.simple === true) {
63 | console.warn(`forgecss: there is no breakpoint defined for label "${variantSelector}".`);
64 | // -------------------------------------------------------- arbitrary
65 | } else {
66 | classes.forEach(cls => {
67 | if (Array.isArray(variantSelector)) {
68 | variantSelector = variantSelector
69 | .map(({ type, value, selector, payload }) => {
70 | if (type === "token") {
71 | return value;
72 | }
73 | })
74 | .filter(Boolean)
75 | .join(" ");
76 | }
77 | if (["", "true"].includes(variantSelector)) {
78 | return;
79 | }
80 | const I = normalizeLabel(variantSelector) + "_" + cls;
81 | const selector = evaluateArbitrary(variantSelector, I);
82 | const rule = createRule(selector, cls, cache);
83 | if (rule) {
84 | rules.push(rule);
85 | }
86 | })
87 | }
88 | break;
89 | }
90 | }
91 |
92 | function createRule(selector, pickStylesFrom, cache = {}) {
93 | if (cache[selector]) {
94 | return;
95 | }
96 | const newRule = cache[selector] = postcss.rule({ selector });
97 | const decls = getStylesByClassName(pickStylesFrom);
98 | if (decls.length === 0) {
99 | return;
100 | }
101 | decls.forEach((d) => {
102 | newRule.append(
103 | postcss.decl({
104 | prop: d.prop,
105 | value: d.value,
106 | important: d.important
107 | })
108 | );
109 | });
110 | return newRule;
111 | }
112 | function evaluateArbitrary(variant, I) {
113 | variant = variant.replace(/[&]/g, `.${I}`);
114 | return variant;
115 | }
116 |
117 | return rules;
118 | }
119 |
120 | export function rulesToCSS(rules) {
121 | return rules.map((r) => r.toString()).join("\n");
122 | }
--------------------------------------------------------------------------------
/packages/site/public/styles.css:
--------------------------------------------------------------------------------
1 | :root{--background-dark:#1b1b1b;--background:#292929;--background-light:#595959;--background-light2:#7b7b7b;--text:#f0f0f0;--warning:#df6464;--success:#61e481;--bit1:#8ff08f;--bit2:#e65f5f;--bit3:#909eee}*{box-sizing:border-box}body{background:var(--background);color:var(--text);font-family:"Arimo",sans-serif;font-optical-sizing:auto;font-weight:200;font-style:normal;font-size:20px;line-height:1.4em;margin:0;padding:0;width:100%;height:100%}h1,h2,h3,p,ul{margin:0;padding:0;line-height:1.2em}h1,h2,h3{font-family:"Alfa Slab One",serif;font-weight:400;font-style:normal}.warning{color:var(--warning)}.success{color:var(--success)}.paler{color:var(--background-light2)}.p1{padding:1rem}.p2{padding:2rem}.p3{padding:3rem}.pt2{padding-top:2rem}.pl1{padding-left:1rem}.py1{padding-top:1rem;padding-bottom:1rem}.py2{padding-top:2rem;padding-bottom:2rem}.py3{padding-top:3rem;padding-bottom:3rem}.pl3{padding-left:3rem}.pr3{padding-right:3rem}.pt3{padding-top:3rem}.mt1{margin-top:1rem}.mt2{margin-top:2rem}.mt3{margin-top:3rem}.my3{margin-top:3rem;margin-bottom:3rem}.mxauto{margin-left:auto;margin-right:auto}.grid2{display:grid;grid-template-columns:1fr 1fr}.grid2x1{display:grid;grid-template-columns:2fr 1fr}.b{display:block}.flex{display:flex}.flex-center{display:flex;align-items:center;justify-content:center}.flex-v-center{display:flex;align-items:center}.flex-col{display:flex;flex-direction:column}.flex1{flex:1}.span2{grid-column:span 2}.gap05{gap:0.5rem}.gap1{gap:1rem}.gap2{gap:2rem}.maxw600{max-width:600px}.maxw800{max-width:800px}.maxw1000{max-width:1000px}.maxw1400{max-width:1400px}.minh500{min-height:500px}.bg{background:var(--background)}.bg-dark{background:var(--background-dark)}.black-bg{background:black}.fz1{font-size:1rem}.fz15{font-size:1.5rem}.fz2{font-size:2rem}.fz3{font-size:3rem}.fw100{font-weight:100}.hidden{display:none}.tac{text-align:center}.border-t{border-top:solid 1px var(--background-light)}.border-l{border-left:solid 1px var(--background-light)}.border-r{border-right:solid 1px var(--background-light)}.border-b{border-bottom:solid 1px var(--background-light)}.border{border:solid 1px var(--background-light)}header{background-color:#292929;opacity:1;background:radial-gradient(circle,transparent 20%,#292929 20%,#292929 80%,transparent 80%,transparent),radial-gradient(circle,transparent 20%,#292929 20%,#292929 80%,transparent 80%,transparent) 10px 10px,linear-gradient(#000000 0.8px,transparent 0.8px) 0 -0.4px,linear-gradient(90deg,#000000 0.8px,#292929 0.8px) -0.4px 0;background-size:20px 20px,20px 20px,10px 10px,10px 10px}.hero{background:radial-gradient(125% 125% at 50% 10%,#000000 40%,#031724 100%);border-top:solid 1px #575757;border-bottom:solid 1px #272727}.hero nav a{text-align:center;color:var(--text);text-decoration:none;font-size:1rem;background:radial-gradient(125% 125% at 50% 10%,#434343 40%,#000000 100%);padding:1rem 2rem;border-radius:4px}.hero nav a:hover{background:radial-gradient(125% 125% at 50% 10%,#595959 40%,#000000 100%)}hr{border-top:solid 4px #000;border-bottom:none;border-left:none;border-right:none;}.files{list-style:none;display:flex}.files li{font-size:0.8rem;border-top:solid 2px var(--background-light2);border-right:solid 2px var(--background-light2)}.files li:first-child{border-left:solid 2px var(--background-light2)}.files li button{display:block;background:none;border:none;color:var(--text);font-size:1rem;cursor:pointer;padding:0.5rem 1rem;opacity:0.3}.files li.selected button{background:var(--background-light);filter:drop-shadow(0px 2px 0px white);opacity:1}.editor-wrapper{border:solid 2px var(--background-light);background:var(--background);height:520px;overflow-y:auto}.editor{width:100%;height:100%;min-height:400px}.syntax-example .input,.syntax-example .output{position:relative}.syntax-example .input::before,.syntax-example .output::before{display:block;font-size:0.8rem;position:absolute;top:14px;line-height:0;color:var(--background-light2)}.syntax-example .input::before{content:attr(data-label);left:14px}.syntax-example .output::before{content:attr(data-label);right:14px}.syntax-example code{font-size:0.9rem;font-family:inherit;padding:0.2rem 0.2rem;border-radius:4px}.code1{border-bottom:solid 1px var(--bit1)}.code2{border-bottom:solid 1px var(--bit2)}.code3{border-bottom:solid 1px var(--bit3)}.bit1,.bit2,.bit3{position:relative;padding:0 4px 4px 4px}.bit1::before,.bit2::before,.bit3::before{content:"";position:absolute;top:0;left:0;width:2px;height:4px}.bit1::after,.bit2::after,.bit3::after{content:"";position:absolute;top:0;right:0;width:2px;height:4px}.bit1{border-top:solid 1px var(--bit1)}.bit1::after,.bit1::before{background:var(--bit1)}.bit2{border-top:solid 1px var(--bit2)}.bit2::after,.bit2::before{background:var(--bit2)}.bit3{border-top:solid 1px var(--bit3)}.bit3::after,.bit3::before{background:var(--bit3)}:not(pre)>code[class*=language-],pre[class*=language-]{background:none !important;font-size:0.9rem !important}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#569cd6 !important}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#9cdcfe !important}.token.punctuation{color:#808080 !important}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#ce9178 !important}code{white-space:break-spaces !important;word-break:break-word !important}@media all and (min-width:780px){.desktop_py3{padding-top:3rem;padding-bottom:3rem}.desktop_fz3{font-size:3rem}.desktop_mt2{margin-top:2rem}.desktop_grid2{display:grid;grid-template-columns:1fr 1fr}.desktop_pt3{padding-top:3rem}.desktop_pl3{padding-left:3rem}.desktop_pr3{padding-right:3rem}}@media all and (max-width:779px){.mobile_p1{padding:1rem}.mobile_b{display:block}.mobile_mt1{margin-top:1rem}.mobile_mt2{margin-top:2rem}.mobile_hidden{display:none}}
--------------------------------------------------------------------------------
/packages/forgecss/lib/forge-lang/Parser.js:
--------------------------------------------------------------------------------
1 | export function toAST(input, cache = {}) {
2 | if (cache[input]) return cache[input];
3 |
4 | if (Array.isArray(input)) {
5 | const optimized = [];
6 | input.forEach((str) => {
7 | str
8 | .trim()
9 | .split(" ")
10 | .forEach((part) => {
11 | if (!optimized.includes(part)) optimized.push(part);
12 | });
13 | });
14 | input = optimized.join(" ");
15 | }
16 |
17 | const s = String(input ?? "").trim();
18 | let i = 0;
19 |
20 | const isWS = (ch) => ch === " " || ch === "\n" || ch === "\t" || ch === "\r";
21 |
22 | function skipWS() {
23 | while (i < s.length && isWS(s[i])) i++;
24 | }
25 |
26 | function parseSequence(stopChar) {
27 | const nodes = [];
28 | while (i < s.length) {
29 | skipWS();
30 | if (stopChar && s[i] === stopChar) break;
31 | if (i >= s.length) break;
32 | nodes.push(parseItem());
33 | }
34 | return nodes;
35 | }
36 |
37 | function readIdentUntilDelimiter() {
38 | let out = "";
39 | while (i < s.length) {
40 | const ch = s[i];
41 | // stop at whitespace, "(", ")", ":" (variant separator)
42 | if (isWS(ch) || ch === "(" || ch === ")" || ch === ":") break;
43 | // IMPORTANT: DO NOT consume "[" here; it may be:
44 | // - leading bracket variant (handled in parseItem when ch === "[")
45 | // - attribute selector suffix (handled in parseItem after reading head)
46 | if (ch === "[") break;
47 |
48 | out += ch;
49 | i++;
50 | }
51 | return out.trim();
52 | }
53 |
54 | function isVariantLabel(str) {
55 | return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str);
56 | }
57 |
58 | function isCallName(str) {
59 | return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str);
60 | }
61 |
62 | function parseItem() {
63 | skipWS();
64 | const ch = s[i];
65 |
66 | // Bracket variant: [selector]:payload
67 | if (ch === "[") {
68 | const selectorRaw = parseBracketContent(); // returns content WITHOUT outer []
69 | const selectorAst = toAST(selectorRaw, cache);
70 |
71 | if (s[i] === ":") {
72 | i++;
73 | const payload = parseItem();
74 | return { type: "variant", selector: selectorAst, payload };
75 | }
76 |
77 | // If it's just a standalone bracket chunk (not a variant),
78 | // keep it as a token string. (You can change this if you prefer AST here.)
79 | return { type: "token", value: `[${selectorRaw}]` };
80 | }
81 |
82 | // Read label/name/token
83 | let head = readIdentUntilDelimiter();
84 |
85 | // NEW: absorb attribute selector suffixes: foo[...][...]
86 | // This handles &\[type=...\] and similar.
87 | while (s[i] === "[") {
88 | const inner = parseBracketContent(); // consumes the bracket block
89 | head += `[${inner}]`;
90 | }
91 |
92 | // Label variant: hover:..., desktop:..., focus:...
93 | if (s[i] === ":" && isVariantLabel(head)) {
94 | i++; // consume ":"
95 | const payload = parseItem();
96 | return { type: "variant", selector: head, payload, simple: true };
97 | }
98 |
99 | // Call: name(...)
100 | if (s[i] === "(" && isCallName(head)) {
101 | i++; // consume "("
102 | const args = [];
103 | while (i < s.length) {
104 | skipWS();
105 | if (s[i] === ")") {
106 | i++;
107 | break;
108 | }
109 | args.push(parseItem());
110 | skipWS();
111 | if (s[i] === ",") i++;
112 | }
113 | return { type: "call", name: head, args };
114 | }
115 |
116 | if (s[i] === ":") {
117 | head += ":";
118 | i++; // consume ":"
119 |
120 | // absorb following identifier / call / selector chunk
121 | while (i < s.length) {
122 | const ch = s[i];
123 | if (isWS(ch) || ch === ")" || ch === ",") break;
124 |
125 | if (ch === "[") {
126 | const inner = parseBracketContent();
127 | head += `[${inner}]`;
128 | continue;
129 | }
130 |
131 | if (ch === "(") {
132 | head += "(";
133 | i++;
134 | let depth = 1;
135 | while (i < s.length && depth > 0) {
136 | if (s[i] === "(") depth++;
137 | if (s[i] === ")") depth--;
138 | head += s[i++];
139 | }
140 | continue;
141 | }
142 |
143 | head += ch;
144 | i++;
145 | }
146 | }
147 |
148 | return { type: "token", value: head };
149 | }
150 |
151 | function parseBracketContent() {
152 | // assumes s[i] === "["
153 | i++; // consume "["
154 | let out = "";
155 | let bracket = 1;
156 | let quote = null;
157 |
158 | while (i < s.length) {
159 | const ch = s[i];
160 |
161 | if (quote) {
162 | out += ch;
163 | if (ch === "\\" && i + 1 < s.length) {
164 | i++;
165 | out += s[i];
166 | } else if (ch === quote) {
167 | quote = null;
168 | }
169 | i++;
170 | continue;
171 | }
172 |
173 | if (ch === "'" || ch === '"') {
174 | quote = ch;
175 | out += ch;
176 | i++;
177 | continue;
178 | }
179 |
180 | if (ch === "[") {
181 | bracket++;
182 | out += ch;
183 | i++;
184 | continue;
185 | }
186 |
187 | if (ch === "]") {
188 | bracket--;
189 | if (bracket === 0) {
190 | i++; // consume final "]"
191 | break;
192 | }
193 | out += ch;
194 | i++;
195 | continue;
196 | }
197 |
198 | out += ch;
199 | i++;
200 | }
201 |
202 | return out;
203 | }
204 |
205 | const ast = parseSequence(null);
206 | cache[input] = ast;
207 | return ast;
208 | }
209 |
--------------------------------------------------------------------------------
/packages/forgecss/index.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { writeFile } from "fs/promises";
3 | import chokidar from "chokidar";
4 |
5 | import { extractStyles, getStylesByClassName, invalidateInventory, resolveApplys, getAllCSS } from "./lib/inventory.js";
6 | import { invalidateUsageCache, findUsages, getUsages } from "./lib/usages.js";
7 | import { astToRules, rulesToCSS } from './lib/forge-lang/Compiler.js';
8 | import { toAST } from './lib/forge-lang/Parser.js';
9 | import { minifyCSS } from './lib/forge-lang/utils.js';
10 | import { getAllFiles, JSXParser, readFileContent } from './lib/helpers.js';
11 | import { DEFAULT_OPTIONS } from './lib/constants.js';
12 |
13 | export default function ForgeCSS(options) {
14 | const config = { ...DEFAULT_OPTIONS };
15 |
16 | config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {});
17 | config.inventoryFiles = options?.inventoryFiles ?? DEFAULT_OPTIONS.inventoryFiles;
18 | config.usageFiles = options?.usageFiles ?? DEFAULT_OPTIONS.usageFiles;
19 | config.usageAttributes = options?.usageAttributes ?? DEFAULT_OPTIONS.usageAttributes;
20 | config.verbose = options?.verbose ?? DEFAULT_OPTIONS.verbose;
21 | config.minify = options?.minify ?? DEFAULT_OPTIONS.minify;
22 | config.bundleAll = options?.bundleAll ?? DEFAULT_OPTIONS.bundleAll;
23 |
24 | async function result(output) {
25 | try {
26 | const cache = {};
27 | const usages = getUsages();
28 | const ast = toAST(
29 | Object.values(usages).reduce((acc, i) => {
30 | return acc.concat(i);
31 | }, [])
32 | );
33 | let rules = astToRules(ast, {
34 | getStylesByClassName,
35 | cache,
36 | config
37 | });
38 | rules.push(resolveApplys());
39 | let css = rulesToCSS(rules.filter(Boolean), config);
40 | if (config.bundleAll) {
41 | css = getAllCSS() + "\n" + css;
42 | }
43 | if (config.minify) {
44 | css = minifyCSS(css);
45 | }
46 | if (output) {
47 | await writeFile(output, css, "utf-8");
48 | }
49 | if (config.verbose) {
50 | console.log("forgecss: output CSS generated successfully.");
51 | }
52 | return css;
53 | } catch (err) {
54 | console.error(`forgecss: error generating output CSS: ${err}`);
55 | }
56 | return null;
57 | }
58 | function runWatcher(what, output, callback) {
59 | const watcher = chokidar.watch(what, {
60 | persistent: true,
61 | ignoreInitial: true,
62 | ignored: (p, stats) => output && path.resolve(p) === path.resolve(output)
63 | });
64 | watcher.on("change", async (filePath) => {
65 | if (config.verbose) {
66 | invalidateUsageCache(filePath)
67 | invalidateInventory(filePath);
68 | console.log(`forgecss: Detected change in ${filePath}`);
69 | }
70 | callback();
71 | });
72 | if (config.verbose) {
73 | console.log("forgecss: Watch mode enabled. Listening for file changes...");
74 | }
75 | }
76 |
77 | return {
78 | async parseDirectory({ dir, output = null, watch = false }) {
79 | if (!dir) {
80 | throw new Error('forgecss: parseDirectory requires "dir" as an argument.');
81 | }
82 | try {
83 | // filling the inventory
84 | let files = await getAllFiles(dir, config.inventoryFiles);
85 | for (let file of files) {
86 | extractStyles(file, await readFileContent(file));
87 | }
88 | } catch (err) {
89 | console.error(`forgecss: error extracting styles.`, err);
90 | }
91 | // finding the usages
92 | try {
93 | let files = await getAllFiles(dir, config.usageFiles);
94 | for (let file of files) {
95 | await findUsages(file, await readFileContent(file), JSXParser);
96 | }
97 | } catch (err) {
98 | console.error(`forgecss: error extracting usages`, err);
99 | }
100 | watch && runWatcher(dir, output, () => {
101 | this.parseDirectory({ dir, output, watch: false });
102 | });
103 | // generating the output CSS
104 | return result(output);
105 | },
106 | async parseFile({ file, output = null, watch = false }) {
107 | if (!file) {
108 | throw new Error('forgecss: parseFile requires "file" as an argument.');
109 | }
110 | const ext = file.split(".").pop().toLowerCase();
111 | // filling the inventory
112 | try {
113 | if (config.inventoryFiles.includes(ext)) {
114 | extractStyles(file, await readFileContent(file));
115 | }
116 | } catch (err) {
117 | console.error(`forgecss: error extracting styles.`, err);
118 | }
119 | // finding the usages
120 | try {
121 | if (config.usageFiles.includes(ext)) {
122 | invalidateUsageCache(file);
123 | await findUsages(file, await readFileContent(file), JSXParser);
124 | }
125 | } catch (err) {
126 | console.error(`forgecss: error extracting usages.`, err);
127 | }
128 | watch && runWatcher(file, output, () => {
129 | this.parseFile({ file, output, watch: false });
130 | });
131 | // generating the output CSS
132 | return result(output);
133 | },
134 | async parse({ css, html, jsx, output = null }) {
135 | if (!css) {
136 | throw new Error('forgecss: parse requires "css".');
137 | }
138 | if (!html && !jsx) {
139 | throw new Error('forgecss: parse requires "html" or "jsx".');
140 | }
141 | invalidateInventory();
142 | invalidateUsageCache();
143 | // filling the inventory
144 | try {
145 | extractStyles("styles.css", css);
146 | } catch (err) {
147 | console.error(`forgecss: error extracting styles.`, err);
148 | }
149 | // finding the usages
150 | try {
151 | if (html) {
152 | await findUsages("usage.html", html);
153 | } else if (jsx) {
154 | await findUsages("usage.jsx", jsx, JSXParser);
155 | }
156 | } catch (err) {
157 | console.error(`forgecss: error extracting usages.`, err);
158 | }
159 | return result(output);
160 | }
161 | };
162 | }
163 |
--------------------------------------------------------------------------------
/packages/site/public/input.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | input
--------------------------------------------------------------------------------
/packages/site/public/output.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | output
--------------------------------------------------------------------------------
/packages/site/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background-dark: #1b1b1b;
3 | --background: #292929;
4 | --background-light: #595959;
5 | --background-light2: #7b7b7b;
6 | --text: #f0f0f0;
7 | --warning: #df6464;
8 | --success: #61e481;
9 | --bit1: #8ff08f;
10 | --bit2: #e65f5f;
11 | --bit3: #909eee;
12 | }
13 | * {
14 | box-sizing: border-box;
15 | }
16 |
17 | body {
18 | background: var(--background);
19 | color: var(--text);
20 | font-family: "Arimo", sans-serif;
21 | font-optical-sizing: auto;
22 | font-weight: 200;
23 | font-style: normal;
24 | font-size: 20px;
25 | line-height: 1.4em;
26 | margin: 0;
27 | padding: 0;
28 | width: 100%;
29 | height: 100%;
30 | }
31 | h1, h2, h3, p, ul {
32 | margin: 0;
33 | padding: 0;
34 | line-height: 1.2em;
35 | }
36 | h1, h2, h3 {
37 | font-family: "Alfa Slab One", serif;
38 | font-weight: 400;
39 | font-style: normal;
40 | }
41 |
42 | .warning { color: var(--warning); }
43 | .success { color: var(--success); }
44 | .paler { color: var(--background-light2); }
45 | .p1 { padding: 1rem; }
46 | .p2 { padding: 2rem; }
47 | .p3 { padding: 3rem; }
48 | .pt2 { padding-top: 2rem; }
49 | .pl1 { padding-left: 1rem; }
50 | .py1 { padding-top: 1rem; padding-bottom: 1rem; }
51 | .py2 { padding-top: 2rem; padding-bottom: 2rem; }
52 | .py3 { padding-top: 3rem; padding-bottom: 3rem; }
53 | .pl3 { padding-left: 3rem; }
54 | .pr3 { padding-right: 3rem; }
55 | .pt3 { padding-top: 3rem; }
56 | .mt1 { margin-top: 1rem; }
57 | .mt2 { margin-top: 2rem; }
58 | .mt3 { margin-top: 3rem; }
59 | .my3 { margin-top: 3rem; margin-bottom: 3rem; }
60 | .mxauto { margin-left: auto; margin-right: auto; }
61 | .grid2 { display: grid; grid-template-columns: 1fr 1fr; }
62 | .grid2x1 { display: grid; grid-template-columns: 2fr 1fr; }
63 | .b { display: block; }
64 | .flex { display: flex; }
65 | .flex-center { display: flex; align-items: center; justify-content: center; }
66 | .flex-v-center { display: flex; align-items: center; }
67 | .flex-col { display: flex; flex-direction: column; }
68 | .flex1 { flex: 1; }
69 | .span2 { grid-column: span 2; }
70 | .gap05 { gap: 0.5rem; }
71 | .gap1 { gap: 1rem; }
72 | .gap2 { gap: 2rem; }
73 | .maxw600 { max-width: 600px; }
74 | .maxw800 { max-width: 800px; }
75 | .maxw1000 { max-width: 1000px; }
76 | .maxw1400 { max-width: 1400px; }
77 | .minh500 { min-height: 500px; }
78 | .bg { background: var(--background); }
79 | .bg-dark { background: var(--background-dark); }
80 | .black-bg { background: black; }
81 | .fz1 { font-size: 1rem; }
82 | .fz15 { font-size: 1.5rem; }
83 | .fz2 { font-size: 2rem; }
84 | .fz3 { font-size: 3rem; }
85 | .fw100 { font-weight: 100; }
86 | .hidden { display: none; }
87 | .tac { text-align: center; }
88 | .border-t { border-top: solid 1px var(--background-light); }
89 | .border-l { border-left: solid 1px var(--background-light); }
90 | .border-r { border-right: solid 1px var(--background-light); }
91 | .border-b { border-bottom: solid 1px var(--background-light); }
92 | .border { border: solid 1px var(--background-light);}
93 |
94 | header {
95 | background-color: #292929;
96 | opacity: 1;
97 | background: radial-gradient(circle, transparent 20%, #292929 20%, #292929 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #292929 20%, #292929 80%, transparent 80%, transparent) 10px 10px, linear-gradient(#000000 0.8px, transparent 0.8px) 0 -0.4px, linear-gradient(90deg, #000000 0.8px, #292929 0.8px) -0.4px 0;
98 | background-size: 20px 20px, 20px 20px, 10px 10px, 10px 10px;
99 | }
100 | .hero {
101 | background: radial-gradient(125% 125% at 50% 10%, #000000 40%, #031724 100%);
102 | border-top: solid 1px #575757;
103 | border-bottom: solid 1px #272727;
104 | }
105 | .hero nav a {
106 | text-align: center;
107 | color: var(--text);
108 | text-decoration: none;
109 | font-size: 1rem;
110 | background: radial-gradient(125% 125% at 50% 10%, #434343 40%, #000000 100%);
111 | padding: 1rem 2rem;
112 | border-radius: 4px;
113 | }
114 | .hero nav a:hover {
115 | background: radial-gradient(125% 125% at 50% 10%, #595959 40%, #000000 100%);
116 | }
117 | hr {
118 | border-top: solid 4px #000;
119 | border-bottom: none;
120 | border-left: none;
121 | border-right: none;;
122 | }
123 | .files {
124 | list-style: none;
125 | display: flex;
126 | }
127 | .files li {
128 | font-size: 0.8rem;
129 | border-top: solid 2px var(--background-light2);
130 | border-right: solid 2px var(--background-light2);
131 | }
132 | .files li:first-child {
133 | border-left: solid 2px var(--background-light2);
134 | }
135 | .files li button {
136 | display: block;
137 | background: none;
138 | border: none;
139 | color: var(--text);
140 | font-size: 1rem;
141 | cursor: pointer;
142 | padding: 0.5rem 1rem;
143 | opacity: 0.3;
144 | }
145 | .files li.selected button {
146 | background: var(--background-light);
147 | filter: drop-shadow(0px 2px 0px white);
148 | opacity: 1;
149 | }
150 | .editor-wrapper {
151 | border: solid 2px var(--background-light);
152 | background: var(--background);
153 | height: 520px;
154 | overflow-y: auto;
155 | }
156 | .editor {
157 | width: 100%;
158 | height: 100%;
159 | min-height: 400px;
160 | }
161 |
162 | /* Syntax Example */
163 | .syntax-example .input,
164 | .syntax-example .output {
165 | position: relative;
166 | }
167 | .syntax-example .input::before,
168 | .syntax-example .output::before {
169 | display: block;
170 | font-size: 0.8rem;
171 | position: absolute;
172 | top: 14px;
173 | line-height: 0;
174 | color: var(--background-light2);
175 | }
176 | .syntax-example .input::before {
177 | content: attr(data-label);
178 | left: 14px;
179 | }
180 | .syntax-example .output::before {
181 | content: attr(data-label);
182 | right: 14px;
183 | }
184 | .syntax-example code {
185 | font-size: 0.9rem;
186 | font-family: inherit;
187 | padding: 0.2rem 0.2rem;
188 | border-radius: 4px;
189 | }
190 | .code1 {
191 | border-bottom: solid 1px var(--bit1);
192 | }
193 | .code2 {
194 | border-bottom: solid 1px var(--bit2);
195 | }
196 | .code3 {
197 | border-bottom: solid 1px var(--bit3);
198 | }
199 | .bit1, .bit2, .bit3 {
200 | position: relative;
201 | padding: 0 4px 4px 4px;
202 | }
203 | .bit1::before, .bit2::before, .bit3::before {
204 | content: "";
205 | position: absolute;
206 | top: 0;
207 | left: 0;
208 | width: 2px;
209 | height: 4px;
210 | }
211 | .bit1::after, .bit2::after, .bit3::after {
212 | content: "";
213 | position: absolute;
214 | top: 0;
215 | right: 0;
216 | width: 2px;
217 | height: 4px;
218 | }
219 | .bit1 {
220 | border-top: solid 1px var(--bit1);
221 | }
222 | .bit1::after, .bit1::before {
223 | background: var(--bit1);
224 | }
225 | .bit2 {
226 | border-top: solid 1px var(--bit2);
227 | }
228 | .bit2::after, .bit2::before {
229 | background: var(--bit2);
230 | }
231 | .bit3 {
232 | border-top: solid 1px var(--bit3);
233 | }
234 | .bit3::after, .bit3::before {
235 | background: var(--bit3);
236 | }
237 |
238 | /* PrismJS Overrides */
239 | :not(pre)>code[class*=language-], pre[class*=language-] {
240 | background: none !important;
241 | font-size: 0.9rem !important;
242 | }
243 | .token.constant, .token.deleted, .token.property, .token.symbol, .token.tag {
244 | color: #569cd6 !important;
245 | }
246 | .token.attr-name, .token.builtin, .token.char, .token.inserted, .token.selector, .token.string {
247 | color: #9cdcfe !important;
248 | }
249 | .token.punctuation {
250 | color: #808080 !important;
251 | }
252 | .token.atrule, .token.attr-value, .token.class-name, .token.function {
253 | color: #ce9178 !important;
254 | }
255 | code {
256 | white-space: break-spaces !important;
257 | word-break: break-word !important;
258 | }
--------------------------------------------------------------------------------
/packages/site/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useReducer } from 'react';
2 | import fx from 'forgecss/fx'
3 | import { Editor } from './Editor';
4 | import { ACTUAL_HTML_FILE, DEFAULT_FILES, DEFAULT_OUTPUT_FILES, TOTAL_CSS_FILE } from './constants';
5 | import transformHtmlClassAttributes from './utils/transformHtmlClassAttributes';
6 | import Syntax from './Syntax';
7 |
8 | type File = {
9 | filename: string,
10 | content: string,
11 | selected: boolean,
12 | type: string
13 | }
14 |
15 | function filesReducer(state: File[], action: { type: string; payload?: any }) {
16 | if (action.type === "selected") {
17 | state = state.map((file, i) => ({
18 | ...file,
19 | selected: action.payload === i
20 | }))
21 | } else if (action.type === "change") {
22 | const [filename, content] = action.payload;
23 | state = state.map((file) => ({
24 | ...file,
25 | content: file.filename === filename ? content : file.content
26 | }))
27 | }
28 | return state;
29 | }
30 |
31 | function App() {
32 | const [inputFiles, updateInputFiles] = useReducer(filesReducer, DEFAULT_FILES);
33 | const [outputFiles, updateOutputFiles] = useReducer(filesReducer, DEFAULT_OUTPUT_FILES);
34 | const selectedInput = inputFiles.filter((f) => f.selected)[0];
35 | const selectedOutput = outputFiles.filter((f) => f.selected)[0];
36 |
37 | async function compile() {
38 | const css = inputFiles.filter((f) => f.filename === "styles.css")[0].content;
39 | const html = inputFiles.filter((f) => f.filename === "page.html")[0].content;
40 | let config = inputFiles.filter((f) => f.filename === "forgecss.config.json")[0].content;
41 | try {
42 | config = JSON.parse(config);
43 | // @ts-ignore
44 | config.minify = false;
45 | } catch(err) {
46 | console.error(err);
47 | return;
48 | }
49 | // @ts-ignore
50 | const forgecss = ForgeCSS(config);
51 | const result = await forgecss.parse({ css, html });
52 | updateOutputFiles({
53 | type: "change",
54 | payload: [TOTAL_CSS_FILE.filename, result]
55 | });
56 | updateOutputFiles({
57 | type: "change",
58 | payload: [
59 | ACTUAL_HTML_FILE.filename,
60 | transformHtmlClassAttributes(html, (className: string) => {
61 | return fx(className)
62 | })
63 | ]
64 | });
65 | }
66 |
67 | useEffect(() => {
68 | compile();
69 | }, [inputFiles])
70 |
71 | useEffect(() => {
72 | setTimeout(() => {
73 | // @ts-ignore
74 | Prism.highlightAll();
75 | }, 0);
76 | }, [outputFiles])
77 |
78 | return (
79 | <>
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
ForgeCSS is a compiler for utility CSS classes.
91 |
92 | ✔ Compiler that understands CSS class syntax
93 |
94 | ✔ It parses class strings, applies rules and structure, and compiles them
95 | into CSS.
96 |
97 |
98 | What it isn’t!
99 |
100 | ✖ Not a CSS framework or a transformer
101 |
102 |
103 | ✖ Not utility library
104 |
105 |
106 | ✖ Not a Tailwind plugin
107 |
108 |
109 | ✖ Not a runtime style engine
110 |
111 |
112 | ✖ Not a CSS-in-JS solution
113 |
114 |
115 |
116 | ForgeCSS gives you the freedom to create your own utilities and compile them into CSS.
117 |
118 |
119 |
139 |
140 |
141 |
147 |
148 |
149 |
150 |
151 | Test it out!
152 |
153 |
154 |
155 |
updateInputFiles({ type: "selected", payload: i })} />
156 | updateInputFiles({ type: "change", payload: [selectedInput.filename, code] })}
161 | />
162 |
163 |
164 |
165 |
166 |
167 |
updateOutputFiles({ type: "selected", payload: i })} />
168 |
169 |
170 | {selectedOutput.content}
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | >
181 | );
182 | }
183 |
184 | function Tabs({ files, onClick }: { files: File[]; onClick: Function }) {
185 | return (
186 |
187 | {files.map((file, i) => {
188 | return (
189 |
190 | onClick(i)}>{file.filename}
191 |
192 | );
193 | })}
194 |
195 | );
196 | }
197 |
198 | export default App
199 |
--------------------------------------------------------------------------------
/packages/forgecss/tests/cases/00-lang/test.js:
--------------------------------------------------------------------------------
1 | import { toAST } from "../../../lib/forge-lang/Parser.js";
2 | import { astToRules, rulesToCSS } from "../../../lib/forge-lang/Compiler.js";
3 | import fx from '../../../lib/fx.js'
4 | import {minifyCSS} from "../../../lib/forge-lang/utils.js";
5 |
6 | const mockGetStyleByClassName = (_) => [{ prop: "foo", value: "bar", important: false }];
7 |
8 | export default function test() {
9 | const parserCases = [
10 | {
11 | input: "btn primary hover:red",
12 | expected: [
13 | { type: "token", value: "btn" },
14 | { type: "token", value: "primary" },
15 | {
16 | type: "variant",
17 | selector: "hover",
18 | payload: {
19 | type: "token",
20 | value: "red"
21 | },
22 | simple: true
23 | }
24 | ]
25 | },
26 | {
27 | input: "text [.dark &]:text-white mt1",
28 | expected: [
29 | { type: "token", value: "text" },
30 | {
31 | type: "variant",
32 | selector: [
33 | {
34 | type: "token",
35 | value: ".dark"
36 | },
37 | {
38 | type: "token",
39 | value: "&"
40 | }
41 | ],
42 | payload: { type: "token", value: "text-white" }
43 | },
44 | { type: "token", value: "mt1" }
45 | ]
46 | },
47 | {
48 | input: "foo layout(px4 rounded)",
49 | expected: [
50 | {
51 | type: "token",
52 | value: "foo"
53 | },
54 | {
55 | type: "call",
56 | name: "layout",
57 | args: [
58 | {
59 | type: "token",
60 | value: "px4"
61 | },
62 | {
63 | type: "token",
64 | value: "rounded"
65 | }
66 | ]
67 | }
68 | ]
69 | },
70 | {
71 | input: "disabled:opacity-50 desktop:layout(px6 py3)",
72 | expected: [
73 | {
74 | type: "variant",
75 | selector: "disabled",
76 | payload: {
77 | type: "token",
78 | value: "opacity-50"
79 | },
80 | simple: true
81 | },
82 | {
83 | type: "variant",
84 | selector: "desktop",
85 | payload: {
86 | type: "call",
87 | name: "layout",
88 | args: [
89 | {
90 | type: "token",
91 | value: "px6"
92 | },
93 | {
94 | type: "token",
95 | value: "py3"
96 | }
97 | ]
98 | },
99 | simple: true
100 | }
101 | ]
102 | },
103 | {
104 | input: "[.dark &[type='password']]:bg-black",
105 | expected: [
106 | {
107 | type: "variant",
108 | selector: [
109 | {
110 | type: "token",
111 | value: ".dark"
112 | },
113 | {
114 | type: "token",
115 | value: "&[type='password']"
116 | }
117 | ],
118 | payload: {
119 | type: "token",
120 | value: "bg-black"
121 | }
122 | }
123 | ]
124 | },
125 | {
126 | input: `[&:has(.desc[title="a[b] c"])]:text(underline)`,
127 | expected: [
128 | {
129 | type: "variant",
130 | selector: [
131 | {
132 | type: "token",
133 | value: '&:has(.desc[title="a[b] c"])'
134 | }
135 | ],
136 | payload: {
137 | type: "call",
138 | name: "text",
139 | args: [
140 | {
141 | type: "token",
142 | value: "underline"
143 | }
144 | ]
145 | }
146 | }
147 | ]
148 | },
149 | {
150 | input: `
151 | theme(
152 | [.dark &]:text(text-white),
153 | hover:layout(bg-blue-700),
154 | )
155 | `,
156 | expected: [
157 | {
158 | type: "call",
159 | name: "theme",
160 | args: [
161 | {
162 | type: "variant",
163 | selector: [
164 | {
165 | type: "token",
166 | value: ".dark"
167 | },
168 | {
169 | type: "token",
170 | value: "&"
171 | }
172 | ],
173 | payload: {
174 | type: "call",
175 | name: "text",
176 | args: [
177 | {
178 | type: "token",
179 | value: "text-white"
180 | }
181 | ]
182 | }
183 | },
184 | {
185 | type: "variant",
186 | selector: "hover",
187 | payload: {
188 | type: "call",
189 | name: "layout",
190 | args: [
191 | {
192 | type: "token",
193 | value: "bg-blue-700"
194 | }
195 | ]
196 | },
197 | simple: true
198 | }
199 | ]
200 | }
201 | ]
202 | }
203 | ];
204 | // testing the parser
205 | for (let i = 0; i < parserCases.length; i++) {
206 | const testCase = parserCases[i];
207 | const result = toAST(testCase.input);
208 | if (JSON.stringify(result) !== JSON.stringify(testCase.expected)) {
209 | console.error(`#${i} Test failed for input:`, testCase.input);
210 | console.error("Expected:", testCase.expected);
211 | console.error("Got :", JSON.stringify(result, null, 2));
212 | return false;
213 | }
214 | }
215 |
216 | // testing the compiler
217 | const compilerCases = [
218 | {
219 | usage: ["hover:mt1 fz2 active:mt1,fz2,fz3"],
220 | classStr: ["hover_mt1 fz2 active_mt1 active_fz2 active_fz3"],
221 | expectedCSS:
222 | ".hover_mt1:hover{foo:bar}.active_mt1:active{foo:bar}.active_fz2:active{foo:bar}.active_fz3:active{foo:bar}"
223 | },
224 | {
225 | usage: ["desktop:mt1 fz2 desktop:p1", "mt2 pt1 desktop:mt1,fz3 fz2", "mobile:br-l"],
226 | classStr: ["desktop_mt1 fz2 desktop_p1", "mt2 pt1 desktop_mt1 desktop_fz3 fz2", "mobile_br-l"],
227 | expectedCSS:
228 | "@media all and (min-width:1024px){.desktop_mt1{foo:bar}.desktop_p1{foo:bar}.desktop_fz3{foo:bar}}@media all and (max-width:1023px){.mobile_br-l{foo:bar}}"
229 | },
230 | {
231 | usage: ["[&:hover]:red,fz2 mt1", "[.dark &]:b"],
232 | classStr: ["I-hover_red I-hover_fz2 mt1", "dark-I_b"],
233 | expectedCSS: ".I-hover_red:hover{foo:bar}.I-hover_fz2:hover{foo:bar}.dark .dark-I_b{foo:bar}"
234 | },
235 | {
236 | usage: ["desktop:[.dark &]:b desktop:mt1,p1"],
237 | classStr: ["desktop-dark-I_b desktop_mt1 desktop_p1"],
238 | expectedCSS: "@media all and (min-width:1024px){.dark .dark-I_b{foo:bar}.desktop_mt1{foo:bar}.desktop_p1{foo:bar}}"
239 | }
240 | ];
241 | for (let i=0; i {
258 | if (fx(usage) !== testCase.classStr[i]) {
259 | console.error(`#${i} Compiler Test failed (classStr):`);
260 | console.error("Expected:\n", testCase.classStr[i]);
261 | console.error("Got:\n", fx(usage));
262 | return false;
263 | }
264 | return true;
265 | })) {
266 | return false;
267 | }
268 | if (css !== testCase.expectedCSS) {
269 | console.error(`#${i} Compiler Test failed (expectedCSS):`);
270 | console.error("Expected:\n", testCase.expectedCSS);
271 | console.error("Got:\n", css);
272 | return false;
273 | }
274 | }
275 | return true;
276 | }
--------------------------------------------------------------------------------
/examples/react/ast.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Module",
3 | "span": {
4 | "start": 2445,
5 | "end": 2674
6 | },
7 | "body": [
8 | {
9 | "type": "ImportDeclaration",
10 | "span": {
11 | "start": 2445,
12 | "end": 2479
13 | },
14 | "specifiers": [
15 | {
16 | "type": "ImportSpecifier",
17 | "span": {
18 | "start": 2454,
19 | "end": 2464
20 | },
21 | "local": {
22 | "type": "Identifier",
23 | "span": {
24 | "start": 2454,
25 | "end": 2464
26 | },
27 | "ctxt": 2,
28 | "value": "StrictMode",
29 | "optional": false
30 | },
31 | "imported": null,
32 | "isTypeOnly": false
33 | }
34 | ],
35 | "source": {
36 | "type": "StringLiteral",
37 | "span": {
38 | "start": 2472,
39 | "end": 2479
40 | },
41 | "value": "react",
42 | "raw": "'react'"
43 | },
44 | "typeOnly": false,
45 | "with": null,
46 | "phase": "evaluation"
47 | },
48 | {
49 | "type": "ImportDeclaration",
50 | "span": {
51 | "start": 2480,
52 | "end": 2525
53 | },
54 | "specifiers": [
55 | {
56 | "type": "ImportSpecifier",
57 | "span": {
58 | "start": 2489,
59 | "end": 2499
60 | },
61 | "local": {
62 | "type": "Identifier",
63 | "span": {
64 | "start": 2489,
65 | "end": 2499
66 | },
67 | "ctxt": 2,
68 | "value": "createRoot",
69 | "optional": false
70 | },
71 | "imported": null,
72 | "isTypeOnly": false
73 | }
74 | ],
75 | "source": {
76 | "type": "StringLiteral",
77 | "span": {
78 | "start": 2507,
79 | "end": 2525
80 | },
81 | "value": "react-dom/client",
82 | "raw": "'react-dom/client'"
83 | },
84 | "typeOnly": false,
85 | "with": null,
86 | "phase": "evaluation"
87 | },
88 | {
89 | "type": "ImportDeclaration",
90 | "span": {
91 | "start": 2526,
92 | "end": 2546
93 | },
94 | "specifiers": [],
95 | "source": {
96 | "type": "StringLiteral",
97 | "span": {
98 | "start": 2533,
99 | "end": 2546
100 | },
101 | "value": "./index.css",
102 | "raw": "'./index.css'"
103 | },
104 | "typeOnly": false,
105 | "with": null,
106 | "phase": "evaluation"
107 | },
108 | {
109 | "type": "ImportDeclaration",
110 | "span": {
111 | "start": 2547,
112 | "end": 2574
113 | },
114 | "specifiers": [
115 | {
116 | "type": "ImportDefaultSpecifier",
117 | "span": {
118 | "start": 2554,
119 | "end": 2557
120 | },
121 | "local": {
122 | "type": "Identifier",
123 | "span": {
124 | "start": 2554,
125 | "end": 2557
126 | },
127 | "ctxt": 2,
128 | "value": "App",
129 | "optional": false
130 | }
131 | }
132 | ],
133 | "source": {
134 | "type": "StringLiteral",
135 | "span": {
136 | "start": 2563,
137 | "end": 2574
138 | },
139 | "value": "./App.tsx",
140 | "raw": "'./App.tsx'"
141 | },
142 | "typeOnly": false,
143 | "with": null,
144 | "phase": "evaluation"
145 | },
146 | {
147 | "type": "ExpressionStatement",
148 | "span": {
149 | "start": 2576,
150 | "end": 2674
151 | },
152 | "expression": {
153 | "type": "CallExpression",
154 | "span": {
155 | "start": 2576,
156 | "end": 2674
157 | },
158 | "ctxt": 0,
159 | "callee": {
160 | "type": "MemberExpression",
161 | "span": {
162 | "start": 2576,
163 | "end": 2627
164 | },
165 | "object": {
166 | "type": "CallExpression",
167 | "span": {
168 | "start": 2576,
169 | "end": 2620
170 | },
171 | "ctxt": 0,
172 | "callee": {
173 | "type": "Identifier",
174 | "span": {
175 | "start": 2576,
176 | "end": 2586
177 | },
178 | "ctxt": 2,
179 | "value": "createRoot",
180 | "optional": false
181 | },
182 | "arguments": [
183 | {
184 | "spread": null,
185 | "expression": {
186 | "type": "TsNonNullExpression",
187 | "span": {
188 | "start": 2587,
189 | "end": 2619
190 | },
191 | "expression": {
192 | "type": "CallExpression",
193 | "span": {
194 | "start": 2587,
195 | "end": 2618
196 | },
197 | "ctxt": 0,
198 | "callee": {
199 | "type": "MemberExpression",
200 | "span": {
201 | "start": 2587,
202 | "end": 2610
203 | },
204 | "object": {
205 | "type": "Identifier",
206 | "span": {
207 | "start": 2587,
208 | "end": 2595
209 | },
210 | "ctxt": 1,
211 | "value": "document",
212 | "optional": false
213 | },
214 | "property": {
215 | "type": "Identifier",
216 | "span": {
217 | "start": 2596,
218 | "end": 2610
219 | },
220 | "value": "getElementById"
221 | }
222 | },
223 | "arguments": [
224 | {
225 | "spread": null,
226 | "expression": {
227 | "type": "StringLiteral",
228 | "span": {
229 | "start": 2611,
230 | "end": 2617
231 | },
232 | "value": "root",
233 | "raw": "'root'"
234 | }
235 | }
236 | ],
237 | "typeArguments": null
238 | }
239 | }
240 | }
241 | ],
242 | "typeArguments": null
243 | },
244 | "property": {
245 | "type": "Identifier",
246 | "span": {
247 | "start": 2621,
248 | "end": 2627
249 | },
250 | "value": "render"
251 | }
252 | },
253 | "arguments": [
254 | {
255 | "spread": null,
256 | "expression": {
257 | "type": "JSXElement",
258 | "span": {
259 | "start": 2631,
260 | "end": 2671
261 | },
262 | "opening": {
263 | "type": "JSXOpeningElement",
264 | "name": {
265 | "type": "Identifier",
266 | "span": {
267 | "start": 2632,
268 | "end": 2642
269 | },
270 | "ctxt": 2,
271 | "value": "StrictMode",
272 | "optional": false
273 | },
274 | "span": {
275 | "start": 2631,
276 | "end": 2643
277 | },
278 | "attributes": [],
279 | "selfClosing": false,
280 | "typeArguments": null
281 | },
282 | "children": [
283 | {
284 | "type": "JSXText",
285 | "span": {
286 | "start": 2643,
287 | "end": 2648
288 | },
289 | "value": "\n ",
290 | "raw": "\n "
291 | },
292 | {
293 | "type": "JSXElement",
294 | "span": {
295 | "start": 2648,
296 | "end": 2655
297 | },
298 | "opening": {
299 | "type": "JSXOpeningElement",
300 | "name": {
301 | "type": "Identifier",
302 | "span": {
303 | "start": 2649,
304 | "end": 2652
305 | },
306 | "ctxt": 2,
307 | "value": "App",
308 | "optional": false
309 | },
310 | "span": {
311 | "start": 2648,
312 | "end": 2655
313 | },
314 | "attributes": [],
315 | "selfClosing": true,
316 | "typeArguments": null
317 | },
318 | "children": [],
319 | "closing": null
320 | },
321 | {
322 | "type": "JSXText",
323 | "span": {
324 | "start": 2655,
325 | "end": 2658
326 | },
327 | "value": "\n ",
328 | "raw": "\n "
329 | }
330 | ],
331 | "closing": {
332 | "type": "JSXClosingElement",
333 | "span": {
334 | "start": 2658,
335 | "end": 2671
336 | },
337 | "name": {
338 | "type": "Identifier",
339 | "span": {
340 | "start": 2660,
341 | "end": 2670
342 | },
343 | "ctxt": 2,
344 | "value": "StrictMode",
345 | "optional": false
346 | }
347 | }
348 | }
349 | }
350 | ],
351 | "typeArguments": null
352 | }
353 | }
354 | ],
355 | "interpreter": null
356 | }
--------------------------------------------------------------------------------
/packages/site/public/prism.js:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.30.0
2 | https://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+jsx+tsx+typescript */
3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(Cg.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
4 | Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;
5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);
6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
8 | !function(t){var n=t.util.clone(t.languages.javascript),e="(?:\\{*\\.{3}(?:[^{}]|)*\\})";function a(t,n){return t=t.replace(//g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(//g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(//g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend("markup",n),t.languages.jsx.tag.pattern=a("?(?:[\\w.:-]+(?:+(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+|))?|))**/?)?>"),t.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,t.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,t.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore("inside","attr-name",{spread:{pattern:a(""),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("="),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?"string"==typeof t?t:"string"==typeof t.content?t.content:t.content.map(s).join(""):""},g=function(n){for(var e=[],a=0;a0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():"/>"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&"punctuation"===o.type&&"{"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&"punctuation"===o.type&&"}"===o.content?e[e.length-1].openedBraces--:i=!0),(i||"string"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a0&&("string"==typeof n[a-1]||"plain-text"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token("plain-text",r,null,r)}o.content&&"string"!=typeof o.content&&g(o.content)}};t.hooks.add("after-tokenize",(function(t){"jsx"!==t.language&&"tsx"!==t.language||g(t.tokens)}))}(Prism);
9 | !function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism);
10 | !function(e){var a=e.util.clone(e.languages.typescript);e.languages.tsx=e.languages.extend("jsx",a),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"];var t=e.languages.tsx.tag;t.pattern=RegExp("(^|[^\\w$]|(?=))(?:"+t.pattern.source+")",t.pattern.flags),t.lookbehind=!0}(Prism);
11 |
--------------------------------------------------------------------------------
/examples/vanilla/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vanilla",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "express": "^5.2.1"
9 | }
10 | },
11 | "node_modules/accepts": {
12 | "version": "2.0.0",
13 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
14 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
15 | "license": "MIT",
16 | "dependencies": {
17 | "mime-types": "^3.0.0",
18 | "negotiator": "^1.0.0"
19 | },
20 | "engines": {
21 | "node": ">= 0.6"
22 | }
23 | },
24 | "node_modules/body-parser": {
25 | "version": "2.2.1",
26 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
27 | "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
28 | "license": "MIT",
29 | "dependencies": {
30 | "bytes": "^3.1.2",
31 | "content-type": "^1.0.5",
32 | "debug": "^4.4.3",
33 | "http-errors": "^2.0.0",
34 | "iconv-lite": "^0.7.0",
35 | "on-finished": "^2.4.1",
36 | "qs": "^6.14.0",
37 | "raw-body": "^3.0.1",
38 | "type-is": "^2.0.1"
39 | },
40 | "engines": {
41 | "node": ">=18"
42 | },
43 | "funding": {
44 | "type": "opencollective",
45 | "url": "https://opencollective.com/express"
46 | }
47 | },
48 | "node_modules/bytes": {
49 | "version": "3.1.2",
50 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
51 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
52 | "license": "MIT",
53 | "engines": {
54 | "node": ">= 0.8"
55 | }
56 | },
57 | "node_modules/call-bind-apply-helpers": {
58 | "version": "1.0.2",
59 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
60 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
61 | "license": "MIT",
62 | "dependencies": {
63 | "es-errors": "^1.3.0",
64 | "function-bind": "^1.1.2"
65 | },
66 | "engines": {
67 | "node": ">= 0.4"
68 | }
69 | },
70 | "node_modules/call-bound": {
71 | "version": "1.0.4",
72 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
73 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
74 | "license": "MIT",
75 | "dependencies": {
76 | "call-bind-apply-helpers": "^1.0.2",
77 | "get-intrinsic": "^1.3.0"
78 | },
79 | "engines": {
80 | "node": ">= 0.4"
81 | },
82 | "funding": {
83 | "url": "https://github.com/sponsors/ljharb"
84 | }
85 | },
86 | "node_modules/content-disposition": {
87 | "version": "1.0.1",
88 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
89 | "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
90 | "license": "MIT",
91 | "engines": {
92 | "node": ">=18"
93 | },
94 | "funding": {
95 | "type": "opencollective",
96 | "url": "https://opencollective.com/express"
97 | }
98 | },
99 | "node_modules/content-type": {
100 | "version": "1.0.5",
101 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
102 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
103 | "license": "MIT",
104 | "engines": {
105 | "node": ">= 0.6"
106 | }
107 | },
108 | "node_modules/cookie": {
109 | "version": "0.7.2",
110 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
111 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
112 | "license": "MIT",
113 | "engines": {
114 | "node": ">= 0.6"
115 | }
116 | },
117 | "node_modules/cookie-signature": {
118 | "version": "1.2.2",
119 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
120 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
121 | "license": "MIT",
122 | "engines": {
123 | "node": ">=6.6.0"
124 | }
125 | },
126 | "node_modules/debug": {
127 | "version": "4.4.3",
128 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
129 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
130 | "license": "MIT",
131 | "dependencies": {
132 | "ms": "^2.1.3"
133 | },
134 | "engines": {
135 | "node": ">=6.0"
136 | },
137 | "peerDependenciesMeta": {
138 | "supports-color": {
139 | "optional": true
140 | }
141 | }
142 | },
143 | "node_modules/depd": {
144 | "version": "2.0.0",
145 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
146 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
147 | "license": "MIT",
148 | "engines": {
149 | "node": ">= 0.8"
150 | }
151 | },
152 | "node_modules/dunder-proto": {
153 | "version": "1.0.1",
154 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
155 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
156 | "license": "MIT",
157 | "dependencies": {
158 | "call-bind-apply-helpers": "^1.0.1",
159 | "es-errors": "^1.3.0",
160 | "gopd": "^1.2.0"
161 | },
162 | "engines": {
163 | "node": ">= 0.4"
164 | }
165 | },
166 | "node_modules/ee-first": {
167 | "version": "1.1.1",
168 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
169 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
170 | "license": "MIT"
171 | },
172 | "node_modules/encodeurl": {
173 | "version": "2.0.0",
174 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
175 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
176 | "license": "MIT",
177 | "engines": {
178 | "node": ">= 0.8"
179 | }
180 | },
181 | "node_modules/es-define-property": {
182 | "version": "1.0.1",
183 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
184 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
185 | "license": "MIT",
186 | "engines": {
187 | "node": ">= 0.4"
188 | }
189 | },
190 | "node_modules/es-errors": {
191 | "version": "1.3.0",
192 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
193 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
194 | "license": "MIT",
195 | "engines": {
196 | "node": ">= 0.4"
197 | }
198 | },
199 | "node_modules/es-object-atoms": {
200 | "version": "1.1.1",
201 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
202 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
203 | "license": "MIT",
204 | "dependencies": {
205 | "es-errors": "^1.3.0"
206 | },
207 | "engines": {
208 | "node": ">= 0.4"
209 | }
210 | },
211 | "node_modules/escape-html": {
212 | "version": "1.0.3",
213 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
214 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
215 | "license": "MIT"
216 | },
217 | "node_modules/etag": {
218 | "version": "1.8.1",
219 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
220 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
221 | "license": "MIT",
222 | "engines": {
223 | "node": ">= 0.6"
224 | }
225 | },
226 | "node_modules/express": {
227 | "version": "5.2.1",
228 | "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
229 | "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
230 | "license": "MIT",
231 | "dependencies": {
232 | "accepts": "^2.0.0",
233 | "body-parser": "^2.2.1",
234 | "content-disposition": "^1.0.0",
235 | "content-type": "^1.0.5",
236 | "cookie": "^0.7.1",
237 | "cookie-signature": "^1.2.1",
238 | "debug": "^4.4.0",
239 | "depd": "^2.0.0",
240 | "encodeurl": "^2.0.0",
241 | "escape-html": "^1.0.3",
242 | "etag": "^1.8.1",
243 | "finalhandler": "^2.1.0",
244 | "fresh": "^2.0.0",
245 | "http-errors": "^2.0.0",
246 | "merge-descriptors": "^2.0.0",
247 | "mime-types": "^3.0.0",
248 | "on-finished": "^2.4.1",
249 | "once": "^1.4.0",
250 | "parseurl": "^1.3.3",
251 | "proxy-addr": "^2.0.7",
252 | "qs": "^6.14.0",
253 | "range-parser": "^1.2.1",
254 | "router": "^2.2.0",
255 | "send": "^1.1.0",
256 | "serve-static": "^2.2.0",
257 | "statuses": "^2.0.1",
258 | "type-is": "^2.0.1",
259 | "vary": "^1.1.2"
260 | },
261 | "engines": {
262 | "node": ">= 18"
263 | },
264 | "funding": {
265 | "type": "opencollective",
266 | "url": "https://opencollective.com/express"
267 | }
268 | },
269 | "node_modules/finalhandler": {
270 | "version": "2.1.1",
271 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
272 | "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
273 | "license": "MIT",
274 | "dependencies": {
275 | "debug": "^4.4.0",
276 | "encodeurl": "^2.0.0",
277 | "escape-html": "^1.0.3",
278 | "on-finished": "^2.4.1",
279 | "parseurl": "^1.3.3",
280 | "statuses": "^2.0.1"
281 | },
282 | "engines": {
283 | "node": ">= 18.0.0"
284 | },
285 | "funding": {
286 | "type": "opencollective",
287 | "url": "https://opencollective.com/express"
288 | }
289 | },
290 | "node_modules/forwarded": {
291 | "version": "0.2.0",
292 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
293 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
294 | "license": "MIT",
295 | "engines": {
296 | "node": ">= 0.6"
297 | }
298 | },
299 | "node_modules/fresh": {
300 | "version": "2.0.0",
301 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
302 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
303 | "license": "MIT",
304 | "engines": {
305 | "node": ">= 0.8"
306 | }
307 | },
308 | "node_modules/function-bind": {
309 | "version": "1.1.2",
310 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
311 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
312 | "license": "MIT",
313 | "funding": {
314 | "url": "https://github.com/sponsors/ljharb"
315 | }
316 | },
317 | "node_modules/get-intrinsic": {
318 | "version": "1.3.0",
319 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
320 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
321 | "license": "MIT",
322 | "dependencies": {
323 | "call-bind-apply-helpers": "^1.0.2",
324 | "es-define-property": "^1.0.1",
325 | "es-errors": "^1.3.0",
326 | "es-object-atoms": "^1.1.1",
327 | "function-bind": "^1.1.2",
328 | "get-proto": "^1.0.1",
329 | "gopd": "^1.2.0",
330 | "has-symbols": "^1.1.0",
331 | "hasown": "^2.0.2",
332 | "math-intrinsics": "^1.1.0"
333 | },
334 | "engines": {
335 | "node": ">= 0.4"
336 | },
337 | "funding": {
338 | "url": "https://github.com/sponsors/ljharb"
339 | }
340 | },
341 | "node_modules/get-proto": {
342 | "version": "1.0.1",
343 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
344 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
345 | "license": "MIT",
346 | "dependencies": {
347 | "dunder-proto": "^1.0.1",
348 | "es-object-atoms": "^1.0.0"
349 | },
350 | "engines": {
351 | "node": ">= 0.4"
352 | }
353 | },
354 | "node_modules/gopd": {
355 | "version": "1.2.0",
356 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
357 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
358 | "license": "MIT",
359 | "engines": {
360 | "node": ">= 0.4"
361 | },
362 | "funding": {
363 | "url": "https://github.com/sponsors/ljharb"
364 | }
365 | },
366 | "node_modules/has-symbols": {
367 | "version": "1.1.0",
368 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
369 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
370 | "license": "MIT",
371 | "engines": {
372 | "node": ">= 0.4"
373 | },
374 | "funding": {
375 | "url": "https://github.com/sponsors/ljharb"
376 | }
377 | },
378 | "node_modules/hasown": {
379 | "version": "2.0.2",
380 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
381 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
382 | "license": "MIT",
383 | "dependencies": {
384 | "function-bind": "^1.1.2"
385 | },
386 | "engines": {
387 | "node": ">= 0.4"
388 | }
389 | },
390 | "node_modules/http-errors": {
391 | "version": "2.0.1",
392 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
393 | "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
394 | "license": "MIT",
395 | "dependencies": {
396 | "depd": "~2.0.0",
397 | "inherits": "~2.0.4",
398 | "setprototypeof": "~1.2.0",
399 | "statuses": "~2.0.2",
400 | "toidentifier": "~1.0.1"
401 | },
402 | "engines": {
403 | "node": ">= 0.8"
404 | },
405 | "funding": {
406 | "type": "opencollective",
407 | "url": "https://opencollective.com/express"
408 | }
409 | },
410 | "node_modules/iconv-lite": {
411 | "version": "0.7.0",
412 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
413 | "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
414 | "license": "MIT",
415 | "dependencies": {
416 | "safer-buffer": ">= 2.1.2 < 3.0.0"
417 | },
418 | "engines": {
419 | "node": ">=0.10.0"
420 | },
421 | "funding": {
422 | "type": "opencollective",
423 | "url": "https://opencollective.com/express"
424 | }
425 | },
426 | "node_modules/inherits": {
427 | "version": "2.0.4",
428 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
429 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
430 | "license": "ISC"
431 | },
432 | "node_modules/ipaddr.js": {
433 | "version": "1.9.1",
434 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
435 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
436 | "license": "MIT",
437 | "engines": {
438 | "node": ">= 0.10"
439 | }
440 | },
441 | "node_modules/is-promise": {
442 | "version": "4.0.0",
443 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
444 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
445 | "license": "MIT"
446 | },
447 | "node_modules/math-intrinsics": {
448 | "version": "1.1.0",
449 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
450 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
451 | "license": "MIT",
452 | "engines": {
453 | "node": ">= 0.4"
454 | }
455 | },
456 | "node_modules/media-typer": {
457 | "version": "1.1.0",
458 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
459 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
460 | "license": "MIT",
461 | "engines": {
462 | "node": ">= 0.8"
463 | }
464 | },
465 | "node_modules/merge-descriptors": {
466 | "version": "2.0.0",
467 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
468 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
469 | "license": "MIT",
470 | "engines": {
471 | "node": ">=18"
472 | },
473 | "funding": {
474 | "url": "https://github.com/sponsors/sindresorhus"
475 | }
476 | },
477 | "node_modules/mime-db": {
478 | "version": "1.54.0",
479 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
480 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
481 | "license": "MIT",
482 | "engines": {
483 | "node": ">= 0.6"
484 | }
485 | },
486 | "node_modules/mime-types": {
487 | "version": "3.0.2",
488 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
489 | "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
490 | "license": "MIT",
491 | "dependencies": {
492 | "mime-db": "^1.54.0"
493 | },
494 | "engines": {
495 | "node": ">=18"
496 | },
497 | "funding": {
498 | "type": "opencollective",
499 | "url": "https://opencollective.com/express"
500 | }
501 | },
502 | "node_modules/ms": {
503 | "version": "2.1.3",
504 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
505 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
506 | "license": "MIT"
507 | },
508 | "node_modules/negotiator": {
509 | "version": "1.0.0",
510 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
511 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
512 | "license": "MIT",
513 | "engines": {
514 | "node": ">= 0.6"
515 | }
516 | },
517 | "node_modules/object-inspect": {
518 | "version": "1.13.4",
519 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
520 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
521 | "license": "MIT",
522 | "engines": {
523 | "node": ">= 0.4"
524 | },
525 | "funding": {
526 | "url": "https://github.com/sponsors/ljharb"
527 | }
528 | },
529 | "node_modules/on-finished": {
530 | "version": "2.4.1",
531 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
532 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
533 | "license": "MIT",
534 | "dependencies": {
535 | "ee-first": "1.1.1"
536 | },
537 | "engines": {
538 | "node": ">= 0.8"
539 | }
540 | },
541 | "node_modules/once": {
542 | "version": "1.4.0",
543 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
544 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
545 | "license": "ISC",
546 | "dependencies": {
547 | "wrappy": "1"
548 | }
549 | },
550 | "node_modules/parseurl": {
551 | "version": "1.3.3",
552 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
553 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
554 | "license": "MIT",
555 | "engines": {
556 | "node": ">= 0.8"
557 | }
558 | },
559 | "node_modules/path-to-regexp": {
560 | "version": "8.3.0",
561 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
562 | "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
563 | "license": "MIT",
564 | "funding": {
565 | "type": "opencollective",
566 | "url": "https://opencollective.com/express"
567 | }
568 | },
569 | "node_modules/proxy-addr": {
570 | "version": "2.0.7",
571 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
572 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
573 | "license": "MIT",
574 | "dependencies": {
575 | "forwarded": "0.2.0",
576 | "ipaddr.js": "1.9.1"
577 | },
578 | "engines": {
579 | "node": ">= 0.10"
580 | }
581 | },
582 | "node_modules/qs": {
583 | "version": "6.14.0",
584 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
585 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
586 | "license": "BSD-3-Clause",
587 | "dependencies": {
588 | "side-channel": "^1.1.0"
589 | },
590 | "engines": {
591 | "node": ">=0.6"
592 | },
593 | "funding": {
594 | "url": "https://github.com/sponsors/ljharb"
595 | }
596 | },
597 | "node_modules/range-parser": {
598 | "version": "1.2.1",
599 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
600 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
601 | "license": "MIT",
602 | "engines": {
603 | "node": ">= 0.6"
604 | }
605 | },
606 | "node_modules/raw-body": {
607 | "version": "3.0.2",
608 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
609 | "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
610 | "license": "MIT",
611 | "dependencies": {
612 | "bytes": "~3.1.2",
613 | "http-errors": "~2.0.1",
614 | "iconv-lite": "~0.7.0",
615 | "unpipe": "~1.0.0"
616 | },
617 | "engines": {
618 | "node": ">= 0.10"
619 | }
620 | },
621 | "node_modules/router": {
622 | "version": "2.2.0",
623 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
624 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
625 | "license": "MIT",
626 | "dependencies": {
627 | "debug": "^4.4.0",
628 | "depd": "^2.0.0",
629 | "is-promise": "^4.0.0",
630 | "parseurl": "^1.3.3",
631 | "path-to-regexp": "^8.0.0"
632 | },
633 | "engines": {
634 | "node": ">= 18"
635 | }
636 | },
637 | "node_modules/safer-buffer": {
638 | "version": "2.1.2",
639 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
640 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
641 | "license": "MIT"
642 | },
643 | "node_modules/send": {
644 | "version": "1.2.0",
645 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
646 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
647 | "license": "MIT",
648 | "dependencies": {
649 | "debug": "^4.3.5",
650 | "encodeurl": "^2.0.0",
651 | "escape-html": "^1.0.3",
652 | "etag": "^1.8.1",
653 | "fresh": "^2.0.0",
654 | "http-errors": "^2.0.0",
655 | "mime-types": "^3.0.1",
656 | "ms": "^2.1.3",
657 | "on-finished": "^2.4.1",
658 | "range-parser": "^1.2.1",
659 | "statuses": "^2.0.1"
660 | },
661 | "engines": {
662 | "node": ">= 18"
663 | }
664 | },
665 | "node_modules/serve-static": {
666 | "version": "2.2.0",
667 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
668 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
669 | "license": "MIT",
670 | "dependencies": {
671 | "encodeurl": "^2.0.0",
672 | "escape-html": "^1.0.3",
673 | "parseurl": "^1.3.3",
674 | "send": "^1.2.0"
675 | },
676 | "engines": {
677 | "node": ">= 18"
678 | }
679 | },
680 | "node_modules/setprototypeof": {
681 | "version": "1.2.0",
682 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
683 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
684 | "license": "ISC"
685 | },
686 | "node_modules/side-channel": {
687 | "version": "1.1.0",
688 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
689 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
690 | "license": "MIT",
691 | "dependencies": {
692 | "es-errors": "^1.3.0",
693 | "object-inspect": "^1.13.3",
694 | "side-channel-list": "^1.0.0",
695 | "side-channel-map": "^1.0.1",
696 | "side-channel-weakmap": "^1.0.2"
697 | },
698 | "engines": {
699 | "node": ">= 0.4"
700 | },
701 | "funding": {
702 | "url": "https://github.com/sponsors/ljharb"
703 | }
704 | },
705 | "node_modules/side-channel-list": {
706 | "version": "1.0.0",
707 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
708 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
709 | "license": "MIT",
710 | "dependencies": {
711 | "es-errors": "^1.3.0",
712 | "object-inspect": "^1.13.3"
713 | },
714 | "engines": {
715 | "node": ">= 0.4"
716 | },
717 | "funding": {
718 | "url": "https://github.com/sponsors/ljharb"
719 | }
720 | },
721 | "node_modules/side-channel-map": {
722 | "version": "1.0.1",
723 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
724 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
725 | "license": "MIT",
726 | "dependencies": {
727 | "call-bound": "^1.0.2",
728 | "es-errors": "^1.3.0",
729 | "get-intrinsic": "^1.2.5",
730 | "object-inspect": "^1.13.3"
731 | },
732 | "engines": {
733 | "node": ">= 0.4"
734 | },
735 | "funding": {
736 | "url": "https://github.com/sponsors/ljharb"
737 | }
738 | },
739 | "node_modules/side-channel-weakmap": {
740 | "version": "1.0.2",
741 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
742 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
743 | "license": "MIT",
744 | "dependencies": {
745 | "call-bound": "^1.0.2",
746 | "es-errors": "^1.3.0",
747 | "get-intrinsic": "^1.2.5",
748 | "object-inspect": "^1.13.3",
749 | "side-channel-map": "^1.0.1"
750 | },
751 | "engines": {
752 | "node": ">= 0.4"
753 | },
754 | "funding": {
755 | "url": "https://github.com/sponsors/ljharb"
756 | }
757 | },
758 | "node_modules/statuses": {
759 | "version": "2.0.2",
760 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
761 | "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
762 | "license": "MIT",
763 | "engines": {
764 | "node": ">= 0.8"
765 | }
766 | },
767 | "node_modules/toidentifier": {
768 | "version": "1.0.1",
769 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
770 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
771 | "license": "MIT",
772 | "engines": {
773 | "node": ">=0.6"
774 | }
775 | },
776 | "node_modules/type-is": {
777 | "version": "2.0.1",
778 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
779 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
780 | "license": "MIT",
781 | "dependencies": {
782 | "content-type": "^1.0.5",
783 | "media-typer": "^1.1.0",
784 | "mime-types": "^3.0.0"
785 | },
786 | "engines": {
787 | "node": ">= 0.6"
788 | }
789 | },
790 | "node_modules/unpipe": {
791 | "version": "1.0.0",
792 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
793 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
794 | "license": "MIT",
795 | "engines": {
796 | "node": ">= 0.8"
797 | }
798 | },
799 | "node_modules/vary": {
800 | "version": "1.1.2",
801 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
802 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
803 | "license": "MIT",
804 | "engines": {
805 | "node": ">= 0.8"
806 | }
807 | },
808 | "node_modules/wrappy": {
809 | "version": "1.0.2",
810 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
811 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
812 | "license": "ISC"
813 | }
814 | }
815 | }
816 |
--------------------------------------------------------------------------------