@fluxstack/live 0.1.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.
@@ -0,0 +1,27 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ declare const BUILD_VERSION = "0.1.0";
4
+ interface LiveStripPluginOptions {
5
+ /** Import prefix that triggers stripping. Default: '@server/live/' */
6
+ importPrefix?: string;
7
+ /** Resolve server files relative to this base path. Default: auto-detected from Vite config */
8
+ serverDir?: string;
9
+ /** Directory for generated stubs (relative to Vite root). Default: '.live-stubs' */
10
+ stubDir?: string;
11
+ /** Enable verbose logging. Default: false */
12
+ verbose?: boolean;
13
+ }
14
+ /**
15
+ * Vite plugin to strip server-only code from Live Component imports.
16
+ *
17
+ * When client code imports from `@server/live/MyComponent`, this plugin
18
+ * intercepts and redirects to a tiny stub that exports only:
19
+ * - static componentName
20
+ * - static defaultState
21
+ * - static publicActions
22
+ *
23
+ * This ensures server logic never reaches the browser bundle.
24
+ */
25
+ declare function liveStripPlugin(options?: LiveStripPluginOptions): Plugin;
26
+
27
+ export { BUILD_VERSION, type LiveStripPluginOptions, liveStripPlugin };
@@ -0,0 +1,151 @@
1
+ import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
2
+ import { join, resolve, dirname } from 'path';
3
+
4
+ // src/build/index.ts
5
+ var BUILD_VERSION = "0.1.0";
6
+ function extractMeta(filePath) {
7
+ const src = readFileSync(filePath, "utf-8");
8
+ const results = [];
9
+ const re = /export\s+class\s+(\w+)\s+extends\s+LiveComponent/g;
10
+ let m;
11
+ while ((m = re.exec(src)) !== null) {
12
+ const className = m[1];
13
+ const body = extractBlock(src, src.indexOf("{", m.index));
14
+ const name = body.match(/static\s+componentName\s*=\s*['"]([^'"]+)['"]/)?.[1] ?? className;
15
+ const actions = body.match(/static\s+publicActions\s*=\s*(\[[^\]]*\])/)?.[1] ?? "[]";
16
+ const state = extractDefaultState(body);
17
+ results.push({ className, componentName: name, defaultState: state, publicActions: actions });
18
+ }
19
+ return results;
20
+ }
21
+ function extractBlock(src, start) {
22
+ let depth = 1, i = start + 1;
23
+ while (i < src.length && depth > 0) {
24
+ if (src[i] === "{") depth++;
25
+ else if (src[i] === "}") depth--;
26
+ i++;
27
+ }
28
+ return src.substring(start, i);
29
+ }
30
+ function extractDefaultState(classBody) {
31
+ const m = classBody.match(/static\s+defaultState\s*=\s*/);
32
+ if (!m) return "{}";
33
+ const objStart = classBody.indexOf("{", m.index + m[0].length);
34
+ if (objStart === -1) return "{}";
35
+ const raw = extractBlock(classBody, objStart);
36
+ return stripAsCasts(raw);
37
+ }
38
+ function stripAsCasts(s) {
39
+ const RE = /\s+as\s+/g;
40
+ let out = "", last = 0, m;
41
+ while ((m = RE.exec(s)) !== null) {
42
+ out += s.slice(last, m.index);
43
+ let i = m.index + m[0].length;
44
+ const stack = [];
45
+ while (i < s.length) {
46
+ const c = s[i];
47
+ if (c === "{" || c === "<" || c === "(") {
48
+ stack.push(c === "{" ? "}" : c === "<" ? ">" : ")");
49
+ i++;
50
+ } else if (c === "[" && s[i + 1] === "]") {
51
+ i += 2;
52
+ } else if (c === "[") {
53
+ stack.push("]");
54
+ i++;
55
+ } else if (stack.length && c === stack[stack.length - 1]) {
56
+ stack.pop();
57
+ i++;
58
+ while (s[i] === "[" && s[i + 1] === "]") i += 2;
59
+ } else if (!stack.length && (c === "," || c === "\n" || c === "}")) break;
60
+ else i++;
61
+ }
62
+ last = i;
63
+ }
64
+ return out + s.slice(last);
65
+ }
66
+ function buildStub(metas) {
67
+ if (!metas.length) return "export {}";
68
+ return metas.map(
69
+ (m) => `export class ${m.className} {
70
+ static componentName = '${m.componentName}'
71
+ static defaultState = ${m.defaultState}
72
+ static publicActions = ${m.publicActions}
73
+ }`
74
+ ).join("\n\n");
75
+ }
76
+ function norm(p) {
77
+ return p.replace(/\\/g, "/");
78
+ }
79
+ function liveStripPlugin(options = {}) {
80
+ const {
81
+ importPrefix = "@server/live/",
82
+ stubDir: stubDirName = ".live-stubs",
83
+ verbose = false
84
+ } = options;
85
+ let projectRoot;
86
+ let stubDir;
87
+ const nameToFile = /* @__PURE__ */ new Map();
88
+ const fileToName = /* @__PURE__ */ new Map();
89
+ const cache = /* @__PURE__ */ new Map();
90
+ const log = verbose ? (msg) => console.log(`[live-strip] ${msg}`) : () => {
91
+ };
92
+ function writeStub(name, serverPath) {
93
+ const stubPath = join(stubDir, `${name}.js`);
94
+ const content = buildStub(extractMeta(serverPath));
95
+ if (cache.get(name) !== content) {
96
+ writeFileSync(stubPath, content, "utf-8");
97
+ cache.set(name, content);
98
+ log(`Generated stub: ${name}`);
99
+ }
100
+ return stubPath;
101
+ }
102
+ return {
103
+ name: "fluxstack-live-strip",
104
+ enforce: "pre",
105
+ configResolved(config) {
106
+ projectRoot = config.configFile ? dirname(config.configFile) : resolve(config.root, "../..");
107
+ stubDir = join(config.root, stubDirName);
108
+ if (!existsSync(stubDir)) mkdirSync(stubDir, { recursive: true });
109
+ },
110
+ resolveId(source, importer) {
111
+ if (!source.startsWith(importPrefix) || !importer) return null;
112
+ const imp = norm(importer);
113
+ if (!imp.includes("/client/") && !imp.includes("/app/client/")) return null;
114
+ const name = source.replace(importPrefix, "");
115
+ let serverBase;
116
+ if (options.serverDir) {
117
+ serverBase = resolve(projectRoot, options.serverDir);
118
+ } else {
119
+ serverBase = resolve(projectRoot, source.replace("@server/", "app/server/"));
120
+ }
121
+ const ts = serverBase.endsWith(".ts") ? serverBase : serverBase + ".ts";
122
+ nameToFile.set(name, ts);
123
+ fileToName.set(norm(ts), name);
124
+ return writeStub(name, ts);
125
+ },
126
+ handleHotUpdate({ file, server }) {
127
+ const name = fileToName.get(norm(file));
128
+ if (!name) return;
129
+ const serverPath = nameToFile.get(name);
130
+ const oldContent = cache.get(name);
131
+ const newContent = buildStub(extractMeta(serverPath));
132
+ if (newContent === oldContent) return [];
133
+ writeStub(name, serverPath);
134
+ const stubPath = norm(join(stubDir, `${name}.js`));
135
+ const mods = server.moduleGraph.getModulesByFile(stubPath);
136
+ if (mods?.size) {
137
+ const arr = [...mods];
138
+ arr.forEach((m) => server.moduleGraph.invalidateModule(m));
139
+ server.config.logger.info(`[live-strip] HMR: ${name} metadata changed`, { timestamp: true });
140
+ return arr;
141
+ }
142
+ },
143
+ buildEnd() {
144
+ if (existsSync(stubDir)) rmSync(stubDir, { recursive: true, force: true });
145
+ }
146
+ };
147
+ }
148
+
149
+ export { BUILD_VERSION, liveStripPlugin };
150
+ //# sourceMappingURL=index.js.map
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/build/index.ts"],"names":[],"mappings":";;;;AAgBO,IAAM,aAAA,GAAgB;AAyB7B,SAAS,YAAY,QAAA,EAAmC;AACtD,EAAA,MAAM,GAAA,GAAM,YAAA,CAAa,QAAA,EAAU,OAAO,CAAA;AAC1C,EAAA,MAAM,UAA2B,EAAC;AAElC,EAAA,MAAM,EAAA,GAAK,mDAAA;AACX,EAAA,IAAI,CAAA;AAEJ,EAAA,OAAA,CAAQ,CAAA,GAAI,EAAA,CAAG,IAAA,CAAK,GAAG,OAAO,IAAA,EAAM;AAClC,IAAA,MAAM,SAAA,GAAY,EAAE,CAAC,CAAA;AACrB,IAAA,MAAM,IAAA,GAAO,aAAa,GAAA,EAAK,GAAA,CAAI,QAAQ,GAAA,EAAK,CAAA,CAAE,KAAK,CAAC,CAAA;AAExD,IAAA,MAAM,OAAO,IAAA,CAAK,KAAA,CAAM,+CAA+C,CAAA,GAAI,CAAC,CAAA,IAAK,SAAA;AACjF,IAAA,MAAM,UAAU,IAAA,CAAK,KAAA,CAAM,2CAA2C,CAAA,GAAI,CAAC,CAAA,IAAK,IAAA;AAChF,IAAA,MAAM,KAAA,GAAQ,oBAAoB,IAAI,CAAA;AAEtC,IAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,SAAA,EAAW,aAAA,EAAe,MAAM,YAAA,EAAc,KAAA,EAAO,aAAA,EAAe,OAAA,EAAS,CAAA;AAAA,EAC9F;AAEA,EAAA,OAAO,OAAA;AACT;AAGA,SAAS,YAAA,CAAa,KAAa,KAAA,EAAuB;AACxD,EAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,CAAA,GAAI,KAAA,GAAQ,CAAA;AAC3B,EAAA,OAAO,CAAA,GAAI,GAAA,CAAI,MAAA,IAAU,KAAA,GAAQ,CAAA,EAAG;AAClC,IAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,GAAA,EAAK,KAAA,EAAA;AAAA,SAAA,IACX,GAAA,CAAI,CAAC,CAAA,KAAM,GAAA,EAAK,KAAA,EAAA;AACzB,IAAA,CAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,GAAA,CAAI,SAAA,CAAU,KAAA,EAAO,CAAC,CAAA;AAC/B;AAGA,SAAS,oBAAoB,SAAA,EAA2B;AACtD,EAAA,MAAM,CAAA,GAAI,SAAA,CAAU,KAAA,CAAM,8BAA8B,CAAA;AACxD,EAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AAEf,EAAA,MAAM,QAAA,GAAW,UAAU,OAAA,CAAQ,GAAA,EAAK,EAAE,KAAA,GAAS,CAAA,CAAE,CAAC,CAAA,CAAE,MAAM,CAAA;AAC9D,EAAA,IAAI,QAAA,KAAa,IAAI,OAAO,IAAA;AAE5B,EAAA,MAAM,GAAA,GAAM,YAAA,CAAa,SAAA,EAAW,QAAQ,CAAA;AAC5C,EAAA,OAAO,aAAa,GAAG,CAAA;AACzB;AAKA,SAAS,aAAa,CAAA,EAAmB;AACvC,EAAA,MAAM,EAAA,GAAK,WAAA;AACX,EAAA,IAAI,GAAA,GAAM,EAAA,EAAI,IAAA,GAAO,CAAA,EAAG,CAAA;AAExB,EAAA,OAAA,CAAQ,CAAA,GAAI,EAAA,CAAG,IAAA,CAAK,CAAC,OAAO,IAAA,EAAM;AAChC,IAAA,GAAA,IAAO,CAAA,CAAE,KAAA,CAAM,IAAA,EAAM,CAAA,CAAE,KAAK,CAAA;AAC5B,IAAA,IAAI,CAAA,GAAI,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,CAAC,CAAA,CAAE,MAAA;AACvB,IAAA,MAAM,QAAkB,EAAC;AAEzB,IAAA,OAAO,CAAA,GAAI,EAAE,MAAA,EAAQ;AACnB,MAAA,MAAM,CAAA,GAAI,EAAE,CAAC,CAAA;AACb,MAAA,IAAI,CAAA,KAAM,GAAA,IAAO,CAAA,KAAM,GAAA,IAAO,MAAM,GAAA,EAAK;AAAE,QAAA,KAAA,CAAM,KAAK,CAAA,KAAM,GAAA,GAAM,MAAM,CAAA,KAAM,GAAA,GAAM,MAAM,GAAG,CAAA;AAAG,QAAA,CAAA,EAAA;AAAA,MAAI,WAC3F,CAAA,KAAM,GAAA,IAAO,EAAE,CAAA,GAAI,CAAC,MAAM,GAAA,EAAK;AAAE,QAAA,CAAA,IAAK,CAAA;AAAA,MAAE,CAAA,MAAA,IACxC,MAAM,GAAA,EAAK;AAAE,QAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AAAG,QAAA,CAAA,EAAA;AAAA,MAAI,CAAA,MAAA,IAClC,MAAM,MAAA,IAAU,CAAA,KAAM,MAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,EAAG;AAAE,QAAA,KAAA,CAAM,GAAA,EAAI;AAAG,QAAA,CAAA,EAAA;AAAK,QAAA,OAAO,CAAA,CAAE,CAAC,CAAA,KAAM,GAAA,IAAO,EAAE,CAAA,GAAI,CAAC,CAAA,KAAM,GAAA,EAAK,CAAA,IAAK,CAAA;AAAA,MAAE,CAAA,MAAA,IACnH,CAAC,KAAA,CAAM,MAAA,KAAW,MAAM,GAAA,IAAO,CAAA,KAAM,IAAA,IAAQ,CAAA,KAAM,GAAA,CAAA,EAAM;AAAA,WAC7D,CAAA,EAAA;AAAA,IACP;AACA,IAAA,IAAA,GAAO,CAAA;AAAA,EACT;AAEA,EAAA,OAAO,GAAA,GAAM,CAAA,CAAE,KAAA,CAAM,IAAI,CAAA;AAC3B;AAIA,SAAS,UAAU,KAAA,EAAgC;AACjD,EAAA,IAAI,CAAC,KAAA,CAAM,MAAA,EAAQ,OAAO,WAAA;AAC1B,EAAA,OAAO,KAAA,CAAM,GAAA;AAAA,IAAI,CAAA,CAAA,KACf,CAAA,aAAA,EAAgB,CAAA,CAAE,SAAS,CAAA;AAAA,0BAAA,EACE,EAAE,aAAa,CAAA;AAAA,wBAAA,EACjB,EAAE,YAAY;AAAA,yBAAA,EACb,EAAE,aAAa;AAAA,CAAA;AAAA,GAE7C,CAAE,KAAK,MAAM,CAAA;AACf;AAIA,SAAS,KAAK,CAAA,EAAW;AAAE,EAAA,OAAO,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA;AAAE;AAajD,SAAS,eAAA,CAAgB,OAAA,GAAkC,EAAC,EAAW;AAC5E,EAAA,MAAM;AAAA,IACJ,YAAA,GAAe,eAAA;AAAA,IACf,SAAS,WAAA,GAAc,aAAA;AAAA,IACvB,OAAA,GAAU;AAAA,GACZ,GAAI,OAAA;AAEJ,EAAA,IAAI,WAAA;AACJ,EAAA,IAAI,OAAA;AACJ,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoB;AAC3C,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoB;AAC3C,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAoB;AAEtC,EAAA,MAAM,GAAA,GAAM,OAAA,GAAU,CAAC,GAAA,KAAgB,OAAA,CAAQ,IAAI,CAAA,aAAA,EAAgB,GAAG,CAAA,CAAE,CAAA,GAAI,MAAM;AAAA,EAAC,CAAA;AAEnF,EAAA,SAAS,SAAA,CAAU,MAAc,UAAA,EAA4B;AAC3D,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,OAAA,EAAS,CAAA,EAAG,IAAI,CAAA,GAAA,CAAK,CAAA;AAC3C,IAAA,MAAM,OAAA,GAAU,SAAA,CAAU,WAAA,CAAY,UAAU,CAAC,CAAA;AACjD,IAAA,IAAI,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,KAAM,OAAA,EAAS;AAC/B,MAAA,aAAA,CAAc,QAAA,EAAU,SAAS,OAAO,CAAA;AACxC,MAAA,KAAA,CAAM,GAAA,CAAI,MAAM,OAAO,CAAA;AACvB,MAAA,GAAA,CAAI,CAAA,gBAAA,EAAmB,IAAI,CAAA,CAAE,CAAA;AAAA,IAC/B;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,sBAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IAET,eAAe,MAAA,EAAQ;AACrB,MAAA,WAAA,GAAc,MAAA,CAAO,aAAa,OAAA,CAAQ,MAAA,CAAO,UAAU,CAAA,GAAI,OAAA,CAAQ,MAAA,CAAO,IAAA,EAAM,OAAO,CAAA;AAC3F,MAAA,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,IAAA,EAAM,WAAW,CAAA;AACvC,MAAA,IAAI,CAAC,WAAW,OAAO,CAAA,YAAa,OAAA,EAAS,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAAA,IAClE,CAAA;AAAA,IAEA,SAAA,CAAU,QAAQ,QAAA,EAAU;AAC1B,MAAA,IAAI,CAAC,MAAA,CAAO,UAAA,CAAW,YAAY,CAAA,IAAK,CAAC,UAAU,OAAO,IAAA;AAE1D,MAAA,MAAM,GAAA,GAAM,KAAK,QAAQ,CAAA;AAEzB,MAAA,IAAI,CAAC,GAAA,CAAI,QAAA,CAAS,UAAU,CAAA,IAAK,CAAC,GAAA,CAAI,QAAA,CAAS,cAAc,CAAA,EAAG,OAAO,IAAA;AAEvE,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA;AAG5C,MAAA,IAAI,UAAA;AACJ,MAAA,IAAI,QAAQ,SAAA,EAAW;AACrB,QAAA,UAAA,GAAa,OAAA,CAAQ,WAAA,EAAa,OAAA,CAAQ,SAAS,CAAA;AAAA,MACrD,CAAA,MAAO;AACL,QAAA,UAAA,GAAa,QAAQ,WAAA,EAAa,MAAA,CAAO,OAAA,CAAQ,UAAA,EAAY,aAAa,CAAC,CAAA;AAAA,MAC7E;AAEA,MAAA,MAAM,KAAK,UAAA,CAAW,QAAA,CAAS,KAAK,CAAA,GAAI,aAAa,UAAA,GAAa,KAAA;AAElE,MAAA,UAAA,CAAW,GAAA,CAAI,MAAM,EAAE,CAAA;AACvB,MAAA,UAAA,CAAW,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA,EAAG,IAAI,CAAA;AAE7B,MAAA,OAAO,SAAA,CAAU,MAAM,EAAE,CAAA;AAAA,IAC3B,CAAA;AAAA,IAEA,eAAA,CAAgB,EAAE,IAAA,EAAM,MAAA,EAAO,EAAwB;AACrD,MAAA,MAAM,IAAA,GAAO,UAAA,CAAW,GAAA,CAAI,IAAA,CAAK,IAAI,CAAC,CAAA;AACtC,MAAA,IAAI,CAAC,IAAA,EAAM;AAEX,MAAA,MAAM,UAAA,GAAa,UAAA,CAAW,GAAA,CAAI,IAAI,CAAA;AACtC,MAAA,MAAM,UAAA,GAAa,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AACjC,MAAA,MAAM,UAAA,GAAa,SAAA,CAAU,WAAA,CAAY,UAAU,CAAC,CAAA;AAEpD,MAAA,IAAI,UAAA,KAAe,UAAA,EAAY,OAAO,EAAC;AAEvC,MAAA,SAAA,CAAU,MAAM,UAAU,CAAA;AAE1B,MAAA,MAAM,WAAW,IAAA,CAAK,IAAA,CAAK,SAAS,CAAA,EAAG,IAAI,KAAK,CAAC,CAAA;AACjD,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,WAAA,CAAY,gBAAA,CAAiB,QAAQ,CAAA;AACzD,MAAA,IAAI,MAAM,IAAA,EAAM;AACd,QAAA,MAAM,GAAA,GAAM,CAAC,GAAG,IAAI,CAAA;AACpB,QAAA,GAAA,CAAI,QAAQ,CAAA,CAAA,KAAK,MAAA,CAAO,WAAA,CAAY,gBAAA,CAAiB,CAAC,CAAC,CAAA;AACvD,QAAA,MAAA,CAAO,MAAA,CAAO,OAAO,IAAA,CAAK,CAAA,kBAAA,EAAqB,IAAI,CAAA,iBAAA,CAAA,EAAqB,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAC3F,QAAA,OAAO,GAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,QAAA,GAAW;AACT,MAAA,IAAI,UAAA,CAAW,OAAO,CAAA,EAAG,MAAA,CAAO,OAAA,EAAS,EAAE,SAAA,EAAW,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,CAAA;AAAA,IAC3E;AAAA,GACF;AACF","file":"index.js","sourcesContent":["// @fluxstack/live/build - Build utilities\r\n//\r\n// Vite plugin to strip server code from Live Component imports.\r\n// Ensures server-side logic never reaches the browser.\r\n//\r\n// Usage in vite.config.ts:\r\n// import { liveStripPlugin } from '@fluxstack/live/build'\r\n//\r\n// export default defineConfig({\r\n// plugins: [liveStripPlugin({ serverDir: 'src/server/live' })]\r\n// })\r\n\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs'\r\nimport { resolve, dirname, join } from 'path'\r\nimport type { Plugin, ModuleNode } from 'vite'\r\n\r\nexport const BUILD_VERSION = '0.1.0'\r\n\r\n// ===== Types =====\r\n\r\nexport interface LiveStripPluginOptions {\r\n /** Import prefix that triggers stripping. Default: '@server/live/' */\r\n importPrefix?: string\r\n /** Resolve server files relative to this base path. Default: auto-detected from Vite config */\r\n serverDir?: string\r\n /** Directory for generated stubs (relative to Vite root). Default: '.live-stubs' */\r\n stubDir?: string\r\n /** Enable verbose logging. Default: false */\r\n verbose?: boolean\r\n}\r\n\r\n// ===== Metadata Extraction =====\r\n\r\ninterface ComponentMeta {\r\n className: string\r\n componentName: string\r\n defaultState: string\r\n publicActions: string\r\n}\r\n\r\n/** Read a server .ts file and pull out the 3 static fields we need. */\r\nfunction extractMeta(filePath: string): ComponentMeta[] {\r\n const src = readFileSync(filePath, 'utf-8')\r\n const results: ComponentMeta[] = []\r\n\r\n const re = /export\\s+class\\s+(\\w+)\\s+extends\\s+LiveComponent/g\r\n let m: RegExpExecArray | null\r\n\r\n while ((m = re.exec(src)) !== null) {\r\n const className = m[1]!\r\n const body = extractBlock(src, src.indexOf('{', m.index))\r\n\r\n const name = body.match(/static\\s+componentName\\s*=\\s*['\"]([^'\"]+)['\"]/)?.[1] ?? className\r\n const actions = body.match(/static\\s+publicActions\\s*=\\s*(\\[[^\\]]*\\])/)?.[1] ?? '[]'\r\n const state = extractDefaultState(body)\r\n\r\n results.push({ className, componentName: name, defaultState: state, publicActions: actions })\r\n }\r\n\r\n return results\r\n}\r\n\r\n/** Extract a brace-balanced block starting at position `start`. */\r\nfunction extractBlock(src: string, start: number): string {\r\n let depth = 1, i = start + 1\r\n while (i < src.length && depth > 0) {\r\n if (src[i] === '{') depth++\r\n else if (src[i] === '}') depth--\r\n i++\r\n }\r\n return src.substring(start, i)\r\n}\r\n\r\n/** Pull out `static defaultState = { ... }` and strip TS type casts. */\r\nfunction extractDefaultState(classBody: string): string {\r\n const m = classBody.match(/static\\s+defaultState\\s*=\\s*/)\r\n if (!m) return '{}'\r\n\r\n const objStart = classBody.indexOf('{', m.index! + m[0].length)\r\n if (objStart === -1) return '{}'\r\n\r\n const raw = extractBlock(classBody, objStart)\r\n return stripAsCasts(raw)\r\n}\r\n\r\n/**\r\n * Remove `as <Type>` casts, handling nested generics/brackets.\r\n */\r\nfunction stripAsCasts(s: string): string {\r\n const RE = /\\s+as\\s+/g\r\n let out = '', last = 0, m: RegExpExecArray | null\r\n\r\n while ((m = RE.exec(s)) !== null) {\r\n out += s.slice(last, m.index)\r\n let i = m.index + m[0].length\r\n const stack: string[] = []\r\n\r\n while (i < s.length) {\r\n const c = s[i]\r\n if (c === '{' || c === '<' || c === '(') { stack.push(c === '{' ? '}' : c === '<' ? '>' : ')'); i++ }\r\n else if (c === '[' && s[i + 1] === ']') { i += 2 }\r\n else if (c === '[') { stack.push(']'); i++ }\r\n else if (stack.length && c === stack[stack.length - 1]) { stack.pop(); i++; while (s[i] === '[' && s[i + 1] === ']') i += 2 }\r\n else if (!stack.length && (c === ',' || c === '\\n' || c === '}')) break\r\n else i++\r\n }\r\n last = i\r\n }\r\n\r\n return out + s.slice(last)\r\n}\r\n\r\n// ===== Stub Generation =====\r\n\r\nfunction buildStub(metas: ComponentMeta[]): string {\r\n if (!metas.length) return 'export {}'\r\n return metas.map(m =>\r\n `export class ${m.className} {\\n` +\r\n ` static componentName = '${m.componentName}'\\n` +\r\n ` static defaultState = ${m.defaultState}\\n` +\r\n ` static publicActions = ${m.publicActions}\\n` +\r\n `}`\r\n ).join('\\n\\n')\r\n}\r\n\r\n// ===== Plugin =====\r\n\r\nfunction norm(p: string) { return p.replace(/\\\\/g, '/') }\r\n\r\n/**\r\n * Vite plugin to strip server-only code from Live Component imports.\r\n *\r\n * When client code imports from `@server/live/MyComponent`, this plugin\r\n * intercepts and redirects to a tiny stub that exports only:\r\n * - static componentName\r\n * - static defaultState\r\n * - static publicActions\r\n *\r\n * This ensures server logic never reaches the browser bundle.\r\n */\r\nexport function liveStripPlugin(options: LiveStripPluginOptions = {}): Plugin {\r\n const {\r\n importPrefix = '@server/live/',\r\n stubDir: stubDirName = '.live-stubs',\r\n verbose = false,\r\n } = options\r\n\r\n let projectRoot: string\r\n let stubDir: string\r\n const nameToFile = new Map<string, string>()\r\n const fileToName = new Map<string, string>()\r\n const cache = new Map<string, string>()\r\n\r\n const log = verbose ? (msg: string) => console.log(`[live-strip] ${msg}`) : () => {}\r\n\r\n function writeStub(name: string, serverPath: string): string {\r\n const stubPath = join(stubDir, `${name}.js`)\r\n const content = buildStub(extractMeta(serverPath))\r\n if (cache.get(name) !== content) {\r\n writeFileSync(stubPath, content, 'utf-8')\r\n cache.set(name, content)\r\n log(`Generated stub: ${name}`)\r\n }\r\n return stubPath\r\n }\r\n\r\n return {\r\n name: 'fluxstack-live-strip',\r\n enforce: 'pre',\r\n\r\n configResolved(config) {\r\n projectRoot = config.configFile ? dirname(config.configFile) : resolve(config.root, '../..')\r\n stubDir = join(config.root, stubDirName)\r\n if (!existsSync(stubDir)) mkdirSync(stubDir, { recursive: true })\r\n },\r\n\r\n resolveId(source, importer) {\r\n if (!source.startsWith(importPrefix) || !importer) return null\r\n\r\n const imp = norm(importer)\r\n // Only strip imports from client code\r\n if (!imp.includes('/client/') && !imp.includes('/app/client/')) return null\r\n\r\n const name = source.replace(importPrefix, '')\r\n\r\n // Resolve the server-side source file\r\n let serverBase: string\r\n if (options.serverDir) {\r\n serverBase = resolve(projectRoot, options.serverDir)\r\n } else {\r\n serverBase = resolve(projectRoot, source.replace('@server/', 'app/server/'))\r\n }\r\n\r\n const ts = serverBase.endsWith('.ts') ? serverBase : serverBase + '.ts'\r\n\r\n nameToFile.set(name, ts)\r\n fileToName.set(norm(ts), name)\r\n\r\n return writeStub(name, ts)\r\n },\r\n\r\n handleHotUpdate({ file, server }): ModuleNode[] | void {\r\n const name = fileToName.get(norm(file))\r\n if (!name) return\r\n\r\n const serverPath = nameToFile.get(name)!\r\n const oldContent = cache.get(name)\r\n const newContent = buildStub(extractMeta(serverPath))\r\n\r\n if (newContent === oldContent) return []\r\n\r\n writeStub(name, serverPath)\r\n\r\n const stubPath = norm(join(stubDir, `${name}.js`))\r\n const mods = server.moduleGraph.getModulesByFile(stubPath)\r\n if (mods?.size) {\r\n const arr = [...mods]\r\n arr.forEach(m => server.moduleGraph.invalidateModule(m))\r\n server.config.logger.info(`[live-strip] HMR: ${name} metadata changed`, { timestamp: true })\r\n return arr\r\n }\r\n },\r\n\r\n buildEnd() {\r\n if (existsSync(stubDir)) rmSync(stubDir, { recursive: true, force: true })\r\n },\r\n }\r\n}\r\n"]}