Skip to content

Commit a2b8532

Browse files
rahmaniaamRahmania Astrid Mochtar
andauthored
fix: replace path.basename with custom browser-safe implementation (#636)
## Problem `getWorkspaceFoldersFromInit` function is using the node-only `path` module. It is imported by `base-runtime.ts` which in turn is used in `webworker`. This causes consumers to require using polyfills, which is not always an option as it is an unmaintained package. #588 ## Solution Implement a custom platform-agnostic implementation of the `path.basename` function, similar of the `joinUnixPaths` made to replace `path.join`. This should match the behaviour of the original function and thus not break the functionality of `getWorkspaceFoldersFromInit` <!--- REMINDER: - Read CONTRIBUTING.md first. - Add test coverage for your changes. - Link to related issues/commits. - Testing: how did you test your changes? - Screenshots if applicable --> ## License By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Rahmania Astrid Mochtar <[email protected]>
1 parent 6971854 commit a2b8532

File tree

4 files changed

+131
-4
lines changed

4 files changed

+131
-4
lines changed

runtimes/runtimes/lsp/router/initializeUtils.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { URI } from 'vscode-uri'
22
import * as path from 'path'
3+
import os from 'os'
34
import assert = require('assert')
45
import sinon = require('sinon')
56
import { InitializeParams, WorkspaceFolder } from 'vscode-languageserver-protocol'
@@ -80,8 +81,15 @@ describe('initializeUtils', () => {
8081
const params = createParams({ rootPath })
8182

8283
const result = getWorkspaceFoldersFromInit(consoleStub, params)
84+
let expectedName
85+
if (os.platform() === 'win32') {
86+
expectedName = path.basename(URI.parse(pathUri).fsPath)
87+
} else {
88+
// using path.basename on unix with a windows path
89+
// will cause it to return \\Users\\test\\folder instead
90+
expectedName = 'folder'
91+
}
8392

84-
const expectedName = path.basename(URI.parse(pathUri).fsPath)
8593
assert.deepStrictEqual(result, [{ name: expectedName, uri: pathUri }])
8694
})
8795

runtimes/runtimes/lsp/router/initializeUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { InitializeParams, WorkspaceFolder } from 'vscode-languageserver-protocol'
2-
import * as path from 'path'
2+
import { basenamePath } from '../../util/pathUtil'
33
import { URI } from 'vscode-uri'
44
import { RemoteConsole } from 'vscode-languageserver'
55

@@ -12,7 +12,7 @@ export function getWorkspaceFoldersFromInit(console: RemoteConsole, params?: Ini
1212
return params.workspaceFolders
1313
}
1414
try {
15-
const getFolderName = (parsedUri: URI) => path.basename(parsedUri.fsPath) || parsedUri.toString()
15+
const getFolderName = (parsedUri: URI) => basenamePath(parsedUri.fsPath) || parsedUri.toString()
1616

1717
if (params.rootUri) {
1818
const parsedUri = URI.parse(params.rootUri)

runtimes/runtimes/util/pathUtil.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as assert from 'assert'
2-
import { joinUnixPaths } from './pathUtil'
2+
import { joinUnixPaths, basenamePath } from './pathUtil'
33

44
describe('joinUnixPaths', function () {
55
it('handles basic joining', function () {
@@ -55,3 +55,82 @@ describe('joinUnixPaths', function () {
5555
assert.strictEqual(joinUnixPaths('foo/...bar'), 'foo/...bar')
5656
})
5757
})
58+
59+
describe('basenameUnixPath', function () {
60+
it('handles basic filename extraction', function () {
61+
assert.strictEqual(basenamePath('/path/to/file.txt'), 'file.txt')
62+
assert.strictEqual(basenamePath('path/to/file.txt'), 'file.txt')
63+
assert.strictEqual(basenamePath('file.txt'), 'file.txt')
64+
assert.strictEqual(basenamePath('filename'), 'filename')
65+
})
66+
67+
it('handles directory paths', function () {
68+
assert.strictEqual(basenamePath('/path/to/dir/'), 'dir')
69+
assert.strictEqual(basenamePath('/path/to/dir'), 'dir')
70+
assert.strictEqual(basenamePath('path/to/dir/'), 'dir')
71+
assert.strictEqual(basenamePath('dir/'), 'dir')
72+
})
73+
74+
it('handles root and empty paths', function () {
75+
assert.strictEqual(basenamePath('/'), '')
76+
assert.strictEqual(basenamePath(''), '')
77+
assert.strictEqual(basenamePath('//'), '')
78+
assert.strictEqual(basenamePath('///'), '')
79+
})
80+
81+
it('handles extension removal', function () {
82+
assert.strictEqual(basenamePath('/path/to/file.txt', '.txt'), 'file')
83+
assert.strictEqual(basenamePath('/path/to/file.txt', 'txt'), 'file.')
84+
assert.strictEqual(basenamePath('file.js', '.js'), 'file')
85+
assert.strictEqual(basenamePath('file.js', 'js'), 'file.')
86+
})
87+
88+
it('handles extension removal edge cases', function () {
89+
assert.strictEqual(basenamePath('file.txt', '.js'), 'file.txt')
90+
assert.strictEqual(basenamePath('file', '.txt'), 'file')
91+
assert.strictEqual(basenamePath('file.txt.bak', '.txt'), 'file.txt.bak')
92+
assert.strictEqual(basenamePath('file.txt.bak', '.bak'), 'file.txt')
93+
})
94+
95+
it('handles Windows-style paths', function () {
96+
assert.strictEqual(basenamePath('C:\\path\\to\\file.txt'), 'file.txt')
97+
assert.strictEqual(basenamePath('path\\to\\file.txt'), 'file.txt')
98+
assert.strictEqual(basenamePath('C:\\path\\to\\dir\\'), 'dir')
99+
})
100+
101+
it('handles mixed path separators', function () {
102+
assert.strictEqual(basenamePath('/path\\to/file.txt'), 'file.txt')
103+
assert.strictEqual(basenamePath('path/to\\dir/'), 'dir')
104+
})
105+
106+
it('handles multiple consecutive slashes', function () {
107+
assert.strictEqual(basenamePath('/path//to///file.txt'), 'file.txt')
108+
assert.strictEqual(basenamePath('path///to//dir///'), 'dir')
109+
assert.strictEqual(basenamePath('///path///file.txt'), 'file.txt')
110+
})
111+
112+
it('handles special filenames', function () {
113+
assert.strictEqual(basenamePath('/path/to/.hidden'), '.hidden')
114+
assert.strictEqual(basenamePath('/path/to/..'), '..')
115+
assert.strictEqual(basenamePath('/path/to/.'), '.')
116+
assert.strictEqual(basenamePath('/path/to/...'), '...')
117+
})
118+
119+
it('handles invalid inputs', function () {
120+
assert.strictEqual(basenamePath(null as any), '')
121+
assert.strictEqual(basenamePath(undefined as any), '')
122+
assert.strictEqual(basenamePath(123 as any), '')
123+
})
124+
125+
it('handles complex extension scenarios', function () {
126+
assert.strictEqual(basenamePath('file.tar.gz', '.gz'), 'file.tar')
127+
assert.strictEqual(basenamePath('file.tar.gz', '.tar.gz'), 'file')
128+
assert.strictEqual(basenamePath('archive.tar.gz', 'tar.gz'), 'archive.')
129+
})
130+
131+
it('handles files without extensions', function () {
132+
assert.strictEqual(basenamePath('/path/to/README'), 'README')
133+
assert.strictEqual(basenamePath('/path/to/Makefile'), 'Makefile')
134+
assert.strictEqual(basenamePath('LICENSE', '.txt'), 'LICENSE')
135+
})
136+
})

runtimes/runtimes/util/pathUtil.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,43 @@ export function joinUnixPaths(...segments: string[]): string {
2626

2727
return result.join('/')
2828
}
29+
30+
/**
31+
* Simplified version of path.basename that can be safely used on web
32+
* It should match the behaviour of the original
33+
* @param path The path to extract the basename from
34+
* @param ext Optional extension to remove from the result
35+
* @returns The last portion of the path, optionally with extension removed
36+
*/
37+
export function basenamePath(path: string, ext?: string): string {
38+
if (!path || typeof path !== 'string' || path === '') {
39+
return ''
40+
}
41+
42+
// Normalize path separators and remove trailing slashes
43+
const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '')
44+
45+
if (!normalizedPath || normalizedPath === '/') {
46+
return ''
47+
}
48+
49+
// Find the last segment
50+
const lastSlashIndex = normalizedPath.lastIndexOf('/')
51+
const basename = lastSlashIndex === -1 ? normalizedPath : normalizedPath.slice(lastSlashIndex + 1)
52+
53+
if (!basename || !ext) {
54+
return basename
55+
}
56+
57+
// Remove extension if it matches
58+
if (ext.startsWith('.')) {
59+
return basename.endsWith(ext) && basename !== ext ? basename.slice(0, -ext.length) : basename
60+
} else {
61+
// For extensions without dot, check both with and without dot
62+
if (basename.endsWith(ext) && basename !== ext) {
63+
return basename.slice(0, -ext.length)
64+
}
65+
const dotExt = '.' + ext
66+
return basename.endsWith(dotExt) && basename !== dotExt ? basename.slice(0, -dotExt.length) : basename
67+
}
68+
}

0 commit comments

Comments
 (0)