@praxisjs/vite-plugin 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/__tests__/css-esbuild-error.test.d.ts +2 -0
- package/dist/__tests__/css-esbuild-error.test.d.ts.map +1 -0
- package/dist/__tests__/css-esbuild-error.test.js +25 -0
- package/dist/__tests__/css-esbuild-error.test.js.map +1 -0
- package/dist/__tests__/css.test.d.ts +2 -0
- package/dist/__tests__/css.test.d.ts.map +1 -0
- package/dist/__tests__/css.test.js +243 -0
- package/dist/__tests__/css.test.js.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +122 -0
- package/dist/__tests__/index.test.js.map +1 -0
- package/dist/css.d.ts +20 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/css.js +172 -0
- package/dist/css.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/__tests__/css-esbuild-error.test.ts +39 -0
- package/src/__tests__/css.test.ts +319 -0
- package/src/__tests__/index.test.ts +143 -0
- package/src/css.ts +186 -0
- package/src/index.ts +4 -0
package/dist/css.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import vm from "node:vm";
|
|
4
|
+
import { build as esbuildBuild } from "esbuild";
|
|
5
|
+
import { extractionModule } from "@praxisjs/css/extract";
|
|
6
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
7
|
+
// The .css extension is required so Vite recognises this as a CSS asset and
|
|
8
|
+
// emits it as a separate file rather than inlining it into a JS module.
|
|
9
|
+
const VIRTUAL_ID = "virtual:praxisjs/styles.css";
|
|
10
|
+
const RESOLVED_ID = "\0virtual:praxisjs/styles.css";
|
|
11
|
+
const HAS_CSS_CONTENT = /extends\s+(Reactive)?Stylesheet\b|\bStyled\s*\(|\bkeyframes\s*\(|\bglobalStyle\s*\(|\bThemed\s*\(/;
|
|
12
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".cache", "out"]);
|
|
13
|
+
// ─── File scanner ─────────────────────────────────────────────────────────────
|
|
14
|
+
function findSourceFiles(dir) {
|
|
15
|
+
const files = [];
|
|
16
|
+
let entries;
|
|
17
|
+
try {
|
|
18
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const name = entry.name;
|
|
25
|
+
if (name.startsWith(".") || SKIP_DIRS.has(name))
|
|
26
|
+
continue;
|
|
27
|
+
const full = path.join(dir, name);
|
|
28
|
+
if (entry.isDirectory())
|
|
29
|
+
files.push(...findSourceFiles(full));
|
|
30
|
+
else if (name.endsWith(".ts") || name.endsWith(".tsx"))
|
|
31
|
+
files.push(full);
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
function findTsConfig(root) {
|
|
36
|
+
const p = path.join(root, "tsconfig.json");
|
|
37
|
+
return existsSync(p) ? p : undefined;
|
|
38
|
+
}
|
|
39
|
+
// ─── Noop stub for @praxisjs/* packages (except @praxisjs/css) ───────────────
|
|
40
|
+
// Regular function (not arrow) so it can be used as a base class via `extends`.
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
function noop(..._args) {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
44
|
+
return () => { };
|
|
45
|
+
}
|
|
46
|
+
function makePraxisStub() {
|
|
47
|
+
return new Proxy({}, { get: () => noop });
|
|
48
|
+
}
|
|
49
|
+
// ─── CSS extractor ────────────────────────────────────────────────────────────
|
|
50
|
+
async function extractStaticCSS(root) {
|
|
51
|
+
const candidates = findSourceFiles(root)
|
|
52
|
+
.filter((f) => HAS_CSS_CONTENT.test(readFileSync(f, "utf-8")));
|
|
53
|
+
if (candidates.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
// Each candidate is wrapped in try/catch so one bad file doesn't abort extraction.
|
|
56
|
+
const entry = candidates
|
|
57
|
+
.map((f) => `try { require(${JSON.stringify(f)}); } catch {}`)
|
|
58
|
+
.join("\n");
|
|
59
|
+
let bundleCode;
|
|
60
|
+
try {
|
|
61
|
+
const result = await esbuildBuild({
|
|
62
|
+
stdin: { contents: entry, loader: "js", resolveDir: root },
|
|
63
|
+
bundle: true,
|
|
64
|
+
write: false,
|
|
65
|
+
format: "cjs",
|
|
66
|
+
platform: "node",
|
|
67
|
+
target: "node20",
|
|
68
|
+
tsconfig: findTsConfig(root),
|
|
69
|
+
plugins: [
|
|
70
|
+
{
|
|
71
|
+
name: "praxisjs-css-extract",
|
|
72
|
+
setup(build) {
|
|
73
|
+
// @praxisjs/css is handled by the vm sandbox (extractionModule).
|
|
74
|
+
// Keep all @praxisjs/* external so esbuild emits plain require()
|
|
75
|
+
// calls — no __toESM wrapping that would break Proxy property access.
|
|
76
|
+
build.onResolve({ filter: /^@praxisjs\// }, () => ({ external: true }));
|
|
77
|
+
build.onResolve({ filter: /^[^./]/ }, () => ({ external: true }));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
logLevel: "silent",
|
|
82
|
+
});
|
|
83
|
+
bundleCode = result.outputFiles[0].text;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
const collected = [];
|
|
89
|
+
const cssMod = extractionModule((css, prepend) => {
|
|
90
|
+
if (prepend)
|
|
91
|
+
collected.unshift(css);
|
|
92
|
+
else
|
|
93
|
+
collected.push(css);
|
|
94
|
+
});
|
|
95
|
+
const praxisStub = makePraxisStub();
|
|
96
|
+
const sandbox = {
|
|
97
|
+
require: (id) => {
|
|
98
|
+
if (id === "@praxisjs/css")
|
|
99
|
+
return cssMod;
|
|
100
|
+
if (id.startsWith("@praxisjs/"))
|
|
101
|
+
return praxisStub;
|
|
102
|
+
return {};
|
|
103
|
+
},
|
|
104
|
+
console: { log: noop, warn: noop, error: noop },
|
|
105
|
+
process: { env: {} },
|
|
106
|
+
setTimeout: noop,
|
|
107
|
+
clearTimeout: noop,
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
vm.runInNewContext(bundleCode, sandbox);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
return collected.join("\n");
|
|
116
|
+
}
|
|
117
|
+
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* Extracts all `@Styled()`, `keyframes()`, and `globalStyle()` CSS at build
|
|
120
|
+
* time and emits it as `virtual:praxisjs/styles.css`.
|
|
121
|
+
*
|
|
122
|
+
* In production:
|
|
123
|
+
* - CSS lives in a static asset with normal HTTP caching.
|
|
124
|
+
* - Runtime `<style>` injection is disabled (`__PRAXIS_CSS_STATIC__ = true`).
|
|
125
|
+
*
|
|
126
|
+
* In development:
|
|
127
|
+
* - The virtual module is empty; CSS is injected at runtime.
|
|
128
|
+
* - HMR works without extra configuration.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* // vite.config.ts
|
|
132
|
+
* import { praxisjs, praxisjsCSS } from '@praxisjs/vite-plugin'
|
|
133
|
+
* export default defineConfig({ plugins: [praxisjs(), praxisjsCSS()] })
|
|
134
|
+
*/
|
|
135
|
+
export function praxisjsCSS() {
|
|
136
|
+
let config;
|
|
137
|
+
let staticCSS = "";
|
|
138
|
+
return {
|
|
139
|
+
name: "praxisjs:css",
|
|
140
|
+
enforce: "pre",
|
|
141
|
+
configResolved(c) {
|
|
142
|
+
config = c;
|
|
143
|
+
},
|
|
144
|
+
config(_, env) {
|
|
145
|
+
if (env.command === "build") {
|
|
146
|
+
return { define: { __PRAXIS_CSS_STATIC__: JSON.stringify(true) } };
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
resolveId(id) {
|
|
150
|
+
if (id === VIRTUAL_ID)
|
|
151
|
+
return RESOLVED_ID;
|
|
152
|
+
},
|
|
153
|
+
load(id) {
|
|
154
|
+
if (id !== RESOLVED_ID)
|
|
155
|
+
return;
|
|
156
|
+
if (config.command === "serve")
|
|
157
|
+
return "/* @praxisjs/css: runtime injection active */";
|
|
158
|
+
return staticCSS
|
|
159
|
+
? `/* @praxisjs/css: static extraction */\n${staticCSS}`
|
|
160
|
+
: "/* @praxisjs/css: no static styles found */";
|
|
161
|
+
},
|
|
162
|
+
async buildStart() {
|
|
163
|
+
if (config.command !== "build")
|
|
164
|
+
return;
|
|
165
|
+
staticCSS = await extractStaticCSS(config.root);
|
|
166
|
+
if (staticCSS) {
|
|
167
|
+
this.info(`@praxisjs/css: extracted ${String(staticCSS.split("\n").length)} CSS rules`);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=css.js.map
|
package/dist/css.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"css.js","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAElD,MAAM,WAAW,yBAAyB;IACxC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,yBAA8B,GAAG,MAAM,EAAE,CA6C1E;AAED,eAAe,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB,MAAM,WAAW,yBAAyB;IACxC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,yBAA8B,GAAG,MAAM,EAAE,CA6C1E;AAED,eAAe,QAAQ,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAIvC,OAAO,EAAE,WAAW,EAAE,CAAC;AAOvB,MAAM,UAAU,QAAQ,CAAC,UAAqC,EAAE;IAC9D,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,UAAU,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAElD,OAAO;QACL;YACE,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,KAAK;YACd,MAAM;gBACJ,OAAO;oBACL,OAAO,EAAE;wBACP,MAAM,EAAE,QAAQ;qBACjB;iBACF,CAAC;YACJ,CAAC;YACD,SAAS,CAAC,IAAI,EAAE,EAAE;gBAChB,IAAI,CAAC,UAAU;oBAAE,OAAO;gBACxB,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAAE,OAAO;gBACzD,IAAI,IAAI,CAAC,QAAQ,CAAC,2BAA2B,CAAC;oBAAE,OAAO;gBACvD,OAAO,IAAI,CAAC;YACd,CAAC;SACF;QACD,GAAG,CAAC,GAAG;YACL,CAAC,CAAC;gBACE;oBACE,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,MAAe;oBACxB,eAAe,CAAC,EACd,IAAI,EACJ,MAAM,GAIP;wBACC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;4BAAE,OAAO;wBAE5D,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC;4BACb,IAAI,EAAE,QAAQ;4BACd,KAAK,EAAE,2BAA2B;4BAClC,IAAI,EAAE,EAAE,IAAI,EAAE;yBACf,CAAC,CAAC;oBACL,CAAC;iBACF;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED,eAAe,QAAQ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/vite-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"
|
|
14
|
+
"@praxisjs/css": "0.1.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@types/node": "^25.
|
|
17
|
+
"@types/node": "^25.9.1",
|
|
18
|
+
"esbuild": "^0.25.12",
|
|
18
19
|
"typescript": "^5.9.3",
|
|
19
|
-
"vite": "^7.3.1"
|
|
20
|
+
"vite": "^7.3.1",
|
|
21
|
+
"@praxisjs/css": "0.1.0"
|
|
20
22
|
},
|
|
21
23
|
"scripts": {
|
|
22
24
|
"build": "tsc",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Isolated test file: mocks esbuild to throw so we can exercise the
|
|
2
|
+
// esbuild-failure branch in extractStaticCSS (css.ts line 97).
|
|
3
|
+
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
8
|
+
|
|
9
|
+
vi.mock("esbuild", () => ({
|
|
10
|
+
build: vi.fn().mockRejectedValue(new Error("esbuild crash")),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
type PluginHooks = {
|
|
14
|
+
configResolved(c: Partial<ResolvedConfig>): void;
|
|
15
|
+
load(id: string): string | undefined;
|
|
16
|
+
buildStart(): Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
tmpDir = mkdtempSync(path.join(tmpdir(), "praxis-css-esbuild-err-"));
|
|
23
|
+
writeFileSync(
|
|
24
|
+
path.join(tmpDir, "styles.ts"),
|
|
25
|
+
`import { Stylesheet } from "@praxisjs/css";\nclass X extends Stylesheet {}`,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(() => { rmSync(tmpDir, { recursive: true, force: true }); });
|
|
30
|
+
|
|
31
|
+
describe("praxisjsCSS() — esbuild failure (mocked)", () => {
|
|
32
|
+
it("returns empty CSS when esbuild throws during extraction", async () => {
|
|
33
|
+
const { praxisjsCSS } = await import("../css");
|
|
34
|
+
const p = praxisjsCSS() as unknown as Plugin & { configResolved: PluginHooks["configResolved"]; buildStart: PluginHooks["buildStart"]; load: PluginHooks["load"] };
|
|
35
|
+
p.configResolved({ command: "build", root: tmpDir } as Partial<ResolvedConfig>);
|
|
36
|
+
await p.buildStart.call({ info: vi.fn() });
|
|
37
|
+
expect(p.load("\0virtual:praxisjs/styles.css")).toContain("no static styles found");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import vm from "node:vm";
|
|
6
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
7
|
+
import { praxisjsCSS } from "../css";
|
|
8
|
+
|
|
9
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
type PluginHooks = {
|
|
12
|
+
configResolved(c: Partial<ResolvedConfig>): void;
|
|
13
|
+
config(c: unknown, env: { command: "build" | "serve" }): { define: Record<string, string> } | undefined;
|
|
14
|
+
resolveId(id: string): string | undefined;
|
|
15
|
+
load(id: string): string | undefined;
|
|
16
|
+
buildStart(): Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function hooks(p: Plugin): PluginHooks {
|
|
20
|
+
return p as unknown as PluginHooks;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function servePlugin() {
|
|
24
|
+
const p = praxisjsCSS();
|
|
25
|
+
hooks(p).configResolved({ command: "serve", root: "/" } as Partial<ResolvedConfig>);
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildPlugin(root = "/") {
|
|
30
|
+
const p = praxisjsCSS();
|
|
31
|
+
hooks(p).configResolved({ command: "build", root } as Partial<ResolvedConfig>);
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Plugin shape ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe("praxisjsCSS() — plugin shape", () => {
|
|
38
|
+
it("has name praxisjs:css", () => {
|
|
39
|
+
expect(praxisjsCSS().name).toBe("praxisjs:css");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("enforces 'pre'", () => {
|
|
43
|
+
expect(praxisjsCSS().enforce).toBe("pre");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── resolveId() ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("praxisjsCSS() — resolveId()", () => {
|
|
50
|
+
it("resolves virtual:praxisjs/styles.css to internal null-prefixed id", () => {
|
|
51
|
+
const p = servePlugin();
|
|
52
|
+
expect(hooks(p).resolveId("virtual:praxisjs/styles.css")).toBe("\0virtual:praxisjs/styles.css");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns undefined for any other id", () => {
|
|
56
|
+
const p = servePlugin();
|
|
57
|
+
expect(hooks(p).resolveId("./component.ts")).toBeUndefined();
|
|
58
|
+
expect(hooks(p).resolveId("vite/client")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── config() ────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe("praxisjsCSS() — config()", () => {
|
|
65
|
+
it("injects __PRAXIS_CSS_STATIC__ define in build mode", () => {
|
|
66
|
+
const p = praxisjsCSS();
|
|
67
|
+
const result = hooks(p).config({}, { command: "build" });
|
|
68
|
+
expect(result?.define?.["__PRAXIS_CSS_STATIC__"]).toBe("true");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns undefined in serve mode", () => {
|
|
72
|
+
const p = praxisjsCSS();
|
|
73
|
+
expect(hooks(p).config({}, { command: "serve" })).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── load() ──────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
describe("praxisjsCSS() — load()", () => {
|
|
80
|
+
it("returns runtime-injection comment for virtual id in serve mode", () => {
|
|
81
|
+
const result = hooks(servePlugin()).load("\0virtual:praxisjs/styles.css");
|
|
82
|
+
expect(result).toContain("runtime injection active");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns 'no static styles found' for virtual id in build mode before extraction", () => {
|
|
86
|
+
const result = hooks(buildPlugin()).load("\0virtual:praxisjs/styles.css");
|
|
87
|
+
expect(result).toContain("no static styles found");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns undefined for non-virtual ids", () => {
|
|
91
|
+
expect(hooks(servePlugin()).load("./component.ts")).toBeUndefined();
|
|
92
|
+
expect(hooks(buildPlugin()).load("node_modules/something")).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns extracted CSS header in build mode after extraction", async () => {
|
|
96
|
+
// set up extraction output by running buildStart first (integration test below)
|
|
97
|
+
// this test checks the static CSS header content after buildStart sets staticCSS
|
|
98
|
+
const p = buildPlugin(tmpDir);
|
|
99
|
+
const info = vi.fn();
|
|
100
|
+
await hooks(p).buildStart.call({ info });
|
|
101
|
+
const result = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
102
|
+
expect(result).toContain("static extraction");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── buildStart() — integration ──────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
let tmpDir: string;
|
|
109
|
+
|
|
110
|
+
beforeAll(async () => {
|
|
111
|
+
tmpDir = mkdtempSync(path.join(tmpdir(), "praxis-css-test-"));
|
|
112
|
+
|
|
113
|
+
// tsconfig.json — exercises the findTsConfig() success branch (return p)
|
|
114
|
+
writeFileSync(path.join(tmpDir, "tsconfig.json"), "{}");
|
|
115
|
+
|
|
116
|
+
// File 1: extends Stylesheet + uses Styled()
|
|
117
|
+
writeFileSync(
|
|
118
|
+
path.join(tmpDir, "button.ts"),
|
|
119
|
+
`
|
|
120
|
+
import { Stylesheet, Styled } from "@praxisjs/css";
|
|
121
|
+
class BtnStyles extends Stylesheet {
|
|
122
|
+
$root = this.css({ display: "inline-flex", padding: "8px 16px" });
|
|
123
|
+
$hover = this.css({ opacity: 0.9 });
|
|
124
|
+
}
|
|
125
|
+
Styled(BtnStyles);
|
|
126
|
+
`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// File 2: globalStyle()
|
|
130
|
+
writeFileSync(
|
|
131
|
+
path.join(tmpDir, "base.ts"),
|
|
132
|
+
`
|
|
133
|
+
import { globalStyle } from "@praxisjs/css";
|
|
134
|
+
globalStyle(_css => "*, *::before, *::after { box-sizing: border-box; }");
|
|
135
|
+
`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// File 3: keyframes()
|
|
139
|
+
writeFileSync(
|
|
140
|
+
path.join(tmpDir, "animations.ts"),
|
|
141
|
+
`
|
|
142
|
+
import { keyframes } from "@praxisjs/css";
|
|
143
|
+
const spin = keyframes("spin", {
|
|
144
|
+
from: { transform: "rotate(0deg)" },
|
|
145
|
+
to: { transform: "rotate(360deg)" },
|
|
146
|
+
});
|
|
147
|
+
`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// File 4: Themed()
|
|
151
|
+
writeFileSync(
|
|
152
|
+
path.join(tmpDir, "theme.ts"),
|
|
153
|
+
`
|
|
154
|
+
import { Themed } from "@praxisjs/css";
|
|
155
|
+
class AppTokens { colorBrand = "#6d5bbd"; }
|
|
156
|
+
class LightTheme extends AppTokens {}
|
|
157
|
+
Themed(AppTokens, LightTheme);
|
|
158
|
+
`,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// File that imports another @praxisjs/* package — exercises the praxis stub,
|
|
162
|
+
// calls noop, AND calls noop's return value to achieve full function coverage
|
|
163
|
+
writeFileSync(
|
|
164
|
+
path.join(tmpDir, "with-runtime.ts"),
|
|
165
|
+
`
|
|
166
|
+
import { signal } from "@praxisjs/core";
|
|
167
|
+
import { globalStyle } from "@praxisjs/css";
|
|
168
|
+
globalStyle(_css => "html { font-family: system-ui; }");
|
|
169
|
+
const s = signal(0);
|
|
170
|
+
if (typeof s === 'function') s();
|
|
171
|
+
console.log(s);
|
|
172
|
+
setTimeout(() => {}, 0);
|
|
173
|
+
clearTimeout(0);
|
|
174
|
+
`,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// File that imports a third-party package — exercises require() fallback return {}
|
|
178
|
+
writeFileSync(
|
|
179
|
+
path.join(tmpDir, "with-thirdparty.ts"),
|
|
180
|
+
`
|
|
181
|
+
import { globalStyle } from "@praxisjs/css";
|
|
182
|
+
import { format } from "date-fns";
|
|
183
|
+
globalStyle(_css => "body { color: black; }");
|
|
184
|
+
void format;
|
|
185
|
+
`,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Subdirectory with a .tsx file — exercises isDirectory() and .tsx branches
|
|
189
|
+
const subDir = path.join(tmpDir, "components");
|
|
190
|
+
mkdirSync(subDir);
|
|
191
|
+
writeFileSync(
|
|
192
|
+
path.join(subDir, "card.tsx"),
|
|
193
|
+
`
|
|
194
|
+
import { Stylesheet, Styled } from "@praxisjs/css";
|
|
195
|
+
class CardStyles extends Stylesheet {
|
|
196
|
+
$card = this.css({ borderRadius: "8px" });
|
|
197
|
+
}
|
|
198
|
+
Styled(CardStyles);
|
|
199
|
+
`,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Skipped directory (SKIP_DIRS) — exercises the SKIP_DIRS.has() branch
|
|
203
|
+
mkdirSync(path.join(tmpDir, "node_modules"));
|
|
204
|
+
|
|
205
|
+
// Hidden file — exercises the startsWith(".") branch
|
|
206
|
+
writeFileSync(path.join(tmpDir, ".hidden.ts"), `export {};`);
|
|
207
|
+
|
|
208
|
+
// Non-TS/TSX file — exercises the else-if false branch in findSourceFiles
|
|
209
|
+
writeFileSync(path.join(tmpDir, "styles.css"), `body { margin: 0; }`);
|
|
210
|
+
|
|
211
|
+
// Non-matching TS file (should be ignored)
|
|
212
|
+
writeFileSync(
|
|
213
|
+
path.join(tmpDir, "utils.ts"),
|
|
214
|
+
`export const add = (a: number, b: number) => a + b;`,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
afterAll(() => {
|
|
219
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("praxisjsCSS() — buildStart() integration", () => {
|
|
223
|
+
it("extracts scoped class CSS from Stylesheet subclasses", async () => {
|
|
224
|
+
const p = buildPlugin(tmpDir);
|
|
225
|
+
const info = vi.fn();
|
|
226
|
+
await hooks(p).buildStart.call({ info });
|
|
227
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
228
|
+
expect(css).toContain(".prx-root-");
|
|
229
|
+
expect(css).toContain("display: inline-flex");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("extracts globalStyle CSS", async () => {
|
|
233
|
+
const p = buildPlugin(tmpDir);
|
|
234
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
235
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
236
|
+
expect(css).toContain("box-sizing: border-box");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("extracts @keyframes CSS", async () => {
|
|
240
|
+
const p = buildPlugin(tmpDir);
|
|
241
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
242
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
243
|
+
expect(css).toContain("@keyframes prx-spin-");
|
|
244
|
+
expect(css).toContain("rotate(0deg)");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("extracts :root vars from Themed()", async () => {
|
|
248
|
+
const p = buildPlugin(tmpDir);
|
|
249
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
250
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
251
|
+
expect(css).toContain(":root");
|
|
252
|
+
expect(css).toContain("--color-brand: #6d5bbd");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it(":root vars appear before class rules", async () => {
|
|
256
|
+
const p = buildPlugin(tmpDir);
|
|
257
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
258
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
259
|
+
const rootIdx = css.indexOf(":root");
|
|
260
|
+
const classIdx = css.indexOf(".prx-");
|
|
261
|
+
expect(rootIdx).toBeLessThan(classIdx);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("calls this.info() with extracted rule count", async () => {
|
|
265
|
+
const p = buildPlugin(tmpDir);
|
|
266
|
+
const info = vi.fn();
|
|
267
|
+
await hooks(p).buildStart.call({ info });
|
|
268
|
+
expect(info).toHaveBeenCalledWith(expect.stringContaining("extracted"));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("returns empty when root has no matching source files", async () => {
|
|
272
|
+
const emptyDir = mkdtempSync(path.join(tmpdir(), "praxis-empty-"));
|
|
273
|
+
try {
|
|
274
|
+
const p = buildPlugin(emptyDir);
|
|
275
|
+
const info = vi.fn();
|
|
276
|
+
await hooks(p).buildStart.call({ info });
|
|
277
|
+
expect(info).not.toHaveBeenCalled();
|
|
278
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
279
|
+
expect(css).toContain("no static styles found");
|
|
280
|
+
} finally {
|
|
281
|
+
rmSync(emptyDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("skips non-serve mode in buildStart for serve command", async () => {
|
|
286
|
+
const p = servePlugin();
|
|
287
|
+
const info = vi.fn();
|
|
288
|
+
await hooks(p).buildStart.call({ info });
|
|
289
|
+
expect(info).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("handles a root directory that cannot be read (readdirSync throws)", async () => {
|
|
293
|
+
const p = buildPlugin("/this/path/does/not/exist/at/all");
|
|
294
|
+
const info = vi.fn();
|
|
295
|
+
await hooks(p).buildStart.call({ info });
|
|
296
|
+
expect(info).not.toHaveBeenCalled();
|
|
297
|
+
expect(hooks(p).load("\0virtual:praxisjs/styles.css")).toContain("no static styles found");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("handles vm execution error gracefully (returns empty CSS)", async () => {
|
|
301
|
+
const spy = vi.spyOn(vm, "runInNewContext").mockImplementationOnce(() => { throw new Error("vm crash"); });
|
|
302
|
+
try {
|
|
303
|
+
const p = buildPlugin(tmpDir);
|
|
304
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
305
|
+
expect(hooks(p).load("\0virtual:praxisjs/styles.css")).toContain("no static styles found");
|
|
306
|
+
} finally {
|
|
307
|
+
spy.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("exercises the @praxisjs/* stub path (require for non-css praxis packages)", async () => {
|
|
312
|
+
const p = buildPlugin(tmpDir);
|
|
313
|
+
await hooks(p).buildStart.call({ info: vi.fn() });
|
|
314
|
+
const css = hooks(p).load("\0virtual:praxisjs/styles.css") as string;
|
|
315
|
+
// The with-runtime.ts fixture imports @praxisjs/core which uses the stub
|
|
316
|
+
// If no error was thrown, the stub handled it correctly
|
|
317
|
+
expect(typeof css).toBe("string");
|
|
318
|
+
});
|
|
319
|
+
});
|