├── .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 | [](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [](https://marketplace.visualstudio.com/items?itemName=ritwickdey.vscode-live-server-plus-plus) [](https://travis-ci.com/ritwickdey/vscode-live-server-plus-plus) [](https://github.com/ritwickdey/vscode-live-server-plus-plus/)
9 |
10 | ---
11 |
12 | 
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 |
--------------------------------------------------------------------------------
/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!==""+o.nodeName+">")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)
--------------------------------------------------------------------------------