@tomjs/vite-plugin-hbuilderx 1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/dist/client.iife.js +42 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +336 -0
- package/dist/webview.d.ts +14 -0
- package/dist/webview.js +18 -0
- package/env-hbuilderx.d.ts +36 -0
- package/env.d.ts +24 -0
- package/eslint.config.mjs +6 -0
- package/package.json +64 -0
- package/src/constants.ts +4 -0
- package/src/index.ts +420 -0
- package/src/logger.ts +9 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +36 -0
- package/src/webview/client.ts +47 -0
- package/src/webview/global.d.ts +4 -0
- package/src/webview/template.html +90 -0
- package/src/webview/webview.ts +25 -0
- package/src/webview/window.d.ts +9 -0
- package/tsconfig.json +15 -0
- package/tsconfig.web.json +9 -0
- package/tsdown.config.ts +38 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { InlineConfig as TsdownOptions } from 'tsdown';
|
|
2
|
+
import type { PluginOption, ResolvedConfig, UserConfig } from 'vite';
|
|
3
|
+
import type { ExtensionOptions, PluginOptions, WebviewOption } from './types';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { cwd } from 'node:process';
|
|
7
|
+
import { readFileSync, readJsonSync } from '@tomjs/node';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import merge from 'lodash.merge';
|
|
10
|
+
import { parse as htmlParser } from 'node-html-parser';
|
|
11
|
+
import colors from 'picocolors';
|
|
12
|
+
import { build as tsdownBuild } from 'tsdown';
|
|
13
|
+
import { ORG_NAME, RESOLVED_VIRTUAL_MODULE_ID, VIRTUAL_MODULE_ID } from './constants';
|
|
14
|
+
import { createLogger } from './logger';
|
|
15
|
+
import { resolveServerUrl } from './utils';
|
|
16
|
+
|
|
17
|
+
export * from './types';
|
|
18
|
+
|
|
19
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
20
|
+
const logger = createLogger();
|
|
21
|
+
|
|
22
|
+
function getPkg() {
|
|
23
|
+
const pkgFile = path.resolve(process.cwd(), 'package.json');
|
|
24
|
+
if (!fs.existsSync(pkgFile)) {
|
|
25
|
+
throw new Error('项目中未找到 package.json 文件');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pkg = readJsonSync(pkgFile);
|
|
29
|
+
if (!pkg.main) {
|
|
30
|
+
throw new Error('package.json 文件未配置 main 入口文件');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return pkg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function preMergeOptions(options?: PluginOptions): PluginOptions {
|
|
37
|
+
const pkg = getPkg();
|
|
38
|
+
|
|
39
|
+
const opts: PluginOptions = merge(
|
|
40
|
+
{
|
|
41
|
+
webview: true,
|
|
42
|
+
recommended: true,
|
|
43
|
+
extension: {
|
|
44
|
+
entry: 'extension/index.ts',
|
|
45
|
+
outDir: 'dist-extension',
|
|
46
|
+
target: ['es2019', 'node16'],
|
|
47
|
+
format: 'cjs',
|
|
48
|
+
shims: true,
|
|
49
|
+
clean: true,
|
|
50
|
+
dts: false,
|
|
51
|
+
treeshake: !isDev,
|
|
52
|
+
publint: false,
|
|
53
|
+
// ignore tsdown.config.ts from project
|
|
54
|
+
config: false,
|
|
55
|
+
fixedExtension: false,
|
|
56
|
+
external: ['hbuilderx'],
|
|
57
|
+
} as ExtensionOptions,
|
|
58
|
+
} as PluginOptions,
|
|
59
|
+
options,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const opt = opts.extension || {};
|
|
63
|
+
|
|
64
|
+
if (isDev) {
|
|
65
|
+
opt.sourcemap = opt.sourcemap ?? true;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
opt.minify ??= true;
|
|
69
|
+
opt.clean ??= true;
|
|
70
|
+
}
|
|
71
|
+
if (typeof opt.external !== 'function') {
|
|
72
|
+
opt.external = (['hbuilderx'] as (string | RegExp)[]).concat(opt.external ?? []);
|
|
73
|
+
opt.external = [...new Set(opt.external)];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const fn = opt.external;
|
|
77
|
+
opt.external = function (id, parentId, isResolved) {
|
|
78
|
+
if (id === 'hbuilderx') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return fn(id, parentId, isResolved);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isDev && !opt.skipNodeModulesBundle && !opt.noExternal) {
|
|
86
|
+
opt.noExternal = Object.keys(pkg.dependencies || {}).concat(
|
|
87
|
+
Object.keys(pkg.peerDependencies || {}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
opts.extension = opt;
|
|
92
|
+
|
|
93
|
+
return opts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function genProdWebviewCode(cache: Record<string, string>) {
|
|
97
|
+
function handleHtmlCode(html: string) {
|
|
98
|
+
const root = htmlParser(html);
|
|
99
|
+
const head = root.querySelector('head')!;
|
|
100
|
+
if (!head) {
|
|
101
|
+
root?.insertAdjacentHTML('beforeend', '<head></head>');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
head.insertAdjacentHTML('afterbegin', '<style>:root{--root-background-color:#fffae8;background-color:var(--root-background-color)}</style>');
|
|
105
|
+
|
|
106
|
+
const tags = {
|
|
107
|
+
script: 'src',
|
|
108
|
+
link: 'href',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
Object.keys(tags).forEach((tag) => {
|
|
112
|
+
const elements = root.querySelectorAll(tag);
|
|
113
|
+
elements.forEach((element) => {
|
|
114
|
+
const attr = element.getAttribute(tags[tag]);
|
|
115
|
+
if (attr) {
|
|
116
|
+
element.setAttribute(tags[tag], `{{baseUri}}${attr}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return root.removeWhitespace().toString();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cacheCode = /* js */ `const htmlCode = {
|
|
125
|
+
${Object.keys(cache)
|
|
126
|
+
.map(s => `'${s}': \`${handleHtmlCode(cache[s])}\`,`)
|
|
127
|
+
.join('\n')}
|
|
128
|
+
};`;
|
|
129
|
+
|
|
130
|
+
const code = /* js */ `import path from 'path';
|
|
131
|
+
import { workspace } from 'hbuilderx';
|
|
132
|
+
|
|
133
|
+
${cacheCode}
|
|
134
|
+
|
|
135
|
+
export function getWebviewHtml(options){
|
|
136
|
+
const { context, inputName, injectCode } = options || {};
|
|
137
|
+
const baseUri = path.join(context.extensionPath, process.env.VITE_WEBVIEW_DIST || 'dist');
|
|
138
|
+
let html = htmlCode[inputName || 'index'] || '';
|
|
139
|
+
if (injectCode) {
|
|
140
|
+
html = html.replace('<head>', '<head>'+ injectCode);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return html.replaceAll('{{baseUri}}', baseUri);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default getWebviewHtml;
|
|
147
|
+
`;
|
|
148
|
+
return code;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function useHBuilderxPlugin(options?: PluginOptions): PluginOption {
|
|
152
|
+
const opts = preMergeOptions(options);
|
|
153
|
+
|
|
154
|
+
const handleConfig = (config: UserConfig): UserConfig => {
|
|
155
|
+
let outDir = config?.build?.outDir || 'dist';
|
|
156
|
+
opts.extension ??= {};
|
|
157
|
+
if (opts.recommended) {
|
|
158
|
+
opts.extension.outDir = path.resolve(outDir, 'extension');
|
|
159
|
+
outDir = path.resolve(outDir, 'webview');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// assets
|
|
163
|
+
const assetsDir = config?.build?.assetsDir || 'assets';
|
|
164
|
+
const output = {
|
|
165
|
+
chunkFileNames: `${assetsDir}/[name].js`,
|
|
166
|
+
entryFileNames: `${assetsDir}/[name].js`,
|
|
167
|
+
assetFileNames: `${assetsDir}/[name].[ext]`,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let rollupOutput = config?.build?.rollupOptions?.output ?? {};
|
|
171
|
+
if (Array.isArray(rollupOutput)) {
|
|
172
|
+
rollupOutput.map(s => Object.assign(s, output));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
rollupOutput = Object.assign({}, rollupOutput, output);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
build: {
|
|
180
|
+
outDir,
|
|
181
|
+
sourcemap: isDev ? true : config?.build?.sourcemap,
|
|
182
|
+
rollupOptions: {
|
|
183
|
+
output: rollupOutput,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let devWebviewClientCode: string;
|
|
190
|
+
let devWebviewVirtualCode: string;
|
|
191
|
+
|
|
192
|
+
let resolvedConfig: ResolvedConfig;
|
|
193
|
+
// multiple entry index.html
|
|
194
|
+
const prodHtmlCache: Record<string, string> = {};
|
|
195
|
+
|
|
196
|
+
function isVue() {
|
|
197
|
+
return !!resolvedConfig.plugins.find(s => ['vite:vue', 'vite:vue2'].includes(s.name));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isReact() {
|
|
201
|
+
return !!resolvedConfig.plugins.find(s => ['vite:react-refresh', 'vite:react-swc'].includes(s.name));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getDevtoolsPort() {
|
|
205
|
+
const devtools = opts.devtools;
|
|
206
|
+
if (!devtools) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let port: number | undefined;
|
|
210
|
+
if (typeof devtools === 'number') {
|
|
211
|
+
port = devtools;
|
|
212
|
+
}
|
|
213
|
+
else if (devtools === true) {
|
|
214
|
+
if (isVue()) {
|
|
215
|
+
port = 8098;
|
|
216
|
+
}
|
|
217
|
+
else if (isReact()) {
|
|
218
|
+
port = 8097;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return port;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [
|
|
225
|
+
{
|
|
226
|
+
name: '@tomjs:hbuilderx',
|
|
227
|
+
apply: 'serve',
|
|
228
|
+
config(config) {
|
|
229
|
+
return handleConfig(config);
|
|
230
|
+
},
|
|
231
|
+
configResolved(config) {
|
|
232
|
+
resolvedConfig = config;
|
|
233
|
+
|
|
234
|
+
if (opts.webview) {
|
|
235
|
+
devWebviewClientCode = readFileSync(path.join(__dirname, 'client.iife.js'));
|
|
236
|
+
let refreshKey = '';
|
|
237
|
+
if (opts.webview === true) {
|
|
238
|
+
refreshKey = 'F6';
|
|
239
|
+
}
|
|
240
|
+
else if (typeof opts.webview === 'object' && opts.webview.refreshKey) {
|
|
241
|
+
refreshKey = opts.webview.refreshKey;
|
|
242
|
+
}
|
|
243
|
+
if (refreshKey) {
|
|
244
|
+
devWebviewClientCode = `window.TOMJS_REFRESH_KEY="${refreshKey}";${devWebviewClientCode}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
devWebviewVirtualCode = readFileSync(path.join(__dirname, 'webview.js'));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
configureServer(server) {
|
|
251
|
+
if (!server || !server.httpServer) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
server.httpServer?.once('listening', async () => {
|
|
255
|
+
const env = {
|
|
256
|
+
NODE_ENV: server.config.mode || 'development',
|
|
257
|
+
VITE_DEV_SERVER_URL: resolveServerUrl(server),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
logger.info('extension build start');
|
|
261
|
+
|
|
262
|
+
let buildCount = 0;
|
|
263
|
+
|
|
264
|
+
const webview = opts?.webview as WebviewOption;
|
|
265
|
+
|
|
266
|
+
const { onSuccess: _onSuccess, ignoreWatch, logLevel, watchFiles, ...tsdownOptions } = opts.extension || {};
|
|
267
|
+
await tsdownBuild(
|
|
268
|
+
merge(tsdownOptions, {
|
|
269
|
+
watch: watchFiles ?? (opts.recommended ? ['extension'] : true),
|
|
270
|
+
ignoreWatch: (['.history', '.temp', '.tmp', '.cache', 'dist'] as (string | RegExp)[]).concat(Array.isArray(ignoreWatch) ? ignoreWatch : []),
|
|
271
|
+
env,
|
|
272
|
+
logLevel: logLevel ?? 'silent',
|
|
273
|
+
plugins: !webview
|
|
274
|
+
? []
|
|
275
|
+
: [
|
|
276
|
+
{
|
|
277
|
+
name: `${ORG_NAME}:hbuilderx:inject`,
|
|
278
|
+
resolveId(id) {
|
|
279
|
+
if (id === VIRTUAL_MODULE_ID) {
|
|
280
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
load(id) {
|
|
284
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID)
|
|
285
|
+
return devWebviewVirtualCode;
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
async onSuccess(config, signal) {
|
|
290
|
+
if (_onSuccess) {
|
|
291
|
+
if (typeof _onSuccess === 'string') {
|
|
292
|
+
await execa(_onSuccess);
|
|
293
|
+
}
|
|
294
|
+
else if (typeof _onSuccess === 'function') {
|
|
295
|
+
await _onSuccess(config, signal);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (buildCount++ > 1) {
|
|
300
|
+
logger.info('extension rebuild success');
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
logger.info('extension build success');
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
} as TsdownOptions),
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (opts.devtools) {
|
|
311
|
+
const _printUrls = server.printUrls;
|
|
312
|
+
server.printUrls = () => {
|
|
313
|
+
_printUrls();
|
|
314
|
+
const { green, bold, blue } = colors;
|
|
315
|
+
if (isVue() || isReact()) {
|
|
316
|
+
const port = getDevtoolsPort();
|
|
317
|
+
if (port) {
|
|
318
|
+
const devtoolsUrl = `http://localhost:${port}`;
|
|
319
|
+
console.log(` ${green('➜')} ${bold(isVue() ? 'Vue DevTools' : 'React DevTools')}: 已开启独立应用支持,地址 ${blue(`${devtoolsUrl}`)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
console.log(` ${green('➜')} 仅支持 ${green('react-devtools')} 和 ${green('vue-devtools')}`);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
transformIndexHtml(html) {
|
|
329
|
+
if (!opts.webview) {
|
|
330
|
+
return html;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (opts.devtools) {
|
|
334
|
+
const port = getDevtoolsPort();
|
|
335
|
+
if (port) {
|
|
336
|
+
html = html.replace(/<head>/i, `<head><script src="http://localhost:${port}"></script>`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return html.replace(/<head>/i, `<head><script>${devWebviewClientCode}</script>`);
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: '@tomjs:hbuilderx',
|
|
345
|
+
apply: 'build',
|
|
346
|
+
enforce: 'post',
|
|
347
|
+
config(config) {
|
|
348
|
+
return handleConfig(config);
|
|
349
|
+
},
|
|
350
|
+
configResolved(config) {
|
|
351
|
+
resolvedConfig = config;
|
|
352
|
+
},
|
|
353
|
+
transformIndexHtml(html, ctx) {
|
|
354
|
+
if (!opts.webview) {
|
|
355
|
+
return html;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
prodHtmlCache[ctx.chunk?.name as string] = html;
|
|
359
|
+
return html;
|
|
360
|
+
},
|
|
361
|
+
closeBundle() {
|
|
362
|
+
let webviewVirtualCode: string;
|
|
363
|
+
|
|
364
|
+
const webview = opts?.webview as WebviewOption;
|
|
365
|
+
if (webview) {
|
|
366
|
+
webviewVirtualCode = genProdWebviewCode(prodHtmlCache);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let outDir = resolvedConfig.build.outDir.replace(cwd(), '').replaceAll('\\', '/');
|
|
370
|
+
if (outDir.startsWith('/')) {
|
|
371
|
+
outDir = outDir.substring(1);
|
|
372
|
+
}
|
|
373
|
+
const env = {
|
|
374
|
+
NODE_ENV: resolvedConfig.mode || 'production',
|
|
375
|
+
VITE_WEBVIEW_DIST: outDir,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
logger.info('extension build start');
|
|
379
|
+
|
|
380
|
+
const { onSuccess: _onSuccess, logLevel, ...tsupOptions } = opts.extension || {};
|
|
381
|
+
|
|
382
|
+
tsdownBuild(
|
|
383
|
+
merge(tsupOptions, {
|
|
384
|
+
env,
|
|
385
|
+
logLevel: logLevel ?? 'silent',
|
|
386
|
+
plugins: !webview
|
|
387
|
+
? []
|
|
388
|
+
: [
|
|
389
|
+
{
|
|
390
|
+
name: `${ORG_NAME}:hbuilderx:inject`,
|
|
391
|
+
resolveId(id) {
|
|
392
|
+
if (id === VIRTUAL_MODULE_ID) {
|
|
393
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
load(id) {
|
|
397
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID)
|
|
398
|
+
return webviewVirtualCode;
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
async onSuccess(config, signal) {
|
|
403
|
+
if (_onSuccess) {
|
|
404
|
+
if (typeof _onSuccess === 'string') {
|
|
405
|
+
await execa(_onSuccess);
|
|
406
|
+
}
|
|
407
|
+
else if (typeof _onSuccess === 'function') {
|
|
408
|
+
await _onSuccess(config, signal);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
logger.info('extension build success');
|
|
412
|
+
},
|
|
413
|
+
} as TsdownOptions),
|
|
414
|
+
);
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export default useHBuilderxPlugin;
|
package/src/logger.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { InlineConfig } from 'tsdown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hbuilderx 插件配置. 查看 [tsdown](https://tsdown.dev/) 和 [Config Options](https://tsdown.dev/reference/config-options) 获取更多信息.
|
|
5
|
+
*/
|
|
6
|
+
export interface ExtensionOptions
|
|
7
|
+
extends Omit<
|
|
8
|
+
InlineConfig,
|
|
9
|
+
'entry' | 'format' | 'outDir' | 'watch'
|
|
10
|
+
> {
|
|
11
|
+
/**
|
|
12
|
+
* 插件入口文件.
|
|
13
|
+
* @default "extension/index.ts"
|
|
14
|
+
*/
|
|
15
|
+
entry?: string;
|
|
16
|
+
/**
|
|
17
|
+
* 插件编译后文件输出目录. 默认 `dist-extension`.
|
|
18
|
+
*
|
|
19
|
+
* @default "dist-extension"
|
|
20
|
+
*/
|
|
21
|
+
outDir?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `tsdown`默认监听当前工作目录。可以设置需要监听的文件,这可能会提高性能。
|
|
24
|
+
*
|
|
25
|
+
* 如果未指定值,则 `recommended` 参数为 `true` 时的默认值为 `["extension"]`,否则为 `tsdown` 默认行为
|
|
26
|
+
*/
|
|
27
|
+
watchFiles?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* hbuilderx webview 配置.
|
|
32
|
+
*/
|
|
33
|
+
export interface WebviewOption {
|
|
34
|
+
/**
|
|
35
|
+
* 刷新页面的按键,如 F5/F6
|
|
36
|
+
* @default "F6"
|
|
37
|
+
*/
|
|
38
|
+
refreshKey?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* vite 插件配置.
|
|
43
|
+
*/
|
|
44
|
+
export interface PluginOptions {
|
|
45
|
+
/**
|
|
46
|
+
* 推荐标识. 默认为 `true`.
|
|
47
|
+
* 如果是 `true`, 将会有如下默认行为:
|
|
48
|
+
* - 将会同步修改 `extension/webview` 的输出目录
|
|
49
|
+
* - 如果 vite build.outDir 是 'dist', 将会修改`插件/webview` 目录为 `dist/extension` 和 `dist/webview`
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
recommended?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* 在开发过程中,将代码注入到 `hbuilderx 扩展代码` 和 `web页面` 代码中,以支持 `HMR`;
|
|
55
|
+
*
|
|
56
|
+
* 在生产构建过程中,将最终生成的 `index.html` 代码注入到 `hbuilderx 扩展代码` 中,以最大限度地减少开发工作。
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* extension file
|
|
60
|
+
* ```ts
|
|
61
|
+
*import {getWebviewHtml} from 'virtual:hbuilderx';
|
|
62
|
+
*
|
|
63
|
+
*function setupHtml(webview: Webview, context: ExtensionContext) {
|
|
64
|
+
* return getWebviewHtml({serverUrl:process.env.VITE_DEV_SERVER_URL, context});
|
|
65
|
+
*}
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
webview?: boolean | WebviewOption;
|
|
69
|
+
/**
|
|
70
|
+
* 插件配置
|
|
71
|
+
*/
|
|
72
|
+
extension?: ExtensionOptions;
|
|
73
|
+
/**
|
|
74
|
+
* 是否开启 devtools. 注入 `<script src="http://localhost:<devtools-port>"></script>` 到 webview 端. 默认是 `false`.
|
|
75
|
+
* - `true`:
|
|
76
|
+
* - react: 注入 `<script src="http://localhost:8097"></script>`
|
|
77
|
+
* - vue: 注入 `<script src="http://localhost:8098"></script>`
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
devtools?: boolean | number;
|
|
81
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AddressInfo } from 'node:net';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @see https://github.com/vitejs/vite/blob/v4.0.1/packages/vite/src/node/constants.ts#L137-L147
|
|
6
|
+
*/
|
|
7
|
+
export function resolveHostname(hostname: string) {
|
|
8
|
+
const loopbackHosts = new Set([
|
|
9
|
+
'localhost',
|
|
10
|
+
'127.0.0.1',
|
|
11
|
+
'::1',
|
|
12
|
+
'0000:0000:0000:0000:0000:0000:0000:0001',
|
|
13
|
+
]);
|
|
14
|
+
const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']);
|
|
15
|
+
|
|
16
|
+
return loopbackHosts.has(hostname) || wildcardHosts.has(hostname) ? 'localhost' : hostname;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveServerUrl(server: ViteDevServer) {
|
|
20
|
+
const addressInfo = server.httpServer!.address();
|
|
21
|
+
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
|
22
|
+
|
|
23
|
+
if (isAddressInfo(addressInfo)) {
|
|
24
|
+
const { address, port } = addressInfo;
|
|
25
|
+
const hostname = resolveHostname(address);
|
|
26
|
+
|
|
27
|
+
const options = server.config.server;
|
|
28
|
+
const protocol = options.https ? 'https' : 'http';
|
|
29
|
+
const devBase = server.config.base;
|
|
30
|
+
|
|
31
|
+
const path = typeof options.open === 'string' ? options.open : devBase;
|
|
32
|
+
const url = path.startsWith('http') ? path : `${protocol}://${hostname}:${port}${path}`;
|
|
33
|
+
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
if (window.top === window.self) {
|
|
2
|
+
throw new Error('[hbuilderx:client]: must run in hbuilderx webview');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const POST_MESSAGE_TYPE = '[hbuilderx:client]:postMessage';
|
|
6
|
+
console.log('[@tomjs:hbuilderx:client]: init');
|
|
7
|
+
|
|
8
|
+
const msgListeners: any[] = [];
|
|
9
|
+
window.hbuilderx = window.hbuilderx || (function () {
|
|
10
|
+
// 第一次执行webviewinterface.js,生成hbuilderx对象
|
|
11
|
+
function postMessage(data: any) {
|
|
12
|
+
window.parent.postMessage({ type: POST_MESSAGE_TYPE, data }, '*');
|
|
13
|
+
}
|
|
14
|
+
function dispatchMessage(message: any) {
|
|
15
|
+
for (let i = 0; i < msgListeners.length; i++) {
|
|
16
|
+
const listener = msgListeners[i];
|
|
17
|
+
if (typeof listener === 'function') {
|
|
18
|
+
listener(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function onDidReceiveMessage(callback) {
|
|
23
|
+
msgListeners.push(callback);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
postMessage,
|
|
28
|
+
dispatchMessage,
|
|
29
|
+
onDidReceiveMessage,
|
|
30
|
+
};
|
|
31
|
+
}());
|
|
32
|
+
|
|
33
|
+
window.addEventListener('message', (e) => {
|
|
34
|
+
for (let i = 0; i < msgListeners.length; i++) {
|
|
35
|
+
const listener = msgListeners[i];
|
|
36
|
+
if (typeof listener === 'function') {
|
|
37
|
+
listener(e.data);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
document.addEventListener('keydown', (e) => {
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
if (e.key === (window.TOMJS_REFRESH_KEY || 'F6')) {
|
|
45
|
+
window.location.reload();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<style>
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
padding: 0;
|
|
13
|
+
margin: 0;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#webview-patch-iframe {
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
border: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.outer {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
|
|
30
|
+
<script type="module" id="webview-patch">
|
|
31
|
+
const TAG = '[@tomjs:hbuilderx:extension] ';
|
|
32
|
+
|
|
33
|
+
function onDomReady(callback, doc) {
|
|
34
|
+
const _doc = doc || document;
|
|
35
|
+
if (_doc.readyState === 'interactive' || _doc.readyState === 'complete') {
|
|
36
|
+
callback();
|
|
37
|
+
} else {
|
|
38
|
+
_doc.addEventListener('DOMContentLoaded', callback);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// message handler
|
|
43
|
+
let iframeLoaded = false;
|
|
44
|
+
const cacheMessages = [];
|
|
45
|
+
|
|
46
|
+
onDomReady(function () {
|
|
47
|
+
/** @type {HTMLIFrameElement} */
|
|
48
|
+
const iframe = document.getElementById('webview-patch-iframe');
|
|
49
|
+
|
|
50
|
+
onDomReady(function () {
|
|
51
|
+
iframeLoaded = true;
|
|
52
|
+
}, iframe.contentDocument);
|
|
53
|
+
|
|
54
|
+
iframe.addEventListener('load', function (e) {
|
|
55
|
+
iframeLoaded = true;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function handleMessage(e) {
|
|
60
|
+
const iframe = document.getElementById('webview-patch-iframe');
|
|
61
|
+
if (!iframeLoaded || !iframe) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if ('{{serverUrl}}'.startsWith(e.origin)) {
|
|
65
|
+
const { type, data } = e.data;
|
|
66
|
+
console.log(TAG + ' received:', e.data);
|
|
67
|
+
if (type === '[hbuilderx:client]:postMessage') {
|
|
68
|
+
hbuilderx.postMessage(data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
window.addEventListener('message', function (event) {
|
|
74
|
+
handleMessage(event);
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
</head>
|
|
78
|
+
|
|
79
|
+
<body>
|
|
80
|
+
<div class="outer">
|
|
81
|
+
<iframe
|
|
82
|
+
id="webview-patch-iframe"
|
|
83
|
+
frameborder="0"
|
|
84
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock allow-downloads"
|
|
85
|
+
allow="cross-origin-isolated; autoplay; clipboard-read; clipboard-write"
|
|
86
|
+
src="{{serverUrl}}"
|
|
87
|
+
></iframe>
|
|
88
|
+
</div>
|
|
89
|
+
</body>
|
|
90
|
+
</html>
|