@cfp-studio/iframe 18.0.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.
- package/dist/index.d.mts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.mjs +991 -0
- package/package.json +22 -0
- package/preview/__previewjs_internal__/copy-cut-paste.ts +75 -0
- package/preview/__previewjs_internal__/error-message.ts +36 -0
- package/preview/__previewjs_internal__/index.ts +95 -0
- package/preview/__previewjs_internal__/links.ts +30 -0
- package/preview/__previewjs_internal__/logs.ts +131 -0
- package/preview/__previewjs_internal__/object-inspect.js +668 -0
- package/preview/__previewjs_internal__/renderer/index.tsx +12 -0
- package/preview/__previewjs_internal__/run-renderer.ts +112 -0
- package/preview/__previewjs_internal__/state.ts +11 -0
- package/preview/__previewjs_internal__/vite-hmr-listener.ts +74 -0
- package/preview/__previewjs_internal__/vscode-control.ts +58 -0
- package/preview/index.html +71 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cfp-studio/iframe",
|
|
3
|
+
"version": "18.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"preview"
|
|
11
|
+
],
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"assert-never": "1.4.0",
|
|
14
|
+
"immer": "10.1.1",
|
|
15
|
+
"typescript": "5.7.3",
|
|
16
|
+
"unbuild": "2.0.0",
|
|
17
|
+
"vite": "6.2.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc && unbuild"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function overrideCopyCutPaste() {
|
|
2
|
+
if (
|
|
3
|
+
!navigator.userAgent.includes(" Code/") &&
|
|
4
|
+
!navigator.userAgent.includes(" Code-Insiders/")
|
|
5
|
+
) {
|
|
6
|
+
// We're not running in VS Code.
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
document.addEventListener("keydown", (e) => {
|
|
11
|
+
const hasMeta = e.ctrlKey || e.metaKey;
|
|
12
|
+
if (!hasMeta) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
switch (e.key.toLowerCase()) {
|
|
16
|
+
case "c": {
|
|
17
|
+
// 使用现代的 Clipboard API 替代已弃用的 document.execCommand
|
|
18
|
+
const textToCopy = window.getSelection()?.toString();
|
|
19
|
+
if (textToCopy) {
|
|
20
|
+
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
21
|
+
console.log('Copied to clipboard successfully!');
|
|
22
|
+
}).catch((err) => {
|
|
23
|
+
console.error('Failed to copy text: ', err);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "x": {
|
|
30
|
+
// 使用现代的 Clipboard API 替代已弃用的 document.execCommand
|
|
31
|
+
const textToCut = window.getSelection()?.toString();
|
|
32
|
+
if (textToCut) {
|
|
33
|
+
navigator.clipboard.writeText(textToCut).then(() => {
|
|
34
|
+
console.log('Cut to clipboard successfully!');
|
|
35
|
+
// 移除选中的文本
|
|
36
|
+
const range = document.createRange();
|
|
37
|
+
range.selectNodeContents(document.getSelection()?.anchorNode?.parentNode as Node);
|
|
38
|
+
range.deleteContents();
|
|
39
|
+
}).catch((err) => {
|
|
40
|
+
console.error('Failed to cut text: ', err);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "v":
|
|
47
|
+
// 使用现代的 Clipboard API 替代已弃用的 document.execCommand
|
|
48
|
+
navigator.clipboard.readText().then((text) => {
|
|
49
|
+
const selection = window.getSelection();
|
|
50
|
+
if (selection) {
|
|
51
|
+
const range = selection.getRangeAt(0);
|
|
52
|
+
range.deleteContents();
|
|
53
|
+
range.insertNode(document.createTextNode(text));
|
|
54
|
+
console.log('Pasted from clipboard successfully!');
|
|
55
|
+
}
|
|
56
|
+
}).catch((err) => {
|
|
57
|
+
console.error('Failed to paste text: ', err);
|
|
58
|
+
});
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
break;
|
|
61
|
+
case "a": {
|
|
62
|
+
// 使用现代的 Range 和 Selection API 替代已弃用的 document.execCommand
|
|
63
|
+
const selection = window.getSelection();
|
|
64
|
+
if (selection) {
|
|
65
|
+
const range = document.createRange();
|
|
66
|
+
range.selectNodeContents(document.body);
|
|
67
|
+
selection.removeAllRanges();
|
|
68
|
+
selection.addRange(range);
|
|
69
|
+
}
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function generateMessageFromError(errorMessage: string, stack?: string) {
|
|
2
|
+
let message = errorMessage + (stack ? `\n${stack}` : "");
|
|
3
|
+
// Remove any redundant line breaks (but not spaces,
|
|
4
|
+
// which could be useful indentation).
|
|
5
|
+
message = message.replace(/^\n+/g, "\n").trim();
|
|
6
|
+
const stripPrefix = "Internal server error: ";
|
|
7
|
+
if (message.startsWith(stripPrefix)) {
|
|
8
|
+
// Replace deprecated 'substr' with 'slice'
|
|
9
|
+
message = message.slice(stripPrefix.length);
|
|
10
|
+
}
|
|
11
|
+
if (/^Transform failed with \d+ errors?:?\n.*/.test(message)) {
|
|
12
|
+
const lineBreakPosition = message.indexOf("\n");
|
|
13
|
+
message = message.substring(lineBreakPosition + 1);
|
|
14
|
+
}
|
|
15
|
+
const lineBreakPosition = message.indexOf("\n");
|
|
16
|
+
let title: string;
|
|
17
|
+
let rest: string;
|
|
18
|
+
if (lineBreakPosition > -1) {
|
|
19
|
+
// Replace deprecated 'substr' with 'slice'
|
|
20
|
+
title = message.slice(0, lineBreakPosition).trim();
|
|
21
|
+
// Replace deprecated 'substr' with 'slice'
|
|
22
|
+
rest = message.slice(lineBreakPosition + 1);
|
|
23
|
+
} else {
|
|
24
|
+
title = message;
|
|
25
|
+
rest = "";
|
|
26
|
+
}
|
|
27
|
+
if (title.endsWith(":") || title.endsWith(".")) {
|
|
28
|
+
// Replace deprecated 'substr' with 'slice'
|
|
29
|
+
title = title.slice(0, -1).trim();
|
|
30
|
+
}
|
|
31
|
+
// Note: this isn't relevant to all browsers.
|
|
32
|
+
if (rest.startsWith(`Error: ${title}\n`)) {
|
|
33
|
+
rest = rest.slice(rest.indexOf("\n") + 1);
|
|
34
|
+
}
|
|
35
|
+
return `${title}${rest ? `\n\n${rest}` : ""}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { overrideCopyCutPaste } from "./copy-cut-paste";
|
|
2
|
+
import { setUpLinkInterception } from "./links";
|
|
3
|
+
import { setUpLogInterception } from "./logs";
|
|
4
|
+
import { loadRenderer } from "./renderer";
|
|
5
|
+
import { runRenderer } from "./run-renderer";
|
|
6
|
+
import { setState } from "./state";
|
|
7
|
+
import { setupViteHmrListener } from "./vite-hmr-listener";
|
|
8
|
+
import { setupVSCodeControlListener } from "./vscode-control";
|
|
9
|
+
|
|
10
|
+
let listenersInitialized = false;
|
|
11
|
+
|
|
12
|
+
// Important: initListeners() must be invoked before we try to load any modules
|
|
13
|
+
// that might fail to load, such as a component, so we can intercept Vite errors.
|
|
14
|
+
export function initListeners() {
|
|
15
|
+
if (listenersInitialized) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
listenersInitialized = true;
|
|
19
|
+
setupViteHmrListener();
|
|
20
|
+
setUpLogInterception();
|
|
21
|
+
setUpLinkInterception();
|
|
22
|
+
overrideCopyCutPaste();
|
|
23
|
+
setupVSCodeControlListener();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function initPreview({
|
|
27
|
+
previewableModule,
|
|
28
|
+
previewableName,
|
|
29
|
+
wrapperModule,
|
|
30
|
+
wrapperName,
|
|
31
|
+
}: {
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
33
|
+
previewableModule: any;
|
|
34
|
+
previewableName: string;
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
36
|
+
wrapperModule: any;
|
|
37
|
+
wrapperName: string;
|
|
38
|
+
}): typeof window.__PREVIEWJS_IFRAME__.refresh {
|
|
39
|
+
const root = document.getElementById("root");
|
|
40
|
+
if (!root) {
|
|
41
|
+
throw new Error("未能找到 #root 节点!");
|
|
42
|
+
}
|
|
43
|
+
let renderId = 0;
|
|
44
|
+
window.__PREVIEWJS_IFRAME__.render = async (data) => {
|
|
45
|
+
setState(data);
|
|
46
|
+
await runNewRender();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
50
|
+
kind: "bootstrapped",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
async function runNewRender() {
|
|
54
|
+
if (!root) {
|
|
55
|
+
throw new Error("未能找到 #root 节点!");
|
|
56
|
+
}
|
|
57
|
+
const rootHtml = root.innerHTML;
|
|
58
|
+
try {
|
|
59
|
+
renderId += 1;
|
|
60
|
+
const thisRenderId = renderId;
|
|
61
|
+
await runRenderer({
|
|
62
|
+
wrapperModule,
|
|
63
|
+
wrapperName,
|
|
64
|
+
previewableModule,
|
|
65
|
+
previewableName,
|
|
66
|
+
renderId,
|
|
67
|
+
shouldAbortRender: () => renderId !== thisRenderId,
|
|
68
|
+
loadRenderer,
|
|
69
|
+
});
|
|
70
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
if (root.innerHTML !== rootHtml) {
|
|
73
|
+
// 重新调用前一次渲染,以确保预览不会白屏。
|
|
74
|
+
root.innerHTML = rootHtml;
|
|
75
|
+
}
|
|
76
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
77
|
+
kind: "error",
|
|
78
|
+
source: "renderer",
|
|
79
|
+
message: error.stack || error.message,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
return (options = {}) => {
|
|
86
|
+
if (options.previewableModule) {
|
|
87
|
+
previewableModule = options.previewableModule;
|
|
88
|
+
}
|
|
89
|
+
if (options.wrapperModule) {
|
|
90
|
+
wrapperModule = options.wrapperModule;
|
|
91
|
+
}
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
runNewRender().catch(console.error);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function setUpLinkInterception() {
|
|
2
|
+
document.addEventListener(
|
|
3
|
+
"click",
|
|
4
|
+
(event) => {
|
|
5
|
+
let node = event?.target;
|
|
6
|
+
while (node) {
|
|
7
|
+
if (!(node instanceof Node)) {
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
if (node.nodeName === "A") {
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
12
|
+
const url = (node as any).href;
|
|
13
|
+
// Handle click here by posting data back to VS Code
|
|
14
|
+
// for your extension to handle
|
|
15
|
+
if (url) {
|
|
16
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
17
|
+
kind: "action",
|
|
18
|
+
type: "url",
|
|
19
|
+
path: url,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
node = node.parentNode;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
true
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import type { LogLevel } from "../../src";
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import inspect from "./object-inspect";
|
|
5
|
+
|
|
6
|
+
// 注意: 请与 vite 保持同步:
|
|
7
|
+
// https://github.com/vitejs/vite/blob/2de425d0288bfae345c5ced5c84cf67ffccaef48/packages/vite/src/client/client.ts#L117
|
|
8
|
+
const HMR_FAILED_UPDATE_REGEX =
|
|
9
|
+
/^\[hmr\] Failed to reload (.+)\. This could be due to syntax errors or importing non-existent modules\. \(see errors above\)$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 设置日志拦截器。
|
|
13
|
+
*/
|
|
14
|
+
export function setUpLogInterception() {
|
|
15
|
+
const makeLogger =
|
|
16
|
+
(level: LogLevel, defaultFn: typeof console.log) =>
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
18
|
+
(...args: any[]) => {
|
|
19
|
+
// Prevent an infinite call stack size if anything here logs.
|
|
20
|
+
const self = console[level];
|
|
21
|
+
console[level] = defaultFn;
|
|
22
|
+
defaultFn(...args);
|
|
23
|
+
try {
|
|
24
|
+
const firstArg = args[0];
|
|
25
|
+
if (typeof firstArg === "string") {
|
|
26
|
+
if (firstArg.startsWith("[vite]")) {
|
|
27
|
+
// Silence.
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const hmrMatch = firstArg.match(HMR_FAILED_UPDATE_REGEX);
|
|
31
|
+
if (hmrMatch) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
36
|
+
kind: "log-message",
|
|
37
|
+
level,
|
|
38
|
+
message: formatLogMessage(...args),
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
} finally {
|
|
42
|
+
console[level] = self;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
console.log = makeLogger("log", console.log);
|
|
46
|
+
console.info = makeLogger("info", console.info);
|
|
47
|
+
console.warn = makeLogger("warn", console.warn);
|
|
48
|
+
console.error = makeLogger("error", console.error);
|
|
49
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
50
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
51
|
+
kind: "error",
|
|
52
|
+
source: "renderer",
|
|
53
|
+
message: formatError(error, message),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
window.onunhandledrejection = (event) => {
|
|
57
|
+
const message = formatError(event.reason);
|
|
58
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
59
|
+
kind: "error",
|
|
60
|
+
source:
|
|
61
|
+
message.includes("Failed to fetch dynamically imported module") ||
|
|
62
|
+
message.includes("Failed to reload")
|
|
63
|
+
? "vite"
|
|
64
|
+
: "renderer",
|
|
65
|
+
message,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 格式化错误信息。
|
|
72
|
+
* @param error
|
|
73
|
+
* @param message
|
|
74
|
+
* @returns
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
78
|
+
function formatError(error?: any, message?: any): string {
|
|
79
|
+
// 如果 error 对象存在且包含 stack 和 message 属性
|
|
80
|
+
if (error?.stack && error.message) {
|
|
81
|
+
let stackMessage = error.stack;
|
|
82
|
+
// 确保 stack 信息包含错误消息
|
|
83
|
+
if (!stackMessage.includes(error.message)) {
|
|
84
|
+
stackMessage = `${error.message}\n${stackMessage}`;
|
|
85
|
+
}
|
|
86
|
+
return stackMessage;
|
|
87
|
+
} if (message) {
|
|
88
|
+
return `${message}`;
|
|
89
|
+
}
|
|
90
|
+
return `${error}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 格式化日志消息。
|
|
95
|
+
* @param args
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
98
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
99
|
+
function formatLogMessage(...args: any[]) {
|
|
100
|
+
if (args.length === 0) {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
let message = formatValue(args.shift());
|
|
104
|
+
message = message.replace(/%s/g, () => formatValue(args.shift()));
|
|
105
|
+
return [message, ...args.map(formatValue)].join(" ");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 格式化值。
|
|
110
|
+
* @param value
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
114
|
+
function formatValue(value: any) {
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const formatted = inspect(value);
|
|
119
|
+
if (typeof formatted !== "string") {
|
|
120
|
+
// This can happen in rare cases e.g. toString() not actually returning a string.
|
|
121
|
+
try {
|
|
122
|
+
return JSON.stringify(value);
|
|
123
|
+
} catch {
|
|
124
|
+
return "[Unable to represent value as string]";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (formatted.at(0) === "[" && formatted.at(-1) === "]") {
|
|
128
|
+
return formatted.substring(1, formatted.length - 1);
|
|
129
|
+
}
|
|
130
|
+
return formatted;
|
|
131
|
+
}
|