From c384dfb81579ce054ad3323d353dfa6115a4b977 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 1 Mar 2019 15:51:11 -0600 Subject: [PATCH] Use Coder marketplace (#44) * Allow setting marketplace URL * Add zip fill * Comment out CSP for now * Fill zip on client as well Probably will need it for client-side extensions. * Don't use itemUrl (it's undefined) * Remove extension rating * Hide ratings with CSS instead of patching them out * Add hard-coded fallback for service URL * Only use coder-develop for extapi if env is explicitly development * Don't use coder-develop at all for extapi If you need it, you can set SERVICE_URL. --- packages/vscode/package.json | 6 +- packages/vscode/src/fill/product.ts | 8 +- packages/vscode/src/fill/zip.ts | 187 ++++++++++++++++++++ packages/vscode/src/vscode.scss | 5 + packages/vscode/webpack.bootstrap.config.js | 1 + packages/vscode/yarn.lock | 85 +++++++++ packages/web/webpack.config.js | 1 + scripts/vscode.patch | 28 +++ scripts/webpack.general.config.js | 1 + 9 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 packages/vscode/src/fill/zip.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 73deee1c..f3eb1587 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -3,15 +3,17 @@ "description": "VS Code implementation of the browser-based IDE client.", "main": "src/index.ts", "scripts": { - "build:bootstrap-fork": "cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.bootstrap.config.js" + "build:bootstrap-fork": "cross-env UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.bootstrap.config.js" }, "dependencies": { "iconv-lite": "^0.4.24", "onigasm": "^2.2.1", "spdlog": "^0.7.2", - "string-replace-loader": "^2.1.1" + "string-replace-loader": "^2.1.1", + "tar-stream": "^2.0.1" }, "devDependencies": { + "@types/tar-stream": "^1.6.0", "vscode-textmate": "^4.0.1" } } diff --git a/packages/vscode/src/fill/product.ts b/packages/vscode/src/fill/product.ts index 3be2971e..9b69fc25 100644 --- a/packages/vscode/src/fill/product.ts +++ b/packages/vscode/src/fill/product.ts @@ -5,11 +5,9 @@ const product = { nameLong: "code-server", dataFolderName: ".code-server", extensionsGallery: { - serviceUrl: "https://marketplace.visualstudio.com/_apis/public/gallery", - cacheUrl: "https://vscode.blob.core.windows.net/gallery/index", - itemUrl: "https://marketplace.visualstudio.com/items", - controlUrl: "https://az764295.vo.msecnd.net/extensions/marketplace.json", - recommendationsUrl: "https://az764295.vo.msecnd.net/extensions/workspaceRecommendations.json.gz", + serviceUrl: global && global.process && global.process.env.SERVICE_URL + || process.env.SERVICE_URL + || "https://v1.extapi.coder.com", }, extensionExecutionEnvironments: { "wayou.vscode-todo-highlight": "worker", diff --git a/packages/vscode/src/fill/zip.ts b/packages/vscode/src/fill/zip.ts new file mode 100644 index 00000000..fa098062 --- /dev/null +++ b/packages/vscode/src/fill/zip.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from "vs/nls"; +import * as fs from "fs"; +import * as path from "path"; +import * as tarStream from "tar-stream"; +import { promisify } from "util"; +import { ILogService } from "vs/platform/log/common/log"; +import { CancellationToken } from "vs/base/common/cancellation"; +import { mkdirp } from "vs/base/node/pfs"; + +export interface IExtractOptions { + overwrite?: boolean; + + /** + * Source path within the ZIP archive. Only the files contained in this + * path will be extracted. + */ + sourcePath?: string; +} + +export interface IFile { + path: string; + contents?: Buffer | string; + localPath?: string; +} + +export function zip(tarPath: string, files: IFile[]): Promise { + return new Promise((c, e) => { + const pack = tarStream.pack(); + const chunks: Buffer[] = []; + const ended = new Promise((res, rej) => { + pack.on("end", () => { + res(Buffer.concat(chunks)); + }); + }); + pack.on("data", (chunk) => { + chunks.push(chunk as Buffer); + }); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + pack.entry({ + name: file.path, + }, file.contents); + } + pack.finalize(); + + ended.then((buffer) => { + return promisify(fs.writeFile)(tarPath, buffer); + }).then(() => { + c(tarPath); + }).catch((ex) => { + e(ex); + }); + }); +} + +export async function extract(tarPath: string, targetPath: string, options: IExtractOptions = {}, logService: ILogService, token: CancellationToken): Promise { + const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); + + return new Promise(async (c, e) => { + const buffer = await promisify(fs.readFile)(tarPath); + const extractor = tarStream.extract(); + extractor.once('error', e); + extractor.on('entry', (header, stream, next) => { + const rawName = header.name; + + const nextEntry = (): void => { + stream.resume(); + next(); + }; + + if (token.isCancellationRequested) { + return nextEntry(); + } + + if (!sourcePathRegex.test(rawName)) { + return nextEntry(); + } + + const fileName = rawName.replace(sourcePathRegex, ''); + + const targetFileName = path.join(targetPath, fileName); + if (/\/$/.test(fileName)) { + stream.resume(); + mkdirp(targetFileName).then(() => { + next(); + }, e); + return; + } + + const dirName = path.dirname(fileName); + const targetDirName = path.join(targetPath, dirName); + if (targetDirName.indexOf(targetPath) !== 0) { + e(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName)); + return nextEntry(); + } + + mkdirp(targetDirName, void 0, token).then(() => { + const fstream = fs.createWriteStream(targetFileName, { mode: header.mode }); + fstream.once('close', () => { + next(); + }); + fstream.once('error', (err) => { + e(err); + }); + stream.pipe(fstream); + stream.resume(); + }); + }); + extractor.once('finish', () => { + c(); + }); + extractor.write(buffer); + extractor.end(); + }); +} + +export function buffer(tarPath: string, filePath: string): Promise { + return new Promise(async (c, e) => { + let done: boolean = false; + extractAssets(tarPath, new RegExp(filePath), (path: string, data: Buffer) => { + if (path === filePath) { + done = true; + c(data); + } + }).then(() => { + if (!done) { + e("couldnt find asset " + filePath); + } + }).catch((ex) => { + e(ex); + }); + }); +} + +async function extractAssets(tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise { + const buffer = await promisify(fs.readFile)(tarPath); + const extractor = tarStream.extract(); + let callbackResolve: () => void; + let callbackReject: (ex?) => void; + const complete = new Promise((r, rej) => { + callbackResolve = r; + callbackReject = rej; + }); + extractor.once("error", (err) => { + callbackReject(err); + }); + extractor.on("entry", (header, stream, next) => { + const name = header.name; + if (match.test(name)) { + extractData(stream).then((data) => { + callback(name, data); + next(); + }); + stream.resume(); + } else { + stream.on("end", () => { + next(); + }); + stream.resume(); + } + }); + extractor.on("finish", () => { + callbackResolve(); + }); + extractor.write(buffer); + extractor.end(); + return complete; +} + +async function extractData(stream: NodeJS.ReadableStream): Promise { + return new Promise((res, rej) => { + const fileData: Buffer[] = []; + stream.on('data', (data) => fileData.push(data)); + stream.on('end', () => { + const fd = Buffer.concat(fileData); + res(fd); + }); + stream.on('error', (err) => { + rej(err); + }); + }); +} diff --git a/packages/vscode/src/vscode.scss b/packages/vscode/src/vscode.scss index 4e335adb..77c37912 100644 --- a/packages/vscode/src/vscode.scss +++ b/packages/vscode/src/vscode.scss @@ -19,6 +19,11 @@ margin-left: initial; } +// We don't have rating data. +.extension-ratings { + display: none !important; +} + // Using @supports to keep the Firefox fixes completely separate from vscode's // CSS that is tailored for Chrome. @supports (-moz-appearance:none) { diff --git a/packages/vscode/webpack.bootstrap.config.js b/packages/vscode/webpack.bootstrap.config.js index df3f6a23..37b1385f 100644 --- a/packages/vscode/webpack.bootstrap.config.js +++ b/packages/vscode/webpack.bootstrap.config.js @@ -59,6 +59,7 @@ module.exports = merge( "vs/base/node/paths": path.resolve(vsFills, "paths.ts"), "vs/platform/node/package": path.resolve(vsFills, "package.ts"), "vs/platform/node/product": path.resolve(vsFills, "product.ts"), + "vs/platform/node/zip": path.resolve(vsFills, "zip.ts"), "vs": path.resolve(root, "lib/vscode/src/vs"), }, }, diff --git a/packages/vscode/yarn.lock b/packages/vscode/yarn.lock index f740e460..ef3efe40 100644 --- a/packages/vscode/yarn.lock +++ b/packages/vscode/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@types/node@*": + version "11.9.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.6.tgz#c632bbcc780a1349673a6e2e9b9dfa8c369d3c74" + integrity sha512-4hS2K4gwo9/aXIcoYxCtHpdgd8XUeDmo1siRCAH3RziXB65JlPqUFMtfy9VPj+og7dp3w1TFjGwYga4e0m9GwA== + +"@types/tar-stream@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-1.6.0.tgz#e19893886625c4ec1c7c30a353b8dc10e205c742" + integrity sha512-XG7FGVmxUvC5NW4h63K3PbB0xdC21xZBfoqmEz7YP2DdiTeYKmYAg8quSHMndNP3iXfs7C73rg4Q0W1dOPHBXQ== + dependencies: + "@types/node" "*" + ajv-keywords@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" @@ -27,11 +39,25 @@ bindings@^1.3.0: resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5" integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew== +bl@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" + integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + dependencies: + readable-stream "^3.0.1" + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= +end-of-stream@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -42,6 +68,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -49,6 +80,11 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +inherits@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -100,6 +136,13 @@ nan@^2.10.0, nan@^2.8.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + onigasm@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.1.tgz#d56da809d63d3bb25510e8b8e447ffe98e56bebb" @@ -124,6 +167,20 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +readable-stream@^3.0.1, readable-stream@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +safe-buffer@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -154,6 +211,24 @@ string-replace-loader@^2.1.1: loader-utils "^1.1.0" schema-utils "^0.4.5" +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +tar-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.0.1.tgz#42fbe41cd1cc5e6657c813e7d98e7afca2858a8c" + integrity sha512-I6OJF7wE62BC6zNPdHDtseK0D0187PBjbKSLYY4ffvVkBM6tyBn2O9plDvVM2229/mozfEL/X3++qSvYYQE2xw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -161,6 +236,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + vscode-textmate@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-4.0.1.tgz#6c36f28e9059ce12bc34907f7a33ea43166b26a8" @@ -168,6 +248,11 @@ vscode-textmate@^4.0.1: dependencies: oniguruma "^7.0.0" +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" diff --git a/packages/web/webpack.config.js b/packages/web/webpack.config.js index 5472ee6a..811e52de 100644 --- a/packages/web/webpack.config.js +++ b/packages/web/webpack.config.js @@ -70,6 +70,7 @@ module.exports = merge( "vs/base/common/amd": path.join(vsFills, "amd.ts"), "vs/platform/node/product": path.join(vsFills, "product.ts"), "vs/platform/node/package": path.join(vsFills, "package.ts"), + "vs/platform/node/zip": path.resolve(vsFills, "zip.ts"), "vs": path.join(root, "lib", "vscode", "src", "vs"), }, }, diff --git a/scripts/vscode.patch b/scripts/vscode.patch index 47286e43..54470d9f 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -340,6 +340,13 @@ index 6c52cbc937..09adbe7f51 100644 + // Cannot control GC in the browser. + return Promise.resolve(obj); + +diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +index ee93e07d8d..f1921f02e4 100644 +--- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts ++++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +@@ -306 +306 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv +- ++ diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 1f4a1e100b..1bf605a064 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts @@ -663,6 +670,20 @@ index 81954344b9..2bdce9603e 100644 @@ -320 +320 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter { - if (!fs.existsSync(this.adapterExecutable.command)) { + if (!(await require("util").promisify(fs.exists)(this.adapterExecutable.command))) { +diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +index 65574224a7..9474897de8 100644 +--- a/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts ++++ b/src/vs/workbench/parts/extensions/electron-browser/extensionEditor.ts +@@ -62 +62 @@ function renderBody(body: string): string { +- ++ +diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +index 6d25977a66..788f5c96e7 100644 +--- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts ++++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +@@ -118 +118 @@ class Extension implements IExtension { +- return `${product.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; ++ return undefined; // `${product.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index 1002950c75..8c187d36ff 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -778,6 +799,13 @@ index 6395712ee9..f2d1c0769b 100644 @@ -232 +233 @@ export class TerminalPanel extends Panel { - if (platform.isMacintosh) { + if (browser.isMacintosh) { +diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +index 94afb719e6..9244831637 100644 +--- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts ++++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +@@ -40 +40 @@ function renderBody( +- ++ diff --git a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js b/src/vs/workbench/parts/webview/electron-browser/webview-pre.js index 29593dc6b6..dd3d25098d 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview-pre.js diff --git a/scripts/webpack.general.config.js b/scripts/webpack.general.config.js index 0a6cffdb..3cd93164 100644 --- a/scripts/webpack.general.config.js +++ b/scripts/webpack.general.config.js @@ -117,6 +117,7 @@ module.exports = (options = {}) => ({ new webpack.DefinePlugin({ "process.env.NODE_ENV": `"${environment}"`, "process.env.LOG_LEVEL": `"${process.env.LOG_LEVEL || ""}"`, + "process.env.SERVICE_URL": `"${process.env.SERVICE_URL || ""}"`, }), ], stats: {