@ubean/islands 0.1.2 → 0.1.3
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/dist/index.d.ts +40 -0
- package/dist/index.js +68 -0
- package/dist/runtime.d.ts +35 -0
- package/dist/runtime.js +126 -0
- package/dist/vite.d.ts +13 -0
- package/dist/vite.js +209 -0
- package/package.json +1 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { UbeanIslandsPluginOptions, transformVueSfcIslands, ubeanIslandsPlugin } from "./vite.js";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
type ClientDirective = 'client:load' | 'client:idle' | 'client:visible' | 'client:media' | 'client:only';
|
|
4
|
+
interface IslandDefinition {
|
|
5
|
+
id: string;
|
|
6
|
+
component: string;
|
|
7
|
+
directive: ClientDirective;
|
|
8
|
+
mediaQuery?: string;
|
|
9
|
+
props: Record<string, unknown>;
|
|
10
|
+
slots?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface IslandsContext {
|
|
13
|
+
islands: Map<string, IslandDefinition>;
|
|
14
|
+
counter: number;
|
|
15
|
+
}
|
|
16
|
+
interface ClientHydrationStrategy {
|
|
17
|
+
directive: ClientDirective;
|
|
18
|
+
}
|
|
19
|
+
interface IslandSsrOptions {
|
|
20
|
+
component: string;
|
|
21
|
+
directive: ClientDirective;
|
|
22
|
+
props?: Record<string, unknown>;
|
|
23
|
+
mediaQuery?: string;
|
|
24
|
+
children?: string;
|
|
25
|
+
}
|
|
26
|
+
declare function createIslandsContext(): IslandsContext;
|
|
27
|
+
declare function registerIsland(ctx: IslandsContext, component: string, directive: ClientDirective, props: Record<string, unknown>, mediaQuery?: string): string;
|
|
28
|
+
declare function getIslandsScript(islands: IslandDefinition[]): string;
|
|
29
|
+
declare function generateIslandPlaceholder(id: string, component: string, directive: ClientDirective, props: Record<string, unknown>, renderedHtml: string, mediaQuery?: string): string;
|
|
30
|
+
declare function renderIslandPlaceholder(options: IslandSsrOptions): string;
|
|
31
|
+
declare const hydrationStrategyMeta: Record<ClientDirective, {
|
|
32
|
+
directive: ClientDirective;
|
|
33
|
+
requiresMediaQuery?: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/bootstrap.d.ts
|
|
37
|
+
declare function getIslandsBootstrapScript(): string;
|
|
38
|
+
declare function getIslandsClearScript(): string;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { type ClientDirective, type ClientHydrationStrategy, type IslandDefinition, type IslandSsrOptions, type IslandsContext, type UbeanIslandsPluginOptions, createIslandsContext, generateIslandPlaceholder, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, hydrationStrategyMeta, registerIsland, renderIslandPlaceholder, transformVueSfcIslands, ubeanIslandsPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { transformVueSfcIslands, ubeanIslandsPlugin } from "./vite.js";
|
|
2
|
+
//#region src/types.ts
|
|
3
|
+
function createIslandsContext() {
|
|
4
|
+
return {
|
|
5
|
+
islands: /* @__PURE__ */ new Map(),
|
|
6
|
+
counter: 0
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function registerIsland(ctx, component, directive, props, mediaQuery) {
|
|
10
|
+
const id = `island-${++ctx.counter}`;
|
|
11
|
+
ctx.islands.set(id, {
|
|
12
|
+
id,
|
|
13
|
+
component,
|
|
14
|
+
directive,
|
|
15
|
+
mediaQuery,
|
|
16
|
+
props: serializeProps(props)
|
|
17
|
+
});
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
function serializeProps(props) {
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [key, value] of Object.entries(props)) {
|
|
23
|
+
if (typeof value === "function" || typeof value === "symbol") continue;
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function getIslandsScript(islands) {
|
|
29
|
+
if (islands.length === 0) return "";
|
|
30
|
+
return `<script type="application/json" data-ubean-islands>${JSON.stringify(islands)}<\/script>`;
|
|
31
|
+
}
|
|
32
|
+
function generateIslandPlaceholder(id, component, directive, props, renderedHtml, mediaQuery) {
|
|
33
|
+
const propsJson = JSON.stringify(serializeProps(props));
|
|
34
|
+
const mediaAttr = mediaQuery ? ` data-media="${escapeHtml(mediaQuery)}"` : "";
|
|
35
|
+
return `<ubean-island
|
|
36
|
+
data-island-id="${escapeHtml(id)}"
|
|
37
|
+
data-component="${escapeHtml(component)}"
|
|
38
|
+
data-directive="${directive}"${mediaAttr}
|
|
39
|
+
data-props="${escapeHtml(propsJson)}"
|
|
40
|
+
>${renderedHtml}</ubean-island>`;
|
|
41
|
+
}
|
|
42
|
+
function renderIslandPlaceholder(options) {
|
|
43
|
+
const { component, directive, props = {}, mediaQuery, children = "" } = options;
|
|
44
|
+
return generateIslandPlaceholder(`island-${Math.random().toString(36).slice(2, 10)}`, component, directive, props, children, mediaQuery);
|
|
45
|
+
}
|
|
46
|
+
function escapeHtml(str) {
|
|
47
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
48
|
+
}
|
|
49
|
+
const hydrationStrategyMeta = {
|
|
50
|
+
"client:load": { directive: "client:load" },
|
|
51
|
+
"client:idle": { directive: "client:idle" },
|
|
52
|
+
"client:visible": { directive: "client:visible" },
|
|
53
|
+
"client:media": {
|
|
54
|
+
directive: "client:media",
|
|
55
|
+
requiresMediaQuery: true
|
|
56
|
+
},
|
|
57
|
+
"client:only": { directive: "client:only" }
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/bootstrap.ts
|
|
61
|
+
function getIslandsBootstrapScript() {
|
|
62
|
+
return `<script>(function(){var HYDRATED_KEY='__ubeanIslandsHydrated';function hydrated(){return !!window[HYDRATED_KEY]}function markHydrated(){window[HYDRATED_KEY]=true}function getIslands(){return document.querySelectorAll('ubean-island[data-island-id]')}function resolveProps(el){try{var d=el.getAttribute('data-props');return d?JSON.parse(d.replace(/"/g,'"').replace(/&/g,'&').replace(/</g,'<')):{}}catch(e){return{}}}function shouldHydrate(el){return el.getAttribute('data-hydrating')==='true'}function triggerHydrate(el){if(el.hasAttribute('data-hydrated'))return;el.setAttribute('data-hydrating','true')}function onDirective(el){var d=el.getAttribute('data-directive');if(!d||d==='client:only'){if(d==='client:only')triggerHydrate(el);return}if(d==='client:load'){triggerHydrate(el);return}if(d==='idle'||d==='client:idle'){if('requestIdleCallback'in window){requestIdleCallback(function(){triggerHydrate(el)},{timeout:2000});return}setTimeout(function(){triggerHydrate(el)},200);return}if(d==='visible'||d==='client:visible'){if('IntersectionObserver'in window){var io=new IntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){io.disconnect();triggerHydrate(el)}})},{rootMargin:'200px'});io.observe(el);return}triggerHydrate(el);return}if(d==='media'||d==='client:media'){var media=el.getAttribute('data-media');if(media){var mql=window.matchMedia(media);if(mql.matches){triggerHydrate(el)}else{var fn=function(e){if(e.matches){triggerHydrate(el);mql.removeEventListener?mql.removeEventListener('change',fn):mql.removeListener(fn)}};mql.addEventListener?mql.addEventListener('change',fn):mql.addListener(fn);return}}else{triggerHydrate(el)}return}triggerHydrate(el)}function boot(){if(hydrated())return;markHydrated();getIslands().forEach(function(el){onDirective(el)})}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',boot)}else{boot()}})();<\/script>`;
|
|
63
|
+
}
|
|
64
|
+
function getIslandsClearScript() {
|
|
65
|
+
return `(function(){function clearIslands(){var islands=document.querySelectorAll('ubean-island[data-island-id]');islands.forEach(function(el){if(el.getAttribute('data-directive')!=='client:only'){return}el.innerHTML='';el.setAttribute('data-cleared','true')})}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',clearIslands)}else{clearIslands()}})();`;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { createIslandsContext, generateIslandPlaceholder, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, hydrationStrategyMeta, registerIsland, renderIslandPlaceholder, transformVueSfcIslands, ubeanIslandsPlugin };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { App, Component } from "vue";
|
|
2
|
+
//#region src/runtime.d.ts
|
|
3
|
+
interface DomElement {
|
|
4
|
+
getAttribute(name: string): string | null;
|
|
5
|
+
setAttribute(name: string, value: string): void;
|
|
6
|
+
hasAttribute(name: string): boolean;
|
|
7
|
+
}
|
|
8
|
+
interface NodeListOf<T> {
|
|
9
|
+
forEach(callback: (value: T, key: number, parent: NodeListOf<T>) => void): void;
|
|
10
|
+
}
|
|
11
|
+
interface DomParentNode {
|
|
12
|
+
querySelectorAll(selector: string): NodeListOf<DomElement>;
|
|
13
|
+
}
|
|
14
|
+
interface IslandHydrateOptions {
|
|
15
|
+
getComponent?: (name: string) => Component | Promise<Component> | null;
|
|
16
|
+
appContext?: App;
|
|
17
|
+
onHydrated?: (el: DomElement, component: Component) => void;
|
|
18
|
+
}
|
|
19
|
+
interface IslandRecord {
|
|
20
|
+
el: DomElement;
|
|
21
|
+
id: string;
|
|
22
|
+
componentName: string;
|
|
23
|
+
directive: string;
|
|
24
|
+
mediaQuery?: string;
|
|
25
|
+
props: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
declare function collectIslands(root?: DomParentNode): IslandRecord[];
|
|
28
|
+
declare function hydrateIsland(record: IslandRecord, component: Component, options?: IslandHydrateOptions): void;
|
|
29
|
+
interface HydrateIslandsOptions extends IslandHydrateOptions {
|
|
30
|
+
root?: DomParentNode;
|
|
31
|
+
components?: Record<string, Component | (() => Promise<Component>)>;
|
|
32
|
+
}
|
|
33
|
+
declare function hydrateIslands(options?: HydrateIslandsOptions): void;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { HydrateIslandsOptions, IslandHydrateOptions, IslandRecord, collectIslands, hydrateIsland, hydrateIslands };
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createApp, defineComponent, h } from "vue";
|
|
2
|
+
//#region src/runtime.ts
|
|
3
|
+
const _global = globalThis;
|
|
4
|
+
function decodeProps(raw) {
|
|
5
|
+
if (!raw) return {};
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw.replace(/"/g, "\"").replace(/&/g, "&").replace(/</g, "<"));
|
|
8
|
+
} catch {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function collectIslands(root) {
|
|
13
|
+
const doc = root ?? _global.document;
|
|
14
|
+
if (!doc) return [];
|
|
15
|
+
const nodes = doc.querySelectorAll("ubean-island[data-island-id]");
|
|
16
|
+
const records = [];
|
|
17
|
+
nodes.forEach((el) => {
|
|
18
|
+
const domEl = el;
|
|
19
|
+
const id = domEl.getAttribute("data-island-id") || "";
|
|
20
|
+
const componentName = domEl.getAttribute("data-component") || "";
|
|
21
|
+
const directive = domEl.getAttribute("data-directive") || "client:load";
|
|
22
|
+
const mediaQuery = domEl.getAttribute("data-media") || void 0;
|
|
23
|
+
const props = decodeProps(domEl.getAttribute("data-props"));
|
|
24
|
+
records.push({
|
|
25
|
+
el: domEl,
|
|
26
|
+
id,
|
|
27
|
+
componentName,
|
|
28
|
+
directive,
|
|
29
|
+
mediaQuery,
|
|
30
|
+
props
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
35
|
+
function hydrateIsland(record, component, options = {}) {
|
|
36
|
+
if (record.el.hasAttribute("data-hydrated")) return;
|
|
37
|
+
record.el.setAttribute("data-hydrated", "true");
|
|
38
|
+
const app = createApp(defineComponent({
|
|
39
|
+
name: `Island-${record.componentName}`,
|
|
40
|
+
setup() {
|
|
41
|
+
return () => h(component, record.props);
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
if (options.appContext) {
|
|
45
|
+
if (options.appContext.config?.globalProperties) Object.assign(app.config.globalProperties, options.appContext.config.globalProperties);
|
|
46
|
+
const appContextInternal = options.appContext;
|
|
47
|
+
if (appContextInternal._context?.provides) for (const [key, value] of Object.entries(appContextInternal._context.provides)) app.provide(key, value);
|
|
48
|
+
}
|
|
49
|
+
app.mount(record.el);
|
|
50
|
+
options.onHydrated?.(record.el, component);
|
|
51
|
+
}
|
|
52
|
+
function hydrateIslands(options = {}) {
|
|
53
|
+
const { root, components = {}, getComponent, ...rest } = options;
|
|
54
|
+
const islands = collectIslands(root);
|
|
55
|
+
if (islands.length === 0) return;
|
|
56
|
+
for (const record of islands) {
|
|
57
|
+
if (record.directive === "client:only") {
|
|
58
|
+
const comp = resolveComponent(record.componentName, components, getComponent);
|
|
59
|
+
if (comp) Promise.resolve(comp).then((c) => hydrateIsland(record, c, rest));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const doHydrate = () => {
|
|
63
|
+
const comp = resolveComponent(record.componentName, components, getComponent);
|
|
64
|
+
if (comp) Promise.resolve(comp).then((c) => hydrateIsland(record, c, rest));
|
|
65
|
+
};
|
|
66
|
+
if (record.el.getAttribute("data-hydrating") === "true" && !record.el.hasAttribute("data-hydrated")) {
|
|
67
|
+
doHydrate();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const MutationObserverCtor = _global.MutationObserver ?? null;
|
|
71
|
+
if (MutationObserverCtor) {
|
|
72
|
+
const observer = new MutationObserverCtor(() => {
|
|
73
|
+
if (record.el.getAttribute("data-hydrating") === "true" && !record.el.hasAttribute("data-hydrated")) {
|
|
74
|
+
observer.disconnect();
|
|
75
|
+
doHydrate();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
observer.observe(record.el, {
|
|
79
|
+
attributes: true,
|
|
80
|
+
attributeFilter: ["data-hydrating"]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const directive = record.directive;
|
|
84
|
+
if (directive === "client:load") doHydrate();
|
|
85
|
+
else if (directive === "client:idle") {
|
|
86
|
+
const ric = _global.requestIdleCallback;
|
|
87
|
+
if (typeof ric === "function") ric(() => doHydrate(), { timeout: 2e3 });
|
|
88
|
+
else setTimeout(doHydrate, 200);
|
|
89
|
+
} else if (directive === "client:visible") {
|
|
90
|
+
const IOCtor = _global.IntersectionObserver;
|
|
91
|
+
if (typeof IOCtor === "function") {
|
|
92
|
+
const io = new IOCtor((entries) => {
|
|
93
|
+
entries.forEach((entry) => {
|
|
94
|
+
if (entry.isIntersecting) {
|
|
95
|
+
io.disconnect();
|
|
96
|
+
doHydrate();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}, { rootMargin: "200px" });
|
|
100
|
+
io.observe(record.el);
|
|
101
|
+
} else doHydrate();
|
|
102
|
+
} else if (directive === "client:media" && record.mediaQuery) {
|
|
103
|
+
const mql = _global.window?.matchMedia?.(record.mediaQuery);
|
|
104
|
+
if (mql) if (mql.matches) doHydrate();
|
|
105
|
+
else {
|
|
106
|
+
const fn = (e) => {
|
|
107
|
+
if (e.matches) {
|
|
108
|
+
doHydrate();
|
|
109
|
+
if (mql.removeEventListener) mql.removeEventListener("change", fn);
|
|
110
|
+
else if (mql.removeListener) mql.removeListener(fn);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (mql.addEventListener) mql.addEventListener("change", fn);
|
|
114
|
+
else if (mql.addListener) mql.addListener(fn);
|
|
115
|
+
}
|
|
116
|
+
else doHydrate();
|
|
117
|
+
} else doHydrate();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function resolveComponent(name, components, getComponent) {
|
|
121
|
+
if (components[name]) return components[name];
|
|
122
|
+
if (getComponent) return getComponent(name);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { collectIslands, hydrateIsland, hydrateIslands };
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
//#region src/vite.d.ts
|
|
3
|
+
type ClientDirective = 'client:load' | 'client:idle' | 'client:visible' | 'client:media' | 'client:only';
|
|
4
|
+
declare function transformVueSfcIslands(code: string, filePath: string): {
|
|
5
|
+
code: string;
|
|
6
|
+
islandCount: number;
|
|
7
|
+
};
|
|
8
|
+
interface UbeanIslandsPluginOptions {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare function ubeanIslandsPlugin(_options?: UbeanIslandsPluginOptions): Plugin;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { ClientDirective, UbeanIslandsPluginOptions, transformVueSfcIslands, ubeanIslandsPlugin };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//#region src/vite.ts
|
|
2
|
+
const CLIENT_DIRECTIVES = [
|
|
3
|
+
"client:load",
|
|
4
|
+
"client:idle",
|
|
5
|
+
"client:visible",
|
|
6
|
+
"client:media",
|
|
7
|
+
"client:only"
|
|
8
|
+
];
|
|
9
|
+
const DIRECTIVE_RE = /\bclient:(load|idle|visible|media|only)\b/;
|
|
10
|
+
function isVueSfc(id) {
|
|
11
|
+
return /\.vue(?:\?.*)?$/.test(id) && !id.includes("&type=");
|
|
12
|
+
}
|
|
13
|
+
function extractTemplateBlock(code) {
|
|
14
|
+
const openMatch = code.match(/<template([^>]*)>/);
|
|
15
|
+
if (!openMatch) return null;
|
|
16
|
+
const openTagEnd = openMatch.index + openMatch[0].length;
|
|
17
|
+
const closeIdx = code.indexOf("</template>", openTagEnd);
|
|
18
|
+
if (closeIdx === -1) return null;
|
|
19
|
+
return {
|
|
20
|
+
start: openMatch.index,
|
|
21
|
+
end: closeIdx + 11,
|
|
22
|
+
content: code.slice(openTagEnd, closeIdx),
|
|
23
|
+
attrs: openMatch[1] || "",
|
|
24
|
+
openTagEnd
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function escapeAttr(str) {
|
|
28
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
29
|
+
}
|
|
30
|
+
function findTagAt(template, pos) {
|
|
31
|
+
const lt = template.indexOf("<", pos);
|
|
32
|
+
if (lt === -1 || lt !== pos) return null;
|
|
33
|
+
if (template[lt + 1] === "/" || template[lt + 1] === "!" || template[lt + 1] === "?") return null;
|
|
34
|
+
const nameMatch = template.slice(lt + 1).match(/^([A-Z][A-Za-z0-9._-]*)/);
|
|
35
|
+
if (!nameMatch) return null;
|
|
36
|
+
const tagName = nameMatch[1];
|
|
37
|
+
const gt = findClosingAngleBracket(template, lt + 1 + tagName.length);
|
|
38
|
+
if (gt === -1) return null;
|
|
39
|
+
const openTagContent = template.slice(lt, gt + 1);
|
|
40
|
+
const selfClosing = template[gt - 1] === "/";
|
|
41
|
+
const attrsStr = template.slice(lt + 1 + tagName.length, selfClosing ? gt - 1 : gt).trim();
|
|
42
|
+
const attrs = parseAttrs(attrsStr);
|
|
43
|
+
const openTagEnd = gt + 1;
|
|
44
|
+
let end = openTagEnd;
|
|
45
|
+
let innerHTML = "";
|
|
46
|
+
if (!selfClosing) {
|
|
47
|
+
const closeTag = `</${tagName}>`;
|
|
48
|
+
const closePos = findMatchingClose(template, lt, tagName);
|
|
49
|
+
if (closePos === -1) return null;
|
|
50
|
+
innerHTML = template.slice(openTagEnd, closePos);
|
|
51
|
+
end = closePos + closeTag.length;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
tagName,
|
|
55
|
+
fullOpenTag: openTagContent,
|
|
56
|
+
selfClosing,
|
|
57
|
+
attrs,
|
|
58
|
+
attrsStr,
|
|
59
|
+
start: lt,
|
|
60
|
+
openTagEnd,
|
|
61
|
+
end,
|
|
62
|
+
innerHTML
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function findClosingAngleBracket(str, start) {
|
|
66
|
+
let inQuote = null;
|
|
67
|
+
for (let i = start; i < str.length; i++) {
|
|
68
|
+
const ch = str[i];
|
|
69
|
+
if (inQuote) {
|
|
70
|
+
if (ch === inQuote && str[i - 1] !== "\\") inQuote = null;
|
|
71
|
+
} else if (ch === "\"" || ch === "'") inQuote = ch;
|
|
72
|
+
else if (ch === ">") return i;
|
|
73
|
+
}
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
function findMatchingClose(template, openLt, tagName) {
|
|
77
|
+
const openRe = new RegExp(`<${tagName}[\\s/>]`, "g");
|
|
78
|
+
const closeRe = new RegExp(`</${tagName}>`, "g");
|
|
79
|
+
openRe.lastIndex = openLt + 1;
|
|
80
|
+
closeRe.lastIndex = openLt + 1;
|
|
81
|
+
let depth = 1;
|
|
82
|
+
while (depth > 0) {
|
|
83
|
+
const nextOpen = openRe.exec(template);
|
|
84
|
+
const nextClose = closeRe.exec(template);
|
|
85
|
+
if (!nextClose) return -1;
|
|
86
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
87
|
+
depth++;
|
|
88
|
+
closeRe.lastIndex = nextOpen.index + tagName.length + 2;
|
|
89
|
+
} else {
|
|
90
|
+
depth--;
|
|
91
|
+
if (depth === 0) return nextClose.index;
|
|
92
|
+
openRe.lastIndex = nextClose.index + tagName.length + 3;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
function parseAttrs(str) {
|
|
98
|
+
const map = /* @__PURE__ */ new Map();
|
|
99
|
+
const re = /(?:^|\s)([:@a-zA-Z_][\w.:-]*)(?:=(?:"([^"]*)"|'([^']*)'|(\{[^}]*\}|[^\s"'=<>`]+)))?/g;
|
|
100
|
+
let m;
|
|
101
|
+
while ((m = re.exec(str)) !== null) {
|
|
102
|
+
const name = m[1];
|
|
103
|
+
const val = m[2] !== void 0 ? m[2] : m[3] !== void 0 ? m[3] : m[4] !== void 0 ? m[4] : true;
|
|
104
|
+
map.set(name, val);
|
|
105
|
+
}
|
|
106
|
+
return map;
|
|
107
|
+
}
|
|
108
|
+
function collectStaticProps(attrs) {
|
|
109
|
+
const props = {};
|
|
110
|
+
for (const [key, val] of attrs) {
|
|
111
|
+
if (key.startsWith("client:")) continue;
|
|
112
|
+
if (key.startsWith("v-") || key.startsWith("@") || key.startsWith(":")) continue;
|
|
113
|
+
if (key === "key" || key === "ref") continue;
|
|
114
|
+
if (val === true) props[key] = true;
|
|
115
|
+
else props[key] = val;
|
|
116
|
+
}
|
|
117
|
+
return props;
|
|
118
|
+
}
|
|
119
|
+
function transformTemplate(template, islandCounter, filePath) {
|
|
120
|
+
let out = "";
|
|
121
|
+
let pos = 0;
|
|
122
|
+
while (pos < template.length) {
|
|
123
|
+
const lt = template.indexOf("<", pos);
|
|
124
|
+
if (lt === -1) {
|
|
125
|
+
out += template.slice(pos);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
out += template.slice(pos, lt);
|
|
129
|
+
if (template[lt + 1] === "/" || template[lt + 1] === "!" || template[lt + 1] === "?") {
|
|
130
|
+
out += template[lt];
|
|
131
|
+
pos = lt + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!template.slice(lt + 1).match(/^([A-Z][A-Za-z0-9._-]*)/)) {
|
|
135
|
+
const gt = findClosingAngleBracket(template, lt + 1);
|
|
136
|
+
if (gt === -1) {
|
|
137
|
+
out += template.slice(lt);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
out += template.slice(lt, gt + 1);
|
|
141
|
+
pos = gt + 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const tag = findTagAt(template, lt);
|
|
145
|
+
if (!tag) {
|
|
146
|
+
out += template[lt];
|
|
147
|
+
pos = lt + 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const directive = CLIENT_DIRECTIVES.find((d) => tag.attrs.has(d));
|
|
151
|
+
if (!directive) {
|
|
152
|
+
const inner = tag.selfClosing ? "" : transformTemplate(tag.innerHTML, islandCounter, filePath);
|
|
153
|
+
out += tag.fullOpenTag;
|
|
154
|
+
out += inner;
|
|
155
|
+
if (!tag.selfClosing) out += `</${tag.tagName}>`;
|
|
156
|
+
pos = tag.end;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
islandCounter.count++;
|
|
160
|
+
const islandId = `island-${filePath.replace(/[^a-zA-Z0-9]/g, "-")}-${islandCounter.count}`;
|
|
161
|
+
const mediaRaw = tag.attrs.get("client:media");
|
|
162
|
+
let mediaStr = "";
|
|
163
|
+
if (directive === "client:media" && mediaRaw && mediaRaw !== true) mediaStr = ` data-media="${escapeAttr(String(mediaRaw).replace(/^["']|["']$/g, ""))}"`;
|
|
164
|
+
const props = collectStaticProps(tag.attrs);
|
|
165
|
+
const propsJson = escapeAttr(JSON.stringify(props));
|
|
166
|
+
out += `<ubean-island data-island-id="${islandId}" data-component="${tag.tagName}" data-directive="${directive}" data-props="${propsJson}"${mediaStr}>`;
|
|
167
|
+
out += tag.selfClosing ? "" : tag.innerHTML;
|
|
168
|
+
out += `</ubean-island>`;
|
|
169
|
+
pos = tag.end;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function transformVueSfcIslands(code, filePath) {
|
|
174
|
+
const tpl = extractTemplateBlock(code);
|
|
175
|
+
if (!tpl) return {
|
|
176
|
+
code,
|
|
177
|
+
islandCount: 0
|
|
178
|
+
};
|
|
179
|
+
if (!DIRECTIVE_RE.test(tpl.content)) return {
|
|
180
|
+
code,
|
|
181
|
+
islandCount: 0
|
|
182
|
+
};
|
|
183
|
+
const counter = { count: 0 };
|
|
184
|
+
const newContent = transformTemplate(tpl.content, counter, filePath);
|
|
185
|
+
return {
|
|
186
|
+
code: `${code.slice(0, tpl.start)}<template${tpl.attrs}>${newContent}</template>${code.slice(tpl.end)}`,
|
|
187
|
+
islandCount: counter.count
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function ubeanIslandsPlugin(_options = {}) {
|
|
191
|
+
let viteConfig;
|
|
192
|
+
let enabled = true;
|
|
193
|
+
return {
|
|
194
|
+
name: "ubean:islands",
|
|
195
|
+
enforce: "pre",
|
|
196
|
+
configResolved(config) {
|
|
197
|
+
viteConfig = config;
|
|
198
|
+
enabled = _options.enabled !== false;
|
|
199
|
+
},
|
|
200
|
+
transform(code, id) {
|
|
201
|
+
if (!enabled) return null;
|
|
202
|
+
if (!isVueSfc(id)) return null;
|
|
203
|
+
if (!DIRECTIVE_RE.test(code)) return null;
|
|
204
|
+
return transformVueSfcIslands(code, id.split("?")[0].replace(viteConfig.root, "").replace(/^[/\\]/, ""));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
export { transformVueSfcIslands, ubeanIslandsPlugin };
|
package/package.json
CHANGED