├── .github └── workflows │ ├── ci.yml │ └── vsce-publish.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── icon.png ├── license.txt ├── package-lock.json ├── package.json ├── readme.md ├── source ├── codelens.ts ├── extension.ts ├── server.ts ├── state.ts └── vscode.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | Lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | cache: npm 15 | - run: npm ci 16 | - name: XO 17 | run: npx xo 18 | 19 | Build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | cache: npm 26 | - run: npm ci 27 | - run: npm run build 28 | -------------------------------------------------------------------------------- /.github/workflows/vsce-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | Version: 10 | description: 'Version accepted by `npm version *`' 11 | required: true 12 | 13 | jobs: 14 | Marketplace: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - run: npm ci 22 | - uses: fregante/setup-git-user@v2 23 | - name: Create version 24 | # Get the generated version, this enables support for keywords: `npm version patch` 25 | run: | 26 | VERSION="$(npm version "${{ github.event.inputs.Version }}")" 27 | echo "VERSION=$VERSION" >> $GITHUB_ENV 28 | - run: npx @vscode/vsce@2 publish 29 | env: 30 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 31 | - run: git push --follow-tags 32 | - run: gh release create "$VERSION" --generate-notes 33 | env: 34 | GH_TOKEN: ${{ github.token }} 35 | - run: npx ovsx publish 36 | env: 37 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 38 | 39 | # Token generated on https://dev.azure.com/fregante/GhostText 40 | # Extension manageable on https://marketplace.visualstudio.com/manage/publishers/fregante 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | distribution 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/distribution/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fregante/GhostText-for-VSCode/30a9705feae307ccc02bc8a2c46365a7cf83e6db/icon.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Federico Brigante (https://fregante.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghost-text", 3 | "displayName": "GhostText Official", 4 | "version": "1.4.1", 5 | "description": "Write in the browser with VSCode", 6 | "categories": [ 7 | "Other" 8 | ], 9 | "homepage": "https://ghosttext.fregante.com", 10 | "bugs": "https://github.com/fregante/GhostText/issues", 11 | "repository": "fregante/GhostText-for-VSCode", 12 | "publisher": "fregante", 13 | "main": "./distribution/extension.js", 14 | "scripts": { 15 | "build": "tsc", 16 | "test": "tsc && xo", 17 | "vscode:prepublish": "npm run build", 18 | "watch": "tsc --watch" 19 | }, 20 | "prettier": { 21 | "printWidth": 100 22 | }, 23 | "contributes": { 24 | "title": "GhostText", 25 | "commands": [ 26 | { 27 | "command": "ghostText.startServer", 28 | "title": "GhostText: Start server", 29 | "enablement": "!ghostText.server" 30 | }, 31 | { 32 | "command": "ghostText.stopServer", 33 | "title": "GhostText: Stop server", 34 | "enablement": "ghostText.server" 35 | } 36 | ], 37 | "configuration": { 38 | "title": "GhostText", 39 | "properties": { 40 | "ghostText.serverPort": { 41 | "type": "number", 42 | "default": 4001, 43 | "minimum": 0, 44 | "description": "The port to open for the browser extension to connect to VS Code (default: 4001)" 45 | }, 46 | "ghostText.fileExtension": { 47 | "type": "string", 48 | "description": "The default filetype used when opening a new editor. You can use md for markdown, js for javascript, etc. It's advised to leave it empty so VS Code will guess the type automatically." 49 | } 50 | } 51 | } 52 | }, 53 | "activationEvents": [ 54 | "onStartupFinished" 55 | ], 56 | "xo": { 57 | "prettier": true 58 | }, 59 | "dependencies": { 60 | "filenamify": "^4.3.0", 61 | "ws": "^8.13.0" 62 | }, 63 | "devDependencies": { 64 | "@sindresorhus/tsconfig": "^3.0.1", 65 | "@types/vscode": "^1.76.0", 66 | "@types/ws": "^8.5.4", 67 | "@typescript-eslint/eslint-plugin": "^5.53.0", 68 | "@typescript-eslint/parser": "^5.53.0", 69 | "typescript": "^4.9.5", 70 | "xo": "^0.56.0" 71 | }, 72 | "engines": { 73 | "vscode": "^1.76.0" 74 | }, 75 | "icon": "icon.png", 76 | "sponsor": { 77 | "url": "https://github.com/sponsors/fregante" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [![GhostText for VS Code](https://raw.githubusercontent.com/fregante/GhostText/main/promo/gt_banner-for-vscode.png)](https://github.com/fregante/GhostText-for-VSCode) 2 | Use VS Code to write in your browser. Everything you type in the editor will be instantly updated in the browser (and vice versa). 3 | 4 | ## Download 5 | 6 | [ Download GhostText for VS Code](https://marketplace.visualstudio.com/items?itemName=fregante.ghost-text) 7 | 8 | [ Download GhostText for VSCodium](https://open-vsx.org/extension/fregante/ghost-text) 9 | 10 | 11 | Demo screencast 12 | 13 | ## Documentation 14 | 15 | [ GhostText website](https://ghosttext.fregante.com) 16 | 17 | [ Bug tracker](https://github.com/fregante/GhostText/issues) 18 | 19 | ## Repositories 20 | 21 | GhostText for the browser 22 | 23 | GhostText for VS Code 24 | -------------------------------------------------------------------------------- /source/codelens.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import {documents} from './state.js'; 3 | import {type Subscriptions} from './vscode.js'; 4 | 5 | class GhostTextCodeLensProvider implements vscode.CodeLensProvider { 6 | public readonly _onDidChangeCodeLenses = new vscode.EventEmitter(); 7 | public readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event; 8 | 9 | provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { 10 | if (!documents.has(document.uri.toString())) { 11 | return []; 12 | } 13 | 14 | const range = new vscode.Range(0, 0, 0, 0); 15 | const command: vscode.Command = { 16 | title: '👻 🌕 GhostText connected | Disconnect', 17 | command: 'ghostText.disconnect', 18 | arguments: [document.uri.toString()], 19 | }; 20 | return [new vscode.CodeLens(range, command)]; 21 | } 22 | } 23 | 24 | export function activate(subscriptions: Subscriptions): void { 25 | const codeLensProvider = new GhostTextCodeLensProvider(); 26 | const codeLensDisposable = vscode.languages.registerCodeLensProvider( 27 | {pattern: '**/*'}, 28 | codeLensProvider, 29 | ); 30 | subscriptions.push(codeLensDisposable); 31 | documents.onRemove( 32 | () => { 33 | codeLensProvider._onDidChangeCodeLenses.fire(); 34 | }, 35 | null, 36 | subscriptions, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /source/extension.ts: -------------------------------------------------------------------------------- 1 | import {promisify} from 'node:util'; 2 | import {tmpdir} from 'node:os'; 3 | import {execFile} from 'node:child_process'; 4 | import process from 'node:process'; 5 | import {type IncomingMessage} from 'node:http'; 6 | import * as vscode from 'vscode'; 7 | import {type WebSocket} from 'ws'; 8 | import filenamify from 'filenamify'; 9 | import * as codelens from './codelens.js'; 10 | import {documents} from './state.js'; 11 | import {Eaddrinuse, startServer, stopServer} from './server.js'; 12 | import {registerCommand, type Subscriptions} from './vscode.js'; 13 | 14 | /** When the browser sends new content, the editor should not detect this "change" event and echo it */ 15 | let updateFromBrowserInProgress = false; 16 | 17 | const exec = promisify(execFile); 18 | let context: vscode.ExtensionContext; 19 | 20 | const osxFocus = ` 21 | tell application "Visual Studio Code" 22 | activate 23 | end tell`; 24 | function bringEditorToFront() { 25 | if (process.platform === 'darwin') { 26 | void exec('osascript', ['-e', osxFocus]); 27 | } 28 | } 29 | 30 | type Tab = {document: vscode.TextDocument; editor: vscode.TextEditor}; 31 | 32 | async function initView(title: string, socket: WebSocket) { 33 | const t = new Date(); 34 | // This string is visible if multiple tabs are open from the same page 35 | const avoidsOverlappingFiles = `${t.getHours()}-${t.getMinutes()}-${t.getSeconds()}`; 36 | const filename = `${filenamify(title.trim(), {replacement: '-'})}.${getFileExtension()}`; 37 | const file = vscode.Uri.from({ 38 | scheme: 'untitled', 39 | path: `${tmpdir()}/${avoidsOverlappingFiles}/${filename}`, 40 | }); 41 | const document = await vscode.workspace.openTextDocument(file); 42 | const editor = await vscode.window.showTextDocument(document, { 43 | viewColumn: vscode.ViewColumn.Active, 44 | preview: false, 45 | }); 46 | 47 | bringEditorToFront(); 48 | const uriString = file.toString(); 49 | documents.set(uriString, { 50 | uri: uriString, 51 | document, 52 | editor, 53 | socket, 54 | }); 55 | documents.onRemove((removedUriString) => { 56 | if (uriString === removedUriString) { 57 | socket.close(); 58 | } 59 | }); 60 | return {document, editor}; 61 | } 62 | 63 | function openConnection(socket: WebSocket, request: IncomingMessage) { 64 | // Only the background page can connect to this server 65 | try { 66 | if (!new URL(request.headers.origin!).protocol.endsWith('extension:')) { 67 | socket.close(); 68 | return; 69 | } 70 | } catch { 71 | socket.close(); 72 | return; 73 | } 74 | 75 | let tab: Promise; 76 | 77 | socket.on('close', async () => { 78 | const {document} = await tab; 79 | documents.delete(document.uri.toString()); 80 | }); 81 | 82 | // Listen for incoming messages on the WebSocket 83 | // Don't `await` anything before this or else it might come too late 84 | socket.on('message', async (rawMessage) => { 85 | const {text, selections, title} = JSON.parse(String(rawMessage)) as { 86 | text: string; 87 | title: string; 88 | selections: Array<{start: number; end: number}>; 89 | }; 90 | 91 | tab ??= initView(title, socket); 92 | const {document, editor} = await tab; 93 | 94 | // When a message is received, replace the document content with the message 95 | const edit = new vscode.WorkspaceEdit(); 96 | edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), text); 97 | 98 | updateFromBrowserInProgress = true; 99 | await vscode.workspace.applyEdit(edit); 100 | updateFromBrowserInProgress = false; 101 | 102 | editor.selections = selections.map( 103 | (selection) => 104 | new vscode.Selection( 105 | document.positionAt(selection.start), 106 | document.positionAt(selection.end), 107 | ), 108 | ); 109 | }); 110 | } 111 | 112 | function getFileExtension(): string { 113 | // Use || to set the default or else an empty field will override it 114 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 115 | return vscode.workspace.getConfiguration('ghostText').get('fileExtension') || 'ghosttext'; 116 | } 117 | 118 | function mapEditorSelections( 119 | document: vscode.TextDocument, 120 | selections: readonly vscode.Selection[], 121 | ) { 122 | return selections.map((selection) => ({ 123 | start: document.offsetAt(selection.start), 124 | end: document.offsetAt(selection.end), 125 | })); 126 | } 127 | 128 | function onDisconnectCommand( 129 | uriString: string | undefined = vscode.window.activeTextEditor?.document.uri.toString(), 130 | ) { 131 | if (uriString) { 132 | documents.delete(uriString); 133 | } 134 | } 135 | 136 | async function onDocumentClose(closedDocument: vscode.TextDocument) { 137 | // https://github.com/fregante/GhostText-for-VSCode/issues/2 138 | if (closedDocument.isClosed) { 139 | documents.delete(closedDocument.uri.toString()); 140 | } 141 | } 142 | 143 | async function onLocalSelection(event: vscode.TextEditorSelectionChangeEvent) { 144 | const document = event.textEditor.document; 145 | const field = documents.get(document.uri.toString()); 146 | if (!field) { 147 | return; 148 | } 149 | 150 | const content = document.getText(); 151 | const selections = mapEditorSelections(document, field.editor.selections); 152 | field.socket.send(JSON.stringify({text: content, selections})); 153 | } 154 | 155 | async function onConfigurationChange(event: vscode.ConfigurationChangeEvent) { 156 | if (event.affectsConfiguration('ghostText.serverPort')) { 157 | await startServer(context.subscriptions, openConnection); 158 | } 159 | } 160 | 161 | async function onLocalEdit(event: vscode.TextDocumentChangeEvent) { 162 | if (updateFromBrowserInProgress || event.contentChanges.length === 0) { 163 | return; 164 | } 165 | 166 | const document = event.document; 167 | const field = documents.get(document.uri.toString()); 168 | if (!field) { 169 | return; 170 | } 171 | 172 | const content = document.getText(); 173 | const selections = mapEditorSelections(document, field.editor.selections); 174 | field.socket.send(JSON.stringify({text: content, selections})); 175 | } 176 | 177 | function registerListeners(subscriptions: Subscriptions) { 178 | const setup = [null, subscriptions] as const; 179 | 180 | codelens.activate(subscriptions); 181 | // Watch for changes to the HTTP port option 182 | // This event is already debounced 183 | vscode.workspace.onDidChangeConfiguration(onConfigurationChange, ...setup); 184 | vscode.workspace.onDidCloseTextDocument(onDocumentClose, ...setup); 185 | vscode.window.onDidChangeTextEditorSelection(onLocalSelection, ...setup); 186 | vscode.workspace.onDidChangeTextDocument(onLocalEdit, ...setup); 187 | registerCommand('ghostText.disconnect', onDisconnectCommand, subscriptions); 188 | registerCommand('ghostText.stopServer', stopServer, subscriptions); 189 | registerCommand( 190 | 'ghostText.startServer', 191 | async () => { 192 | await startServer(subscriptions, openConnection); 193 | }, 194 | subscriptions, 195 | ); 196 | } 197 | 198 | export async function activate(_context: vscode.ExtensionContext) { 199 | // Set global 200 | context = _context; 201 | const {subscriptions} = context; 202 | 203 | // Listen to commands before starting the server 204 | registerListeners(subscriptions); 205 | 206 | try { 207 | await startServer(subscriptions, openConnection); 208 | } catch (error: unknown) { 209 | if (error instanceof Eaddrinuse) { 210 | return; 211 | } 212 | 213 | throw error; 214 | } 215 | 216 | subscriptions.push({ 217 | dispose() { 218 | documents.clear(); 219 | }, 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /source/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'node:http'; 2 | import * as vscode from 'vscode'; 3 | import {type WebSocket, Server} from 'ws'; 4 | import {type Subscriptions} from './vscode.js'; 5 | 6 | let server: http.Server | undefined; 7 | 8 | function getPort() { 9 | return vscode.workspace.getConfiguration('ghostText').get('serverPort', 4001); 10 | } 11 | 12 | async function pingResponder(_: unknown, response: http.ServerResponse) { 13 | response.writeHead(200, { 14 | 'Content-Type': 'application/json', 15 | }); 16 | response.end( 17 | JSON.stringify({ 18 | // eslint-disable-next-line @typescript-eslint/naming-convention 19 | ProtocolVersion: 1, 20 | // eslint-disable-next-line @typescript-eslint/naming-convention 21 | WebSocketPort: getPort(), 22 | }), 23 | ); 24 | } 25 | 26 | export class Eaddrinuse extends Error {} 27 | 28 | async function listen(server: http.Server) { 29 | const port = getPort(); 30 | return new Promise((resolve, reject) => { 31 | server 32 | .listen(getPort()) 33 | .once('listening', resolve) 34 | .once('error', (error) => { 35 | if ((error as any).code === 'EADDRINUSE') { 36 | reject(new Eaddrinuse(`The port ${port} is already in use`)); 37 | } else { 38 | reject(error); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | export async function startServer( 45 | subscriptions: Subscriptions, 46 | onConnection: (socket: WebSocket, request: http.IncomingMessage) => void, 47 | ) { 48 | console.log('GhostText: Server starting'); 49 | server?.close(); 50 | server = http.createServer(pingResponder); 51 | 52 | await listen(server); 53 | const ws = new Server({server}); 54 | ws.on('connection', onConnection); 55 | console.log('GhostText: Server started'); 56 | 57 | void vscode.commands.executeCommand('setContext', 'ghostText.server', true); 58 | subscriptions.push({ 59 | dispose() { 60 | server?.close(); 61 | }, 62 | }); 63 | } 64 | 65 | export function stopServer(): void { 66 | server?.close(); 67 | server = undefined; 68 | console.log('GhostText: Server stopped'); 69 | 70 | void vscode.commands.executeCommand('setContext', 'ghostText.server', false); 71 | } 72 | -------------------------------------------------------------------------------- /source/state.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import {type WebSocket} from 'ws'; 3 | 4 | type Uri = string; 5 | type Field = { 6 | uri: string; 7 | document: vscode.TextDocument; 8 | editor: vscode.TextEditor; 9 | socket: WebSocket; 10 | }; 11 | 12 | class State extends Map { 13 | private readonly remove = new vscode.EventEmitter(); 14 | private readonly add = new vscode.EventEmitter(); 15 | // eslint-disable-next-line @typescript-eslint/member-ordering 16 | readonly onRemove = this.remove.event; 17 | // eslint-disable-next-line @typescript-eslint/member-ordering 18 | readonly onAdd = this.add.event; 19 | 20 | override set(uri: Uri, field: Field) { 21 | super.set(uri, field); 22 | this.add.fire(uri); 23 | return this; 24 | } 25 | 26 | override delete(uri: Uri) { 27 | const removed = super.delete(uri); 28 | if (removed) { 29 | this.remove.fire(uri); 30 | } 31 | 32 | return removed; 33 | } 34 | } 35 | 36 | export const documents = new State(); 37 | -------------------------------------------------------------------------------- /source/vscode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export type Subscriptions = vscode.ExtensionContext['subscriptions']; 4 | 5 | export function registerCommand( 6 | command: string, 7 | callback: (...args: any[]) => any, 8 | subscriptions: Subscriptions, 9 | ) { 10 | subscriptions.push(vscode.commands.registerCommand(command, callback)); 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "rootDir": "source", 6 | "outDir": "distribution", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "noEmitOnError": false 10 | }, 11 | } 12 | --------------------------------------------------------------------------------