Merge branch 'master' into feature/forwarding

feature/forwarding
Kelvin Schoofs 3 years ago
commit b7a68da3ec

2
.gitattributes vendored

@ -0,0 +1,2 @@
/.yarn/releases/** binary
/.yarn/plugins/** binary

@ -33,13 +33,9 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install VSCE
run: |
yarn global add vsce
echo "$(yarn global bin)" >> $GITHUB_PATH
- name: Get Yarn cache directory
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
- name: Yarn cache
uses: actions/cache@v2.1.4
with:
@ -47,13 +43,10 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies in /
run: yarn --frozen-lockfile
- name: Install dependencies in /webview/
working-directory: webview
run: yarn --frozen-lockfile
- name: Install dependencies
run: yarn --immutable
- name: Build extension
run: vsce package -o ${{ steps.utils.outputs.artifact_name }}
run: yarn dlx vsce package -o ${{ steps.utils.outputs.artifact_name }} --yarn
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1
with:

15
.gitignore vendored

@ -1,5 +1,18 @@
# Build output
dist
util
node_modules
# Build artifacts
*.vsix
stats.json
# Yarn
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
!.yarn/yarn.lock
.pnp.*

@ -2,6 +2,7 @@
// See https://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"eamodio.tsl-problem-matcher"
"eamodio.tsl-problem-matcher",
"arcanis.vscode-zipfs"
]
}

@ -15,6 +15,14 @@
"outFiles": [
"${workspaceRoot}/out/src/**/*.js"
],
"env": {
"VSCODE_SSHFS_DEBUG": "TRUE"
},
"windows": {
"env": {
"VSCODE_SSHFS_DEBUG": "TRUE"
}
}
// "preLaunchTask": "npm:watch"
}
]

@ -1,11 +1,27 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"files.exclude": {
"*.vsix": true,
"**/*.lock": true,
"**/node_modules/": true,
"dist/": true,
"util/": true,
"webview/build/": true
}
"webview/build/": true,
".yarn/": true,
".yarnrc.yml": true,
".pnp.*": true,
"LICENSE.txt": true,
"**/.*ignore": true,
"**/.gitattributes": true,
"**/.eslintcache": true,
"**/webpack.config.js": true,
"webpack.plugin.js": true,
"**/tslint.json": true,
"**/tsconfig.json": true
},
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"typescript.enablePromptUseWorkspaceTsdk": true
}

52
.vscode/tasks.json vendored

@ -32,57 +32,17 @@
{
"type": "shell",
"label": "Extension WebView - Watch",
"command": "npm start",
"command": "yarn start",
"options": {
"cwd": "./webview"
},
"group": "build",
"problemMatcher": [
{
"source": "parser",
"owner": "react",
"fileLocation": "absolute",
"applyTo": "allDocuments",
"pattern": [
{
"regexp": "^SyntaxError: (.*): (.+) \\((\\d+):(\\d+)\\)$",
"file": 1,
"message": 2,
"line": 3,
"column": 4
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^Compiling.*",
"endsPattern": "^(Compiled successfully|Failed to compile)"
}
},
{
"source": "typescript",
"owner": "react",
"fileLocation": "absolute",
"applyTo": "allDocuments",
"pattern": [
{
"regexp": "^TypeScript error in (.*)\\((\\d+),(\\d+)\\):",
"file": 1,
"line": 2,
"column": 3
"problemMatcher": {
"base": "$ts-webpack-watch",
"source": "webpack-ts-loader",
"owner": "webpack-ts-loader",
"applyTo": "allDocuments"
},
{
"regexp": "^(.{5,})$",
"message": 1,
"loop": true
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^Compiling.*",
"endsPattern": "^(Compiled successfully|Failed to compile)"
}
}
],
"isBackground": true
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
# This file is automatically generated by @yarnpkg/sdks.
# Manual changes might be lost!
integrations:
- vscode

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);

@ -0,0 +1,157 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
case `vscode`: {
str = `^zip:${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
}
}
return str;
}
function fromEditorPath(str) {
return process.platform === `win32`
? str.replace(/^\^?zip:\//, ``)
: str.replace(/^\^?zip:/, ``);
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
}
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

@ -0,0 +1,157 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
case `vscode`: {
str = `^zip:${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile:${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
}
}
return str;
}
function fromEditorPath(str) {
return process.platform === `win32`
? str.replace(/^\^?zip:\//, ``)
: str.replace(/^\^?zip:/, ``);
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
}
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);

@ -0,0 +1,6 @@
{
"name": "typescript",
"version": "4.3.4-sdk",
"main": "./lib/typescript.js",
"type": "commonjs"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
enableGlobalCache: true
lockfileFilename: .yarn/yarn.lock
preferInteractive: true
nodeLinker: pnp
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.0.1-git-fixed.cjs

@ -0,0 +1,237 @@
# Changelog
## Development changes
- Webpack setup has been improved quite a bit, mostly to clean up long ugly paths and make builds deterministic:
- The custom `ProblemMatcherReporter` plugin is moved to `/webpack.plugin.js` and renamed to `WebpackPlugin`
- Now both webpack configs (extension and webview) make use of this plugin
- The plugin has the ability to remap module names/paths, accounting for several things:
- Paths in the global `/Yarn/Berry/` folder are now displayed as `/yarn/` and are simplified for easier reading
- Paths in the local `.yarn` folder get the same treatment as global ones, but using `.yarn/` as the prefix
- Other paths that are located within the (config's) project are made relative to the (config's) project root
- The plugin enhances the stats printer to use the clean simplified paths instead of e.g. `../../../Yarn/etc`
- The plugin handles generating chunk ids (`optimization.chunkIds` option)
- Acts mostly like a simplified version of the built-in `deterministic` option
- Uses the path remapping, resulting in paths not being different depending on where your global Yarn folder is
- These deterministic builds result in e.g. the same output chunk filenames
- Building the same commit on GitHub Actions or your own PC should result in e.g. the same source maps
- The `excludeModules` is now configured (and better handled) by the plugin
- The problem matcher for the `Extension Webview - Watch` task has been simplified and fixed due to the above change
## 1.22.0 (2021-09-21)
### Fixes
- Partially fix issue with debug mode on code-server (05e1b69, #279)
### Development changes
- I've added a `CHANGELOG.md` file to the repository containing the changelog for earlier versions. It'll contain already committed changes that have yet to be released.
- The extension now only enters debug mode when the environment variable `VSCODE_SSHFS_DEBUG` is the (case insensitive) string `"true"`. The `ExtensionContext.extensionMode` provided by Code does not influence this anymore. This is part due to #279, implemented in 05e1b69 which supersedes 48ef229.
## 1.21.2 (2021-08-05)
### Fixes
- Fix bug in connect command with `Root` starting with `~` (803dc59, #280)
### Changes
- Remove `(SSH FS)` label from editor titles (fcbd6d7, #278)
## 1.21.1 (2021-08-01)
### Fixes
- Improve effect of `CHECK_HOME` flag (ef40b07b2d, #277)
### Changes
- Better error handling and `CHECK_HOME` flag support for `tryGetHome` (87d2cf845a)
### Development changes
- Improve `map-error.js` to work for `/dist/extension.js` and error report better (bda36c998c)
- Improve logging of errors through promises (c7f1261311)
## 1.21.0 (2021-07-01)
### Major change (315c255)
- An internal change happened, making URIs now represent an absolute path on the server
- In the past, `ssh://config/some/path` for a config with `/root` as Root would actually point to `/root/some/path` on the remote server
- Now, the Root is ignored, so `ssh://config/some/path` will actually point at `/some/path` on the remote server
- The Root field is now only checked by the "Add as Workspace Folder" and "Open remote SSH terminal" for the default path. In the above example, you'd get the workspace folder `ssh://config/root` and have your terminal open with the current directory being `/root`, assuming you didn't open the terminal by using the context menu in the explorer
- **While this shouldn't affect most people**, it means that people that have saved/open workspaces with configs with a non-`/` Root, it might point at the wrong file/directory. Updating the URI or removing and re-adding the workspace folder should fix it
- This change simplifies a lot of complex code accounting for calculating/validating relative paths, and also allows for future improvements, a good example being a beta feature shown in #267
Fixes:
- Fix proxies breaking when no port is defined (which should default to 22) (a41c435, #266)
New features:
- Added `statusBar/remoteIndicator` (remote button left-bottom) (d3a3640, #260)
See microsoft/vscode#122102 for info and [this](https://code.visualstudio.com/updates/v1_56#_remote-indicator-menu) for an example (with different extensions)
- Add support for environment variables (3109e97, #241)
Currently you have to manually edit your JSON settings files to add environment variables.
This can be done by adding e.g. `"environment": { "FOO": "BAR" }`.
Variables will be `export FOO=BAR`'d (fully escaped) before running the shell command.
This affects both terminals and `ssh-shell` tasks.
- Added a `CHECK_HOME` flag (default: true) to toggle checking the home directory (315c255)
The extension checks whether your home directory (queried using `echo ~`) is a directory. Since some exotic setups might have home-less users, you can add `-CHECK_HOME` as a flag (see #270)
- Add `code` as a remote command to open files/directories locally (7d930d3, #267)
**Still a beta feature** which requires the `REMOTE_COMMANDS` flag (see #270) enabled.
Tries to inject an alias (well, function) named `code` in the remote terminal's shell.
The "command" only accepts a single argument, the (relative or absolute) path to a file/directory.
It will tell the extension (and thus VS Code) to open the file/directory. Files are opened in an editor, directories are added as workspace folders. Errors are displayed in VS Code, **not** your terminal.
Due to how complex and unreliable it is to inject aliases, this feature is still in beta and subject to change.
Minor changes:
- Added `virtualWorkspaces` capabilities to `package.json` (8789dd6)
- Added `untrustedWorkspaces` capabilities (cca8be2, #259, microsoft/vscode#120251)
- The `Disconnect` command now only shows connections to choose from (36a440d)
- Added `resourceLabelFormatters` contribution, for better Explorer tooltips (5dbb36b)
- Added `viewsWelcome ` contribution, to fill in empty configs/connections panes (4edc2ef)
Development changes:
- Added some initial when clause contexts (b311fec)
- Currently only `sshfs.openConnections`, `sshfs.openTerminals` and `sshfs.openFileSystems`
- Some small refactors and improvements (5e5286d, 06bce85, f17dae8, 1258a8e, f86e33a)
## 1.20.2 (2021-06-28)
### Fixes
- Allow usernames with dashes for instant connection strings (#264, f05108a)
This only affected the "Create instant connection" option within certain commands in the public version of the extension.
This also affected people (manually) using connection strings to interact with file systems or use as `"hop"` within JSON configs.
### New features
- Add config option for agent forwarding (#265, d167ac8)
The settings UI now has a simple checkbox to toggle agent forwarding.
Mind that this automatically gets disabled if you authenticate without an agent!
### Development changes
- Updated to TypeScript 4.3.4
- Updated `defaultStyles.css` for VS Code CSS variables
- Settings UI now supports checkbox fields
- Extension code base now uses webpack 5 instead of webpack 4
## 1.20.1 (2021-04-14)
### Fixes
- Closing connection shouldn't delete workspace folders if a related filesystem exists (cdf0f99)
Basically you have connection A (with a terminal or so) and connection B (with a file system) both for the same config name.
Before, closing connection A would also delete/remove the workspace folder, even though connection B still provides the file system.
With this change, closing a connection won't delete the folder if it detects another connection (for the same name) providing SFTP.
- Add `WINDOWS_COMMAND_SEPARATOR` config flag to support Windows OpenSSH servers (see #255)
Mind that you'll also need to change `Terminal Command` into e.g. `powershell`, as Windows doesn't support the `$SHELL` variable
### Changes
- The extension now tracks which version was last used (fdb3b66)
Currently unused, but might be used in the future to notify the user of breaking changes, optionally with auto-fix.
- Config flags can now be specified per config (9de1d03)
- An example use of this an be seen in #255.
- **Note**: Configs (and thus their flags) are cached when a connection is created!
- This means that changes to the config flags won't apply until the connection is closed and a new one is created.
- The extension already starts a new (parallel) connection when the currently saved config mismatches a running connection's config.
- The extension will now replace task variables (e.g. `remoteWorkspaceFolder`) in `Terminal Command` (#249)
This does ***not** handle VS Code's built-in "local" task variables like `workspaceFolder`, although support for this could be added later.
## 1.20.0 (2021-03-19)
### New features
- Add task variables for remote files #232 ([example](https://user-images.githubusercontent.com/14597409/111828756-0d326d00-88ec-11eb-9988-0768e1194cca.png))
- Supported variables (e.g. `${remoteFile}`) can be seen [here](https://github.com/SchoofsKelvin/vscode-sshfs/blob/v1.20.0/src/manager.ts#L216)
- Some variables support a workspace name as argument, similar to the built-in variables, e.g. `${remoteWorkspaceFolder:FolderName}`
- Add `taskCommand` #235
- Similar to `terminalCommand`, but for `ssh-shell` tasks
- Setting it to e.g. `echo A; $COMMAND; echo B` results in the task echoing `A`, running the task command, then echoing `B`
### Development changes
- Switched from official `ssh2-streams` to [Timmmm/ssh2-streams#patch1](https://github.com/Timmmm/ssh2-streams/tree/patch-1)
- Potentially fixing #244
- Updated to TypeScript 4.2.3
- Updated all other dependencies within the existing specified version range _(minor and patch updates)_
- Build workflow now caches the Yarn cache directory
- Build workflow now uses Node v12 instead of v10
- Added a Publish workflow to publish the extension to VS Marketplace and Open VSX Registry
## 1.19.4 (2021-03-02)
### Changes
- Flag system is improved. The `DF-GE` flag (see #239) will now automatically enable/disable for affected Electron versions.
People that were making use of the `DF-GE` flag to **disable** this fix, should now use `-DF-GE` or `DF-GE=false` instead.
### Development changes
- GitHub Actions workflow now makes use of the [Event Utilities](https://github.com/marketplace/actions/event-utilities) GitHub action (6d124f8)
This is mostly the old code, but now better maintained and made publicly available to anyone.
Doesn't really affect the extension. Just cleans up the workflow file, instead of requiring a relatively big complex chunk of bash script.
## 1.19.3 (2021-02-15)
### Changes
- Instant connections with as hostname an existing config will result in the configs being merged
- e.g. `user2@my-config` will use the same config as `my-config`, but with `user2` as the user
- The "instant connection bonuses" are still applied, e.g. trying to match the (new) config against a PuTTY session on Windows
- Typing in a config/connection picker (e.g. the `SSH FS: Open a remote SSH terminal` command) acts smarter for instant connections
- Entering a value and selecting `Create instant connection` will carry over the entered value to the new input box
- Instant connections are now much better at matching against PuTTY sessions
- The discovery process of PuTTY sessions will no longer spam the output _(only "interesting" fields are outputted)_
- It will now try to first find a session with the given host as name, then try again by matching username/hostname
- This improved matching should also work for non-instant connections, aka regular configurations
- Overhauled README with updated graphics, list of features, ...
- Fixed a bug regarding the `SFTP Sudo` config field misbehaving in the config editor
### Other news
I'm in the process of claiming the "Kelvin" namespace on the Open VSX Registry.
In the future, new versions will also be pushed to it, instead of relying on their semi-automated system to do it _sometime_.
## 1.19.2 (2021-02-11)
### Hotfix
- Add an auto-enabled patch for #239
- Disables all `diffie-hellman-group-exchange` KEX algorithms _(unless the user overrides this option)_
- Adding the flag `DF-GE` to your `sshfs.flags`, e.g. `"sshfs.flags": ["DF-GE"]` **disables** this fix
### New features
- **Instant connections**
- The "Add as Workspace Folder" and "Open remote SSH terminal" now suggest "Create instant connection"
- Allows easily setting up unconfigured connections, e.g. `user@example.com:22/home/user`
- The connection string supports omitting user (defaults to `$USERNAME`), port (22) and path (`/`)
- On Windows, the extension will automatically try to resolve it to a PuTTY session (e.g. `user@SessionName/home/user`)
- This part is still not fully finished, and currently has bugs. Use `user@domain.as.configured.in.putty` to make it work
- Better support for PuTTY will be added soon
- A workspace file can add instant connections as workspace folders by using the instant connection string
- If the connecting string does **not** contain a `@`, it's assumed to be a config name _(old default behavior)_
- Roadmap: once #107 is fully added, instant connections will also support OpenSSH config files, similar to PuTTY support
- Flag system, available under the `sshfs.flags` config option.
Allows specifying flags to change certain options/features that aren't supported by the UI.
- Adding `"debug": true` to your SSH FS config will enable `ssh2`/`ssh2-streams` debug logging for that config
### Development changes
- The GitHub repository now has a workflow (GitHub Actions) to build the extension and draft releases
- Improve how the extension "simplifies" error messages for missing files for (built-in) extension checks
- Now supports workspace folders with `ssh://name/path` as URI instead of just `ssh://name/`
- Added `/app/src/main/AndroidManifest.xml` to the ignore list (recently added in VS Code itself)
- WebView went through a small refactorization/cleanup, to make future work on it easier
- Unused files removed, small basically identical files merged, ...
- Switch from deprecated `react-scripts-ts` to `react-scripts` (with ESLint support)
- Removed the custom `react-dev-utils` module _(was required for `react-scripts-ts` with VS Code)_
- Fix problemMatcher for the webview watch build task
- Remove `streams.ts` + simplify `tryGetHome` in `manager.ts` to not depend on it anymore
## 1.19.1 (2020-12-17)
### New features
- Add TerminalLinkProvider for absolute paths
### Changes
- Upgrade `@types/vscode` and minimum VSCode version from 1.46.0 to 1.49.0
- Small internal improvements
- Fix some bugs
## 1.19.0 (2020-12-17)
### New features
- `SSH FS` view with nice UI for listing configs, managing connections/terminals, ...
- Support prompting the `Host` field
- Add `Terminal command` field to change the terminal launch command _(defaults to `$SHELL`)_
### Changes
- Upgrade codebase to typescript@4.0.2
- Refactor Manager, add ConnectionManager
- Small bug fixes, improved logging, ...
## Earlier
Check the [releases](https://github.com/SchoofsKelvin/vscode-sshfs/releases) page to compare commits for older versions.

@ -18,8 +18,8 @@ for (const file of fs.readdirSync('./dist')) {
console.log();
const SOURCE_NAME_REGEX = /^\s*at .*? \(.*?[/\\]dist[/\\](\d+\.extension\.js):(\d+):(\d+)\)$/;
const SOURCE_ANOM_REGEX = /^\s*at .*?[/\\]dist[/\\](\d+\.extension\.js):(\d+):(\d+)$/;
const SOURCE_NAME_REGEX = /^\s*at .*? \(.*?[/\\]dist[/\\]((?:\d+\.)?extension\.js):(\d+):(\d+)\)$/;
const SOURCE_ANOM_REGEX = /^\s*at .*?[/\\]dist[/\\]((?:\d+\.)?extension\.js):(\d+):(\d+)$/;
let error = '';
rl.createInterface(process.stdin).on('line', async l => {
@ -32,8 +32,17 @@ rl.createInterface(process.stdin).on('line', async l => {
line = parseInt(line);
column = parseInt(column);
const map = await maps[file];
if (!map) throw new Error(`Missing map for '${file}'`);
if (!map) {
stack += ' [MISSING]';
console.log(stack);
continue;
}
const pos = map.originalPositionFor({ line, column });
if (!pos.line) {
stack += ' [MISMAPPED]';
console.log(stack);
continue;
}
const ws = stack.match(/^\s*/)[0];
if (named && pos.name) {
stack = `${ws}at ${pos.name} (${pos.source}:${pos.line}:${pos.column})`;

@ -3,7 +3,7 @@
"displayName": "SSH FS",
"description": "File system, terminal and task provider using SSH",
"publisher": "Kelvin",
"version": "1.21.0",
"version": "1.22.0",
"engines": {
"vscode": "^1.49.0"
},
@ -400,7 +400,7 @@
{
"scheme": "ssh",
"formatting": {
"label": "${scheme}://${authority}/${path} (SSH FS)",
"label": "${scheme}://${authority}/${path}",
"separator": "/",
"tildify": false,
"stripPathStartingSeparator": true,
@ -426,7 +426,7 @@
}
},
"scripts": {
"vscode:prepublish": "yarn run build && cd webview && yarn run build",
"vscode:prepublish": "yarn workspaces foreach -vip -j 2 run build",
"build": "webpack --mode production",
"compile": "webpack --mode development",
"watch": "webpack --mode development --watch"
@ -438,12 +438,11 @@
"@types/vscode": "~1.49.0",
"@types/webpack": "^4.4.25",
"@types/winreg": "^1.2.30",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"source-map": "^0.7.3",
"source-map-support": "^0.5.19",
"ts-loader": "^9.2.3",
"typescript": "^4.3.4",
"webpack": "^5.40.0",
"webpack": "^5.50.0",
"webpack-cli": "^4.7.2"
},
"dependencies": {
@ -453,9 +452,15 @@
"node-socksv5": "^1.0.3",
"socks": "^2.6.1",
"ssh2": "^0.8.9",
"ssh2-streams": "^0.4.10",
"webview": "workspace:*",
"winreg": "^1.2.4"
},
"resolutions": {
"ssh2-streams": "Timmmm/ssh2-streams#patch-1"
}
},
"workspaces": [
"./webview"
],
"packageManager": "yarn@3.0.1"
}

@ -143,16 +143,18 @@ export class ConnectionManager {
// Complains about ssh2 library connecting a 'drain' event for every channel
client.setMaxListeners(0);
// Query home directory
let home = await tryGetHome(client);
if (!home) {
let home = await tryGetHome(client).catch((e: Error) => e);
if (typeof home !== 'string') {
const [flagCH] = getFlagBoolean('CHECK_HOME', true, config.flags);
logging.error('Could not detect home directory');
logging.error('Could not detect home directory', LOGGING_NO_STACKTRACE);
if (flagCH) {
if (home) logging.error(home);
logging.info('If this is expected, disable the CHECK_HOME flag with \'-CHECK_HOME\':');
logging.info('https://github.com/SchoofsKelvin/vscode-sshfs/issues/270');
await vscode.window.showErrorMessage(`Couldn't detect the home directory for '${name}'`, 'Okay');
throw new Error(`Could not detect home directory`);
} else {
if (home) logging.warning(home);
logging.warning('The CHECK_HOME flag is disabled, default to \'/\' and ignore the error');
home = '';
}

@ -31,7 +31,7 @@ interface CommandHandler {
export function activate(context: vscode.ExtensionContext) {
Logging.info(`Extension activated, version ${getVersion()}, mode ${context.extensionMode}`);
setDebug(context.extensionMode !== vscode.ExtensionMode.Production);
setDebug(process.env.VSCODE_SSHFS_DEBUG?.toLowerCase() === 'true');
// Likely that we'll have a breaking change in the future that requires users to check
// their configs, or at least reconfigure already existing workspaces with new URIs.

@ -9,7 +9,12 @@ export function setDebug(debug: boolean) {
console.warn(`[vscode-sshfs] Debug mode set to ${debug}`);
DEBUG = debug;
if (!debug) return;
import('source-map-support/register').catch(e => console.warn('Could not register source-map-support:', e));
try { require('.pnp.cjs').setup(); } catch (e) {
console.warn('Could not set up .pnp.cjs:', e);
}
try { require('source-map-support').install(); } catch (e) {
console.warn('Could not install source-map-support:', e);
}
}
const outputChannel = vscode.window.createOutputChannel('SSH FS');
@ -44,6 +49,10 @@ export interface LoggingOptions {
export const LOGGING_NO_STACKTRACE: Partial<LoggingOptions> = { callStacktrace: 0 };
export const LOGGING_SINGLE_LINE_STACKTRACE: Partial<LoggingOptions> = { callStacktrace: 1 };
function hasPromiseCause(error: Error): error is Error & { promiseCause: string } {
return typeof (error as any).promiseCause === 'string';
}
export type LoggerDefaultLevels = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
class Logger {
protected parent?: Logger;
@ -95,16 +104,24 @@ class Logger {
if (message instanceof Error && message.stack) {
let msg = message.message;
try {
msg += `\nJSON: ${JSON.stringify(message)}`;
const json = JSON.stringify(message);
if (json !== '{}') msg += `\nJSON: ${json}`;
} finally { }
const { maxErrorStack } = options;
if (message.stack && maxErrorStack) {
let { stack } = message;
if (maxErrorStack > 0) {
stack = stack.split(/\n/g).slice(0, maxErrorStack).join('\n');
stack = stack.split(/\n/g).slice(0, maxErrorStack + 1).join('\n');
}
msg += '\n' + stack;
}
if (hasPromiseCause(message) && maxErrorStack) {
let { promiseCause } = message;
if (maxErrorStack > 0) {
promiseCause = promiseCause.split(/\n/g).slice(1, maxErrorStack + 1).join('\n');
}
msg += '\nCaused by promise:\n' + promiseCause;
}
message = msg;
}
// Do we need to also output a stacktrace?
@ -130,7 +147,7 @@ class Logger {
public info(message: string, options: Partial<LoggingOptions> = {}) {
this.print('INFO', message, options);
}
public warning(message: string, options: Partial<LoggingOptions> = {}) {
public warning(message: string | Error, options: Partial<LoggingOptions> = {}) {
this.print('WARNING', message, options);
}
public error(message: string | Error, options: Partial<LoggingOptions> = {}) {

@ -216,7 +216,7 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
let { root = '/' } = config;
if (root.startsWith('~')) {
const con = await this.connectionManager.createConnection(config.name, config);
if (con) root = con.home + root.substring(1);
root = con.home + root.substring(1);
}
if (root.startsWith('/')) root = root.substring(1);
vscode.workspace.updateWorkspaceFolders(folders ? folders.length : 0, 0, {

@ -1,20 +1,9 @@
import type { EnvironmentVariable } from "./fileSystemConfig";
export type toPromiseCallback<T> = (err?: Error | null | void, res?: T) => void;
/** Wrapper around async callback-based functions */
export async function toPromise<T>(func: (cb: toPromiseCallback<T>) => void): Promise<T> {
return new Promise<T>((resolve, reject) => {
try {
func((err, res) => err ? reject(err) : resolve(res!));
} catch (e) {
reject(e);
}
});
}
/** Wrapper around async callback-based functions */
export async function catchingPromise<T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => any): Promise<T> {
const promiseCause = new Error();
Error.captureStackTrace(promiseCause, catchingPromise);
return new Promise<T>((resolve, reject) => {
try {
const p = executor(resolve, reject);
@ -24,6 +13,27 @@ export async function catchingPromise<T>(executor: (resolve: (value?: T | Promis
} catch (e) {
reject(e);
}
}).catch(e => {
if (e instanceof Error) {
let t = (e as any).promiseCause;
if (!(t instanceof Error)) t = e;
if (!('promiseCause' in t)) {
Object.defineProperty(e, 'promiseCause', {
value: promiseCause.stack,
configurable: true,
enumerable: false,
});
}
}
throw e;
});
}
export type toPromiseCallback<T> = (err?: Error | null | void, res?: T) => void;
/** Wrapper around async callback-based functions */
export async function toPromise<T>(func: (cb: toPromiseCallback<T>) => void): Promise<T> {
return catchingPromise((resolve, reject) => {
func((err, res) => err ? reject(err) : resolve(res!));
});
}

@ -5,16 +5,17 @@
const { join, resolve, dirname } = require('path');
const fs = require('fs');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { WebpackPlugin } = require('./webpack.plugin');
/**
* @template T
* @param { (cb: (e?: Error, r?: T) => void) => any } func
* @param { (cb: (e?: Error | null, r?: T) => void) => any } func
* @return { Promise<T> }
*/
function wrap(func) {
return new Promise((res, rej) => {
try {
// @ts-ignore
func((e, r) => e ? rej(e) : res(r));
} catch (e) {
rej(e);
@ -27,7 +28,8 @@ class CopyPuttyExecutable {
* @param {webpack.Compiler} compiler
*/
apply(compiler) {
const path = resolve('./node_modules/ssh2/util/pagent.exe');
const path = require.resolve('ssh2/util/pagent.exe');
// @ts-ignore
const target = join(compiler.options.output.path, '../util/pagent.exe');
compiler.hooks.beforeRun.tapPromise('CopyPuttyExecutable-BeforeRun', () => new Promise((resolve, reject) => {
fs.exists(path, exists => exists ? resolve() : reject(`Couldn't find executable at: ${path}`));
@ -44,20 +46,6 @@ class CopyPuttyExecutable {
}
}
class ProblemMatcherReporter {
/**
* @param {webpack.Compiler} compiler
*/
apply(compiler) {
compiler.hooks.beforeCompile.tap('ProblemMatcherReporter-BeforeCompile', () => {
console.log('Compilation starting');
});
compiler.hooks.afterCompile.tap('ProblemMatcherReporter-AfterCompile', () => {
console.log('Compilation finished');
});
}
}
/**@type {webpack.Configuration}*/
const config = {
mode: 'development',
@ -67,17 +55,19 @@ const config = {
output: {
path: resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: "commonjs2",
devtoolModuleFilenameTemplate: "../[resource-path]",
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]',
clean: true,
},
devtool: 'source-map',
performance: {
hints: 'warning'
},
externals: {
vscode: "commonjs vscode",
request: "commonjs request",
'source-map-support/register': "commonjs source-map-support/register",
vscode: 'commonjs vscode',
request: 'commonjs request',
'.pnp.cjs': 'commonjs ../.pnp.cjs',
'source-map-support': 'commonjs source-map-support',
},
resolve: {
extensions: ['.ts', '.js']
@ -92,9 +82,8 @@ const config = {
}]
},
plugins: [
new CleanWebpackPlugin(),
new CopyPuttyExecutable(),
new ProblemMatcherReporter(),
new WebpackPlugin(),
],
optimization: {
splitChunks: {
@ -113,10 +102,6 @@ const config = {
modules: true,
groupModulesByPath: true,
modulesSpace: 50,
excludeModules(name, { issuerPath }) {
if (name.startsWith('external ')) return true;
return issuerPath && issuerPath[issuerPath.length - 1].name.startsWith('./node_modules');
},
},
}

@ -0,0 +1,134 @@
//@ts-check
'use strict';
const webpack = require('webpack');
const { createHash } = require('crypto');
class WebpackPlugin {
_formatIdCache = new Map();
/** @type {(id: string, rootPath: string) => string} */
formatId(id, rootPath) {
// Make sure all paths use /
id = id.replace(/\\/g, '/');
// For `[path]` we unwrap, format then rewrap
if (id[0] === '[' && id.endsWith(']')) {
return `[${this.formatId(id.slice(1, id.length - 1), rootPath)}]`;
}
// When dealing with `path1!path2`, format each segment separately
if (id.includes('!')) {
id = id.split('!').map(s => this.formatId(s, rootPath)).join('!');
}
// Make the paths relative to the project's rooth path if possible
if (id.startsWith(rootPath)) {
id = id.slice(rootPath.length);
id = (id[0] === '/' ? '.' : './') + id;
}
let formatted = this._formatIdCache.get(id);
if (formatted) return formatted;
// Check if we're dealing with a Yarn directory
let match = id.match(/^.*\/(\.?Yarn\/Berry|\.yarn)\/(.*)$/i);
if (!match) {
this._formatIdCache.set(id, formatted = id);
return formatted;
}
const [, yarn, filepath] = match;
// Check if we can extract the package name/version from the path
match = filepath.match(/^unplugged\/([^/]+?)\-[\da-f]{10}\/node_modules\/(.*)$/i)
|| filepath.match(/^cache\/([^/]+?)\-[\da-f]{10}\-\d+\.zip\/node_modules\/(.*)$/i);
if (!match) {
formatted = `/${yarn.toLowerCase() === '.yarn' ? '.' : ''}yarn/${filepath}`;
this._formatIdCache.set(id, formatted);
return formatted;
}
const [, name, path] = match;
formatted = `${yarn.toLowerCase() === '.yarn' ? '.' : '/'}yarn/${name}/${path}`;
this._formatIdCache.set(id, formatted);
return formatted;
}
_hashModuleCache = new Map();
/** @type {(mod: webpack.Module, rootPath: string) => string} */
hashModule(mod, rootPath) {
// Prefer `nameForCondition()` as it usually gives the actual file path
// while `identifier()` can have extra `!` or `|` suffixes, i.e. a hash that somehow differs between devices
const identifier = this.formatId(mod.nameForCondition() || mod.identifier(), rootPath);
let hash = this._hashModuleCache.get(identifier);
if (hash) return hash;
hash = createHash('sha1').update(identifier).digest('hex');
this._hashModuleCache.set(identifier, hash);
return hash;
}
/** @param {webpack.Compiler} compiler */
apply(compiler) {
// Output start/stop messages making the $ts-webpack-watch problemMatcher (provided by an extension) work
let compilationDepth = 0; // We ignore nested compilations
compiler.hooks.beforeCompile.tap('WebpackPlugin-BeforeCompile', (params) => {
if (compilationDepth++) return;
console.log('Compilation starting');
});
compiler.hooks.afterCompile.tap('WebpackPlugin-AfterCompile', () => {
if (--compilationDepth) return;
console.log('Compilation finished');
});
compiler.hooks.compilation.tap('WebpackPlugin-Compilation', compilation => {
const rootPath = (compilation.options.context || '').replace(/\\/g, '/');
compilation.options.optimization.chunkIds = false;
// Format `../../../Yarn/Berry/` with all the `cache`/`unplugged`/`__virtual__` to be more readable
// (i.e. `/yarn/package-npm-x.y.z/package/index.js` for global Yarn cache or `/.yarn/...` for local)
compilation.hooks.statsPrinter.tap('WebpackPlugin-StatsPrinter', stats => {
/** @type {(id: string | {}, context: any) => string} */
const tapModId = (id, context) => typeof id === 'string' ? this.formatId(context.formatModuleId(id), rootPath) : '???';
stats.hooks.print.for('module.name').tap('WebpackPlugin-ModuleName', tapModId);
});
// Include an `excludeModules` to `options.stats` to exclude modules loaded by dependencies
compilation.hooks.statsNormalize.tap('WebpackPlugin-StatsNormalize', stats => {
(stats.excludeModules || (stats.excludeModules = [])).push((name, { issuerPath }) => {
if (name.startsWith('external "')) return true;
const issuer = issuerPath && (issuerPath[issuerPath.length - 1].name || '').replace(/\\/g, '/');
if (!issuer) return false;
const lower = this.formatId(issuer, rootPath).toLowerCase();
if (lower.startsWith('/yarn/')) return true;
if (lower.startsWith('.yarn/')) return true;
return false;
});
});
// Determines how chunk IDs are generated, which is now actually deterministic
// (we make sure to clean Yarn paths to prevent issues with `../../Yarn/Berry` being different on devices)
compilation.hooks.chunkIds.tap('WebpackPlugin-ChunkIds', chunks => {
const chunkIds = new Map();
const overlapMap = new Set();
let minLength = 4; // show at least 3 characters
// Calculate the hashes for all the chunks
for (const chunk of chunks) {
if (chunk.id) {
console.log(`Chunk ${chunk.id} already has an ID`);
}
// We're kinda doing something similar to Webpack 5's DeterministicChunkIdsPlugin but different
const modules = compilation.chunkGraph.getChunkRootModules(chunk);
const hashes = modules.map(m => this.hashModule(m, rootPath)).sort();
const hasher = createHash('sha1');
for (const hash of hashes) hasher.update(hash);
const hash = hasher.digest('hex');
// With a 160-bit value, a clash is very unlikely, but let's check anyway
if (chunkIds.has(hash)) throw new Error('Hash collision for chunk IDs');
chunkIds.set(chunk, hash);
chunk.id = hash;
// Make sure the minLength remains high enough to avoid collisions
for (let i = minLength; i < hash.length; i++) {
const part = hash.slice(0, i);
if (overlapMap.has(part)) continue;
overlapMap.add(part);
minLength = i;
break;
}
}
// Assign the shortened (collision-free) hashes for all the chunks
for (const [chunk, hash] of chunkIds) {
chunk.id = hash.slice(0, minLength);
chunk.ids = [chunk.id];
}
});
});
}
}
module.exports.WebpackPlugin = WebpackPlugin;

@ -1,4 +0,0 @@
WDS_SOCKET_HOST=localhost
WDS_SOCKET_PORT=3000
BROWSER=none
SKIP_PREFLIGHT_CHECK=true

@ -2,25 +2,52 @@
"name": "webview",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-redux": "^7.2.0",
"redux": "^4.0.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "webpack serve",
"build": "webpack --mode production"
},
"devDependencies": {
"@types/jest": "^25.1.4",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@babel/core": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.0-rc.3",
"@types/react": "^17.0.18",
"@types/react-dom": "^17.0.9",
"@types/react-redux": "^7.1.7",
"react-scripts": "^4.0.1",
"typescript": "^4.3.4"
"@types/ssh2": "^0.5.47",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"babel-eslint": "^10.1.0",
"babel-loader": "8.1.0",
"babel-preset-react-app": "^10.0.0",
"css-loader": "4.3.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv": "8.2.0",
"eslint": "^7.11.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-testing-library": "^3.9.0",
"eslint-webpack-plugin": "^3.0.1",
"html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "0.11.3",
"pnp-webpack-plugin": "^1.7.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.0",
"react-refresh": "^0.10.0",
"redux": "^4.0.5",
"style-loader": "1.3.0",
"ts-loader": "^9.2.5",
"tslib": "^2.3.1",
"typescript": "^4.3.4",
"url-loader": "4.1.1",
"webpack": "^5.40.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^4.0.0-rc.0"
},
"browserslist": [
"Electron >= 9.0.0"
@ -32,7 +59,14 @@
],
"rules": {
"no-sparse-arrays": 0,
"no-sequences": 0
"no-sequences": 0,
"react/jsx-uses-react": 0,
"react/react-in-jsx-scope": 0
}
},
"babel": {
"presets": [
"react-app"
]
}
}

@ -4,32 +4,10 @@
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' $WEBVIEW_CSPSOURCE; style-src 'unsafe-inline' $WEBVIEW_CSPSOURCE; $WEBVIEW_CSPEXTRA">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!--<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

@ -1,12 +1,12 @@
import * as React from 'react';
import { FieldDropdown } from '../FieldTypes/dropdown';
import { FieldDropdownWithInput } from '../FieldTypes/dropdownwithinput';
import { FieldNumber } from '../FieldTypes/number';
import { FieldString } from '../FieldTypes/string';
import { connect } from '../redux';
import { FileSystemConfig } from '../types/fileSystemConfig';
import type { FieldFactory, FSCChanged, FSCChangedMultiple } from './fields';
export function proxy(config: FileSystemConfig, onChange: FSCChanged<'proxy'>): React.ReactElement {
function proxy(config: FileSystemConfig, onChange: FSCChanged<'proxy'>): React.ReactElement {
const onChangeHost = (host: string) => onChange('proxy', { ...config.proxy!, host });
const onChangePort = (port: number) => onChange('proxy', { ...config.proxy!, port });
console.log('Current config:', config);
@ -19,12 +19,24 @@ export function proxy(config: FileSystemConfig, onChange: FSCChanged<'proxy'>):
</React.Fragment>;
}
export function hop(config: FileSystemConfig, onChange: FSCChanged<'hop'>): React.ReactElement {
const callback = (newValue?: string) => onChange('hop', newValue);
interface HopFieldProps {
config: FileSystemConfig;
configs: [name: string, label: string][];
onChange: FSCChanged<'hop'>;
}
const HopField = connect(({ config, configs, onChange }: HopFieldProps) => {
const callback = (newValue?: [string, string]) => onChange('hop', newValue?.[0]);
const description = 'Use another configuration as proxy, using a SSH tunnel through the targeted config to the actual remote system';
const values = ['TO', ' DO'];
return <FieldDropdownWithInput key="hop" label="Hop" {...{ values, description }} value={config.hop} onChange={callback} optional={true} />;
const displayName = (item: [string, string]) => item[1];
const value = config.hop ? [config.hop, configs.find(c => c[0] === config.hop)?.[1] || config.hop] as const : undefined;
return <FieldDropdown key="hop" label="Hop" {...{ value, values: configs, description, displayName } as const} onChange={callback} optional />;
})<Pick<HopFieldProps, 'configs'>>(state => {
const pairs = new Map<string, string>();
for (const { name, label } of state.data.configs) {
pairs.set(name, label || name);
}
return { configs: Array.from(pairs) };
});
const ProxyTypeToString = {
http: 'HTTP',
@ -39,7 +51,7 @@ const ProxyStringToType = {
} as const;
type ProxyStrings = keyof typeof ProxyStringToType;
export function merged(config: FileSystemConfig, onChange: FSCChanged, onChangeMultiple: FSCChangedMultiple): React.ReactElement | null {
function merged(config: FileSystemConfig, onChange: FSCChanged, onChangeMultiple: FSCChangedMultiple): React.ReactElement | null {
function callback(newValue?: ProxyStrings) {
// Fields starting with _ don't get saved to file
// We use it here so we know when to display the hop stuff
@ -74,8 +86,8 @@ export function merged(config: FileSystemConfig, onChange: FSCChanged, onChangeM
const type = config.proxy && config.proxy.type;
const value = showHop ? 'SSH Hop' : (type && ProxyTypeToString[type]);
return <React.Fragment key="proxy">
<FieldDropdown<ProxyStrings | undefined> key="proxy" label="Proxy" {...{ value, values, description }} onChange={callback} optional={true} />
{showHop && hop(config, onChange)}
<FieldDropdown key="proxy" label="Proxy" {...{ value, values, description } as const} onChange={callback} optional />
{showHop && <HopField config={config} onChange={onChange} />}
{config.proxy && proxy(config, onChange)}
</React.Fragment>;
}

@ -1,4 +1,3 @@
import * as React from 'react';
import { FieldBase } from './base';
interface Props {

@ -30,7 +30,7 @@ class NewConfig extends React.Component<StateProps & DispatchProps> {
<h2>Create new configuration</h2>
<FieldString
label="Name"
description="Name of the config. Can only exists of lowercase alphanumeric characters, slashes and any of these: _.+-@"
description="Name of the config. Accepted characters: [0-9a-z_.+-@]"
value={name}
validator={invalidConfigName}
onChange={setName}

@ -1,4 +1,3 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';

@ -1 +1,44 @@
/// <reference types="react-scripts" />
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
}
}
declare module '*.avif' {
const src: string;
export default src;
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}

@ -1,4 +1,3 @@
import * as React from 'react';
import ConfigEditor from './ConfigEditor';
import ConfigLocator from './ConfigLocator';
import NewConfig from './NewConfig';

@ -9,7 +9,7 @@
],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
@ -26,8 +26,7 @@
"strict": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
"isolatedModules": true
},
"include": [
"src",

@ -0,0 +1,183 @@
//@ts-check
'use strict';
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const PnpWebpackPlugin = require(`pnp-webpack-plugin`);
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { WebpackPlugin } = require('../webpack.plugin');
require('dotenv').config();
/**
* @template T
* @param arr {(T | false | undefined)[]}
* @returns {T[]}
*/
// @ts-ignore
const truthyArray = arr => arr.filter(Boolean);
/**
* @param options {{ mode?: 'development' | 'production'; watch?: boolean; serve?: boolean; env: object }}
*/
module.exports = (env, options) => {
options = {
mode: 'development',
...env.WEBPACK_SERVE && { serve: true },
...options,
};
console.log('options:', options);
const isEnvDevelopment = options.mode === 'development';
const isEnvProduction = options.mode === 'production';
process.env.NODE_ENV = options.env.NODE_ENV = options.mode;
// In serve mode, we serve inside VS Code through localhost:3000
const publicPath = options.serve ? 'http://localhost:3000/' : '/';
/** @type {webpack.Configuration & { devServer: any }} */
const config = {
mode: options.mode,
target: 'web',
bail: isEnvProduction,
devtool: 'source-map',
entry: './src/index.tsx',
output: {
path: isEnvProduction ? path.resolve('./build') : undefined,
pathinfo: isEnvDevelopment,
filename: 'static/js/[name].bundle.js',
chunkFilename: 'static/js/[name].chunk.js',
publicPath,
devtoolModuleFilenameTemplate(info) {
if (isEnvProduction) return path.relative('./src', info.absoluteResourcePath).replace(/\\/g, '/');
return path.resolve(info.absoluteResourcePath).replace(/\\/g, '/');
},
clean: true,
},
optimization: {
minimize: isEnvProduction,
minimizer: [new CssMinimizerPlugin(), '...'],
splitChunks: { chunks: 'all', name: isEnvDevelopment ? undefined : false },
runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` },
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
plugins: [PnpWebpackPlugin]
},
resolveLoader: {
plugins: [
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
rules: [
{
parser: {
requireEnsure: false,
strictExportPresence: true,
}
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
{
test: /\.(mjs|jsx?)$/,
include: path.resolve('src'),
loader: require.resolve('babel-loader'),
options: {
presets: [
['@babel/preset-react', { runtime: 'automatic' }],
],
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
},
{
test: /\.(tsx?)$/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
['@babel/preset-react', { runtime: 'automatic' }],
],
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
plugins: [
options.serve && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
{ loader: 'ts-loader' },
],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
sideEffects: true,
},
],
},
plugins: truthyArray([
new HtmlWebpackPlugin({ inject: true, template: 'public/index.html', publicPath }),
options.serve && new webpack.HotModuleReplacementPlugin(),
options.serve && new ReactRefreshWebpackPlugin(),
new webpack.DefinePlugin(options.env),
new WebpackPlugin(),
isEnvProduction && new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// @ts-ignore
new ESLintWebpackPlugin({
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
eslintPath: require.resolve('eslint'),
failOnError: !isEnvDevelopment,
context: path.resolve('src'),
cache: true,
cacheLocation: '.eslintcache',
cwd: __dirname,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve('eslint-config-react-app/base')],
rules: {
'react/react-in-jsx-scope': 'error',
},
},
}),
]),
node: false,
performance: false,
devServer: {
hot: 'only',
open: false,
host: 'localhost',
port: 3000,
allowedHosts: 'all',
headers: {
'Access-Control-Allow-Origin': '*',
},
},
stats: {
ids: true,
assets: false,
chunks: false,
entrypoints: true,
modules: true,
groupModulesByPath: true,
modulesSpace: 50,
},
};
return config;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save