├── .gitignore ├── src ├── extension │ ├── middlewares │ │ ├── index.ts │ │ ├── setMIME.ts │ │ └── fileSelector.ts │ ├── utils │ │ ├── urlJoin.ts │ │ ├── getNormalizedBrowserName.ts │ │ ├── showPopUpMsg.ts │ │ ├── workSpaceUtils.ts │ │ └── extensionConfig.ts │ ├── services │ │ ├── NotificationService.ts │ │ ├── StatusbarService.ts │ │ └── BrowserService.ts │ └── index.ts ├── core │ ├── types │ │ ├── index.ts │ │ ├── IApplyMiddlware.ts │ │ ├── ILSPPIncomingMessage.ts │ │ └── ILiveServerPlusPlus.ts │ ├── LSPPError.ts │ ├── utils │ │ ├── injectedText.ts │ │ └── index.ts │ ├── assets │ │ ├── inject.html │ │ └── inject │ │ │ ├── live-reload.js │ │ │ └── diffDOM.js │ ├── FileSystem.ts │ └── LiveServerPlusPlus.ts └── test │ ├── extension.test.ts │ └── index.ts ├── images ├── vscode-live-server-plus-plus.png ├── vscode-live-server-plus-plus_preview1.gif └── vscode-live-server-plus-plus.svg ├── .vscodeignore ├── CHANGELOG.md ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── tslint.json ├── tsconfig.json ├── .travis.yml ├── LICENCE ├── docs └── settings.md ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /src/extension/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fileSelector'; 2 | export * from './setMIME'; -------------------------------------------------------------------------------- /images/vscode-live-server-plus-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritwickdey/vscode-live-server-plus-plus/HEAD/images/vscode-live-server-plus-plus.png -------------------------------------------------------------------------------- /src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ILiveServerPlusPlus'; 2 | export * from './IApplyMiddlware'; 3 | export * from './ILSPPIncomingMessage'; 4 | -------------------------------------------------------------------------------- /images/vscode-live-server-plus-plus_preview1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritwickdey/vscode-live-server-plus-plus/HEAD/images/vscode-live-server-plus-plus_preview1.gif -------------------------------------------------------------------------------- /src/extension/utils/urlJoin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | export const urlJoin = (...paths: string[]): string => { 3 | return path.join(...paths).replace(/\\/g, '/'); 4 | }; 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts -------------------------------------------------------------------------------- /src/core/types/IApplyMiddlware.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http'; 2 | 3 | export type IMiddlewareTypes = ( 4 | req: IncomingMessage, 5 | res: ServerResponse 6 | ) => any; 7 | -------------------------------------------------------------------------------- /src/core/types/ILSPPIncomingMessage.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | 3 | export interface ILSPPIncomingMessage extends IncomingMessage { 4 | file?: string; 5 | contentType?: string; 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | * ## v0.0.1 (##DATE##) 4 | 5 | - Initial release 6 | - hot Reload supported 7 | - No need to save 8 | - 5 settings are added (Port, Root, indexFile, timeout, browser) 9 | -------------------------------------------------------------------------------- /src/core/LSPPError.ts: -------------------------------------------------------------------------------- 1 | import { LSPPServerErrorCodes } from './types'; 2 | 3 | export class LSPPError extends Error { 4 | constructor(message: string, public code?: LSPPServerErrorCodes) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.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 | "ms-vscode.vscode-typescript-tslint-plugin" 6 | ] 7 | } -------------------------------------------------------------------------------- /src/core/utils/injectedText.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export const INJECTED_TEXT = fs.readFileSync( 5 | path.join(__dirname, '../assets/inject.html'), 6 | { 7 | encoding: 'utf-8' 8 | } 9 | ); 10 | -------------------------------------------------------------------------------- /src/core/assets/inject.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": false, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, 12 | "esModuleInterop": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/extension/utils/getNormalizedBrowserName.ts: -------------------------------------------------------------------------------- 1 | import { IBrowserList } from './extensionConfig'; 2 | 3 | export function getNormalizedBrowserName(browserName: IBrowserList): string { 4 | if (browserName === 'chrome') { 5 | const chromes = { 6 | darwin: 'google chrome', 7 | linux: 'google-chrome', 8 | win32: 'chrome' 9 | }; 10 | return (chromes as any)[process.platform]; 11 | } 12 | return browserName!; 13 | } 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/extension/middlewares/setMIME.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponse } from 'http'; 2 | import { ILSPPIncomingMessage } from '../../core/types'; 3 | import { contentType } from 'mime-types'; 4 | import * as path from 'path'; 5 | 6 | export const setMIME = (req: ILSPPIncomingMessage, res: ServerResponse) => { 7 | const extname = path.extname(req.file!); 8 | 9 | req.contentType = String(contentType(extname)); 10 | res.setHeader('content-type', String(contentType(extname))); 11 | }; 12 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /src/extension/utils/showPopUpMsg.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | type IPopupMsgConfig = { msgType: 'info' | 'error' | 'warn' }; 4 | 5 | export function showPopUpMsg(msg: string, config?: IPopupMsgConfig) { 6 | const { msgType = 'info' } = config || {}; 7 | if (msgType === 'error') { 8 | return window.showErrorMessage(msg); 9 | } 10 | if (msgType === 'info') { 11 | return window.showInformationMessage(msg); 12 | } 13 | if (msgType === 'warn') { 14 | return window.showWarningMessage(msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export { INJECTED_TEXT } from './injectedText'; 4 | 5 | /** 6 | * Live Server++ only do dirty read if it's supported 7 | */ 8 | 9 | export const SUPPORTED_FILES = ['.js', '.html', '.css']; 10 | 11 | /** 12 | * Live Server++ will inject extra js code. 13 | */ 14 | export const INJECTABLE_FILES = ['.html']; 15 | 16 | export const isInjectableFile = (filePath: string) => { 17 | const ext = path.extname(filePath).toLowerCase(); 18 | return INJECTABLE_FILES.includes(ext); 19 | }; 20 | 21 | export const isSupportedFile = (filePath: string) => { 22 | const ext = path.extname(filePath).toLowerCase(); 23 | return SUPPORTED_FILES.includes(ext); 24 | }; 25 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "name": "Run Extension", 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "runtimeExecutable": "${execPath}", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/out/**/*.js" 13 | ], 14 | "preLaunchTask": "npm: watch" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceFolder}", 23 | "--extensionTestsPath=${workspaceFolder}/out/test" 24 | ], 25 | "outFiles": [ 26 | "${workspaceFolder}/out/test/**/*.js" 27 | ], 28 | "preLaunchTask": "npm: watch" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.14.2" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | yarn: false 9 | 10 | os: 11 | - osx 12 | - linux 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - libsecret-1-dev 18 | 19 | before_install: 20 | - rm -fr package-lock.json 21 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 22 | export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; 23 | sh -e /etc/init.d/xvfb start; 24 | sleep 3; 25 | fi 26 | 27 | install: 28 | - npm i -g vsce 29 | - npm install 30 | - npm run vscode:prepublish 31 | 32 | script: 33 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 34 | npm dedupe; 35 | fi 36 | - npm test --silent 37 | - vsce package -o LiveServer-$TRAVIS_TAG-$TRAVIS_OS_NAME.vsix 38 | 39 | deploy: 40 | provider: releases 41 | api_key: $github_token 42 | file: "*.vsix" 43 | file_glob: true 44 | skip_cleanup: true 45 | on: 46 | tags: true -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testsRoot: string, clb: (error: Error, failures?: number) => void): void 9 | // that the extension host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by configuring the test runner below 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options 17 | // for more info 18 | testRunner.configure({ 19 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 20 | useColors: true // colored output from test results 21 | }); 22 | 23 | module.exports = testRunner; -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ritwick Dey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/extension/middlewares/fileSelector.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponse } from 'http'; 2 | import path from 'path'; 3 | import * as url from 'url'; 4 | import { ILSPPIncomingMessage } from '../../core/types'; 5 | import { extensionConfig } from '../utils/extensionConfig'; 6 | 7 | const LIVE_SERVER_ASSETS = path.join(__dirname, '../../core/assets'); 8 | 9 | export const fileSelector = (req: ILSPPIncomingMessage, res: ServerResponse) => { 10 | let fileUrl = getReqFileUrl(req); 11 | 12 | if (fileUrl.startsWith('/_live-server_/')) { 13 | fileUrl = path.join(LIVE_SERVER_ASSETS, fileUrl.replace('/_live-server_/', '')); 14 | res.setHeader('cache-control', 'public, max-age=30672000'); 15 | } else if (fileUrl.startsWith('/')) { 16 | fileUrl = `.${fileUrl}`; 17 | } 18 | 19 | req.file = fileUrl; 20 | }; 21 | 22 | function getReqFileUrl(req: ILSPPIncomingMessage): string { 23 | const { pathname = '/' } = url.parse(req.url || '/'); 24 | 25 | if (!path.extname(pathname)) { 26 | //TODO: THIS NEED TO FIX. WE HAVE TO LOOK INTO DISK 27 | return `.${path.join(pathname, extensionConfig.indexFile.get())}`; 28 | } 29 | return pathname; 30 | } 31 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | - **`liveServer++.port`:** Customize Port Number of your Live Server++. If you want random port number, set it as `0`. 4 | 5 | - _Default value is `5555`._ 6 | 7 |
8 | 9 | - **`liveServer++.root`:** relative path from workspace. 10 | 11 | - _Example: `./sub_folder1/sub_folder2`_. Now `sub_folder2` will be root of the server. 12 | 13 | - _Default value is "`./`".(The Workspace Root)_. 14 | 15 |
16 | 17 | - **`liveServer++.browser`:** To change your system's default browser. 18 | 19 | - _Default value is `default`. (It will open your system's default browser.)_ 20 | - _Available Options :_ 21 | - `null` 22 | - `default` 23 | - `chrome` 24 | - `firefox` 25 | - `microsoft-edge` 26 | - Set `null` if you don't want to open browser. 27 | 28 | _Not enough? need more? open an/a issue/pull request on github._ 29 | 30 |
31 | 32 | - **`liveServer++.indexFile:`** : Path to the entry point file. 33 | 34 | - Default: `"index.html"` 35 | 36 |
37 | 38 | - **`liveServer++.timeout:`** : Delay before live reloading. Value in milliseconds. 39 | 40 | - Default: `300` 41 | 42 |
43 | -------------------------------------------------------------------------------- /src/extension/utils/workSpaceUtils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { extensionConfig } from './extensionConfig'; 4 | 5 | export const workspaceUtils = { 6 | get activeWorkspace() { 7 | const workspaces = vscode.workspace.workspaceFolders; 8 | if (workspaces && workspaces.length) { 9 | return workspaces[0]; 10 | } 11 | return null; 12 | }, 13 | 14 | getActiveDoc({ relativeToWorkSpace = true } = {}) { 15 | const { activeTextEditor } = vscode.window; 16 | if (!this.activeWorkspace || !activeTextEditor) return null; 17 | 18 | const activeDocUrl = activeTextEditor.document.uri.fsPath; 19 | const workspaceUrl = this.activeWorkspace.uri.fsPath; 20 | const isParentPath = isParent(workspaceUrl).of(activeDocUrl); 21 | 22 | if (!isParentPath) return null; 23 | 24 | return relativeToWorkSpace ? activeDocUrl.replace(this.cwd!, '') : activeDocUrl; 25 | }, 26 | 27 | get cwd() { 28 | const workspace = this.activeWorkspace; 29 | if (workspace) { 30 | return path.join(workspace.uri.fsPath, extensionConfig.root.get()); 31 | } 32 | return null; 33 | } 34 | }; 35 | 36 | function isParent(parentPath: string) { 37 | return { 38 | of: (childPath: string) => { 39 | return childPath.startsWith(parentPath); 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/core/types/ILiveServerPlusPlus.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'vscode'; 2 | import { ReloadingStrategy } from '../../extension/utils/extensionConfig'; 3 | 4 | export type LSPPServerErrorCodes = 5 | | 'serverIsAlreadyRunning' 6 | | 'portAlreadyInUse' 7 | | 'serverIsNotRunning' 8 | | 'cwdUndefined'; 9 | 10 | export interface ILiveServerPlusPlus { 11 | readonly onDidGoLive: Event; 12 | readonly onDidGoOffline: Event; 13 | readonly onServerError: Event; 14 | readonly port: number; 15 | readonly pathUri?: string; 16 | } 17 | 18 | export interface LSPPEvent { 19 | readonly LSPP: ILiveServerPlusPlus; 20 | } 21 | 22 | export interface GoLiveEvent extends LSPPEvent {} 23 | 24 | export interface GoOfflineEvent extends LSPPEvent {} 25 | 26 | export interface ServerErrorEvent extends LSPPEvent { 27 | readonly message: string; 28 | readonly code: LSPPServerErrorCodes; 29 | } 30 | 31 | export interface ILiveServerPlusPlusService { 32 | register(): void; 33 | } 34 | 35 | export interface ILiveServerPlusPlusServiceCtor { 36 | new (liveServerPlusPlus: ILiveServerPlusPlus): ILiveServerPlusPlusService; 37 | } 38 | 39 | export interface ILiveServerPlusPlusConfig { 40 | cwd: string; 41 | port?: number; 42 | subpath?: string; 43 | debounceTimeout?: number; 44 | indexFile?: string; 45 | reloadingStrategy?: ReloadingStrategy; 46 | } 47 | -------------------------------------------------------------------------------- /src/extension/services/NotificationService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ILiveServerPlusPlus, 3 | GoLiveEvent, 4 | GoOfflineEvent, 5 | ILiveServerPlusPlusService, 6 | ServerErrorEvent 7 | } from '../../core/types/ILiveServerPlusPlus'; 8 | import { showPopUpMsg } from '../utils/showPopUpMsg'; 9 | 10 | export class NotificationService implements ILiveServerPlusPlusService { 11 | constructor(private liveServerPlusPlus: ILiveServerPlusPlus) {} 12 | 13 | register() { 14 | this.liveServerPlusPlus.onDidGoLive(this.showLSPPOpened.bind(this)); 15 | this.liveServerPlusPlus.onDidGoOffline(this.showLSPPClosed.bind(this)); 16 | this.liveServerPlusPlus.onServerError(this.showServerErrorMsg.bind(this)); 17 | } 18 | 19 | private showLSPPOpened(event: GoLiveEvent) { 20 | showPopUpMsg(`Server is started at ${event.LSPP.port}`); 21 | } 22 | private showLSPPClosed(event: GoOfflineEvent) { 23 | showPopUpMsg(`Server is closed`); 24 | } 25 | 26 | private showServerErrorMsg(event: ServerErrorEvent) { 27 | if (event.code === 'serverIsAlreadyRunning') { 28 | //shhhh! keep silent. bcz we'll open the browser with running port :D 29 | return; 30 | } 31 | if (event.code === 'cwdUndefined') { 32 | return showPopUpMsg('Please open a workspace', { msgType: 'error' }); 33 | } 34 | showPopUpMsg(event.message || 'Something went wrong', { msgType: 'error' }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/extension/utils/extensionConfig.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { workspace } from 'vscode'; 4 | 5 | export type IBrowserList = 'default' | 'chrome' | 'firefox' | 'microsoft-edge' | null; 6 | export type ReloadingStrategy = 'hot' | 'partial-reload' | 'reload'; 7 | 8 | export const extensionConfig = { 9 | port: { 10 | get: () => getSettings('port'), 11 | set: (portNo: number) => setSettings('port', portNo) 12 | }, 13 | browser: { 14 | get: () => getSettings('browser'), 15 | set: (value: IBrowserList) => setSettings('browser', value) 16 | }, 17 | root: { 18 | get: () => getSettings('root') || '/', 19 | set: (value: string) => setSettings('root', value) 20 | }, 21 | timeout: { 22 | get: () => getSettings('timeout'), 23 | set: (value: number) => setSettings('timeout', value) 24 | }, 25 | indexFile: { 26 | get: () => getSettings('indexFile'), 27 | set: (value: string) => setSettings('indexFile', value) 28 | }, 29 | reloadingStrategy: { 30 | get: () => getSettings('reloadingStrategy'), 31 | set: (value: ReloadingStrategy) => setSettings('reloadingStrategy', value) 32 | } 33 | }; 34 | 35 | function getSettings(settingsName: string) { 36 | return workspace.getConfiguration('liveServer++').get(settingsName) as T; 37 | } 38 | function setSettings(settingsName: string, settingsValue: T, isGlobal = false) { 39 | return workspace 40 | .getConfiguration('liveServer++') 41 | .update(settingsName, settingsValue, isGlobal); 42 | } 43 | -------------------------------------------------------------------------------- /src/extension/services/StatusbarService.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | ILiveServerPlusPlus, 4 | ILiveServerPlusPlusService, 5 | GoLiveEvent, 6 | GoOfflineEvent 7 | } from '../../core/types'; 8 | 9 | export class StatusbarService implements ILiveServerPlusPlusService, vscode.Disposable { 10 | private statusbar: vscode.StatusBarItem; 11 | 12 | constructor(private liveServerPlusPlus: ILiveServerPlusPlus) { 13 | this.statusbar = vscode.window.createStatusBarItem( 14 | vscode.StatusBarAlignment.Right, 15 | 200 16 | ); 17 | } 18 | 19 | register() { 20 | this.init(); 21 | this.liveServerPlusPlus.onDidGoLive(this.showOfflineStatusbar.bind(this)); 22 | this.liveServerPlusPlus.onDidGoOffline(this.showLiveStatusbar.bind(this)); 23 | } 24 | 25 | private init() { 26 | this.placeStatusbar(); 27 | this.showLiveStatusbar(); 28 | } 29 | 30 | private placeStatusbar(workingMsg: string = 'loading...') { 31 | this.statusbar.text = `$(pulse) ${workingMsg}`; 32 | this.statusbar.tooltip = 33 | 'In case if it takes long time, try to close all browser window.'; 34 | this.statusbar.command = undefined; 35 | this.statusbar.show(); 36 | } 37 | 38 | private showLiveStatusbar(event?: GoOfflineEvent) { 39 | this.statusbar.text = '$(radio-tower) Go Live++'; 40 | this.statusbar.command = 'extension.live-server++.open'; 41 | this.statusbar.tooltip = 'Click to run live server++'; 42 | } 43 | 44 | private showOfflineStatusbar(event: GoLiveEvent) { 45 | this.statusbar.text = `$(x) Port : ${event.LSPP.port}`; 46 | this.statusbar.command = 'extension.live-server++.close'; 47 | this.statusbar.tooltip = 'Click to close server++'; 48 | } 49 | 50 | dispose() { 51 | this.statusbar.dispose(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/extension/services/BrowserService.ts: -------------------------------------------------------------------------------- 1 | import open from 'open'; 2 | import { extensionConfig } from '../utils/extensionConfig'; 3 | import { 4 | ILiveServerPlusPlusService, 5 | ILiveServerPlusPlus, 6 | GoLiveEvent, 7 | ServerErrorEvent 8 | } from '../../core/types'; 9 | import { workspaceUtils } from '../utils/workSpaceUtils'; 10 | import { getNormalizedBrowserName } from '../utils/getNormalizedBrowserName'; 11 | import { isInjectableFile } from '../../core/utils'; 12 | import { urlJoin } from '../utils/urlJoin'; 13 | 14 | export class BrowserService implements ILiveServerPlusPlusService { 15 | constructor(private liveServerPlusPlus: ILiveServerPlusPlus) {} 16 | 17 | register() { 18 | this.liveServerPlusPlus.onDidGoLive(this.openInBrowser.bind(this)); 19 | this.liveServerPlusPlus.onServerError(this.openIfServerIsAlreadyRunning.bind(this)); 20 | } 21 | 22 | private openInBrowser(event: GoLiveEvent) { 23 | const host = '127.0.0.1'; 24 | const port = event.LSPP.port; 25 | const pathname = this.getPathname(); 26 | const protocol = 'http:'; 27 | const browserName = extensionConfig.browser.get(); 28 | if (!browserName) return; 29 | 30 | const openParams: string[] = []; 31 | 32 | if (browserName !== 'default') { 33 | openParams.push(getNormalizedBrowserName(browserName)); 34 | } 35 | 36 | open(`${protocol}//${host}:${port}${pathname}`, { app: openParams }); 37 | } 38 | 39 | private getPathname() { 40 | const activeDoc = workspaceUtils.getActiveDoc(); 41 | if (!activeDoc || !isInjectableFile(activeDoc)) return '/'; 42 | return urlJoin('/', activeDoc); 43 | } 44 | 45 | private openIfServerIsAlreadyRunning(event: ServerErrorEvent) { 46 | if (event.code === 'serverIsAlreadyRunning') { 47 | this.openInBrowser(event); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/extension/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LiveServerPlusPlus } from '../core/LiveServerPlusPlus'; 3 | import { NotificationService } from './services/NotificationService'; 4 | import { fileSelector, setMIME } from './middlewares'; 5 | import { ILiveServerPlusPlusConfig } from '../core/types'; 6 | import { extensionConfig } from './utils/extensionConfig'; 7 | import { BrowserService } from './services/BrowserService'; 8 | import { workspaceUtils } from './utils/workSpaceUtils'; 9 | import { StatusbarService } from './services/StatusbarService'; 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | const liveServerPlusPlus = new LiveServerPlusPlus(getLSPPConfig()); 13 | 14 | liveServerPlusPlus.useMiddleware(fileSelector, setMIME); 15 | liveServerPlusPlus.useService(NotificationService, BrowserService, StatusbarService); 16 | 17 | const openServer = vscode.commands.registerCommand(getCmdWithPrefix('open'), () => { 18 | liveServerPlusPlus.reloadConfig(getLSPPConfig()); 19 | liveServerPlusPlus.goLive(); 20 | }); 21 | 22 | const closeServer = vscode.commands.registerCommand(getCmdWithPrefix('close'), () => { 23 | liveServerPlusPlus.shutdown(); 24 | }); 25 | 26 | context.subscriptions.push(openServer); 27 | context.subscriptions.push(closeServer); 28 | } 29 | 30 | export function deactivate() {} 31 | 32 | function getCmdWithPrefix(commandName: string) { 33 | return `extension.live-server++.${commandName}`; 34 | } 35 | 36 | function getLSPPConfig(): ILiveServerPlusPlusConfig { 37 | const LSPPconfig: ILiveServerPlusPlusConfig = { cwd: workspaceUtils.cwd! }; 38 | LSPPconfig.port = extensionConfig.port.get(); 39 | LSPPconfig.subpath = extensionConfig.root.get(); 40 | LSPPconfig.debounceTimeout = extensionConfig.timeout.get(); 41 | LSPPconfig.indexFile = extensionConfig.indexFile.get(); 42 | LSPPconfig.reloadingStrategy = extensionConfig.reloadingStrategy.get(); 43 | return LSPPconfig; 44 | } 45 | -------------------------------------------------------------------------------- /src/core/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as vscode from 'vscode'; 3 | import { Readable } from 'stream'; 4 | import { Buffer } from 'buffer'; 5 | import { isSupportedFile } from './utils/index'; 6 | 7 | // Stream version 8 | export const readFileStream = (filePath: string, encoding?: string) => { 9 | const dirtyFile = getDirtyFileFromVscode(filePath); 10 | 11 | if (dirtyFile) { 12 | console.log('[Stream]Reading Dirty file:', filePath); 13 | const stream = new Readable({ encoding }); 14 | setImmediate(() => { 15 | stream.emit('open'); 16 | stream.push(dirtyFile.getText()); 17 | stream.push(null); 18 | }); 19 | return stream; 20 | } 21 | 22 | console.log('[Stream]Reading file from disk: ', filePath); 23 | return fs.createReadStream(filePath, { encoding }); 24 | }; 25 | 26 | // Promise version -- Most probably will not be used. 27 | export const readFile = (filePath: string): Promise => { 28 | const dirtyFile = getDirtyFileFromVscode(filePath); 29 | 30 | if (dirtyFile) { 31 | console.log('[Promise]Reading Dirty file: ', filePath); 32 | return readFileFromVscodeWorkspace(dirtyFile); 33 | } 34 | 35 | console.log('[Promise]Reading file from disk: ', filePath); 36 | return readFileFromFileSystem(filePath); 37 | }; 38 | 39 | const readFileFromVscodeWorkspace = (filePath: string | vscode.TextDocument) => { 40 | return new Promise(async (resolve, reject) => { 41 | let doc: vscode.TextDocument; 42 | try { 43 | if (typeof filePath === 'string') { 44 | doc = await vscode.workspace.openTextDocument(filePath); 45 | } else { 46 | doc = filePath; 47 | } 48 | const text = doc.getText(); 49 | return resolve(Buffer.from(text)); 50 | } catch (error) { 51 | reject(error); 52 | } 53 | }); 54 | }; 55 | 56 | const readFileFromFileSystem = (filePath: string) => { 57 | return new Promise((resolve, reject) => { 58 | fs.readFile(filePath, function(err, data) { 59 | if (err) { 60 | return reject(err); 61 | } 62 | return resolve(data); 63 | }); 64 | }); 65 | }; 66 | 67 | // Private Utils 68 | 69 | const getDirtyFileFromVscode = (filePath: string) => { 70 | return vscode.workspace.textDocuments.find( 71 | doc => doc.isDirty && doc.fileName === filePath && isSupportedFile(filePath) 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Vscode Live Server++ (BETA)

5 |

It's Truly Live

6 | 7 | 8 | [![VSCode Marketplace](https://img.shields.io/vscode-marketplace/v/ritwickdey.vscode-live-server-plus-plus.svg?style=flat-square&label=vscode%20marketplace)](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [![Total Installs](https://img.shields.io/vscode-marketplace/d/ritwickdey.vscode-live-server-plus-plus.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [![Avarage Rating](https://img.shields.io/vscode-marketplace/r/ritwickdey.vscode-live-server-plus-plus.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [![Travis branch](https://img.shields.io/travis/com/ritwickdey/vscode-live-server-plus-plus/master.svg?style=flat-square&label=travis%20branch)](https://travis-ci.com/ritwickdey/vscode-live-server-plus-plus) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/ritwickdey/vscode-live-server-plus-plus/) 9 | 10 | --- 11 | 12 | ![VSCode Live Server++](./images/vscode-live-server-plus-plus_preview1.gif) 13 | 14 | --- 15 | ## Features 16 | 17 | - **No Need to save HTML, CSS, JS** :smile: 18 | - **No Browser full reload** (for HTML & CSS) 19 | - Customizable Server Root 20 | - Customizable Server Port 21 | - Customizable reloading time 22 | - Customizable index file (e.g `index.html`) 23 | - Auto Browser open (Mozila, Chrome & Edge) 24 | - Control from statusbar 25 | 26 | --- 27 | 28 | ## Downside 29 | 30 | - `Live Server++` will work well if your project only contents `css` & `html` and minimal `JavaScript`. If you do lot of DOM Manupulation with JavaScript, `Live Server++` is not recommended. 31 | 32 | --- 33 | ## How to Start/Stop Server ? 34 | 35 | 1. Open a project and click to `Go Live++` from the status bar to turn the server on/off. 36 | 37 | 2. Open the Command Pallete by pressing `F1` or `ctrl+shift+P` and type `Live Server++: Open Server` to start a server or type `Live Server++: Close Server` to stop a server. 38 | 39 | --- 40 | 41 | ## Settings 42 | 43 | [Click here to read settings Docs](./docs/settings.md). 44 | 45 | ## What's new ? 46 | 47 | - ### v0.0.1 (##DATE##) 48 | - Initial release 49 | - hot Reload supported 50 | - No need to save 51 | - 5 settings are added (Port, Root, indexFile, timeout, browser) 52 | 53 | --- 54 | 55 | ## Changelog 56 | 57 | To check full changelog [click here](CHANGELOG.md). 58 | 59 | --- 60 | 61 | ## Why `Live Server++` when there is a `Live Server` ? 62 | 63 | Actually, I was receiving a lot of emails, PR, comments (and also there was few issue request, e.g. [#12080](https://github.com/Microsoft/vscode/issues/12080)) - `why auto reload only happens when we save the file`? - `why it's not realtime?`... blah blah.... 64 | 65 | Well, in Live Server Extension, I'm using a popular npm module (named `live-server`) and it's the core library of Live Server. _(yaa! too many "Live Server" 😜)_. In the way it's working - it never possible auto reload without saving the file. 66 | 67 | And yaa, to be honest, when I made (in mid of `2017`) the live server extension, I didn't know Node.js or JavaScript well _(Hold on! I still don't know `Node.js` but I'm now confident)_. I even didn't know `promise`/`callback` well. I understood the `callback` _(& `callback hell` too)_ while making the extension. And `Promise`? Only I knew how to use it like `.then().then().then()` and `IIFE`? or `closure`? - I didn't even hear about those names at that time. 😬 68 | 69 | Okay, now coming to the point, Code of the `Live Server` can't be migrated with `Live Server++`. `Live Server++` is not depended on `live-server`(the npm module) - I've written the server side code from scratch & it has minimal dependency (still under development). 70 | 71 | --- 72 | 73 | ## LICENSE 74 | 75 | This extension is licensed under the [MIT License](LICENSE) 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-live-server-plus-plus", 3 | "displayName": "Live Server++", 4 | "description": "Static Server for your HTML CSS Project. It's Truly Live", 5 | "version": "0.0.1", 6 | "publisher": "ritwickdey", 7 | "engines": { 8 | "vscode": "^1.33.0" 9 | }, 10 | "author": { 11 | "name": "Ritwick Dey", 12 | "email": "ritwickdey@outlook.com", 13 | "url": "https://ritwickdey.github.io" 14 | }, 15 | "icon": "images/vscode-live-server-plus-plus.png", 16 | "categories": [ 17 | "Other" 18 | ], 19 | "preview": true, 20 | "galleryBanner": { 21 | "color": "#3c1c59", 22 | "theme": "dark" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/ritwickdey/vscode-live-server-plus-plus/issues", 26 | "email": "ritwickdey@outlook.com" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/ritwickdey/vscode-live-server-plus-plus.git" 31 | }, 32 | "homepage": "https://ritwickdey.github.io/vscode-live-server-plus-plus", 33 | "activationEvents": [ 34 | "*" 35 | ], 36 | "scripts": { 37 | "vscode:prepublish": "npm run compile", 38 | "copy-assets": "cpx \"./src/core/assets/**/*\" \"./out/core/assets\"", 39 | "copy-assets:watch": "npm run copy-assets -- --w", 40 | "compile": "tsc -p ./ && npm run copy-assets", 41 | "watch": "npm run copy-assets && tsc -watch -p ./", 42 | "postinstall": "node ./node_modules/vscode/bin/install", 43 | "test": "npm run compile && node ./node_modules/vscode/bin/test" 44 | }, 45 | "main": "./out/extension/index.js", 46 | "contributes": { 47 | "commands": [ 48 | { 49 | "command": "extension.live-server++.open", 50 | "title": "Live Server++ : Open Server" 51 | }, 52 | { 53 | "command": "extension.live-server++.close", 54 | "title": "Live Server++ : Close Server" 55 | } 56 | ], 57 | "configuration": { 58 | "title": "LiveServer++", 59 | "properties": { 60 | "liveServer++.port": { 61 | "type": "number", 62 | "default": 5555, 63 | "minimum": 0, 64 | "maximum": 65535, 65 | "description": "Use 0 for random port." 66 | }, 67 | "liveServer++.browser": { 68 | "type": [ 69 | "string", 70 | "null" 71 | ], 72 | "default": "default", 73 | "enum": [ 74 | "default", 75 | "chrome", 76 | "firefox", 77 | "microsoft-edge", 78 | null 79 | ], 80 | "description": "Set your favorite browser" 81 | }, 82 | "liveServer++.root": { 83 | "type": "string", 84 | "default": "./", 85 | "pattern": "./|/[^\\/]", 86 | "description": "Change root of Live Server.\nE.g.: ./subfolder1/subfolder2" 87 | }, 88 | "liveServer++.timeout": { 89 | "type": "number", 90 | "default": 300, 91 | "description": "In millisecond." 92 | }, 93 | "liveServer++.indexFile": { 94 | "type": "string", 95 | "default": "index.html", 96 | "description": "Index File of server" 97 | }, 98 | "liveServer++.reloadingStrategy": { 99 | "type": "string", 100 | "default": "hot", 101 | "enum": [ 102 | "hot", 103 | "partial-reload", 104 | "reload" 105 | ], 106 | "description": "Reloading Strategy.\n'hot' = Inplace DOM update[Experimental] \n 'partial-reload' = Reload DOM without refreshing page\n 'reload'= Full page reload" 107 | } 108 | } 109 | } 110 | }, 111 | "devDependencies": { 112 | "@types/mime-types": "^2.1.0", 113 | "@types/mocha": "^2.2.42", 114 | "@types/node": "^10.12.21", 115 | "@types/open": "^6.1.0", 116 | "@types/ws": "^6.0.1", 117 | "cpx": "^1.5.0", 118 | "tslint": "^5.12.1", 119 | "typescript": "^3.3.1", 120 | "vscode": "^1.1.28" 121 | }, 122 | "dependencies": { 123 | "mime-types": "^2.1.22", 124 | "open": "^6.1.0", 125 | "ws": "^6.2.1" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/core/assets/inject/live-reload.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | window.__live_server_log__ = []; 3 | 4 | const storageKeyIsThisFirstTime = 'IsThisFirstTime_Log_From_LiveServer++'; 5 | const { DiffDOM } = diffDOM; 6 | const dd = new DiffDOM({ 7 | trimNodeTextValue: true 8 | }); 9 | const bodyRegex = /*>((.|[\n\r])*)<\/body>/im; // https://stackoverflow.com/a/3642850/6120338 10 | const log = (...args) => window.__live_server_log__.push(...args); 11 | 12 | window.addEventListener('DOMContentLoaded', () => { 13 | if (!('WebSocket' in window)) { 14 | return console.error( 15 | 'Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.' 16 | ); 17 | } 18 | 19 | const protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://'; 20 | const address = protocol + window.location.host + '/_ws_lspp'; 21 | const socket = new WebSocket(address); 22 | 23 | socket.onmessage = function(msg) { 24 | const res = JSON.parse(msg.data); 25 | const { action, data } = res; 26 | if (action === 'refreshcss') return refreshCSS(); 27 | if (action === 'reload') return fullBrowserReload(); 28 | if (action === 'hot') return updateDOM(data.dom); 29 | if (action === 'partial-reload') return fullHTMLRerender(data.dom); 30 | }; 31 | 32 | socket.onopen = event => { 33 | log(event); 34 | socket.send(JSON.stringify({ watchList: getWatchList() })); 35 | if (!sessionStorage.getItem(storageKeyIsThisFirstTime)) { 36 | console.log('Live Server++: connected!'); 37 | sessionStorage.setItem(storageKeyIsThisFirstTime, true); 38 | } 39 | }; 40 | 41 | socket.onerror = event => { 42 | log(event); 43 | console.log(`Live Server++: Opps! Can't able to connect.`); 44 | }; 45 | }); 46 | 47 | function getWatchList() { 48 | return [window.location.pathname]; 49 | } 50 | 51 | function updateDOM(html) { 52 | tryOneOf(onDemandHTMLRender, fullHTMLRerender, fullBrowserReload)(html); 53 | } 54 | 55 | function fullHTMLRerender(html) { 56 | const body = bodyRegex.exec(html)[0]; 57 | const template = document.createElement('body'); 58 | template.innerHTML = body; 59 | document.body.replaceWith(template); 60 | } 61 | 62 | function onDemandHTMLRender(html) { 63 | const newBody = bodyRegex.exec(html)[0]; 64 | const diff = dd.diff(document.body, newBody); 65 | const result = dd.apply(document.body, diff); 66 | if (!result) throw "Can't able to update DOM"; 67 | } 68 | 69 | function fullBrowserReload() { 70 | window.location.reload(); 71 | } 72 | 73 | function tryOneOf(...fns) { 74 | return (...args) => { 75 | for (let i = 0; i < fns.length; i++) { 76 | const fn = fns[i]; 77 | try { 78 | fn(...args); 79 | break; 80 | } catch (error) { 81 | log(error); 82 | } 83 | } 84 | }; 85 | } 86 | 87 | function isSameUrl(url1, url2) { 88 | if (!url1 || url1 === '/') url1 = 'index.html'; 89 | if (!url2 || url2 === '/') url2 = 'index.html'; 90 | 91 | if (url1.startsWith('/')) url1 = url1.substr(1); 92 | if (url2.startsWith('/')) url2 = url2.substr(1); 93 | 94 | return url1 === url2; 95 | } 96 | 97 | // THIS FUNCTION IS MODIFIED FROM `https://www.npmjs.com/package/live-server` 98 | function refreshCSS() { 99 | const sheets = [].slice.call(document.getElementsByTagName('link')); 100 | const head = document.getElementsByTagName('head')[0]; 101 | for (let i = 0; i < sheets.length; ++i) { 102 | const elem = sheets[i]; 103 | 104 | const href = elem.getAttribute('href'); 105 | if (!href || href.startsWith('http')) continue; 106 | 107 | const parent = elem.parentElement || head; 108 | parent.removeChild(elem); 109 | const rel = elem.rel; 110 | if ( 111 | (href && typeof rel != 'string') || 112 | rel.length == 0 || 113 | rel.toLowerCase() == 'stylesheet' 114 | ) { 115 | const url = href.replace(/(&|\?)_cacheOverride=\d+/, ''); 116 | elem.setAttribute( 117 | 'href', 118 | url + 119 | (url.indexOf('?') >= 0 ? '&' : '?') + 120 | '_cacheOverride=' + 121 | new Date().valueOf() 122 | ); 123 | } 124 | parent.appendChild(elem); 125 | } 126 | } 127 | 128 | function refreshJS() { 129 | const links = [...document.querySelectorAll('script[src]')].filter(e => { 130 | if (!e.getAttribute || e.getAttribute('data-live-server-ignore')) 131 | return false; 132 | const src = e.getAttribute('src') || ''; 133 | return !src.startsWith('http'); // Target links are local scripts 134 | }); 135 | const body = document.querySelector('body'); 136 | for (let i = 0; i < links.length; ++i) { 137 | const link = links[i]; 138 | const parent = link.parentElement || body; 139 | parent.removeChild(link); 140 | 141 | setTimeout(() => { 142 | const src = link.getAttribute('src'); 143 | const newLink = document.createElement('script'); 144 | link.getAttributeNames().forEach(name => { 145 | newLink.setAttribute(name, link.getAttribute(name)); 146 | }); 147 | 148 | if (src) { 149 | var url = src.replace(/(&|\?)_cacheOverride=\d+/, ''); 150 | newLink.src = 151 | url + 152 | (url.indexOf('?') >= 0 ? '&' : '?') + 153 | '_cacheOverride=' + 154 | new Date().valueOf(); 155 | } 156 | 157 | parent.appendChild(newLink); 158 | }, 50); 159 | } 160 | } 161 | })(); 162 | -------------------------------------------------------------------------------- /images/vscode-live-server-plus-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | background 10 | 11 | 12 | 13 | Layer 1 14 | 15 | 16 | 17 | 18 | 19 | 36 | 37 | 38 | background 39 | 40 | 41 | 42 | Layer 1 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/core/LiveServerPlusPlus.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as http from 'http'; 3 | import * as WebSocket from 'ws'; 4 | import * as path from 'path'; 5 | import { IncomingMessage, ServerResponse } from 'http'; 6 | import { readFileStream } from './FileSystem'; 7 | import { INJECTED_TEXT, isInjectableFile } from './utils'; 8 | import { 9 | ILiveServerPlusPlus, 10 | GoOfflineEvent, 11 | GoLiveEvent, 12 | ServerErrorEvent, 13 | IMiddlewareTypes, 14 | ILiveServerPlusPlusServiceCtor, 15 | ILSPPIncomingMessage, 16 | ILiveServerPlusPlusConfig 17 | } from './types'; 18 | import { LSPPError } from './LSPPError'; 19 | import { urlJoin } from '../extension/utils/urlJoin'; 20 | import { ReloadingStrategy } from '../extension/utils/extensionConfig'; 21 | 22 | interface IWsWatcher { 23 | watchingPaths: string[]; //relative paths 24 | client: WebSocket; 25 | } 26 | 27 | type BroadcastActions = 'hot' | 'partial-reload' | 'reload' | 'refreshcss'; 28 | 29 | export class LiveServerPlusPlus implements ILiveServerPlusPlus { 30 | port!: number; 31 | private cwd: string | undefined; 32 | private server: http.Server | undefined; 33 | private ws: WebSocket.Server | undefined; 34 | private indexFile!: string; 35 | private debounceTimeout!: number; 36 | private reloadingStrategy!: ReloadingStrategy; 37 | private goLiveEvent: vscode.EventEmitter; 38 | private goOfflineEvent: vscode.EventEmitter; 39 | private serverErrorEvent: vscode.EventEmitter; 40 | private middlewares: IMiddlewareTypes[] = []; 41 | private wsWatcherList: IWsWatcher[] = []; 42 | 43 | constructor(config: ILiveServerPlusPlusConfig) { 44 | this.init(config); 45 | this.goLiveEvent = new vscode.EventEmitter(); 46 | this.goOfflineEvent = new vscode.EventEmitter(); 47 | this.serverErrorEvent = new vscode.EventEmitter(); 48 | } 49 | 50 | get onDidGoLive() { 51 | return this.goLiveEvent.event; 52 | } 53 | 54 | get onDidGoOffline() { 55 | return this.goOfflineEvent.event; 56 | } 57 | 58 | get onServerError() { 59 | return this.serverErrorEvent.event; 60 | } 61 | 62 | get isServerRunning() { 63 | return this.server ? this.server!.listening : false; 64 | } 65 | 66 | reloadConfig(config: ILiveServerPlusPlusConfig) { 67 | this.init(config); 68 | } 69 | 70 | async goLive() { 71 | if (this.isServerRunning) { 72 | return this.serverErrorEvent.fire({ 73 | LSPP: this, 74 | code: 'serverIsAlreadyRunning', 75 | message: 'Server is already running' 76 | }); 77 | } 78 | try { 79 | await this.listenServer(); 80 | this.registerOnChangeReload(); 81 | this.goLiveEvent.fire({ LSPP: this }); 82 | } catch (error) { 83 | if (error.code === 'EADDRINUSE') { 84 | return this.serverErrorEvent.fire({ 85 | LSPP: this, 86 | code: 'portAlreadyInUse', 87 | message: `${this.port} is already in use!` 88 | }); 89 | } 90 | 91 | return this.serverErrorEvent.fire({ 92 | LSPP: this, 93 | code: error.code, 94 | message: error.message 95 | }); 96 | } 97 | } 98 | 99 | async shutdown() { 100 | if (!this.isServerRunning) { 101 | return this.serverErrorEvent.fire({ 102 | LSPP: this, 103 | code: 'serverIsNotRunning', 104 | message: 'Server is not running' 105 | }); 106 | } 107 | await this.closeWs(); 108 | await this.closeServer(); 109 | this.goOfflineEvent.fire({ LSPP: this }); 110 | } 111 | 112 | useMiddleware(...fns: IMiddlewareTypes[]) { 113 | fns.forEach(fn => this.middlewares.push(fn)); 114 | } 115 | 116 | useService(...fns: ILiveServerPlusPlusServiceCtor[]) { 117 | fns.forEach(fn => { 118 | const instance = new fn(this); 119 | instance.register.call(instance); 120 | }); 121 | } 122 | 123 | private init(config: ILiveServerPlusPlusConfig) { 124 | this.cwd = config.cwd; 125 | this.indexFile = config.indexFile || 'index.html'; 126 | this.port = config.port || 9000; 127 | this.debounceTimeout = config.debounceTimeout || 400; 128 | this.reloadingStrategy = config.reloadingStrategy || 'hot'; 129 | } 130 | 131 | private registerOnChangeReload() { 132 | let timeout: NodeJS.Timeout; 133 | vscode.workspace.onDidChangeTextDocument(event => { 134 | //debouncing 135 | clearTimeout(timeout); 136 | timeout = setTimeout(() => { 137 | const fileName = event.document.fileName; 138 | const action = this.getReloadingActionType(fileName); 139 | const filePathFromRoot = urlJoin(fileName.replace(this.cwd!, '')); // bit tricky. This will change Windows's \ to / 140 | this.broadcastWs( 141 | { 142 | dom: 143 | ['hot', 'partial-reload'].indexOf(action) !== -1 144 | ? event.document.getText() 145 | : undefined, 146 | fileName: filePathFromRoot 147 | }, 148 | action 149 | ); 150 | }, this.debounceTimeout); 151 | }); 152 | } 153 | 154 | private getReloadingActionType(fileName: string): BroadcastActions { 155 | const extName = path.extname(fileName); 156 | const isCSS = extName === '.css'; 157 | const isInjectable = isInjectableFile(fileName); 158 | 159 | if (isCSS) { 160 | return 'refreshcss'; 161 | } 162 | 163 | if (isInjectable) { 164 | return this.reloadingStrategy; 165 | } 166 | 167 | return 'reload'; 168 | } 169 | 170 | private listenServer() { 171 | return new Promise((resolve, reject) => { 172 | if (!this.cwd) { 173 | const error = new LSPPError('CWD is not defined', 'cwdUndefined'); 174 | return reject(error); 175 | } 176 | 177 | this.server = http.createServer(this.routesHandler.bind(this)); 178 | 179 | const onPortError = reject; 180 | this.server.on('error', onPortError); 181 | 182 | this.attachWSListeners(); 183 | this.server.listen(this.port, () => { 184 | this.server!.removeListener('error', onPortError); 185 | resolve(); 186 | }); 187 | }); 188 | } 189 | 190 | private closeServer() { 191 | return new Promise((resolve, reject) => { 192 | this.server!.close(err => { 193 | return err ? reject(err) : resolve(); 194 | }); 195 | this.server!.emit('close'); 196 | }); 197 | } 198 | 199 | private closeWs() { 200 | return new Promise((resolve, reject) => { 201 | if (!this.ws) return resolve(); 202 | this.ws.close(err => (err ? reject(err) : resolve())); 203 | }); 204 | } 205 | 206 | private broadcastWs( 207 | data: { dom?: string; fileName: string }, 208 | action: BroadcastActions = 'reload' 209 | ) { 210 | if (!this.ws) return; 211 | 212 | let clients: WebSocket[] = this.ws.clients as any; 213 | 214 | //TODO: WE SHOULD WATCH ALL FILE. FOR NOW, THE LIB WORKS ONLY FOR HTML 215 | if (isInjectableFile(data.fileName)) { 216 | clients = this.wsWatcherList.reduce( 217 | (allClients, { client, watchingPaths }) => { 218 | if (this.isInWatchingList(data.fileName, watchingPaths)) 219 | allClients.push(client); 220 | return allClients; 221 | }, 222 | [] as WebSocket[] 223 | ); 224 | } 225 | 226 | clients.forEach(client => { 227 | if (client.readyState === WebSocket.OPEN) { 228 | client.send(JSON.stringify({ data, action })); 229 | } 230 | }); 231 | } 232 | 233 | isInWatchingList(target: string, dirList: string[]) { 234 | for (let i = 0; i < dirList.length; i++) { 235 | let dir = dirList[i]; 236 | 237 | //TODO: THIS IS NOT THE BEST WAY. IF FOLDER CONTANTS `.`, this will not work 238 | if (!path.extname(dir)) { 239 | dir = urlJoin(dir, this.indexFile); 240 | } 241 | 242 | if (target.startsWith('/')) target = target.substr(1); 243 | if (dir.startsWith('/')) dir = dir.substr(1); 244 | 245 | if (dir === target) { 246 | return true; 247 | } 248 | } 249 | 250 | return false; 251 | } 252 | 253 | private attachWSListeners() { 254 | if (!this.server) throw new Error('Server is not defined'); 255 | 256 | this.ws = new WebSocket.Server({ noServer: true }); 257 | 258 | this.ws.on('connection', ws => { 259 | ws.send(JSON.stringify({ action: 'connected' })); 260 | ws.on('message', (_data: string) => { 261 | const { watchList } = JSON.parse(_data); 262 | if (watchList) { 263 | this.addToWsWatcherList(ws as any, watchList); 264 | } 265 | }); 266 | ws.on('close', () => this.removeFromWsWatcherList(ws as any)); 267 | }); 268 | 269 | this.ws.on('close', () => { 270 | console.log('disconnected'); 271 | }); 272 | 273 | this.server.on('upgrade', (request, socket, head) => { 274 | if (request.url === '/_ws_lspp') { 275 | this.ws!.handleUpgrade(request, socket, head, ws => { 276 | this.ws!.emit('connection', ws, request); 277 | }); 278 | } else { 279 | socket.destroy(); 280 | } 281 | }); 282 | } 283 | 284 | private removeFromWsWatcherList(client: WebSocket) { 285 | const index = this.wsWatcherList.findIndex(e => e.client === client); 286 | if (index !== -1) { 287 | this.wsWatcherList.splice(index, 1); 288 | } 289 | } 290 | 291 | private addToWsWatcherList(client: WebSocket, watchDirs: string | string[]) { 292 | const _watchDirs = Array.isArray(watchDirs) ? watchDirs : [watchDirs]; 293 | 294 | this.wsWatcherList.push({ client, watchingPaths: _watchDirs }); 295 | } 296 | 297 | private applyMiddlware(req: IncomingMessage, res: ServerResponse) { 298 | this.middlewares.forEach(middleware => { 299 | middleware(req, res); 300 | }); 301 | } 302 | 303 | private routesHandler(req: ILSPPIncomingMessage, res: ServerResponse) { 304 | const cwd = this.cwd; 305 | if (!cwd) return res.end('Root Path is missing'); 306 | 307 | this.applyMiddlware(req, res); 308 | 309 | const file = req.file!; //file comes from one of middlware 310 | const filePath = path.isAbsolute(file) ? file : path.join(cwd!, file); 311 | const contentType = req.contentType || ''; 312 | const fileStream = readFileStream( 313 | filePath, 314 | contentType.indexOf('image') !== -1 ? undefined : 'utf8' 315 | ); 316 | 317 | fileStream.on('open', () => { 318 | // TOOD: MAY BE, WE SHOULD INJECT IT INSIDE TAG (although browser are not smart enought) 319 | if (isInjectableFile(filePath)) res.write(INJECTED_TEXT); 320 | fileStream.pipe(res); 321 | }); 322 | 323 | fileStream.on('error', err => { 324 | console.error('ERROR ', err); 325 | res.statusCode = err.code === 'ENOENT' ? 404 : 500; 326 | return res.end(null); 327 | }); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/core/assets/inject/diffDOM.js: -------------------------------------------------------------------------------- 1 | var diffDOM=function(e){"use strict";function t(e,o,n){var s;return"#text"===e.nodeName?s=n.document.createTextNode(e.data):"#comment"===e.nodeName?s=n.document.createComment(e.data):("svg"===e.nodeName||o?(s=n.document.createElementNS("http://www.w3.org/2000/svg",e.nodeName),o=!0):s=n.document.createElement(e.nodeName),e.attributes&&Object.entries(e.attributes).forEach(function(e){var t=e[0],o=e[1];return s.setAttribute(t,o)}),e.childNodes&&e.childNodes.forEach(function(e){return s.appendChild(t(e,o,n))}),n.valueDiffing&&(e.value&&(s.value=e.value),e.checked&&(s.checked=e.checked),e.selected&&(s.selected=e.selected))),s}function o(e,t){for(t=t.slice();t.length>0;){if(!e.childNodes)return!1;var o=t.splice(0,1)[0];e=e.childNodes[o]}return e}function n(e,n,s){var i,a,l,c,r=o(e,n[s._const.route]),u={diff:n,node:r};if(s.preDiffApply(u))return!0;switch(n[s._const.action]){case s._const.addAttribute:if(!r||!r.setAttribute)return!1;r.setAttribute(n[s._const.name],n[s._const.value]);break;case s._const.modifyAttribute:if(!r||!r.setAttribute)return!1;r.setAttribute(n[s._const.name],n[s._const.newValue]),"INPUT"===r.nodeName&&"value"===n[s._const.name]&&(r.value=n[s._const.newValue]);break;case s._const.removeAttribute:if(!r||!r.removeAttribute)return!1;r.removeAttribute(n[s._const.name]);break;case s._const.modifyTextElement:if(!r||3!==r.nodeType)return!1;s.textDiff(r,r.data,n[s._const.oldValue],n[s._const.newValue]);break;case s._const.modifyValue:if(!r||void 0===r.value)return!1;r.value=n[s._const.newValue];break;case s._const.modifyComment:if(!r||void 0===r.data)return!1;s.textDiff(r,r.data,n[s._const.oldValue],n[s._const.newValue]);break;case s._const.modifyChecked:if(!r||void 0===r.checked)return!1;r.checked=n[s._const.newValue];break;case s._const.modifySelected:if(!r||void 0===r.selected)return!1;r.selected=n[s._const.newValue];break;case s._const.replaceElement:r.parentNode.replaceChild(t(n[s._const.newValue],"http://www.w3.org/2000/svg"===r.namespaceURI,s),r);break;case s._const.relocateGroup:Array.apply(void 0,new Array(n.groupLength)).map(function(){return r.removeChild(r.childNodes[n[s._const.from]])}).forEach(function(e,t){0===t&&(a=r.childNodes[n[s._const.to]]),r.insertBefore(e,a||null)});break;case s._const.removeElement:r.parentNode.removeChild(r);break;case s._const.addElement:c=(l=n[s._const.route].slice()).splice(l.length-1,1)[0],(r=o(e,l)).insertBefore(t(n[s._const.element],"http://www.w3.org/2000/svg"===r.namespaceURI,s),r.childNodes[c]||null);break;case s._const.removeTextElement:if(!r||3!==r.nodeType)return!1;r.parentNode.removeChild(r);break;case s._const.addTextElement:if(c=(l=n[s._const.route].slice()).splice(l.length-1,1)[0],i=s.document.createTextNode(n[s._const.value]),!(r=o(e,l))||!r.childNodes)return!1;r.insertBefore(i,r.childNodes[c]||null);break;default:console.log("unknown action")}return u.newNode=i,s.postDiffApply(u),!0}function s(e,t,o){var n=e[t];e[t]=e[o],e[o]=n}function i(e,t,o){t.length||(t=[t]),(t=t.slice()).reverse(),t.forEach(function(t){!function(e,t,o){switch(t[o._const.action]){case o._const.addAttribute:t[o._const.action]=o._const.removeAttribute,n(e,t,o);break;case o._const.modifyAttribute:s(t,o._const.oldValue,o._const.newValue),n(e,t,o);break;case o._const.removeAttribute:t[o._const.action]=o._const.addAttribute,n(e,t,o);break;case o._const.modifyTextElement:case o._const.modifyValue:case o._const.modifyComment:case o._const.modifyChecked:case o._const.modifySelected:case o._const.replaceElement:s(t,o._const.oldValue,o._const.newValue),n(e,t,o);break;case o._const.relocateGroup:s(t,o._const.from,o._const.to),n(e,t,o);break;case o._const.removeElement:t[o._const.action]=o._const.addElement,n(e,t,o);break;case o._const.addElement:t[o._const.action]=o._const.removeElement,n(e,t,o);break;case o._const.removeTextElement:t[o._const.action]=o._const.addTextElement,n(e,t,o);break;case o._const.addTextElement:t[o._const.action]=o._const.removeTextElement,n(e,t,o);break;default:console.log("unknown action")}}(e,t,o)})}var a=function(e){var t=this;void 0===e&&(e={}),Object.entries(e).forEach(function(e){var o=e[0],n=e[1];return t[o]=n})};function l(e){var t=[];return"#text"!==e.nodeName&&"#comment"!==e.nodeName&&(t.push(e.nodeName),e.attributes&&(e.attributes.class&&t.push(e.nodeName+"."+e.attributes.class.replace(/ /g,".")),e.attributes.id&&t.push(e.nodeName+"#"+e.attributes.id))),t}function c(e){var t={},o={};return e.forEach(function(e){l(e).forEach(function(e){var n=e in t;n||e in o?n&&(delete t[e],o[e]=!0):t[e]=!0})}),t}function r(e,t){var o=c(e),n=c(t),s={};return Object.keys(o).forEach(function(e){n[e]&&(s[e]=!0)}),s}function u(e){return delete e.outerDone,delete e.innerDone,delete e.valueDone,!e.childNodes||e.childNodes.every(u)}function d(e,t){if(!["nodeName","value","checked","selected","data"].every(function(o){return e[o]===t[o]}))return!1;if(Boolean(e.attributes)!==Boolean(t.attributes))return!1;if(Boolean(e.childNodes)!==Boolean(t.childNodes))return!1;if(e.attributes){var o=Object.keys(e.attributes),n=Object.keys(t.attributes);if(o.length!==n.length)return!1;if(!o.every(function(o){return e.attributes[o]===t.attributes[o]}))return!1}if(e.childNodes){if(e.childNodes.length!==t.childNodes.length)return!1;if(!e.childNodes.every(function(e,o){return d(e,t.childNodes[o])}))return!1}return!0}function h(e,t,o,n,s){if(!e||!t)return!1;if(e.nodeName!==t.nodeName)return!1;if("#text"===e.nodeName)return!!s||e.data===t.data;if(e.nodeName in o)return!0;if(e.attributes&&t.attributes){if(e.attributes.id){if(e.attributes.id!==t.attributes.id)return!1;if(e.nodeName+"#"+e.attributes.id in o)return!0}if(e.attributes.class&&e.attributes.class===t.attributes.class)if(e.nodeName+"."+e.attributes.class.replace(/ /g,".")in o)return!0}if(n)return!0;var i=e.childNodes?e.childNodes.slice().reverse():[],a=t.childNodes?t.childNodes.slice().reverse():[];if(i.length!==a.length)return!1;if(s)return i.every(function(e,t){return e.nodeName===a[t].nodeName});var l=r(i,a);return i.every(function(e,t){return h(e,a[t],l,!0,!0)})}function f(e){return JSON.parse(JSON.stringify(e))}function p(e,t,o,n){var s=0,i=[],a=e.length,c=t.length,u=Array.apply(void 0,new Array(a+1)).map(function(){return[]}),d=r(e,t),f=a===c;f&&e.some(function(e,o){var n=l(e),s=l(t[o]);return n.length!==s.length?(f=!1,!0):(n.some(function(e,t){if(e!==s[t])return f=!1,!0}),!f||void 0)});for(var p=0;p=s&&(s=u[p+1][_+1],i=[p+1,_+1]))}return 0!==s&&{oldValue:i[0]-s,newValue:i[1]-s,length:s}}function m(e,t){return Array.apply(void 0,new Array(e)).map(function(){return t})}a.prototype.toString=function(){return JSON.stringify(this)},a.prototype.setValue=function(e,t){return this[e]=t,this};var _=function(){this.list=[]};function V(e,t){var o,n,s=e;for(t=t.slice();t.length>0;){if(!s.childNodes)return!1;n=t.splice(0,1)[0],o=s,s=s.childNodes[n]}return{node:s,parentNode:o,nodeIndex:n}}function g(e,t,o){return t.forEach(function(t){!function(e,t,o){var n,s,i,a=V(e,t[o._const.route]),l=a.node,c=a.parentNode,r=a.nodeIndex,u=[],d={diff:t,node:l};if(o.preDiffApply(d))return!0;switch(t[o._const.action]){case o._const.addAttribute:l.attributes||(l.attributes={}),l.attributes[t[o._const.name]]=t[o._const.value],"checked"===t[o._const.name]?l.checked=!0:"selected"===t[o._const.name]?l.selected=!0:"INPUT"===l.nodeName&&"value"===t[o._const.name]&&(l.value=t[o._const.value]);break;case o._const.modifyAttribute:l.attributes[t[o._const.name]]=t[o._const.newValue];break;case o._const.removeAttribute:delete l.attributes[t[o._const.name]],0===Object.keys(l.attributes).length&&delete l.attributes,"checked"===t[o._const.name]?l.checked=!1:"selected"===t[o._const.name]?delete l.selected:"INPUT"===l.nodeName&&"value"===t[o._const.name]&&delete l.value;break;case o._const.modifyTextElement:l.data=t[o._const.newValue];break;case o._const.modifyValue:l.value=t[o._const.newValue];break;case o._const.modifyComment:l.data=t[o._const.newValue];break;case o._const.modifyChecked:l.checked=t[o._const.newValue];break;case o._const.modifySelected:l.selected=t[o._const.newValue];break;case o._const.replaceElement:(n=f(t[o._const.newValue])).outerDone=!0,n.innerDone=!0,n.valueDone=!0,c.childNodes[r]=n;break;case o._const.relocateGroup:l.childNodes.splice(t[o._const.from],t.groupLength).reverse().forEach(function(e){return l.childNodes.splice(t[o._const.to],0,e)}),l.subsets&&l.subsets.forEach(function(e){if(t[o._const.from]t[o._const.from]){e.oldValue-=t.groupLength;var n=e.oldValue+e.length-t[o._const.to];n>0&&(u.push({oldValue:t[o._const.to]+t.groupLength,newValue:e.newValue+e.length-n,length:n}),e.length-=n)}else if(t[o._const.from]>t[o._const.to]&&e.oldValue>t[o._const.to]&&e.oldValue0&&(u.push({oldValue:t[o._const.to]+t.groupLength,newValue:e.newValue+e.length-s,length:s}),e.length-=s)}else e.oldValue===t[o._const.from]&&(e.oldValue=t[o._const.to])});break;case o._const.removeElement:c.childNodes.splice(r,1),c.subsets&&c.subsets.forEach(function(e){e.oldValue>r?e.oldValue-=1:e.oldValue===r?e.delete=!0:e.oldValuer&&(e.oldValue+e.length-1===r?e.length--:(u.push({newValue:e.newValue+r-e.oldValue,oldValue:r,length:e.length-r+e.oldValue-1}),e.length=r-e.oldValue))}),l=c;break;case o._const.addElement:s=t[o._const.route].slice(),i=s.splice(s.length-1,1)[0],l=V(e,s).node,(n=f(t[o._const.element])).outerDone=!0,n.innerDone=!0,n.valueDone=!0,l.childNodes||(l.childNodes=[]),i>=l.childNodes.length?l.childNodes.push(n):l.childNodes.splice(i,0,n),l.subsets&&l.subsets.forEach(function(e){if(e.oldValue>=i)e.oldValue+=1;else if(e.oldValuei){var t=e.oldValue+e.length-i;u.push({newValue:e.newValue+e.length-t,oldValue:i+1,length:t}),e.length-=t}});break;case o._const.removeTextElement:c.childNodes.splice(r,1),"TEXTAREA"===c.nodeName&&delete c.value,c.subsets&&c.subsets.forEach(function(e){e.oldValue>r?e.oldValue-=1:e.oldValue===r?e.delete=!0:e.oldValuer&&(e.oldValue+e.length-1===r?e.length--:(u.push({newValue:e.newValue+r-e.oldValue,oldValue:r,length:e.length-r+e.oldValue-1}),e.length=r-e.oldValue))}),l=c;break;case o._const.addTextElement:s=t[o._const.route].slice(),i=s.splice(s.length-1,1)[0],(n={}).nodeName="#text",n.data=t[o._const.value],(l=V(e,s).node).childNodes||(l.childNodes=[]),i>=l.childNodes.length?l.childNodes.push(n):l.childNodes.splice(i,0,n),"TEXTAREA"===l.nodeName&&(l.value=t[o._const.newValue]),l.subsets&&l.subsets.forEach(function(e){if(e.oldValue>=i&&(e.oldValue+=1),e.oldValuei){var t=e.oldValue+e.length-i;u.push({newValue:e.newValue+e.length-t,oldValue:i+1,length:t}),e.length-=t}});break;default:console.log("unknown action")}l.subsets&&(l.subsets=l.subsets.filter(function(e){return!e.delete&&e.oldValue!==e.newValue}),u.length&&(l.subsets=l.subsets.concat(u))),d.newNode=n,o.postDiffApply(d)}(e,t,o)}),!0}function v(e,t){void 0===t&&(t={});var o={};if(o.nodeName=e.nodeName,"#text"===o.nodeName||"#comment"===o.nodeName)o.data=t.trimNodeTextValue?e.data.trim():e.data;else{if(e.attributes&&e.attributes.length>0)o.attributes={},Array.prototype.slice.call(e.attributes).forEach(function(e){return o.attributes[e.name]=e.value});if("TEXTAREA"===o.nodeName)o.value=e.value;else if(e.childNodes&&e.childNodes.length>0){o.childNodes=[],Array.prototype.slice.call(e.childNodes).forEach(function(e){return o.childNodes.push(v(e,t))})}t.valueDiffing&&(void 0!==e.checked&&e.type&&["radio","checkbox"].includes(e.type.toLowerCase())?o.checked=e.checked:void 0!==e.value&&(o.value=e.value),void 0!==e.selected&&(o.selected=e.selected))}return o}_.prototype.add=function(e){var t;(t=this.list).push.apply(t,e)},_.prototype.forEach=function(e){this.list.forEach(function(t){return e(t)})};var N=/<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>/g,b=Object.create?Object.create(null):{},y=/([\w-:]+)|(['"])([^'"]*)\2/g,w={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,menuItem:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0};function E(e,t){void 0===t&&(t={components:b});var o,n=[],s=-1,i=[],a={},l=!1;return e.replace(N,function(c,r){if(l){if(c!=="")return;l=!1}var u,d="/"!==c.charAt(1),h=r+c.length,f=e.charAt(h);if(d){if(s++,"tag"===(o=function(e){var t,o=0,n={nodeName:""};return e.replace(y,function(s){o%2?t=s:0===o?((w[s]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.nodeName=s.toUpperCase()):(n.attributes||(n.attributes={}),n.attributes[t]=s.replace(/^['"]|['"]$/g,"")),o++}),n}(c)).type&&t.components[o.nodeName]&&(o.type="component",l=!0),!o.voidElement&&!l&&f&&"<"!==f){o.childNodes||(o.childNodes=[]);var p=e.slice(h,e.indexOf("<",h));o.childNodes.push({nodeName:"#text",data:t.trimNodeTextValue?p.trim():p})}a[o.tagName]=o,0===s&&n.push(o),(u=i[s-1])&&(u.childNodes||(u.childNodes=[]),u.childNodes.push(o)),i[s]=o}if((!d||o.voidElement)&&(s--,!l&&"<"!==f&&f)){u=-1===s?n:i[s].childNodes||[];var m=e.indexOf("<",h),_=e.slice(h,-1===m?void 0:m);u.push({nodeName:"#text",data:t.trimNodeTextValue?_.trim():_})}}),n[0]}function k(e,t){return void 0===t&&(t={}),function e(t){return delete t.voidElement,t.childNodes&&t.childNodes.forEach(function(t){return e(t)}),t}(E(e,t))}var x=function(e,t,o){this.options=o,this.t1=e instanceof HTMLElement?v(e,this.options):"string"==typeof e?k(e,this.options):JSON.parse(JSON.stringify(e)),this.t2=t instanceof HTMLElement?v(t,this.options):"string"==typeof t?k(t,this.options):JSON.parse(JSON.stringify(t)),this.diffcount=0,this.foundAll=!1,this.debug&&(this.t1Orig=v(e,this.options),this.t2Orig=v(t,this.options)),this.tracker=new _};x.prototype.init=function(){return this.findDiffs(this.t1,this.t2)},x.prototype.findDiffs=function(e,t){var o;do{if(this.options.debug&&(this.diffcount+=1,this.diffcount>this.options.diffcap))throw window.diffError=[this.t1Orig,this.t2Orig],new Error("surpassed diffcap:"+JSON.stringify(this.t1Orig)+" -> "+JSON.stringify(this.t2Orig));0===(o=this.findNextDiff(e,t,[])).length&&(d(e,t)||(this.foundAll?console.error("Could not find remaining diffs!"):(this.foundAll=!0,u(e),o=this.findNextDiff(e,t,[])))),o.length>0&&(this.foundAll=!1,this.tracker.add(o),g(e,o,this.options))}while(o.length>0);return this.tracker.list},x.prototype.findNextDiff=function(e,t,o){var n,s;if(this.options.maxDepth&&o.length>this.options.maxDepth)return[];if(!e.outerDone){if(n=this.findOuterDiff(e,t,o),this.options.filterOuterDiff&&(s=this.options.filterOuterDiff(e,t,n))&&(n=s),n.length>0)return e.outerDone=!0,n;e.outerDone=!0}if(!e.innerDone){if((n=this.findInnerDiff(e,t,o)).length>0)return n;e.innerDone=!0}if(this.options.valueDiffing&&!e.valueDone){if((n=this.findValueDiff(e,t,o)).length>0)return e.valueDone=!0,n;e.valueDone=!0}return[]},x.prototype.findOuterDiff=function(e,t,o){var n,s,i,l,c,r,u=[];if(e.nodeName!==t.nodeName){if(!o.length)throw new Error("Top level nodes have to be of the same kind.");return[(new a).setValue(this.options._const.action,this.options._const.replaceElement).setValue(this.options._const.oldValue,f(e)).setValue(this.options._const.newValue,f(t)).setValue(this.options._const.route,o)]}if(o.length&&this.options.maxNodeDiffCount0&&(c=this.attemptGroupRelocation(e,t,u,o)).length>0)return c}for(var h=0;hs.length?(c=c.concat([(new a).setValue(this.options._const.action,this.options._const.removeElement).setValue(this.options._const.element,f(_)).setValue(this.options._const.route,o.concat(r))]),n.splice(h,1),r-=1,l-=1):n.lengthu+1&&"#text"===e.childNodes[u+1].nodeName;)if(u+=1,t.childNodes[v].data===e.childNodes[u].data){r=!0;break}if(!r)return g.push((new a).setValue(this.options._const.action,this.options._const.modifyTextElement).setValue(this.options._const.route,n.concat(v)).setValue(this.options._const.oldValue,c.data).setValue(this.options._const.newValue,t.childNodes[v].data)),g}g.push((new a).setValue(this.options._const.action,this.options._const.removeTextElement).setValue(this.options._const.route,n.concat(v)).setValue(this.options._const.value,c.data)),p.splice(v,1),V=Math.min(p.length,_.length),v-=1}else g.push((new a).setValue(this.options._const.action,this.options._const.removeElement).setValue(this.options._const.route,n.concat(v)).setValue(this.options._const.element,f(c))),p.splice(v,1),V=Math.min(p.length,_.length),v-=1;else if(!0===_[v])"#text"===(c=t.childNodes[v]).nodeName?(g.push((new a).setValue(this.options._const.action,this.options._const.addTextElement).setValue(this.options._const.route,n.concat(v)).setValue(this.options._const.value,c.data)),p.splice(v,0,!0),V=Math.min(p.length,_.length),N-=1):(g.push((new a).setValue(this.options._const.action,this.options._const.addElement).setValue(this.options._const.route,n.concat(v)).setValue(this.options._const.element,f(c))),p.splice(v,0,!0),V=Math.min(p.length,_.length),N-=1);else if(p[v]!==_[v]){if(g.length>0)return g;if(l=o[p[v]],(i=Math.min(l.newValue,e.childNodes.length-l.length))!==l.oldValue){s=!1;for(var b=0;b entering "+e,t)},T.prototype.fout=function(e,t){this.log("│<──┘ generated return value",t),this.padding=this.padding.substring(0,this.padding.length-this.pad.length)},T.prototype.format=function(e,t){return function(e){for(e=""+e;e.length<4;)e="0"+e;return e}(t)+"> "+this.padding+e},T.prototype.log=function(){var e=Array.prototype.slice.call(arguments),t=function(e){return e?"string"==typeof e?e:e instanceof HTMLElement?e.outerHTML||"":e instanceof Array?"["+e.map(t).join(",")+"]":e.toString()||e.valueOf()||"":""};e=e.map(t).join(", "),this.messages.push(this.format(e,this.tick++))},T.prototype.toString=function(){for(var e="└───";e.length<=this.padding.length+this.pad.length;)e+="× ";var t=this.padding;return this.padding="",e=this.format(e,this.tick),this.padding=t,this.messages.join("\n")+"\n"+e},e.DiffDOM=D,e.TraceLogger=T,e.nodeToObj=v,e.stringToObj=k,e}({}); 2 | // Forked Copy: https://github.com/ritwickdey/diffDOM . Thanks johanneswilm (https://github.com/johanneswilm) --------------------------------------------------------------------------------