diff --git a/extensions/vscode-vue-language-features/package.json b/extensions/vscode-vue-language-features/package.json index 6660c7017..474a2def6 100644 --- a/extensions/vscode-vue-language-features/package.json +++ b/extensions/vscode-vue-language-features/package.json @@ -637,16 +637,17 @@ "devDependencies": { "@types/vscode": "1.63.0", "@types/ws": "^8.5.3", + "@volar/preview": "0.34.2", "@volar/shared": "0.34.2", "@volar/vue-language-server": "0.34.2", "@vue/compiler-dom": "^3.2.31", "@vue/compiler-sfc": "^3.2.31", "@vue/reactivity": "^3.2.31", "esbuild": "latest", + "esbuild-plugin-copy": "latest", "path-browserify": "^1.0.1", "vsce": "latest", "vscode-languageclient": "^8.0.0-next.14", - "vscode-nls": "5.0.0", - "ws": "^8.5.0" + "vscode-nls": "5.0.0" } } diff --git a/extensions/vscode-vue-language-features/scripts/build-node.js b/extensions/vscode-vue-language-features/scripts/build-node.js index 3e9b974ab..0cdc62998 100644 --- a/extensions/vscode-vue-language-features/scripts/build-node.js +++ b/extensions/vscode-vue-language-features/scripts/build-node.js @@ -15,19 +15,29 @@ require('esbuild').build({ define: { 'process.env.NODE_ENV': '"production"' }, minify: process.argv.includes('--minify'), watch: process.argv.includes('--watch'), - plugins: [{ - name: 'umd2esm', - setup(build) { - build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, args => { - const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) - const pathEsm = pathUmdMay.replace('/umd/', '/esm/') - return { path: pathEsm } - }) - build.onResolve({ filter: /^\@vue\/compiler-sfc$/ }, args => { - const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) - const pathEsm = pathUmdMay.replace('compiler-sfc.cjs.js', 'compiler-sfc.esm-browser.js') - return { path: pathEsm } - }) + plugins: [ + { + name: 'umd2esm', + setup(build) { + build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, args => { + const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) + const pathEsm = pathUmdMay.replace('/umd/', '/esm/') + return { path: pathEsm } + }) + build.onResolve({ filter: /^\@vue\/compiler-sfc$/ }, args => { + const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) + const pathEsm = pathUmdMay.replace('compiler-sfc.cjs.js', 'compiler-sfc.esm-browser.js') + return { path: pathEsm } + }) + }, }, - }], + require('esbuild-plugin-copy').copy({ + resolveFrom: 'cwd', + assets: { + from: ['./node_modules/@volar/preview/bin/**/*'], + to: ['./dist/preview-bin'], + }, + keepStructure: true, + }), + ], }).catch(() => process.exit(1)) diff --git a/extensions/vscode-vue-language-features/src/features/preview.ts b/extensions/vscode-vue-language-features/src/features/preview.ts index c52e61d75..5f34d37bc 100644 --- a/extensions/vscode-vue-language-features/src/features/preview.ts +++ b/extensions/vscode-vue-language-features/src/features/preview.ts @@ -5,7 +5,7 @@ import * as fs from '../utils/fs'; import * as shared from '@volar/shared'; import { userPick } from './splitEditors'; import { parse, SFCParseResult } from '@vue/compiler-sfc'; -import * as WebSocket from 'ws'; +import * as preview from '@volar/preview'; interface PreviewState { mode: 'vite' | 'nuxt', @@ -28,40 +28,30 @@ export async function activate(context: vscode.ExtensionContext) { statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); context.subscriptions.push(statusBar); - const wsList: WebSocket.WebSocket[] = []; - let wss: WebSocket.Server | undefined; + let ws: ReturnType | undefined; - function startWsServer() { - wss = new WebSocket.Server({ - port: 56789 - }); - - wss.on('connection', ws => { - wsList.push(ws); - ws.on('message', msg => { - webviewEventHandler(JSON.parse(msg.toString())); - }); - }); - } if (vscode.window.terminals.some(terminal => terminal.name.startsWith('volar-preview:'))) { - startWsServer(); + ws = preview.createPreviewWebSocket({ + goToCode: handleGoToCode, + getOpenFileUrl: (fileName, range) => 'vscode://files:/' + fileName, + }); } vscode.window.onDidOpenTerminal(e => { if (e.name.startsWith('volar-preview:')) { - startWsServer(); + ws = preview.createPreviewWebSocket({ + goToCode: handleGoToCode, + getOpenFileUrl: (fileName, range) => 'vscode://files:/' + fileName, + }); } }); vscode.window.onDidCloseTerminal(e => { if (e.name.startsWith('volar-preview:')) { - wss?.close(); - wsList.length = 0; + ws?.stop(); } }); const sfcs = new WeakMap(); - let goToTemplateReq = 0; - class FinderPanelSerializer implements vscode.WebviewPanelSerializer { async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: PreviewState) { @@ -166,31 +156,16 @@ export async function activate(context: vscode.ExtensionContext) { } })); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(e => { - for (const panel of panels) { - updateSelectionHighlights(e.textEditor, panel, undefined); - } - for (const ws of wsList) { - updateSelectionHighlights(e.textEditor, undefined, ws); - } + updateSelectionHighlights(e.textEditor); })); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(e => { if (vscode.window.activeTextEditor) { - for (const panel of panels) { - updateSelectionHighlights(vscode.window.activeTextEditor, panel, undefined); - } - for (const ws of wsList) { - updateSelectionHighlights(vscode.window.activeTextEditor, undefined, ws); - } + updateSelectionHighlights(vscode.window.activeTextEditor); } })); context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(e => { if (vscode.window.activeTextEditor) { - for (const panel of panels) { - updateSelectionHighlights(vscode.window.activeTextEditor, panel, undefined); - } - for (const ws of wsList) { - updateSelectionHighlights(vscode.window.activeTextEditor, undefined, ws); - } + updateSelectionHighlights(vscode.window.activeTextEditor); } })); @@ -223,33 +198,21 @@ export async function activate(context: vscode.ExtensionContext) { } } - function updateSelectionHighlights(textEditor: vscode.TextEditor, panel: vscode.WebviewPanel | undefined, ws: WebSocket.WebSocket | undefined) { + function updateSelectionHighlights(textEditor: vscode.TextEditor) { if (textEditor.document.languageId === 'vue') { const sfc = getSfc(textEditor.document); const offset = sfc.descriptor.template?.loc.start.offset ?? 0; - const msg = { - sender: 'volar', - command: 'highlightSelections', - data: { - fileName: textEditor.document.fileName, - ranges: textEditor.selections.map(selection => ({ - start: textEditor.document.offsetAt(selection.start) - offset, - end: textEditor.document.offsetAt(selection.end) - offset, - })), - isDirty: textEditor.document.isDirty, - }, - }; - panel?.webview.postMessage(msg); - ws?.send(JSON.stringify(msg)); + ws?.highlight( + textEditor.document.fileName, + textEditor.selections.map(selection => ({ + start: textEditor.document.offsetAt(selection.start) - offset, + end: textEditor.document.offsetAt(selection.end) - offset, + })), + textEditor.document.isDirty, + ); } else { - const msg = { - sender: 'volar', - command: 'highlightSelections', - data: undefined, - }; - panel?.webview.postMessage(JSON.stringify(msg)); - ws?.send(JSON.stringify(msg)); + ws?.unhighlight(); } } @@ -394,33 +357,29 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showErrorMessage(text); break; } - case 'goToTemplate': { - const req = ++goToTemplateReq; - const data = message.data as { - fileName: string, - range: [number, number], - }; - const doc = await vscode.workspace.openTextDocument(data.fileName); - - if (req !== goToTemplateReq) - return; - - const sfc = getSfc(doc); - const offset = sfc.descriptor.template?.loc.start.offset ?? 0; - const start = doc.positionAt(data.range[0] + offset); - const end = doc.positionAt(data.range[1] + offset); - await vscode.window.showTextDocument(doc, vscode.ViewColumn.One); - - if (req !== goToTemplateReq) - return; - - const editor = vscode.window.activeTextEditor; - if (editor) { - editor.selection = new vscode.Selection(start, end); - editor.revealRange(new vscode.Range(start, end)); - } - break; - } + } + } + + async function handleGoToCode(fileName: string, range: [number, number], cancleToken: { readonly isCancelled: boolean }) { + + const doc = await vscode.workspace.openTextDocument(fileName); + + if (cancleToken.isCancelled) + return; + + const sfc = getSfc(doc); + const offset = sfc.descriptor.template?.loc.start.offset ?? 0; + const start = doc.positionAt(range[0] + offset); + const end = doc.positionAt(range[1] + offset); + await vscode.window.showTextDocument(doc, vscode.ViewColumn.One); + + if (cancleToken.isCancelled) + return; + + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.selection = new vscode.Selection(start, end); + editor.revealRange(new vscode.Range(start, end)); } } @@ -429,8 +388,8 @@ export async function activate(context: vscode.ExtensionContext) { const port = await shared.getLocalHostAvaliablePort(vscode.workspace.getConfiguration('volar').get('preview.port') ?? 3334); const terminal = vscode.window.createTerminal('volar-preview:' + port); const viteProxyPath = type === 'vite' - ? require.resolve('./bin/vite', { paths: [context.extensionPath] }) - : require.resolve('./bin/nuxi', { paths: [context.extensionPath] }); + ? require.resolve('./dist/preview-bin/vite', { paths: [context.extensionPath] }) + : require.resolve('./dist/preview-bin/nuxi', { paths: [context.extensionPath] }); terminal.sendText(`cd ${viteDir}`); diff --git a/extensions/vscode-vue-language-features/tsconfig.build.json b/extensions/vscode-vue-language-features/tsconfig.build.json index 4610c01fb..c9f976f7b 100644 --- a/extensions/vscode-vue-language-features/tsconfig.build.json +++ b/extensions/vscode-vue-language-features/tsconfig.build.json @@ -16,6 +16,9 @@ { "path": "../../packages/vue-language-server/tsconfig.build.json" }, + { + "path": "../../packages/preview/tsconfig.build.json" + }, { "path": "../../packages/shared/tsconfig.build.json" } diff --git a/packages/preview/LICENSE b/packages/preview/LICENSE new file mode 100644 index 000000000..b55e47a7e --- /dev/null +++ b/packages/preview/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Johnson Chu + +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. diff --git a/packages/preview/README.md b/packages/preview/README.md new file mode 100644 index 000000000..9e1a66427 --- /dev/null +++ b/packages/preview/README.md @@ -0,0 +1,36 @@ +# vue-tsc + +Install: `npm i vue-tsc -D` + +Usage: `vue-tsc --noEmit && vite build` + +Vue 3 command line Type-Checking tool base on IDE plugin [Volar](https://github.com/johnsoncodehk/volar). + +Roadmap: + +- [x] Type-Checking with `--noEmit` +- [x] Use released LSP module +- [x] Make `typescript` as peerDependencies +- [x] Cleaner dependencies (remove `prettyhtml`, `prettier` etc.) (with `vscode-vue-languageservice` version >= 0.26.4) +- [x] dts emit support +- [x] Watch mode support + +## Using + +Type check: + +`vue-tsc --noEmit` + +Build dts: + +`vue-tsc --declaration --emitDeclarationOnly` + +Check out https://github.com/johnsoncodehk/volar/discussions/640#discussioncomment-1555479 for example repo. + +## Sponsors + +

+ + + +

diff --git a/extensions/vscode-vue-language-features/bin/nuxi.js b/packages/preview/bin/nuxi.js similarity index 100% rename from extensions/vscode-vue-language-features/bin/nuxi.js rename to packages/preview/bin/nuxi.js diff --git a/extensions/vscode-vue-language-features/bin/nuxi/configExtraContent.ts b/packages/preview/bin/nuxi/configExtraContent.ts similarity index 100% rename from extensions/vscode-vue-language-features/bin/nuxi/configExtraContent.ts rename to packages/preview/bin/nuxi/configExtraContent.ts diff --git a/extensions/vscode-vue-language-features/bin/nuxi/plugin.ts b/packages/preview/bin/nuxi/plugin.ts similarity index 96% rename from extensions/vscode-vue-language-features/bin/nuxi/plugin.ts rename to packages/preview/bin/nuxi/plugin.ts index 49f093d0d..592b0dc49 100644 --- a/extensions/vscode-vue-language-features/bin/nuxi/plugin.ts +++ b/packages/preview/bin/nuxi/plugin.ts @@ -43,12 +43,6 @@ export default defineNuxtPlugin(app => { const cursorInOverlays = new Map(); const rangeCoverOverlays = new Map(); - window.addEventListener('message', event => { - if (event.data?.command === 'highlightSelections') { - selection = event.data.data; - updateHighlights(); - } - }); window.addEventListener('scroll', updateHighlights); ws.addEventListener('message', event => { @@ -198,6 +192,13 @@ export default defineNuxtPlugin(app => { } }); + ws.addEventListener('message', event => { + const data = JSON.parse(event.data); + if (data?.command === 'openFile') { + window.open(data.data); + } + }); + const overlay = createOverlay(); const clickMask = createClickMask(); @@ -216,7 +217,7 @@ export default defineNuxtPlugin(app => { document.body.appendChild(clickMask); updateOverlay(); } - function disable(openVscode: boolean) { + function disable(openEditor: boolean) { if (enabled) { enabled = false; clickMask.style.pointerEvents = ''; @@ -224,8 +225,11 @@ export default defineNuxtPlugin(app => { updateOverlay(); if (lastCodeLoc) { ws.send(JSON.stringify(lastCodeLoc)); - if (openVscode) { - window.open('vscode://files:/' + lastCodeLoc.fileName); + if (openEditor) { + ws.send(JSON.stringify({ + command: 'requestOpenFile', + data: lastCodeLoc.data, + })); } lastCodeLoc = undefined; } diff --git a/extensions/vscode-vue-language-features/bin/vite.js b/packages/preview/bin/vite.js similarity index 97% rename from extensions/vscode-vue-language-features/bin/vite.js rename to packages/preview/bin/vite.js index 68b8644ac..6ce7362cf 100755 --- a/extensions/vscode-vue-language-features/bin/vite.js +++ b/packages/preview/bin/vite.js @@ -40,12 +40,6 @@ function __createAppProxy(...args) { const cursorInOverlays = new Map(); const rangeCoverOverlays = new Map(); - window.addEventListener('message', event => { - if (event.data?.command === 'highlightSelections') { - selection = event.data.data; - updateHighlights(); - } - }); window.addEventListener('scroll', updateHighlights); ws.addEventListener('message', event => { @@ -194,6 +188,13 @@ function __createAppProxy(...args) { } }); + ws.addEventListener('message', event => { + const data = JSON.parse(event.data); + if (data?.command === 'openFile') { + window.open(data.data); + } + }); + var overlay = createOverlay(); var clickMask = createClickMask(); var highlightNodes = []; @@ -211,7 +212,7 @@ function __createAppProxy(...args) { document.body.appendChild(clickMask); updateOverlay(); } - function disable(openVscode) { + function disable(openEditor) { if (enabled) { enabled = false; clickMask.style.pointerEvents = ''; @@ -219,8 +220,11 @@ function __createAppProxy(...args) { updateOverlay(); if (lastCodeLoc) { ws.send(JSON.stringify(lastCodeLoc)); - if (openVscode) { - window.open('vscode://files:/' + lastCodeLoc.fileName); + if (openEditor) { + ws.send(JSON.stringify({ + command: 'requestOpenFile', + data: lastCodeLoc.data, + })); } lastCodeLoc = undefined; } diff --git a/packages/preview/package.json b/packages/preview/package.json new file mode 100644 index 000000000..9090747d3 --- /dev/null +++ b/packages/preview/package.json @@ -0,0 +1,23 @@ +{ + "name": "@volar/preview", + "version": "0.34.2", + "license": "MIT", + "files": [ + "bin", + "out/**/*.js", + "out/**/*.d.ts" + ], + "repository": { + "type": "git", + "url": "https://github.com/johnsoncodehk/volar.git", + "directory": "packages/preview" + }, + "main": "out/index.js", + "bin": { + "volar-preview-vite": "./bin/vite.js", + "volar-preview-nuxi": "./bin/nuxi.js" + }, + "dependencies": { + "ws": "^8.5.0" + } +} diff --git a/packages/preview/src/index.ts b/packages/preview/src/index.ts new file mode 100644 index 000000000..aa52627db --- /dev/null +++ b/packages/preview/src/index.ts @@ -0,0 +1,88 @@ +import * as WebSocket from 'ws'; + +export function createPreviewWebSocket(options: { + goToCode: (fileName: string, range: [number, number], cancleToken: { readonly isCancelled: boolean }) => void, + getOpenFileUrl: (fileName: string, range: [number, number]) => string, +}) { + + const wsList: WebSocket.WebSocket[] = []; + let wss: WebSocket.Server | undefined; + let goToTemplateReq = 0; + + wss = new WebSocket.Server({ port: 56789 }); + wss.on('connection', ws => { + + wsList.push(ws); + + ws.on('message', msg => { + + const message = JSON.parse(msg.toString()); + + if (message.command === 'goToTemplate') { + + const req = ++goToTemplateReq; + const data = message.data as { + fileName: string, + range: [number, number], + }; + const token = { + get isCancelled() { + return req !== goToTemplateReq; + } + }; + + options.goToCode(data.fileName, data.range, token); + } + + if (message.command === 'requestOpenFile') { + + const data = message.data as { + fileName: string, + range: [number, number], + }; + console.log(data); + const url = options.getOpenFileUrl(data.fileName, data.range); + + ws.send(JSON.stringify({ + command: 'openFile', + data: url, + })); + } + }); + }); + + return { + stop, + highlight, + unhighlight, + }; + + function stop() { + wss?.close(); + wsList.length = 0; + } + + function highlight(fileName: string, ranges: { start: number, end: number }[], isDirty: boolean) { + const msg = { + command: 'highlightSelections', + data: { + fileName, + ranges, + isDirty, + }, + }; + for (const ws of wsList) { + ws.send(JSON.stringify(msg)); + } + } + + function unhighlight() { + const msg = { + command: 'highlightSelections', + data: undefined, + }; + for (const ws of wsList) { + ws.send(JSON.stringify(msg)); + } + } +} diff --git a/packages/preview/tsconfig.build.json b/packages/preview/tsconfig.build.json new file mode 100644 index 000000000..6f513ea3e --- /dev/null +++ b/packages/preview/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "out", + "rootDir": "src", + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ], +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 496614800..8ddd3e005 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,31 +31,33 @@ importers: specifiers: '@types/vscode': 1.63.0 '@types/ws': ^8.5.3 + '@volar/preview': 0.34.2 '@volar/shared': 0.34.2 '@volar/vue-language-server': 0.34.2 '@vue/compiler-dom': ^3.2.31 '@vue/compiler-sfc': ^3.2.31 '@vue/reactivity': ^3.2.31 esbuild: latest + esbuild-plugin-copy: latest path-browserify: ^1.0.1 vsce: latest vscode-languageclient: ^8.0.0-next.14 vscode-nls: 5.0.0 - ws: ^8.5.0 devDependencies: '@types/vscode': 1.63.0 '@types/ws': 8.5.3 + '@volar/preview': link:../../packages/preview '@volar/shared': link:../../packages/shared '@volar/vue-language-server': link:../../packages/vue-language-server '@vue/compiler-dom': 3.2.31 '@vue/compiler-sfc': 3.2.31 '@vue/reactivity': 3.2.31 esbuild: 0.14.34 + esbuild-plugin-copy: 1.3.0_esbuild@0.14.34 path-browserify: 1.0.1 vsce: 2.7.0 vscode-languageclient: 8.0.0-next.14 vscode-nls: 5.0.0 - ws: 8.5.0 packages/code-gen: specifiers: @@ -63,6 +65,12 @@ importers: dependencies: '@volar/source-map': link:../source-map + packages/preview: + specifiers: + ws: ^8.5.0 + dependencies: + ws: 8.5.0 + packages/pug-language-service: specifiers: '@volar/code-gen': 0.34.2 @@ -2818,6 +2826,17 @@ packages: dev: true optional: true + /esbuild-plugin-copy/1.3.0_esbuild@0.14.34: + resolution: {integrity: sha512-LOx1xJOlAaCFMRtokHjsJfEkrosy3RDRa8SUHmn7loo0gwrouBQQwLAmOyMECshf7gSR1cPSRtAHu3KF/kQsyw==} + peerDependencies: + esbuild: ^0.14.0 + dependencies: + chalk: 4.1.2 + esbuild: 0.14.34 + fs-extra: 10.0.1 + globby: 11.1.0 + dev: true + /esbuild-sunos-64/0.14.28: resolution: {integrity: sha512-zlIxePhZxKYheR2vBCgPVvTixgo/ozOfOMoP6RZj8dxzquU1NgeyhjkcRXucbLCtmoNJ+i4PtWwPZTLuDd3bGg==} engines: {node: '>=12'} @@ -3121,6 +3140,15 @@ packages: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: true + /fs-extra/10.0.1: + resolution: {integrity: sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.9 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + /fs-extra/9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -6737,7 +6765,7 @@ packages: optional: true utf-8-validate: optional: true - dev: true + dev: false /xml2js/0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}