@pyreon/vite-plugin 0.13.1 → 0.15.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"e8011ffc-1"}]}],"isRoot":true},"nodeParts":{"e8011ffc-1":{"renderedLength":13659,"gzipLength":4823,"brotliLength":0,"metaUid":"e8011ffc-0"}},"nodeMetas":{"e8011ffc-0":{"id":"/src/index.ts","moduleParts":{"index.js":"e8011ffc-1"},"imported":[{"uid":"e8011ffc-2"},{"uid":"e8011ffc-3"},{"uid":"e8011ffc-4"}],"importedBy":[],"isEntry":true},"e8011ffc-2":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8011ffc-0"}]},"e8011ffc-3":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8011ffc-0"}]},"e8011ffc-4":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"e8011ffc-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"170e8aa8-1"}]}],"isRoot":true},"nodeParts":{"170e8aa8-1":{"renderedLength":22365,"gzipLength":7629,"brotliLength":0,"metaUid":"170e8aa8-0"}},"nodeMetas":{"170e8aa8-0":{"id":"/src/index.ts","moduleParts":{"index.js":"170e8aa8-1"},"imported":[{"uid":"170e8aa8-2"},{"uid":"170e8aa8-3"},{"uid":"170e8aa8-4"}],"importedBy":[],"isEntry":true},"170e8aa8-2":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"170e8aa8-0"}]},"170e8aa8-3":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"170e8aa8-0"}]},"170e8aa8-4":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"170e8aa8-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
  import { generateContext, transformJSX } from "@pyreon/compiler";
4
4
 
5
5
  //#region src/index.ts
@@ -38,12 +38,6 @@ import { generateContext, transformJSX } from "@pyreon/compiler";
38
38
  */
39
39
  const HMR_RUNTIME_ID = "\0pyreon/hmr-runtime";
40
40
  const HMR_RUNTIME_IMPORT = "virtual:pyreon/hmr-runtime";
41
- const COMPAT_JSX_SOURCE = {
42
- react: "@pyreon/react-compat",
43
- preact: "@pyreon/preact-compat",
44
- vue: "@pyreon/vue-compat",
45
- solid: "@pyreon/solid-compat"
46
- };
47
41
  const COMPAT_ALIASES = {
48
42
  react: {
49
43
  react: "@pyreon/react-compat",
@@ -71,6 +65,59 @@ const COMPAT_ALIASES = {
71
65
  }
72
66
  };
73
67
  /**
68
+ * Detect whether a file id resolves to a `@pyreon/*` framework-package source
69
+ * (i.e. a published Pyreon package whose .tsx is being pulled in via the
70
+ * `bun` condition workspace-link, NOT user code, NOT an example app).
71
+ *
72
+ * Why this exists: in compat mode, OXC's per-project `importSource` is set
73
+ * to `@pyreon/core` and the resolveId hook redirects `@pyreon/core/jsx-runtime`
74
+ * to the compat package. That's correct for user code (the whole point of
75
+ * compat mode) but WRONG for framework-internal sources like
76
+ * `@pyreon/zero/src/link.tsx`, which need the real `@pyreon/core` runtime.
77
+ * The fix skips the redirect when the importer is a `@pyreon/*` framework
78
+ * file. Result: published-package consumers (where `@pyreon/zero` resolves
79
+ * to its pre-built `lib/`) and workspace-dev consumers (where it resolves
80
+ * to source) both get correct JSX runtime resolution.
81
+ *
82
+ * Detection heuristic: walk to nearest `package.json`, require BOTH:
83
+ * 1. `name` starts with `@pyreon/` (workspace member of the @pyreon scope)
84
+ * 2. file path contains `/packages/` AND NOT `/examples/`
85
+ *
86
+ * Step 2 excludes the existing `@pyreon/example-{react,vue,solid,preact}-compat`
87
+ * apps under `examples/`. Without it, user code in those apps would skip the
88
+ * compat-mode JSX-runtime redirect and import `@pyreon/core/jsx-runtime`
89
+ * directly — breaking the React/Vue/Solid/Preact compat layer's contract.
90
+ *
91
+ * Result cached per directory. The `/packages/` + `/examples/` check is a
92
+ * structural property of the monorepo (workspace layout), not the package
93
+ * name — so it's robust against renames.
94
+ */
95
+ function isPyreonWorkspaceFile(id, cache) {
96
+ const queryIdx = id.indexOf("?");
97
+ const filePath = queryIdx === -1 ? id : id.slice(0, queryIdx);
98
+ if (!filePath || filePath[0] === "\0") return false;
99
+ if (!filePath.includes("/packages/") || filePath.includes("/examples/")) return false;
100
+ let dir = dirname(filePath);
101
+ for (let i = 0; i < 12; i++) {
102
+ const cached = cache.get(dir);
103
+ if (cached !== void 0) return cached;
104
+ const pkgPath = join(dir, "package.json");
105
+ if (existsSync(pkgPath)) {
106
+ let isPyreon = false;
107
+ try {
108
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
109
+ isPyreon = typeof pkg.name === "string" && pkg.name.startsWith("@pyreon/");
110
+ } catch {}
111
+ cache.set(dir, isPyreon);
112
+ return isPyreon;
113
+ }
114
+ const parent = dirname(dir);
115
+ if (parent === dir) break;
116
+ dir = parent;
117
+ }
118
+ return false;
119
+ }
120
+ /**
74
121
  * Return the Pyreon compat target for an import specifier, or undefined if
75
122
  * the import should not be redirected.
76
123
  */
@@ -90,20 +137,21 @@ function pyreonPlugin(options) {
90
137
  const compat = options?.compat;
91
138
  let isBuild = false;
92
139
  let projectRoot = "";
140
+ const signalExportRegistry = /* @__PURE__ */ new Map();
141
+ const resolveCache = /* @__PURE__ */ new Map();
142
+ const pyreonWorkspaceDirCache = /* @__PURE__ */ new Map();
93
143
  return {
94
144
  name: "pyreon",
95
145
  enforce: "pre",
96
146
  config(userConfig, env) {
97
147
  isBuild = env.command === "build";
98
148
  projectRoot = userConfig.root ?? process.cwd();
99
- const optimizeDepsExclude = compat ? Object.keys(COMPAT_ALIASES[compat]) : [];
100
- const jsxSource = compat ? COMPAT_JSX_SOURCE[compat] : "@pyreon/core";
101
149
  return {
102
150
  resolve: { conditions: ["bun"] },
103
- optimizeDeps: { exclude: optimizeDepsExclude },
151
+ optimizeDeps: { exclude: compat ? Object.keys(COMPAT_ALIASES[compat]) : [] },
104
152
  oxc: { jsx: {
105
153
  runtime: "automatic",
106
- importSource: jsxSource
154
+ importSource: "@pyreon/core"
107
155
  } },
108
156
  ...env.isSsrBuild && ssrConfig ? { build: {
109
157
  ssr: true,
@@ -111,8 +159,12 @@ function pyreonPlugin(options) {
111
159
  } } : {}
112
160
  };
113
161
  },
162
+ async buildStart() {
163
+ await prescanSignalExports(projectRoot, signalExportRegistry);
164
+ },
114
165
  async resolveId(id, importer) {
115
166
  if (id === HMR_RUNTIME_IMPORT) return HMR_RUNTIME_ID;
167
+ if (compat && (id === "@pyreon/core/jsx-runtime" || id === "@pyreon/core/jsx-dev-runtime") && importer && isPyreonWorkspaceFile(importer, pyreonWorkspaceDirCache)) return;
116
168
  const target = getCompatTarget(compat, id);
117
169
  if (!target) return;
118
170
  return (await this.resolve(target, importer, { skipSelf: true }))?.id;
@@ -120,7 +172,7 @@ function pyreonPlugin(options) {
120
172
  load(id) {
121
173
  if (id === HMR_RUNTIME_ID) return HMR_RUNTIME_SOURCE;
122
174
  },
123
- transform(code, id, transformOptions) {
175
+ async transform(code, id, transformOptions) {
124
176
  const ext = getExt(id);
125
177
  if (ext !== ".tsx" && ext !== ".jsx" && ext !== ".pyreon") return;
126
178
  if (compat === "react" || compat === "preact" || compat === "vue" || compat === "solid") {
@@ -133,7 +185,12 @@ function pyreonPlugin(options) {
133
185
  }
134
186
  return;
135
187
  }
136
- const result = transformJSX(code, id, { ssr: transformOptions?.ssr === true });
188
+ scanSignalExports(code, normalizeModuleId(id), signalExportRegistry);
189
+ const knownSignals = await resolveImportedSignals(code, id, signalExportRegistry, this, resolveCache);
190
+ const result = transformJSX(code, id, {
191
+ ssr: transformOptions?.ssr === true,
192
+ knownSignals
193
+ });
137
194
  for (const w of result.warnings) this.warn(`${w.message} (${id}:${w.line}:${w.column})`);
138
195
  let output = result.code;
139
196
  if (!isBuild) {
@@ -212,8 +269,18 @@ function generateProjectContext(root) {
212
269
  * The arguments are extracted via balanced-paren matching in `injectHmr`.
213
270
  * A brace-depth check filters out matches inside functions/blocks — only
214
271
  * module-scope (depth 0) signals are rewritten for HMR state preservation.
272
+ *
273
+ * The optional `<...>` group accepts a TypeScript type parameter so that
274
+ * `signal<T>(initial)` declarations are also rewritten — without it, any
275
+ * generic-typed module-scope signal silently skipped HMR preservation.
276
+ *
277
+ * The inner `(?:[^<>]|<[^<>]*>)*` permits one level of generic nesting
278
+ * (e.g. `signal<Array<Row>>([])`, `signal<Map<string, number>>(m)`).
279
+ * Deeper nesting (`signal<Array<{ id: T<U> }>>(...)`) falls back to
280
+ * not-rewritten — tracked as a follow-up if real consumers need it,
281
+ * but unlikely at module scope where generics are usually shallow.
215
282
  */
216
- const SIGNAL_PREFIX_RE = /^((?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*)signal\(/gm;
283
+ const SIGNAL_PREFIX_RE = /^((?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*)signal(?:<(?:[^<>]|<[^<>]*>)*>)?\(/gm;
217
284
  /**
218
285
  * Detect whether the module exports any component-like functions
219
286
  * (uppercase first letter — standard convention for JSX components).
@@ -366,6 +433,137 @@ function getExt(id) {
366
433
  function isAssetRequest(url) {
367
434
  return url.startsWith("/@") || url.startsWith("/__") || url.includes("/node_modules/") || /\.(css|js|ts|tsx|jsx|json|ico|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|map)(\?|$)/.test(url);
368
435
  }
436
+ /**
437
+ * Normalize a Vite module ID by stripping query strings (?v=..., ?t=...)
438
+ * and resolving to an absolute path for consistent registry lookups.
439
+ */
440
+ function normalizeModuleId(id) {
441
+ const queryIndex = id.indexOf("?");
442
+ return queryIndex >= 0 ? id.slice(0, queryIndex) : id;
443
+ }
444
+ /**
445
+ * Pre-scan all source files in the project for signal exports.
446
+ *
447
+ * Called from `buildStart` so the registry is fully populated before any
448
+ * transforms run. This solves the build ordering problem where component.tsx
449
+ * is transformed before store.ts — without pre-scanning, the registry would
450
+ * be empty and imported signals would not be auto-called.
451
+ */
452
+ async function prescanSignalExports(root, registry) {
453
+ const files = [];
454
+ function walk(dir) {
455
+ try {
456
+ for (const entry of readdirSync(dir)) {
457
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "lib" || entry === "build") continue;
458
+ const full = join(dir, entry);
459
+ try {
460
+ if (statSync(full).isDirectory()) walk(full);
461
+ else if (/\.(ts|tsx|js|jsx)$/.test(entry)) files.push(full);
462
+ } catch {}
463
+ }
464
+ } catch {}
465
+ }
466
+ walk(root);
467
+ for (const file of files) try {
468
+ scanSignalExports(readFileSync(file, "utf-8"), file, registry);
469
+ } catch {}
470
+ }
471
+ /**
472
+ * Scan a module's source for exported signal declarations and register them.
473
+ *
474
+ * Detects patterns:
475
+ * 1. `export const x = signal(...)` or `export const x = computed(...)` — inline export
476
+ * 2. `const x = signal(...); export { x }` — separate declaration + named export
477
+ * 3. `export default signal(...)` — default export (tracked as 'default')
478
+ *
479
+ * Re-exports (`export { x } from './signals'`) are NOT detected — the source
480
+ * module must be scanned directly. This is a known limitation.
481
+ *
482
+ * Uses simple regex — no AST parse needed.
483
+ */
484
+ function scanSignalExports(code, moduleId, registry) {
485
+ const normalizedId = normalizeModuleId(moduleId);
486
+ let match;
487
+ const signals = /* @__PURE__ */ new Set();
488
+ const EXPORT_CONST_RE = /export\s+const\s+(\w+)\s*=\s*(?:signal|computed)\s*[<(]/g;
489
+ while ((match = EXPORT_CONST_RE.exec(code)) !== null) signals.add(match[1]);
490
+ const localSignals = /* @__PURE__ */ new Set();
491
+ const LOCAL_SIGNAL_RE = /(?:^|[\s;])const\s+(\w+)\s*=\s*(?:signal|computed)\s*[<(]/gm;
492
+ while ((match = LOCAL_SIGNAL_RE.exec(code)) !== null) localSignals.add(match[1]);
493
+ if (localSignals.size > 0) {
494
+ const NAMED_EXPORT_RE = /export\s*\{([^}]+)\}/g;
495
+ while ((match = NAMED_EXPORT_RE.exec(code)) !== null) {
496
+ if (code.slice(match.index + match[0].length).trimStart().startsWith("from")) continue;
497
+ for (const spec of match[1].split(",")) {
498
+ const trimmed = spec.trim();
499
+ if (!trimmed) continue;
500
+ const parts = trimmed.split(/\s+as\s+/);
501
+ const localName = parts[0].trim();
502
+ const exportedName = (parts[1] ?? parts[0]).trim();
503
+ if (localSignals.has(localName)) signals.add(exportedName);
504
+ }
505
+ }
506
+ }
507
+ if (/export\s+default\s+(?:signal|computed)\s*[<(]/.test(code)) signals.add("default");
508
+ if (signals.size > 0) registry.set(normalizedId, signals);
509
+ else registry.delete(normalizedId);
510
+ }
511
+ /**
512
+ * Resolve imported signal names from the signal export registry.
513
+ *
514
+ * For each import in the source, resolves the module and checks if it has
515
+ * signal exports in the registry. Returns the local names of imported signals.
516
+ *
517
+ * Handles named imports (`import { x } from ...`) and default imports
518
+ * (`import x from ...` — matched against 'default' in the registry).
519
+ */
520
+ async function resolveImportedSignals(code, _moduleId, registry, pluginCtx, resolveCache) {
521
+ if (registry.size === 0) return [];
522
+ const knownSignals = [];
523
+ let match;
524
+ /** Resolve a source specifier to a normalized module ID, using the cache. */
525
+ async function resolveSource(source) {
526
+ const cacheKey = `${_moduleId}::${source}`;
527
+ if (resolveCache.has(cacheKey)) return resolveCache.get(cacheKey) ?? null;
528
+ let resolvedId = null;
529
+ try {
530
+ const resolved = await pluginCtx.resolve(source, _moduleId, { skipSelf: true });
531
+ resolvedId = resolved?.id ? normalizeModuleId(resolved.id) : null;
532
+ } catch {}
533
+ resolveCache.set(cacheKey, resolvedId);
534
+ return resolvedId;
535
+ }
536
+ const IMPORT_RE = /import\s+(?!type\s)\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
537
+ while ((match = IMPORT_RE.exec(code)) !== null) {
538
+ const specifiers = match[1];
539
+ const source = match[2];
540
+ const resolvedId = await resolveSource(source);
541
+ if (!resolvedId) continue;
542
+ const exportedSignals = registry.get(resolvedId);
543
+ if (!exportedSignals) continue;
544
+ for (const spec of specifiers.split(",")) {
545
+ const trimmed = spec.trim();
546
+ if (!trimmed) continue;
547
+ const parts = trimmed.split(/\s+as\s+/);
548
+ const importedName = parts[0].trim();
549
+ const localName = (parts[1] ?? parts[0]).trim();
550
+ if (exportedSignals.has(importedName)) knownSignals.push(localName);
551
+ }
552
+ }
553
+ const DEFAULT_IMPORT_RE = /import\s+(?!type\s)(\w+)\s+from\s*['"]([^'"]+)['"]/g;
554
+ while ((match = DEFAULT_IMPORT_RE.exec(code)) !== null) {
555
+ const fullMatch = match[0];
556
+ if (/import\s+type\s+/.test(fullMatch)) continue;
557
+ const localName = match[1];
558
+ const source = match[2];
559
+ const resolvedId = await resolveSource(source);
560
+ if (!resolvedId) continue;
561
+ const exportedSignals = registry.get(resolvedId);
562
+ if (!exportedSignals) continue;
563
+ if (exportedSignals.has("default")) knownSignals.push(localName);
564
+ }
565
+ return knownSignals;
566
+ }
369
567
  const HMR_RUNTIME_SOURCE = `
370
568
  const REGISTRY_KEY = "__pyreon_hmr_registry__";
371
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/vite-plugin",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Vite plugin for Pyreon — .pyreon SFC support, HMR, compiler integration",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/vite-plugin#readme",
6
6
  "bugs": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "lib",
17
+ "!lib/**/*.map",
17
18
  "src",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -42,7 +43,7 @@
42
43
  "prepublishOnly": "bun run build"
43
44
  },
44
45
  "dependencies": {
45
- "@pyreon/compiler": "^0.13.1"
46
+ "@pyreon/compiler": "^0.15.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "vite": "^8.0.0"