feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+123
View File
@@ -0,0 +1,123 @@
# vite-plugin-vue-server-ref
[![NPM version](https://img.shields.io/npm/v/vite-plugin-vue-server-ref?color=a1b858&label=)](https://www.npmjs.com/package/vite-plugin-vue-server-ref)
Share state between clients and Vite server.
## Install
```bash
npm i -D vite-plugin-vue-server-ref
```
Add plugin to your `vite.config.ts`:
```ts
// vite.config.ts
import ServerRef from 'vite-plugin-vue-server-ref'
export default {
plugins: [
ServerRef({
state: {
/* Your custom initial state */
foo: 'bar',
object: {
count: 0
}
}
})
]
}
```
Use import it in your modules (`server-ref:[key]`)
```ts
import foo from 'server-ref:foo'
console.log(foo.value) // bar
foo.value = 'foobar'
// same as other modules / clients imported the server ref with same key
// or even refresh the pages
console.log(foo.value) // foobar
```
Or working with reactive object (`server-reactive:[key]`)
```ts
import object from 'server-reactive:object'
console.log(object.count) // 0
```
## Type Support
As server import can't infer the type correctly (by default it's `ServerRef<any>`), you can using `as` to specify the type.
```ts
import type { ServerReactive, ServerRef } from 'vite-plugin-vue-server-ref/client'
import _foo from 'server-ref:foo'
import _object from 'server-ref:object'
const foo = _foo as ServerRef<string>
const object = _object as ServerReactive<{ count: number }>
foo.value // string
object.count // number
```
## Controls
```ts
import foo from 'server-ref:foo'
foo.$syncUp = false // make it download only
foo.value = 'foobar' // won't send to server or other clients
```
```ts
import foo from 'server-ref:foo'
foo.$syncDown = false // make it upload only
// changes from other clients won't be received
```
```ts
import foo from 'server-ref:foo'
// listen to server change
foo.$onSet((value) => {
console.log(`Changes from server: ${value}`)
})
```
## Diffing
When working with reactive objects, you can add `?diff` to make the syncing incremental (deep diff).
```ts
import object from 'server-ref:object?diff'
console.log(object) // { foo: ..., bar: ... }
object.foo.nested = 'bar'
// the patch will be sent as '{ foo: { nested: 'bar' }}}'
// instead of the entire object
```
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
## License
[MIT](./LICENSE) License © 2021 [Anthony Fu](https://github.com/antfu)
+1
View File
@@ -0,0 +1 @@
export * from './dist/client'
+133
View File
@@ -0,0 +1,133 @@
'use strict';
const json = require('klona/json');
function apply(obj, diff) {
if (typeof obj !== typeof diff)
return diff;
if (typeof diff === "object") {
if (Array.isArray(diff))
return diff;
let out;
if (Array.isArray(obj)) {
out = obj;
for (const i in diff)
out[i] = apply(obj[i], diff[i]);
} else {
out = obj;
for (const i in diff)
out[i] = apply(obj[i], diff[i]);
}
return out;
}
return diff;
}
function diff(obj, old) {
if (typeof obj === "object") {
const isArray = Array.isArray(obj);
if (!old || typeof old !== "object" || isArray !== Array.isArray(old))
return obj;
if (isArray) {
let out2;
let i = 0;
const max = Math.min(obj.length, old.length);
for (; i < max; i++) {
const differs = different(obj[i], old[i]);
if (differs)
break;
}
const useArray = old.length === 0;
const offset = obj.length - old.length;
for (let j = obj.length; j-- > i; ) {
const oldJ = j - offset;
if (oldJ >= 0) {
const differs = different(obj[j], old[oldJ]);
if (differs) {
if (!out2)
out2 = useArray ? [] : {};
out2[j] = diff(obj[j], old[oldJ]);
}
} else {
if (!out2)
out2 = useArray ? [] : {};
out2[j] = obj[j];
}
}
return out2;
}
let out;
for (const key in obj) {
if (!(key in old) || obj[key] !== old[key]) {
if (!out)
out = {};
const r = diff(obj[key], old[key]);
if (r !== void 0)
out[key] = r;
}
}
for (const key in old) {
if (obj == null || !(key in obj)) {
if (!out)
out = {};
out[key] = void 0;
}
}
return out;
} else if (obj !== old) {
return obj;
}
}
function different(obj, old) {
for (const key in obj) {
if (old == null || !(key in old) || obj[key] !== old[key])
return true;
}
for (const key in old) {
if (obj == null || !(key in obj))
return true;
}
return false;
}
const UNDEFINED = "__UNDEFINED___";
function stringify(data) {
return JSON.stringify(data, (key, value) => value === void 0 ? UNDEFINED : value);
}
function parse(json) {
return JSON.parse(json, (key, value) => value === UNDEFINED ? void 0 : value);
}
function randId() {
return Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
}
function isObject(obj) {
return typeof obj === "object" && obj !== null;
}
function reactiveSet(target, value) {
for (const key of Object.keys(value)) {
if (target[key] !== value[key])
target[key] = value[key];
}
const originalKeys = new Set(Object.keys(target));
Object.keys(value).forEach((i) => originalKeys.delete(i));
for (const key of originalKeys)
delete target[key];
}
function define(target, key, value) {
Object.defineProperty(target, key, {
value,
writable: true,
enumerable: false
});
}
exports.clone = json.klona;
exports.UNDEFINED = UNDEFINED;
exports.apply = apply;
exports.define = define;
exports.diff = diff;
exports.isObject = isObject;
exports.parse = parse;
exports.randId = randId;
exports.reactiveSet = reactiveSet;
exports.stringify = stringify;
@@ -0,0 +1,23 @@
export { klona as clone } from 'klona/json';
/**
* Use this to apply diffs on the main thread.
* newObject = apply(oldObject, patch);
*/
declare function apply(obj: any, diff: any): any;
/**
* Use this to create a "patch" object in the worker thread.
* patch = diff(newObject, oldObject);
*/
declare function diff(obj: any, old: any): any;
declare const UNDEFINED = "__UNDEFINED___";
declare function stringify(data: any): string;
declare function parse(json: string): any;
declare function randId(): string;
declare function isObject(obj: any): boolean;
declare function reactiveSet(target: any, value: any): void;
declare function define(target: any, key: string, value: any): void;
export { UNDEFINED, apply, define, diff, isObject, parse, randId, reactiveSet, stringify };
@@ -0,0 +1,23 @@
export { klona as clone } from 'klona/json';
/**
* Use this to apply diffs on the main thread.
* newObject = apply(oldObject, patch);
*/
declare function apply(obj: any, diff: any): any;
/**
* Use this to create a "patch" object in the worker thread.
* patch = diff(newObject, oldObject);
*/
declare function diff(obj: any, old: any): any;
declare const UNDEFINED = "__UNDEFINED___";
declare function stringify(data: any): string;
declare function parse(json: string): any;
declare function randId(): string;
declare function isObject(obj: any): boolean;
declare function reactiveSet(target: any, value: any): void;
declare function define(target: any, key: string, value: any): void;
export { UNDEFINED, apply, define, diff, isObject, parse, randId, reactiveSet, stringify };
+23
View File
@@ -0,0 +1,23 @@
export { klona as clone } from 'klona/json';
/**
* Use this to apply diffs on the main thread.
* newObject = apply(oldObject, patch);
*/
declare function apply(obj: any, diff: any): any;
/**
* Use this to create a "patch" object in the worker thread.
* patch = diff(newObject, oldObject);
*/
declare function diff(obj: any, old: any): any;
declare const UNDEFINED = "__UNDEFINED___";
declare function stringify(data: any): string;
declare function parse(json: string): any;
declare function randId(): string;
declare function isObject(obj: any): boolean;
declare function reactiveSet(target: any, value: any): void;
declare function define(target: any, key: string, value: any): void;
export { UNDEFINED, apply, define, diff, isObject, parse, randId, reactiveSet, stringify };
+122
View File
@@ -0,0 +1,122 @@
export { klona as clone } from 'klona/json';
function apply(obj, diff) {
if (typeof obj !== typeof diff)
return diff;
if (typeof diff === "object") {
if (Array.isArray(diff))
return diff;
let out;
if (Array.isArray(obj)) {
out = obj;
for (const i in diff)
out[i] = apply(obj[i], diff[i]);
} else {
out = obj;
for (const i in diff)
out[i] = apply(obj[i], diff[i]);
}
return out;
}
return diff;
}
function diff(obj, old) {
if (typeof obj === "object") {
const isArray = Array.isArray(obj);
if (!old || typeof old !== "object" || isArray !== Array.isArray(old))
return obj;
if (isArray) {
let out2;
let i = 0;
const max = Math.min(obj.length, old.length);
for (; i < max; i++) {
const differs = different(obj[i], old[i]);
if (differs)
break;
}
const useArray = old.length === 0;
const offset = obj.length - old.length;
for (let j = obj.length; j-- > i; ) {
const oldJ = j - offset;
if (oldJ >= 0) {
const differs = different(obj[j], old[oldJ]);
if (differs) {
if (!out2)
out2 = useArray ? [] : {};
out2[j] = diff(obj[j], old[oldJ]);
}
} else {
if (!out2)
out2 = useArray ? [] : {};
out2[j] = obj[j];
}
}
return out2;
}
let out;
for (const key in obj) {
if (!(key in old) || obj[key] !== old[key]) {
if (!out)
out = {};
const r = diff(obj[key], old[key]);
if (r !== void 0)
out[key] = r;
}
}
for (const key in old) {
if (obj == null || !(key in obj)) {
if (!out)
out = {};
out[key] = void 0;
}
}
return out;
} else if (obj !== old) {
return obj;
}
}
function different(obj, old) {
for (const key in obj) {
if (old == null || !(key in old) || obj[key] !== old[key])
return true;
}
for (const key in old) {
if (obj == null || !(key in obj))
return true;
}
return false;
}
const UNDEFINED = "__UNDEFINED___";
function stringify(data) {
return JSON.stringify(data, (key, value) => value === void 0 ? UNDEFINED : value);
}
function parse(json) {
return JSON.parse(json, (key, value) => value === UNDEFINED ? void 0 : value);
}
function randId() {
return Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
}
function isObject(obj) {
return typeof obj === "object" && obj !== null;
}
function reactiveSet(target, value) {
for (const key of Object.keys(value)) {
if (target[key] !== value[key])
target[key] = value[key];
}
const originalKeys = new Set(Object.keys(target));
Object.keys(value).forEach((i) => originalKeys.delete(i));
for (const key of originalKeys)
delete target[key];
}
function define(target, key, value) {
Object.defineProperty(target, key, {
value,
writable: true,
enumerable: false
});
}
export { UNDEFINED, apply, define, diff, isObject, parse, randId, reactiveSet, stringify };
+242
View File
@@ -0,0 +1,242 @@
'use strict';
const mlly = require('mlly');
const utils = require('@antfu/utils');
const ufo = require('ufo');
const client = require('./client.cjs');
require('klona/json');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
const URL_PREFIX = ["/@server-ref/", "/@server-reactive/"];
const VIRTUAL_PREFIX = ["server-ref:", "server-reactive:"];
const WS_EVENT = "vue-server-ref";
const PREFIXES = [...URL_PREFIX, ...VIRTUAL_PREFIX];
function getBodyJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => body += chunk);
req.on("error", reject);
req.on("end", () => {
try {
resolve(client.parse(body) || {});
} catch (e) {
reject(e);
}
});
});
}
function get(obj, key) {
return key.split(".").reduce((acc, key2) => acc?.[key2], obj);
}
function set(obj, key, value) {
const keys = key.split(".");
let acc = obj;
keys.slice(0, -1).forEach((key2) => {
if (!acc[key2])
acc[key2] = {};
acc = acc[key2];
});
acc[keys[keys.length - 1]] = value;
}
function parseId(id) {
for (const pre of PREFIXES) {
if (id.startsWith(pre)) {
const { pathname: key, search } = ufo.parseURL(id.substr(pre.length));
const query = ufo.parseQuery(search);
return {
key: key.replace(/\//g, "."),
type: pre.includes("ref") ? "ref" : "reactive",
prefix: pre,
diff: "diff" in query
};
}
}
}
function genCode(options, id, clientUrl = "vite-plugin-vue-server-ref/client") {
const { state, defaultValue, clientVue, debug, debounce } = options;
const { key, type, prefix, diff } = id;
const access = type === "ref" ? "data.value" : "data";
return `
import { ${type}, watch } from "${clientVue}"
import { randId, stringify, parse, define, apply,${type === "reactive" ? " reactiveSet," : ""}${diff ? " clone, diff" : ""} } from ${JSON.stringify(clientUrl)}
const data = ${type}(${JSON.stringify(get(state, key) ?? defaultValue(key))})
let onSet = []
let onPatch = []
define(data, '$syncUp', true)
define(data, '$syncDown', true)
define(data, '$paused', false)
define(data, '$onSet', fn => onSet.push(fn))
define(data, '$onPatch', fn => onPatch.push(fn))
const id = randId()
if (import.meta.hot) {
let skipWatch = false
let timer = null
${diff ? "let copy = null" : ""}
function post(payload) {
${debug ? `console.log("[server-ref] [${key}] outgoing", payload)` : ""}
return fetch('${prefix + key}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: stringify(payload)
})
}
${diff ? `
function makeClone() {
copy = clone(${access})
}
function getDiff() {
return diff(${access}, copy)
}
` : ""}
function applyPatch(patch, trigger = true) {
skipWatch = true
if (trigger) {
onPatch.forEach(fn => fn(patch))
${debug ? `console.log("[server-ref] [${key}] patch incoming", patch)` : ""}
}
apply(${access}, patch)
${diff ? "makeClone()" : ""}
skipWatch = false
}
function applySet(newData) {
skipWatch = true
onSet.forEach(fn => fn(newData))
${type === "ref" ? "data.value = newData" : "reactiveSet(data, newData)"}
${debug ? `console.log("[server-ref] [${key}] incoming", newData)` : ""}
${diff ? "makeClone()" : ""}
skipWatch = false
}
${debug ? `console.log("[server-ref] [${key}] ref", data)` : ""}
${debug ? `console.log("[server-ref] [${key}] initial", ${access})` : ""}
import.meta.hot.on("${WS_EVENT}", (payload) => {
if (!data.$syncDown || data.$paused || payload.key !== "${key}" || payload.source === id)
return
if (payload.patch)
applyPatch(payload.patch)
else
applySet(payload.data)
})
define(data, '$patch', async (patch) => {
if (!data.$syncUp || data.$paused)
return false
applyPatch(patch, false)
return post({
source: id,
patch,
timestamp: Date.now(),
})
})
watch(data, (v) => {
if (timer)
clearTimeout(timer)
if (!data.$syncUp || data.$paused || skipWatch)
return
timer = setTimeout(()=>{
post({
source: id,
${diff ? "patch: getDiff()" : `data: ${access}`},
timestamp: Date.now(),
})
}, ${debounce})
}, { flush: 'sync', deep: true })
}
else {
define(data, '$patch', async () => false)
}
export default data
`.replace(/\n\s*\n/gm, "\n");
}
const defaultOptions = {
state: {},
debounce: 10,
debug: false,
defaultValue: () => void 0,
clientVue: "vue",
onChanged: () => {
}
};
function resolveOptions(options) {
return Object.assign({}, defaultOptions, options);
}
function VitePluginServerRef(options = {}) {
const resolved = resolveOptions(options);
const { state } = resolved;
const idMaps = {};
return {
name: "vite-plugin-vue-server-ref",
resolveId(id) {
const idx = VIRTUAL_PREFIX.findIndex((pre) => id.startsWith(pre));
if (idx > -1)
return URL_PREFIX[idx] + id.slice(VIRTUAL_PREFIX[idx].length);
return URL_PREFIX.some((pre) => id.startsWith(pre)) ? id : null;
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url || req.method !== "POST")
return next();
const id = parseId(req.url);
if (!id)
return next();
const key = id.key;
const payload = await getBodyJson(req);
for (const id2 of idMaps[key] || []) {
const module = server.moduleGraph.getModuleById(id2);
if (module)
server.moduleGraph.invalidateModule(module);
}
if (payload.patch)
set(state, key, client.apply(get(state, key), payload.patch));
else
set(state, key, payload.data);
server.ws.send({
type: "custom",
event: WS_EVENT,
data: { ...payload, key }
});
options.onChanged?.(key, get(state, key), payload.patch, payload.timestamp);
res.write("");
res.end();
});
},
async load(id) {
const res = parseId(id);
if (!res)
return;
if (!idMaps[res.key])
idMaps[res.key] = /* @__PURE__ */ new Set();
idMaps[res.key].add(id);
return genCode(
resolved,
res,
toAtFS(await mlly.resolvePath("vite-plugin-vue-server-ref/client", { url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)) }))
);
}
};
}
function toAtFS(path) {
return `/@fs${utils.ensurePrefix("/", utils.slash(path))}`;
}
module.exports = VitePluginServerRef;
+45
View File
@@ -0,0 +1,45 @@
import { Plugin } from 'vite';
import { Ref } from 'vue';
interface ServerRefOptions<T extends Record<string, unknown>> {
state?: T;
/**
* Milliseconds to debounce the server ref updates.
*
* @default 10
*/
debounce?: number;
/**
* Default value for the client when the data is not presented.
*
* @default () => undefined
*/
defaultValue?: (key: string) => unknown;
/**
* Log info on the client
*/
debug?: boolean;
/**
* The vue entry name for the generated client code.
*
* @default 'vue'
*/
clientVue?: string;
/**
* Callback on server data change
*/
onChanged?: <K extends keyof T>(name: K, data: T[K], patch: Partial<T[K]> | undefined, timestamp: number) => void;
}
interface ServerRefExtend<T> {
$syncDown: boolean;
$syncUp: boolean;
$onChange(fn: (data: T) => void): void;
$onPatch(fn: (patch: Partial<T>) => void): void;
$patch(patch: Partial<T>): Promise<boolean>;
}
type ServerRef<T> = Ref<T> & ServerRefExtend<T>;
type ServerReactive<T> = T & ServerRefExtend<T>;
declare function VitePluginServerRef(options?: ServerRefOptions<any>): Plugin;
export { type ServerReactive, type ServerRef, type ServerRefExtend, type ServerRefOptions, VitePluginServerRef as default };
+45
View File
@@ -0,0 +1,45 @@
import { Plugin } from 'vite';
import { Ref } from 'vue';
interface ServerRefOptions<T extends Record<string, unknown>> {
state?: T;
/**
* Milliseconds to debounce the server ref updates.
*
* @default 10
*/
debounce?: number;
/**
* Default value for the client when the data is not presented.
*
* @default () => undefined
*/
defaultValue?: (key: string) => unknown;
/**
* Log info on the client
*/
debug?: boolean;
/**
* The vue entry name for the generated client code.
*
* @default 'vue'
*/
clientVue?: string;
/**
* Callback on server data change
*/
onChanged?: <K extends keyof T>(name: K, data: T[K], patch: Partial<T[K]> | undefined, timestamp: number) => void;
}
interface ServerRefExtend<T> {
$syncDown: boolean;
$syncUp: boolean;
$onChange(fn: (data: T) => void): void;
$onPatch(fn: (patch: Partial<T>) => void): void;
$patch(patch: Partial<T>): Promise<boolean>;
}
type ServerRef<T> = Ref<T> & ServerRefExtend<T>;
type ServerReactive<T> = T & ServerRefExtend<T>;
declare function VitePluginServerRef(options?: ServerRefOptions<any>): Plugin;
export { type ServerReactive, type ServerRef, type ServerRefExtend, type ServerRefOptions, VitePluginServerRef as default };
+45
View File
@@ -0,0 +1,45 @@
import { Plugin } from 'vite';
import { Ref } from 'vue';
interface ServerRefOptions<T extends Record<string, unknown>> {
state?: T;
/**
* Milliseconds to debounce the server ref updates.
*
* @default 10
*/
debounce?: number;
/**
* Default value for the client when the data is not presented.
*
* @default () => undefined
*/
defaultValue?: (key: string) => unknown;
/**
* Log info on the client
*/
debug?: boolean;
/**
* The vue entry name for the generated client code.
*
* @default 'vue'
*/
clientVue?: string;
/**
* Callback on server data change
*/
onChanged?: <K extends keyof T>(name: K, data: T[K], patch: Partial<T[K]> | undefined, timestamp: number) => void;
}
interface ServerRefExtend<T> {
$syncDown: boolean;
$syncUp: boolean;
$onChange(fn: (data: T) => void): void;
$onPatch(fn: (patch: Partial<T>) => void): void;
$patch(patch: Partial<T>): Promise<boolean>;
}
type ServerRef<T> = Ref<T> & ServerRefExtend<T>;
type ServerReactive<T> = T & ServerRefExtend<T>;
declare function VitePluginServerRef(options?: ServerRefOptions<any>): Plugin;
export { type ServerReactive, type ServerRef, type ServerRefExtend, type ServerRefOptions, VitePluginServerRef as default };
+239
View File
@@ -0,0 +1,239 @@
import { resolvePath } from 'mlly';
import { ensurePrefix, slash } from '@antfu/utils';
import { parseURL, parseQuery } from 'ufo';
import { parse, apply } from './client.mjs';
import 'klona/json';
const URL_PREFIX = ["/@server-ref/", "/@server-reactive/"];
const VIRTUAL_PREFIX = ["server-ref:", "server-reactive:"];
const WS_EVENT = "vue-server-ref";
const PREFIXES = [...URL_PREFIX, ...VIRTUAL_PREFIX];
function getBodyJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => body += chunk);
req.on("error", reject);
req.on("end", () => {
try {
resolve(parse(body) || {});
} catch (e) {
reject(e);
}
});
});
}
function get(obj, key) {
return key.split(".").reduce((acc, key2) => acc?.[key2], obj);
}
function set(obj, key, value) {
const keys = key.split(".");
let acc = obj;
keys.slice(0, -1).forEach((key2) => {
if (!acc[key2])
acc[key2] = {};
acc = acc[key2];
});
acc[keys[keys.length - 1]] = value;
}
function parseId(id) {
for (const pre of PREFIXES) {
if (id.startsWith(pre)) {
const { pathname: key, search } = parseURL(id.substr(pre.length));
const query = parseQuery(search);
return {
key: key.replace(/\//g, "."),
type: pre.includes("ref") ? "ref" : "reactive",
prefix: pre,
diff: "diff" in query
};
}
}
}
function genCode(options, id, clientUrl = "vite-plugin-vue-server-ref/client") {
const { state, defaultValue, clientVue, debug, debounce } = options;
const { key, type, prefix, diff } = id;
const access = type === "ref" ? "data.value" : "data";
return `
import { ${type}, watch } from "${clientVue}"
import { randId, stringify, parse, define, apply,${type === "reactive" ? " reactiveSet," : ""}${diff ? " clone, diff" : ""} } from ${JSON.stringify(clientUrl)}
const data = ${type}(${JSON.stringify(get(state, key) ?? defaultValue(key))})
let onSet = []
let onPatch = []
define(data, '$syncUp', true)
define(data, '$syncDown', true)
define(data, '$paused', false)
define(data, '$onSet', fn => onSet.push(fn))
define(data, '$onPatch', fn => onPatch.push(fn))
const id = randId()
if (import.meta.hot) {
let skipWatch = false
let timer = null
${diff ? "let copy = null" : ""}
function post(payload) {
${debug ? `console.log("[server-ref] [${key}] outgoing", payload)` : ""}
return fetch('${prefix + key}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: stringify(payload)
})
}
${diff ? `
function makeClone() {
copy = clone(${access})
}
function getDiff() {
return diff(${access}, copy)
}
` : ""}
function applyPatch(patch, trigger = true) {
skipWatch = true
if (trigger) {
onPatch.forEach(fn => fn(patch))
${debug ? `console.log("[server-ref] [${key}] patch incoming", patch)` : ""}
}
apply(${access}, patch)
${diff ? "makeClone()" : ""}
skipWatch = false
}
function applySet(newData) {
skipWatch = true
onSet.forEach(fn => fn(newData))
${type === "ref" ? "data.value = newData" : "reactiveSet(data, newData)"}
${debug ? `console.log("[server-ref] [${key}] incoming", newData)` : ""}
${diff ? "makeClone()" : ""}
skipWatch = false
}
${debug ? `console.log("[server-ref] [${key}] ref", data)` : ""}
${debug ? `console.log("[server-ref] [${key}] initial", ${access})` : ""}
import.meta.hot.on("${WS_EVENT}", (payload) => {
if (!data.$syncDown || data.$paused || payload.key !== "${key}" || payload.source === id)
return
if (payload.patch)
applyPatch(payload.patch)
else
applySet(payload.data)
})
define(data, '$patch', async (patch) => {
if (!data.$syncUp || data.$paused)
return false
applyPatch(patch, false)
return post({
source: id,
patch,
timestamp: Date.now(),
})
})
watch(data, (v) => {
if (timer)
clearTimeout(timer)
if (!data.$syncUp || data.$paused || skipWatch)
return
timer = setTimeout(()=>{
post({
source: id,
${diff ? "patch: getDiff()" : `data: ${access}`},
timestamp: Date.now(),
})
}, ${debounce})
}, { flush: 'sync', deep: true })
}
else {
define(data, '$patch', async () => false)
}
export default data
`.replace(/\n\s*\n/gm, "\n");
}
const defaultOptions = {
state: {},
debounce: 10,
debug: false,
defaultValue: () => void 0,
clientVue: "vue",
onChanged: () => {
}
};
function resolveOptions(options) {
return Object.assign({}, defaultOptions, options);
}
function VitePluginServerRef(options = {}) {
const resolved = resolveOptions(options);
const { state } = resolved;
const idMaps = {};
return {
name: "vite-plugin-vue-server-ref",
resolveId(id) {
const idx = VIRTUAL_PREFIX.findIndex((pre) => id.startsWith(pre));
if (idx > -1)
return URL_PREFIX[idx] + id.slice(VIRTUAL_PREFIX[idx].length);
return URL_PREFIX.some((pre) => id.startsWith(pre)) ? id : null;
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url || req.method !== "POST")
return next();
const id = parseId(req.url);
if (!id)
return next();
const key = id.key;
const payload = await getBodyJson(req);
for (const id2 of idMaps[key] || []) {
const module = server.moduleGraph.getModuleById(id2);
if (module)
server.moduleGraph.invalidateModule(module);
}
if (payload.patch)
set(state, key, apply(get(state, key), payload.patch));
else
set(state, key, payload.data);
server.ws.send({
type: "custom",
event: WS_EVENT,
data: { ...payload, key }
});
options.onChanged?.(key, get(state, key), payload.patch, payload.timestamp);
res.write("");
res.end();
});
},
async load(id) {
const res = parseId(id);
if (!res)
return;
if (!idMaps[res.key])
idMaps[res.key] = /* @__PURE__ */ new Set();
idMaps[res.key].add(id);
return genCode(
resolved,
res,
toAtFS(await resolvePath("vite-plugin-vue-server-ref/client", { url: import.meta.url }))
);
}
};
}
function toAtFS(path) {
return `/@fs${ensurePrefix("/", slash(path))}`;
}
export { VitePluginServerRef as default };
+5
View File
@@ -0,0 +1,5 @@
import './module'
import './client'
export * from './dist/index'
export { default } from './dist/index'
+25
View File
@@ -0,0 +1,25 @@
declare module 'server-ref:*' {
import type { Ref } from 'vue'
type ServerRef<T> = Ref<T> & {
$syncDown: boolean
$syncUp: boolean
$onChange(fn: (data: T) => void): void
$onPatch(fn: (patch: Partial<T>) => void): void
$patch(patch: Partial<T>): Promise<boolean>
}
const ref: ServerRef<any>
export default ref
}
declare module 'server-reactive:*' {
type ServerReactive<T> = T & {
$syncDown: boolean
$syncUp: boolean
$onChange(fn: (data: T) => void): void
$onPatch(fn: (patch: Partial<T>) => void): void
$patch(patch: Partial<T>): Promise<boolean>
}
const ref: ServerReactive<any>
export default ref
}
+74
View File
@@ -0,0 +1,74 @@
{
"name": "vite-plugin-vue-server-ref",
"type": "module",
"version": "0.4.2",
"packageManager": "pnpm@8.15.1",
"description": "Share state between clients and Vite server",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/antfu/vite-plugin-vue-server-ref#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/antfu/vite-plugin-vue-server-ref.git"
},
"bugs": {
"url": "https://github.com/antfu/vite-plugin-vue-server-ref/issues"
},
"keywords": [
"vite-plugin"
],
"sideEffects": false,
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./client": {
"import": "./dist/client.mjs",
"require": "./dist/client.cjs"
}
},
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"*.d.ts",
"dist"
],
"scripts": {
"prepublishOnly": "nr build",
"build": "unbuild",
"dev": "unbuild --stub",
"demo": "pnpm -C example run dev",
"release": "bumpp && npm publish",
"lint": "eslint .",
"test": "vitest"
},
"peerDependencies": {
"vite": ">=2.0.0",
"vue": "^3.0.0"
},
"dependencies": {
"@antfu/utils": "^0.7.7",
"debug": "^4.3.4",
"klona": "^2.0.6",
"mlly": "^1.5.0",
"ufo": "^1.3.2"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.4",
"@antfu/ni": "^0.21.12",
"@types/debug": "^4.1.12",
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.11.16",
"bumpp": "^9.3.0",
"eslint": "^8.56.0",
"esno": "^4.0.0",
"typescript": "^5.3.3",
"unbuild": "^2.0.0",
"vite": "^5.0.12",
"vitest": "^1.2.2",
"vue": "^3.4.15"
}
}