vite-plugin-sinwan 0.2.6 → 0.2.7

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/README.md CHANGED
@@ -34,6 +34,8 @@ export default defineConfig({
34
34
  sinwan({
35
35
  // Enable template hoisting (default: true)
36
36
  hoist: true,
37
+ // Warn when hoisting is skipped (default: Vite mode !== "production")
38
+ dev: true,
37
39
  // Emit explicit binding descriptors (default: false)
38
40
  explicitBindings: false,
39
41
  // Path to reactive-props metadata from `sinwan analyze`
@@ -53,6 +55,7 @@ sinwan({
53
55
  | Option | Type | Default | Description |
54
56
  | ------------------ | ------------------------------- | ----------- | ----------------------------------------------------- |
55
57
  | `hoist` | `boolean` | `true` | Hoist static DOM to module-level templates |
58
+ | `dev` | `boolean` | Vite `mode !== "production"` | Warn when template hoisting is skipped |
56
59
  | `explicitBindings` | `boolean` | `false` | Emit compiler-driven binding descriptors |
57
60
  | `analyze` | `string` | `undefined` | Path to reactive-props metadata from `sinwan analyze` |
58
61
  | `cache` | `boolean \| SinwanCacheOptions` | `false` | Enable incremental in-memory cross-file analysis |
@@ -33,6 +33,317 @@ describe("sinwan vite plugin", () => {
33
33
  });
34
34
  });
35
35
 
36
+ const SPREAD_JSX = "const Card = (props) => <div {...props}><p>Hello</p></div>;";
37
+
38
+ function collectWarns(run: () => void): string[] {
39
+ const warns: string[] = [];
40
+ const originalWarn = console.warn;
41
+ console.warn = (...args: unknown[]) => {
42
+ warns.push(String(args[0]));
43
+ };
44
+ try {
45
+ run();
46
+ return warns;
47
+ } finally {
48
+ console.warn = originalWarn;
49
+ }
50
+ }
51
+
52
+ describe("compiler dev warnings", () => {
53
+ it("does not warn about skipped hoists when dev is false", () => {
54
+ const plugin = sinwan({ cache: false, dev: false });
55
+ const warns = collectWarns(() => {
56
+ plugin.transform(SPREAD_JSX, "Card.tsx");
57
+ });
58
+ expect(warns.some((line) => line.includes("template hoisting skipped"))).toBe(
59
+ false,
60
+ );
61
+ });
62
+
63
+ it("warns about skipped hoists when dev is true", () => {
64
+ const plugin = sinwan({ cache: false, dev: true });
65
+ const warns = collectWarns(() => {
66
+ plugin.transform(SPREAD_JSX, "Card.tsx");
67
+ });
68
+ expect(warns.some((line) => line.includes("template hoisting skipped"))).toBe(
69
+ true,
70
+ );
71
+ });
72
+
73
+ it("uses Vite production mode after configResolved unless dev is set", () => {
74
+ const plugin = sinwan({ cache: false });
75
+ plugin.configResolved({ command: "build", mode: "production", root: "/app" });
76
+ const warns = collectWarns(() => {
77
+ plugin.transform(SPREAD_JSX, "Card.tsx");
78
+ });
79
+ expect(warns.some((line) => line.includes("template hoisting skipped"))).toBe(
80
+ false,
81
+ );
82
+ });
83
+
84
+ it("uses Vite development mode after configResolved unless dev is set", () => {
85
+ const plugin = sinwan({ cache: false });
86
+ plugin.configResolved({ command: "serve", mode: "development", root: "/app" });
87
+ const warns = collectWarns(() => {
88
+ plugin.transform(SPREAD_JSX, "Card.tsx");
89
+ });
90
+ expect(warns.some((line) => line.includes("template hoisting skipped"))).toBe(
91
+ true,
92
+ );
93
+ });
94
+
95
+ it("keeps an explicit dev option after configResolved production mode", () => {
96
+ const plugin = sinwan({ cache: false, dev: true });
97
+ plugin.configResolved({ command: "build", mode: "production", root: "/app" });
98
+ const warns = collectWarns(() => {
99
+ plugin.transform(SPREAD_JSX, "Card.tsx");
100
+ });
101
+ expect(warns.some((line) => line.includes("template hoisting skipped"))).toBe(
102
+ true,
103
+ );
104
+ });
105
+
106
+ it("defaults to quiet when NODE_ENV is production before configResolved", () => {
107
+ const previous = process.env.NODE_ENV;
108
+ process.env.NODE_ENV = "production";
109
+ try {
110
+ const plugin = sinwan({ cache: false });
111
+ const warns = collectWarns(() => {
112
+ plugin.transform(SPREAD_JSX, "Card.tsx");
113
+ });
114
+ expect(
115
+ warns.some((line) => line.includes("template hoisting skipped")),
116
+ ).toBe(false);
117
+ } finally {
118
+ if (previous === undefined) {
119
+ delete process.env.NODE_ENV;
120
+ } else {
121
+ process.env.NODE_ENV = previous;
122
+ }
123
+ }
124
+ });
125
+
126
+ it("does not override captured env when config.mode is not a string", () => {
127
+ const previous = process.env.NODE_ENV;
128
+ process.env.NODE_ENV = "production";
129
+ try {
130
+ const plugin = sinwan({ cache: false });
131
+ plugin.configResolved({ command: "serve", root: "/app" });
132
+ const warns = collectWarns(() => {
133
+ plugin.transform(SPREAD_JSX, "Card.tsx");
134
+ });
135
+ expect(
136
+ warns.some((line) => line.includes("template hoisting skipped")),
137
+ ).toBe(false);
138
+ } finally {
139
+ if (previous === undefined) {
140
+ delete process.env.NODE_ENV;
141
+ } else {
142
+ process.env.NODE_ENV = previous;
143
+ }
144
+ }
145
+ });
146
+ });
147
+
148
+ describe("vite transform, cache, and Fast Refresh", () => {
149
+ it("returns null for non-jsx files", () => {
150
+ const plugin = sinwan({ cache: false, fastRefresh: false });
151
+ expect(plugin.transform("const x = 1;", "file.ts")).toBeNull();
152
+ });
153
+
154
+ it("transforms jsx and updates cache when bunfig exists", () => {
155
+ const tmpDir = fs.mkdtempSync(path.join(import.meta.dir, ".tmp-"));
156
+ const bunfig = path.join(tmpDir, "bunfig.toml");
157
+ fs.writeFileSync(bunfig, "[install]\n");
158
+ try {
159
+ const plugin = sinwan({
160
+ cache: {
161
+ root: tmpDir,
162
+ tsConfigPath: path.join(tmpDir, "tsconfig.json"),
163
+ cachePath: path.join(tmpDir, "cache.json"),
164
+ bunfigPath: bunfig,
165
+ workspaces: tmpDir,
166
+ },
167
+ fastRefresh: false,
168
+ dev: false,
169
+ });
170
+ plugin.configResolved({
171
+ command: "build",
172
+ mode: "production",
173
+ root: tmpDir,
174
+ });
175
+ const result = plugin.transform(
176
+ "export function Hello() { return <div>hi</div>; }",
177
+ path.join(tmpDir, "Hello.tsx"),
178
+ );
179
+ expect(result?.code).toContain("_$createTemplate");
180
+ } finally {
181
+ fs.rmSync(tmpDir, { recursive: true });
182
+ }
183
+ });
184
+
185
+ it("uses projectRoot when cache is a boolean", () => {
186
+ const plugin = sinwan({ cache: true, fastRefresh: false, dev: false });
187
+ plugin.configResolved({
188
+ command: "build",
189
+ mode: "production",
190
+ root: process.cwd(),
191
+ });
192
+ const result = plugin.transform("const App = () => <div>ok</div>;", "App.tsx");
193
+ expect(result?.code).toBeDefined();
194
+ });
195
+
196
+ it("falls back to cwd when cache is a boolean and root is omitted", () => {
197
+ const plugin = sinwan({ cache: true, fastRefresh: false, dev: false });
198
+ plugin.configResolved({ command: "build", mode: "production" });
199
+ const result = plugin.transform("const App = () => <div>ok</div>;", "App.tsx");
200
+ expect(result?.code).toBeDefined();
201
+ });
202
+
203
+ it("uses cache object defaults without root or bunfigPath", () => {
204
+ const plugin = sinwan({
205
+ cache: {},
206
+ fastRefresh: false,
207
+ dev: false,
208
+ });
209
+ plugin.configResolved({ command: "build", mode: "production", root: "/app" });
210
+ const result = plugin.transform("const App = () => <div>ok</div>;", "App.jsx");
211
+ expect(result?.code).toBeDefined();
212
+ });
213
+
214
+ it("uses cwd when a cache object has no root and config has no root", () => {
215
+ const plugin = sinwan({
216
+ cache: {},
217
+ fastRefresh: false,
218
+ dev: false,
219
+ });
220
+ plugin.configResolved({ command: "build", mode: "production" });
221
+ const result = plugin.transform("const App = () => <div>ok</div>;", "App.tsx");
222
+ expect(result?.code).toBeDefined();
223
+ });
224
+
225
+ it("injects Fast Refresh in serve for an exported component", () => {
226
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
227
+ plugin.configResolved({
228
+ command: "serve",
229
+ mode: "development",
230
+ root: "/app",
231
+ });
232
+ const result = plugin.transform(
233
+ "export function Hello() { return <div>hi</div>; }",
234
+ "/app/src/Hello.tsx",
235
+ );
236
+ expect(result?.code).toContain("$$sinwanReplace");
237
+ });
238
+
239
+ it("injects Fast Refresh using original source when the id has a query", () => {
240
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
241
+ plugin.configResolved({
242
+ command: "serve",
243
+ mode: "development",
244
+ root: "/app",
245
+ });
246
+ const result = plugin.transform(
247
+ "export function Hello() { return <div>hi</div>; }",
248
+ "/app/src/Hello.tsx?t=1",
249
+ );
250
+ expect(result?.code).toContain("$$sinwanReplace");
251
+ });
252
+
253
+ it("skips Fast Refresh for node_modules", () => {
254
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
255
+ plugin.configResolved({
256
+ command: "serve",
257
+ mode: "development",
258
+ root: "/app",
259
+ });
260
+ const result = plugin.transform(
261
+ "export function Hello() { return <div>hi</div>; }",
262
+ "/app/node_modules/lib/Hello.tsx",
263
+ );
264
+ expect(result?.code ?? "").not.toContain("$$sinwanReplace");
265
+ });
266
+
267
+ it("skips Fast Refresh for virtual modules", () => {
268
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
269
+ plugin.configResolved({
270
+ command: "serve",
271
+ mode: "development",
272
+ root: "/app",
273
+ });
274
+ const result = plugin.transform(
275
+ "export function Hello() { return <div>hi</div>; }",
276
+ "\0virtual/Hello.tsx",
277
+ );
278
+ expect(result?.code ?? "").not.toContain("$$sinwanReplace");
279
+ });
280
+
281
+ it("skips Fast Refresh when the module exports no component", () => {
282
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
283
+ plugin.configResolved({
284
+ command: "serve",
285
+ mode: "development",
286
+ root: "/app",
287
+ });
288
+ const result = plugin.transform("const x = 1;", "/app/src/util.tsx");
289
+ expect(result?.code ?? "").not.toContain("$$sinwanReplace");
290
+ });
291
+
292
+ it("does not inject Fast Refresh during build", () => {
293
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
294
+ plugin.configResolved({
295
+ command: "build",
296
+ mode: "production",
297
+ root: "/app",
298
+ });
299
+ const result = plugin.transform(
300
+ "export function Hello() { return <div>hi</div>; }",
301
+ "/app/src/Hello.tsx",
302
+ );
303
+ expect(result?.code ?? "").not.toContain("$$sinwanReplace");
304
+ });
305
+
306
+ it("does not inject Fast Refresh when disabled", () => {
307
+ const plugin = sinwan({ cache: false, fastRefresh: false, dev: false });
308
+ plugin.configResolved({
309
+ command: "serve",
310
+ mode: "development",
311
+ root: "/app",
312
+ });
313
+ const result = plugin.transform(
314
+ "export function Hello() { return <div>hi</div>; }",
315
+ "/app/src/Hello.tsx",
316
+ );
317
+ expect(result?.code ?? "").not.toContain("$$sinwanReplace");
318
+ });
319
+
320
+ it("does not double-inject Fast Refresh", () => {
321
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
322
+ plugin.configResolved({
323
+ command: "serve",
324
+ mode: "development",
325
+ root: "/app",
326
+ });
327
+ const alreadyWired =
328
+ "export function Hello() { return <div>hi</div>; }\nconst $$sinwanReplace = 1;";
329
+ const result = plugin.transform(alreadyWired, "/app/src/Hello.tsx");
330
+ expect(result?.code).not.toContain("import.meta.hot.accept");
331
+ });
332
+
333
+ it("skips Fast Refresh when the refresh helper is already present", () => {
334
+ const plugin = sinwan({ cache: false, fastRefresh: true, dev: false });
335
+ plugin.configResolved({
336
+ command: "serve",
337
+ mode: "development",
338
+ root: "/app",
339
+ });
340
+ const alreadyWired =
341
+ "export function Hello() { return <div>hi</div>; }\nconst __$sinwanRefresh = 1;";
342
+ const result = plugin.transform(alreadyWired, "/app/src/Hello.tsx");
343
+ expect(result?.code).not.toContain("import.meta.hot.accept");
344
+ });
345
+ });
346
+
36
347
  const COMPILER_RANGE = ">=0.2.5 <1.0.0";
37
348
 
38
349
  describe("sinwan-compiler dependency range", () => {
package/dist/index.d.ts CHANGED
@@ -31,6 +31,12 @@ export interface SinwanOptions {
31
31
  * Only active in `vite serve`; never affects production builds.
32
32
  */
33
33
  fastRefresh?: boolean;
34
+ /**
35
+ * Warn when template hoisting is skipped (spreads, enhancer tags, …).
36
+ * Default: Vite `config.mode !== "production"` after `configResolved`,
37
+ * otherwise `process.env.NODE_ENV !== "production"` when `sinwan()` runs.
38
+ */
39
+ dev?: boolean;
34
40
  }
35
41
  /**
36
42
  * Unified Sinwan Vite plugin.
package/dist/index.js CHANGED
@@ -30,6 +30,9 @@ ${replaces}
30
30
  // src/index.ts
31
31
  import * as fs from "fs";
32
32
  import * as path from "path";
33
+ function defaultCompilerDev() {
34
+ return process.env["NODE_ENV"] !== "production";
35
+ }
33
36
  var DEFAULT_SINWAN_OPTIONS = {
34
37
  hoist: true,
35
38
  explicitBindings: false,
@@ -39,6 +42,8 @@ var DEFAULT_SINWAN_OPTIONS = {
39
42
  };
40
43
  function sinwan(options = {}) {
41
44
  const opts = { ...DEFAULT_SINWAN_OPTIONS, ...options };
45
+ const explicitDev = options.dev;
46
+ let compilerDev = explicitDev ?? defaultCompilerDev();
42
47
  let isServe = false;
43
48
  let projectRoot;
44
49
  let cache = null;
@@ -55,6 +60,9 @@ function sinwan(options = {}) {
55
60
  configResolved(config) {
56
61
  isServe = config?.command === "serve";
57
62
  projectRoot = config?.root;
63
+ if (explicitDev === undefined && typeof config?.mode === "string") {
64
+ compilerDev = config.mode !== "production";
65
+ }
58
66
  if (opts.cache) {
59
67
  const root = typeof opts.cache === "object" ? opts.cache.root ?? projectRoot ?? process.cwd() : projectRoot ?? process.cwd();
60
68
  const tsConfigPath = typeof opts.cache === "object" ? opts.cache.tsConfigPath : undefined;
@@ -79,7 +87,7 @@ function sinwan(options = {}) {
79
87
  result = transformJSX(code, id, {
80
88
  hoist: opts.hoist,
81
89
  explicitBindings: opts.explicitBindings,
82
- dev: true,
90
+ dev: compilerDev,
83
91
  analyze: opts.analyze,
84
92
  analyzeMetadata: cache ? cache.reactiveProps : undefined,
85
93
  resolveImport: cache ? cache.resolve : undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-sinwan",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Vite plugin for sinwan",
5
5
  "author": "Mohammed Ben Cheikh",
6
6
  "license": "MIT",
@@ -29,7 +29,8 @@
29
29
  "sinwan": "./dist/cli.js"
30
30
  },
31
31
  "scripts": {
32
- "build": "bun run build.ts"
32
+ "build": "bun run build.ts",
33
+ "test": "bun test --coverage __tests__/plugin.test.ts"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@types/bun": "latest",
package/src/index.ts CHANGED
@@ -38,6 +38,20 @@ export interface SinwanOptions {
38
38
  * Only active in `vite serve`; never affects production builds.
39
39
  */
40
40
  fastRefresh?: boolean;
41
+ /**
42
+ * Warn when template hoisting is skipped (spreads, enhancer tags, …).
43
+ * Default: Vite `config.mode !== "production"` after `configResolved`,
44
+ * otherwise `process.env.NODE_ENV !== "production"` when `sinwan()` runs.
45
+ */
46
+ dev?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Bracket access so Bun.build cannot fold `process.env.NODE_ENV` into `true`
51
+ * when this plugin itself is bundled.
52
+ */
53
+ function defaultCompilerDev(): boolean {
54
+ return process.env["NODE_ENV"] !== "production";
41
55
  }
42
56
 
43
57
  const DEFAULT_SINWAN_OPTIONS: Required<
@@ -66,6 +80,8 @@ const DEFAULT_SINWAN_OPTIONS: Required<
66
80
  */
67
81
  export function sinwan(options: SinwanOptions = {}) {
68
82
  const opts = { ...DEFAULT_SINWAN_OPTIONS, ...options };
83
+ const explicitDev = options.dev;
84
+ let compilerDev = explicitDev ?? defaultCompilerDev();
69
85
 
70
86
  // Whether we are running the dev server (`vite serve`). Fast Refresh
71
87
  // injection only happens here; production builds are never touched.
@@ -94,6 +110,9 @@ export function sinwan(options: SinwanOptions = {}) {
94
110
  configResolved(config: any) {
95
111
  isServe = config?.command === "serve";
96
112
  projectRoot = config?.root;
113
+ if (explicitDev === undefined && typeof config?.mode === "string") {
114
+ compilerDev = config.mode !== "production";
115
+ }
97
116
 
98
117
  if (opts.cache) {
99
118
  const root =
@@ -129,7 +148,7 @@ export function sinwan(options: SinwanOptions = {}) {
129
148
  result = transformJSX(code, id, {
130
149
  hoist: opts.hoist,
131
150
  explicitBindings: opts.explicitBindings,
132
- dev: process.env.NODE_ENV !== "production",
151
+ dev: compilerDev,
133
152
  analyze: opts.analyze,
134
153
  analyzeMetadata: cache ? cache.reactiveProps : undefined,
135
154
  resolveImport: cache ? cache.resolve : undefined,