@lvce-editor/shared-process 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/bin/sharedProcess.js +3 -0
  4. package/index.js +6 -0
  5. package/package.json +96 -0
  6. package/src/parts/Assert/Assert.js +57 -0
  7. package/src/parts/Callback/Callback.js +48 -0
  8. package/src/parts/ChildProcess/ChildProcess.js +98 -0
  9. package/src/parts/ClipBoard/ClipBoard.ipc.js +7 -0
  10. package/src/parts/ClipBoard/ClipBoard.js +23 -0
  11. package/src/parts/ClipBoard/ClipBoardGnome.js +78 -0
  12. package/src/parts/ClipBoard/ClipBoardNoop.js +14 -0
  13. package/src/parts/ClipBoard/ClipBoardWindows.js +22 -0
  14. package/src/parts/Command/Command.js +264 -0
  15. package/src/parts/Configuration/Configuration.ipc.js +7 -0
  16. package/src/parts/Configuration/Configuration.js +39 -0
  17. package/src/parts/Credentials/Credentials.ipc.js +10 -0
  18. package/src/parts/Credentials/Credentials.js +69 -0
  19. package/src/parts/Debug/Debug.js +3 -0
  20. package/src/parts/Developer/Developer.ipc.js +20 -0
  21. package/src/parts/Developer/Developer.js +132 -0
  22. package/src/parts/Download/Download.js +33 -0
  23. package/src/parts/Electron/Electron.ipc.js +19 -0
  24. package/src/parts/Electron/Electron.js +99 -0
  25. package/src/parts/Env/Env.js +3 -0
  26. package/src/parts/Error/Error.js +30 -0
  27. package/src/parts/ErrorHandling/ErrorHandling.js +40 -0
  28. package/src/parts/Exec/Exec.js +16 -0
  29. package/src/parts/Extension/Extension.js +7 -0
  30. package/src/parts/ExtensionHost/ExtensionHost.ipc.js +80 -0
  31. package/src/parts/ExtensionHost/ExtensionHost.js +163 -0
  32. package/src/parts/ExtensionHost/ExtensionHostBraceCompletion.js +14 -0
  33. package/src/parts/ExtensionHost/ExtensionHostClosingTag.js +14 -0
  34. package/src/parts/ExtensionHost/ExtensionHostColorTheme.js +60 -0
  35. package/src/parts/ExtensionHost/ExtensionHostCommand.js +19 -0
  36. package/src/parts/ExtensionHost/ExtensionHostCompletion.js +12 -0
  37. package/src/parts/ExtensionHost/ExtensionHostDefinition.js +12 -0
  38. package/src/parts/ExtensionHost/ExtensionHostDiagnostic.js +7 -0
  39. package/src/parts/ExtensionHost/ExtensionHostFileSystem.js +23 -0
  40. package/src/parts/ExtensionHost/ExtensionHostFormatting.js +4 -0
  41. package/src/parts/ExtensionHost/ExtensionHostHover.js +12 -0
  42. package/src/parts/ExtensionHost/ExtensionHostIconTheme.js +35 -0
  43. package/src/parts/ExtensionHost/ExtensionHostImplementation.js +12 -0
  44. package/src/parts/ExtensionHost/ExtensionHostKeyBindings.js +23 -0
  45. package/src/parts/ExtensionHost/ExtensionHostLanguages.js +84 -0
  46. package/src/parts/ExtensionHost/ExtensionHostManagement.js +26 -0
  47. package/src/parts/ExtensionHost/ExtensionHostOutput.js +3 -0
  48. package/src/parts/ExtensionHost/ExtensionHostQuickPick.js +6 -0
  49. package/src/parts/ExtensionHost/ExtensionHostReference.js +12 -0
  50. package/src/parts/ExtensionHost/ExtensionHostRename.js +27 -0
  51. package/src/parts/ExtensionHost/ExtensionHostSemanticTokens.js +10 -0
  52. package/src/parts/ExtensionHost/ExtensionHostSourceControl.js +15 -0
  53. package/src/parts/ExtensionHost/ExtensionHostStats.js +3 -0
  54. package/src/parts/ExtensionHost/ExtensionHostStatusBar.js +7 -0
  55. package/src/parts/ExtensionHost/ExtensionHostTabCompletion.js +12 -0
  56. package/src/parts/ExtensionHost/ExtensionHostTextDocument.js +35 -0
  57. package/src/parts/ExtensionHost/ExtensionHostTypeDefinition.js +12 -0
  58. package/src/parts/ExtensionHost/ExtensionHostWorkspace.js +3 -0
  59. package/src/parts/ExtensionManagement/ExtensionManagement.ipc.js +11 -0
  60. package/src/parts/ExtensionManagement/ExtensionManagement.js +319 -0
  61. package/src/parts/FileSystem/FileSystem.ipc.js +32 -0
  62. package/src/parts/FileSystem/FileSystem.js +228 -0
  63. package/src/parts/Git/Git.js +58 -0
  64. package/src/parts/Json/Json.js +67 -0
  65. package/src/parts/JsonFile/JsonFile.js +14 -0
  66. package/src/parts/JsonRpc/JsonRpc.js +22 -0
  67. package/src/parts/Native/Native.ipc.js +6 -0
  68. package/src/parts/Native/Native.js +14 -0
  69. package/src/parts/OutputChannel/OutputChannel.ipc.js +51 -0
  70. package/src/parts/OutputChannel/OutputChannel.js +37 -0
  71. package/src/parts/ParentIpc/ParentIpc.js +203 -0
  72. package/src/parts/Path/Path.js +17 -0
  73. package/src/parts/Platform/Platform.ipc.js +18 -0
  74. package/src/parts/Platform/Platform.js +165 -0
  75. package/src/parts/Preferences/Preferences.ipc.js +6 -0
  76. package/src/parts/Preferences/Preferences.js +75 -0
  77. package/src/parts/PrettyError/PrettyError.js +86 -0
  78. package/src/parts/Process/Process.ipc.js +7 -0
  79. package/src/parts/Process/Process.js +7 -0
  80. package/src/parts/Queue/Queue.js +35 -0
  81. package/src/parts/RequiresSocket/RequiresSocket.js +11 -0
  82. package/src/parts/RgPath/RgPath.js +1 -0
  83. package/src/parts/Root/Root.js +6 -0
  84. package/src/parts/Search/Search.ipc.js +6 -0
  85. package/src/parts/Search/Search.js +101 -0
  86. package/src/parts/SearchFile/SearchFile.ipc.js +6 -0
  87. package/src/parts/SearchFile/SearchFile.js +51 -0
  88. package/src/parts/Socket/Socket.js +132 -0
  89. package/src/parts/Stats/Stats.js +45 -0
  90. package/src/parts/Terminal/Terminal.ipc.js +45 -0
  91. package/src/parts/Terminal/Terminal.js +161 -0
  92. package/src/parts/TextDocument/TextDocument.ipc.js +6 -0
  93. package/src/parts/TextDocument/TextDocument.js +37 -0
  94. package/src/parts/Trace/Trace.js +15 -0
  95. package/src/parts/Trash/Trash.js +11 -0
  96. package/src/parts/WebSocketServer/WebSocketServer.ipc.js +6 -0
  97. package/src/parts/WebSocketServer/WebSocketServer.js +43 -0
  98. package/src/parts/Workspace/Workspace.ipc.js +7 -0
  99. package/src/parts/Workspace/Workspace.js +107 -0
  100. package/src/sharedProcessMain.js +61 -0
  101. package/test/Callback.test.js +42 -0
  102. package/test/ClipBoard.test.js +105 -0
  103. package/test/Credentials.test.js +147 -0
  104. package/test/Developer.test.js +34 -0
  105. package/test/Electron.test.js +93 -0
  106. package/test/Error.test.js +16 -0
  107. package/test/Exec.test.js +25 -0
  108. package/test/ExtensionHost.test.js +127 -0
  109. package/test/ExtensionHostColorTheme.test.js +141 -0
  110. package/test/ExtensionHostCommand.test.js +27 -0
  111. package/test/ExtensionHostCompletion.test.js +45 -0
  112. package/test/ExtensionHostDefinition.test.js +35 -0
  113. package/test/ExtensionHostDiagnostic.test.js +23 -0
  114. package/test/ExtensionHostFileSystem.test.js +91 -0
  115. package/test/ExtensionHostFormatting.test.js +27 -0
  116. package/test/ExtensionHostIconTheme.test.js +73 -0
  117. package/test/ExtensionHostLanguages.test.js +212 -0
  118. package/test/ExtensionHostOutput.test.js +23 -0
  119. package/test/ExtensionHostRename.test.js +59 -0
  120. package/test/ExtensionHostSemanticTokens.test.js +26 -0
  121. package/test/ExtensionHostTabCompletion.test.js +35 -0
  122. package/test/ExtensionHostTextDocument.test.js +61 -0
  123. package/test/ExtensionHostTypeDefinition.test.js +35 -0
  124. package/test/ExtensionHostWorkspace.test.js +21 -0
  125. package/test/ExtensionManagement.test.js +408 -0
  126. package/test/FileSystem.test.js +294 -0
  127. package/test/Json.test.js +25 -0
  128. package/test/Native.test.js +31 -0
  129. package/test/OutputChannel.test.js +68 -0
  130. package/test/Path.test.js +17 -0
  131. package/test/Platform.test.js +61 -0
  132. package/test/Preferences.test.js +140 -0
  133. package/test/Process.test.js +9 -0
  134. package/test/RgPath.test.js +5 -0
  135. package/test/Search.test.js +45 -0
  136. package/test/SearchFile.test.js +80 -0
  137. package/test/Terminal.test.js +53 -0
  138. package/test/Trash.test.js +34 -0
  139. package/test/WebSocketServer.test.js +98 -0
  140. package/test/fixture-search-1/index.html +10 -0
  141. package/test/fixture-search-file-1/fileA +0 -0
  142. package/test/fixture-search-file-1/fileB +0 -0
  143. package/test/fixture-search-file-1/nested/fileC +0 -0
@@ -0,0 +1,59 @@
1
+ import * as ExtensionHostRename from '../src/parts/ExtensionHost/ExtensionHostRename.js'
2
+
3
+ test('executePrepareRename', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {
6
+ return {
7
+ canRename: true,
8
+ }
9
+ },
10
+ }
11
+ expect(
12
+ await ExtensionHostRename.executePrepareRename(fakeExtensionHost, 0, 0)
13
+ ).toEqual({
14
+ canRename: true,
15
+ })
16
+ })
17
+
18
+ test('executePrepareRename - error', async () => {
19
+ const fakeExtensionHost = {
20
+ invoke() {
21
+ throw new TypeError('x is not a function')
22
+ },
23
+ }
24
+ await expect(
25
+ ExtensionHostRename.executePrepareRename(fakeExtensionHost, 0, 0)
26
+ ).rejects.toThrowError(new TypeError('x is not a function'))
27
+ })
28
+
29
+ test('executeRename', async () => {
30
+ const fakeExtensionHost = {
31
+ invoke() {
32
+ return [
33
+ {
34
+ file: '/test/file.txt',
35
+ edits: [],
36
+ },
37
+ ]
38
+ },
39
+ }
40
+ expect(
41
+ await ExtensionHostRename.executeRename(fakeExtensionHost, 0, 0, '')
42
+ ).toEqual([
43
+ {
44
+ file: '/test/file.txt',
45
+ edits: [],
46
+ },
47
+ ])
48
+ })
49
+
50
+ test('executeRename - error', async () => {
51
+ const fakeExtensionHost = {
52
+ invoke() {
53
+ throw new TypeError('x is not a function')
54
+ },
55
+ }
56
+ await expect(
57
+ ExtensionHostRename.executeRename(fakeExtensionHost, 0, 0, '')
58
+ ).rejects.toThrowError(new TypeError('x is not a function'))
59
+ })
@@ -0,0 +1,26 @@
1
+ import * as ExtensionHostSemanticTokens from '../src/parts/ExtensionHost/ExtensionHostSemanticTokens.js'
2
+
3
+ test('executeSemanticTokenProvider', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {
6
+ return [1, 2, 3, 4, 5]
7
+ },
8
+ }
9
+ expect(
10
+ await ExtensionHostSemanticTokens.executeSemanticTokenProvider(
11
+ fakeExtensionHost,
12
+ 0
13
+ )
14
+ ).toEqual([1, 2, 3, 4, 5])
15
+ })
16
+
17
+ test('executeCompletionProvider - error', async () => {
18
+ const fakeExtensionHost = {
19
+ invoke() {
20
+ throw new TypeError('x is not a function')
21
+ },
22
+ }
23
+ await expect(
24
+ ExtensionHostSemanticTokens.executeSemanticTokenProvider(fakeExtensionHost)
25
+ ).rejects.toThrowError(new TypeError('x is not a function'))
26
+ })
@@ -0,0 +1,35 @@
1
+ import * as ExtensionHostTabCompletion from '../src/parts/ExtensionHost/ExtensionHostTabCompletion.js'
2
+
3
+ test('executeTabCompletionProvider', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {
6
+ return {
7
+ inserted: '<div></div>',
8
+ deleted: 0,
9
+ type: /* Snippet */ 2,
10
+ }
11
+ },
12
+ }
13
+ expect(
14
+ await ExtensionHostTabCompletion.executeTabCompletionProvider(
15
+ fakeExtensionHost,
16
+ 0,
17
+ 0
18
+ )
19
+ ).toEqual({
20
+ inserted: '<div></div>',
21
+ deleted: 0,
22
+ type: /* Snippet */ 2,
23
+ })
24
+ })
25
+
26
+ test('executeTabCompletionProvider - error', async () => {
27
+ const fakeExtensionHost = {
28
+ invoke() {
29
+ throw new TypeError('x is not a function')
30
+ },
31
+ }
32
+ await expect(
33
+ ExtensionHostTabCompletion.executeTabCompletionProvider(fakeExtensionHost)
34
+ ).rejects.toThrowError(new TypeError('x is not a function'))
35
+ })
@@ -0,0 +1,61 @@
1
+ import * as ExtensionHostTextDocument from '../src/parts/ExtensionHost/ExtensionHostTextDocument.js'
2
+
3
+ test('textDocumentSyncInitial', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {},
6
+ }
7
+ expect(
8
+ await ExtensionHostTextDocument.textDocumentSyncInitial(
9
+ fakeExtensionHost,
10
+ '/test/file.txt',
11
+ 0,
12
+ '',
13
+ ''
14
+ )
15
+ ).toEqual(undefined)
16
+ })
17
+
18
+ test('textDocumentSyncInitial - error', async () => {
19
+ const fakeExtensionHost = {
20
+ async invoke() {
21
+ throw new TypeError('x is not a function')
22
+ },
23
+ }
24
+ await expect(
25
+ ExtensionHostTextDocument.textDocumentSyncInitial(
26
+ fakeExtensionHost,
27
+ '/test/file.txt',
28
+ 0,
29
+ '',
30
+ ''
31
+ )
32
+ ).rejects.toThrowError(new TypeError('x is not a function'))
33
+ })
34
+
35
+ test('textDocumentSyncIncremental', async () => {
36
+ const fakeExtensionHost = {
37
+ invoke() {},
38
+ }
39
+ expect(
40
+ await ExtensionHostTextDocument.textDocumentSyncIncremental(
41
+ fakeExtensionHost,
42
+ 0,
43
+ []
44
+ )
45
+ ).toEqual(undefined)
46
+ })
47
+
48
+ test('textDocumentSyncInitial - error', async () => {
49
+ const fakeExtensionHost = {
50
+ async invoke() {
51
+ throw new TypeError('x is not a function')
52
+ },
53
+ }
54
+ await expect(
55
+ ExtensionHostTextDocument.textDocumentSyncIncremental(
56
+ fakeExtensionHost,
57
+ 0,
58
+ []
59
+ )
60
+ ).rejects.toThrowError(new TypeError('x is not a function'))
61
+ })
@@ -0,0 +1,35 @@
1
+ import * as ExtensionHostTypeDefinition from '../src/parts/ExtensionHost/ExtensionHostTypeDefinition.js'
2
+
3
+ test('executeTypeDefinitionProvider', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {
6
+ return {
7
+ uri: '/test/file.txt',
8
+ startOffset: 0,
9
+ endOffset: 0,
10
+ }
11
+ },
12
+ }
13
+ expect(
14
+ await ExtensionHostTypeDefinition.executeTypeDefinitionProvider(
15
+ fakeExtensionHost,
16
+ 0,
17
+ 0
18
+ )
19
+ ).toEqual({
20
+ uri: '/test/file.txt',
21
+ startOffset: 0,
22
+ endOffset: 0,
23
+ })
24
+ })
25
+
26
+ test('executeTypeDefinitionProvider - error', async () => {
27
+ const fakeExtensionHost = {
28
+ invoke() {
29
+ throw new TypeError('x is not a function')
30
+ },
31
+ }
32
+ await expect(
33
+ ExtensionHostTypeDefinition.executeTypeDefinitionProvider(fakeExtensionHost)
34
+ ).rejects.toThrowError(new TypeError('x is not a function'))
35
+ })
@@ -0,0 +1,21 @@
1
+ import * as ExtensionHostWorkspace from '../src/parts/ExtensionHost/ExtensionHostWorkspace.js'
2
+
3
+ test('setWorkspacePath', async () => {
4
+ const fakeExtensionHost = {
5
+ invoke() {},
6
+ }
7
+ expect(
8
+ await ExtensionHostWorkspace.setWorkspacePath(fakeExtensionHost, '/test')
9
+ ).toEqual(undefined)
10
+ })
11
+
12
+ test('setWorkspacePath - error', async () => {
13
+ const fakeExtensionHost = {
14
+ async invoke() {
15
+ throw new TypeError('x is not a function')
16
+ },
17
+ }
18
+ await expect(
19
+ ExtensionHostWorkspace.setWorkspacePath(fakeExtensionHost, '/test')
20
+ ).rejects.toThrowError(new TypeError('x is not a function'))
21
+ })
@@ -0,0 +1,408 @@
1
+ import { createReadStream, createWriteStream } from 'node:fs'
2
+ import {
3
+ access,
4
+ mkdir,
5
+ mkdtemp,
6
+ readdir,
7
+ readFile,
8
+ rm,
9
+ writeFile,
10
+ } from 'node:fs/promises'
11
+ import http from 'node:http'
12
+ import { tmpdir } from 'node:os'
13
+ import { dirname, join } from 'node:path'
14
+ import { pipeline } from 'node:stream/promises'
15
+ import { constants, createBrotliCompress } from 'node:zlib'
16
+ import getPort from 'get-port'
17
+ import { jest } from '@jest/globals'
18
+ import tar from 'tar-fs'
19
+ import VError from 'verror'
20
+ import * as ExtensionManagement from '../src/parts/ExtensionManagement/ExtensionManagement.js'
21
+ import * as Platform from '../src/parts/Platform/Platform.js'
22
+
23
+ const getTmpDir = () => {
24
+ return mkdtemp(join(tmpdir(), 'foo-'))
25
+ }
26
+
27
+ const exists = async (path) => {
28
+ try {
29
+ await access(path)
30
+ return true
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {string} inDir
38
+ * @param {string} outFile
39
+ */
40
+ export const compress = async (inDir, outFile) => {
41
+ await mkdir(dirname(outFile), { recursive: true })
42
+ await pipeline(
43
+ tar.pack(inDir),
44
+ createBrotliCompress({
45
+ params: {
46
+ [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MIN_QUALITY,
47
+ },
48
+ }),
49
+ createWriteStream(outFile)
50
+ )
51
+ }
52
+
53
+ const createExtensionTarBr = async (files) => {
54
+ const folder = '/tmp/extension-test/test-author.test-extension'
55
+ await rm(folder, { force: true, recursive: true })
56
+ await mkdir(folder, { recursive: true })
57
+ for (const [filePath, content] of Object.entries(files)) {
58
+ await writeFile(`${folder}/${filePath}`, content)
59
+ }
60
+ await compress(folder, `${folder}.tar.br`)
61
+ }
62
+
63
+ let server
64
+ let handler
65
+
66
+ beforeAll(async () => {
67
+ const port = await getPort()
68
+ server = http.createServer((request, response) => {
69
+ handler(request, response)
70
+ })
71
+ await new Promise((resolve) => {
72
+ server.listen(port, () => {
73
+ resolve(undefined)
74
+ })
75
+ })
76
+ process.env.LVCE_MARKETPLACE_URL = `http://localhost:${port}`
77
+ })
78
+
79
+ afterAll(() => {
80
+ server.close()
81
+ })
82
+
83
+ afterEach(() => {
84
+ jest.restoreAllMocks()
85
+ })
86
+
87
+ // TODO this test is flaky sometimes: Jest has detected the following 1 open handle potentially keeping Jest from exiting: ● JSSTREAM
88
+ test.skip('install', async () => {
89
+ await createExtensionTarBr({
90
+ 'extension.json': `{
91
+ "id": "test-author.test-extension",
92
+ "name": "test-extension",
93
+ "publisher": "test-author",
94
+ "version": "0.0.1",
95
+ "main": "main.js"
96
+ }
97
+ `,
98
+ 'main.js': 'export const activate = () => { console.info("hello world") }',
99
+ 'package.json': '{ "type" : "module" }',
100
+ })
101
+ handler = async (request, response) => {
102
+ switch (request.url) {
103
+ case '/download/test-author.test-extension':
104
+ response.statusCode = 200
105
+ await pipeline(
106
+ createReadStream(
107
+ '/tmp/extension-test/test-author.test-extension.tar.br'
108
+ ),
109
+ response
110
+ )
111
+ break
112
+ default:
113
+ response.statusCode = 404
114
+ response.end()
115
+ break
116
+ }
117
+ }
118
+ const tmpDir = await getTmpDir()
119
+ Platform.state.getExtensionsPath = () => tmpDir
120
+ await ExtensionManagement.install('test-author.test-extension')
121
+ expect(
122
+ await readFile(
123
+ join(tmpDir, 'test-author.test-extension/extension.json'),
124
+ 'utf-8'
125
+ )
126
+ ).toBe(`{
127
+ "id": "test-author.test-extension",
128
+ "name": "test-extension",
129
+ "publisher": "test-author",
130
+ "version": "0.0.1",
131
+ "main": "main.js"
132
+ }
133
+ `)
134
+ expect(
135
+ await readFile(join(tmpDir, 'test-author.test-extension/main.js'), 'utf-8')
136
+ ).toBe('export const activate = () => { console.info("hello world") }')
137
+ expect(
138
+ await readFile(
139
+ join(tmpDir, 'test-author.test-extension/package.json'),
140
+ 'utf-8'
141
+ )
142
+ ).toBe('{ "type" : "module" }')
143
+ })
144
+
145
+ test('install should fail when the server sends a bad status code', async () => {
146
+ handler = (request, response) => {
147
+ switch (request.url) {
148
+ default:
149
+ response.statusCode = 404
150
+ return response.end()
151
+ }
152
+ }
153
+ const tmpDir = await getTmpDir()
154
+ Platform.state.getExtensionsPath = () => tmpDir
155
+ await expect(
156
+ ExtensionManagement.install('test-author.test-extension')
157
+ ).rejects.toThrowError(
158
+ /Failed to install extension "test-author.test-extension": Failed to download "http:\/\/localhost:\d+\/download\/test-author.test-extension": Response code 404 \(Not Found\)/
159
+ )
160
+ })
161
+
162
+ test('install should fail when the server sends an invalid compressed object', async () => {
163
+ // TODO avoid side effect in tests, use createServer
164
+ handler = (request, res) => {
165
+ switch (request.url) {
166
+ case '/download/test-author.test-extension':
167
+ res.statusCode = 200
168
+ return res.end('abc')
169
+ default:
170
+ res.statusCode = 404
171
+ return res.end()
172
+ }
173
+ }
174
+ const tmpDir = await getTmpDir()
175
+ Platform.state.getExtensionsPath = () => tmpDir
176
+ await expect(
177
+ ExtensionManagement.install('test-author.test-extension')
178
+ ).rejects.toThrowError(
179
+ /^Failed to install extension "test-author.test-extension": unexpected end of file/
180
+ )
181
+ })
182
+
183
+ test('uninstall', async () => {
184
+ const tmpDir = await getTmpDir()
185
+ Platform.state.getExtensionsPath = () => tmpDir
186
+ await mkdir(join(tmpDir, 'test-author.test-extension'))
187
+ expect(await exists(join(tmpDir, 'test-author.test-extension'))).toBe(true)
188
+ await ExtensionManagement.uninstall('test-author.test-extension')
189
+ expect(await exists(join(tmpDir, 'test-author.test-extension'))).toBe(false)
190
+ })
191
+
192
+ test("uninstall should fail when extension doesn't exist", async () => {
193
+ const tmpDir = await getTmpDir()
194
+ Platform.state.getExtensionsPath = () => tmpDir
195
+ await expect(
196
+ ExtensionManagement.uninstall('test-author.test-extension')
197
+ ).rejects.toThrowError(
198
+ /^Failed to uninstall extension "test-author.test-extension": ENOENT: no such file or directory/
199
+ )
200
+ })
201
+
202
+ // TODO test for extension main not found (extension host)
203
+
204
+ // TODO test for extension json not found (extension host)
205
+
206
+ // TODO test for extension code import fails (extension host)
207
+ // TODO test for extension activation fails (in extension host)
208
+ // TODO test for extension deactivation fails (in extension host)
209
+ // TODO test for global unhandlederror/unhandledrejection (in extension host)
210
+
211
+ test('getAllExtensions', async () => {
212
+ const tmpDir1 = await getTmpDir()
213
+ const manifestPath1 = join(tmpDir1, 'test-extension', 'extension.json')
214
+ await mkdir(dirname(manifestPath1))
215
+ await writeFile(manifestPath1, JSON.stringify({ id: 'test-extension' }))
216
+ const tmpDir2 = await getTmpDir()
217
+ const manifestPath2 = join(tmpDir2, 'builtin-extension', 'extension.json')
218
+ await mkdir(dirname(manifestPath2))
219
+ await writeFile(manifestPath2, JSON.stringify({ id: 'builtin-extension' }))
220
+ const tmpDir3 = await getTmpDir()
221
+ Platform.state.getExtensionsPath = () => tmpDir1
222
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir2
223
+ Platform.state.getDisabledExtensionsPath = () => tmpDir3
224
+ expect(await ExtensionManagement.getAllExtensions()).toEqual([
225
+ {
226
+ status: 'fulfilled',
227
+ id: 'builtin-extension',
228
+ path: join(tmpDir2, 'builtin-extension'),
229
+ },
230
+ {
231
+ status: 'fulfilled',
232
+ id: 'test-extension',
233
+ path: join(tmpDir1, 'test-extension'),
234
+ },
235
+ ])
236
+ })
237
+
238
+ test('getAllExtensions - invalid extension.json', async () => {
239
+ const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
240
+ const tmpDir1 = await getTmpDir()
241
+ const manifestPath = join(tmpDir1, 'test-extension', 'extension.json')
242
+ await mkdir(dirname(manifestPath))
243
+ await writeFile(manifestPath, '{invalid json}')
244
+ const tmpDir2 = await getTmpDir()
245
+ const tmpDir3 = await getTmpDir()
246
+ Platform.state.getExtensionsPath = () => tmpDir1
247
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir2
248
+ Platform.state.getDisabledExtensionsPath = () => tmpDir3
249
+ expect(await ExtensionManagement.getAllExtensions()).toEqual([
250
+ {
251
+ path: join(tmpDir1, 'test-extension'),
252
+ reason: new VError(
253
+ 'Failed to load extension "test-extension": Failed to load extension manifest: JSON parsing error:'
254
+ ),
255
+ status: 'rejected',
256
+ },
257
+ ])
258
+ })
259
+
260
+ test('disable', async () => {
261
+ const tmpDir1 = await getTmpDir()
262
+ const tmpDir2 = await getTmpDir()
263
+ await mkdir(join(tmpDir1, 'test-extension'))
264
+ await writeFile(join(tmpDir1, 'test-extension', 'extension.json'), '{}')
265
+ Platform.state.getExtensionsPath = () => tmpDir1
266
+ Platform.state.getDisabledExtensionsPath = () => tmpDir2
267
+ await ExtensionManagement.disable('test-extension')
268
+ expect(await readdir(tmpDir1)).toEqual([])
269
+ expect(await readdir(tmpDir2)).toEqual(['test-extension'])
270
+ })
271
+
272
+ test('disable should fail if enabled extension path does not exist', async () => {
273
+ const tmpDir1 = await getTmpDir()
274
+ const tmpDir2 = await getTmpDir()
275
+ await mkdir(join(tmpDir1, 'test-extension'))
276
+ await writeFile(join(tmpDir1, 'test-extension', 'extension.json'), '{}')
277
+ Platform.state.getExtensionsPath = () => tmpDir1
278
+ Platform.state.getDisabledExtensionsPath = () => tmpDir2
279
+ const nonExistentPath1 = join(tmpDir1, 'non-existent-extension')
280
+ const nonExistentPath2 = join(tmpDir2, 'non-existent-extension')
281
+ await expect(
282
+ ExtensionManagement.disable('non-existent-extension')
283
+ ).rejects.toThrowError(
284
+ `Failed to disable extension non-existent-extension: ENOENT: no such file or directory, rename '${nonExistentPath1}' -> '${nonExistentPath2}'`
285
+ )
286
+ })
287
+
288
+ test('getExtensions - empty object', async () => {
289
+ const tmpDir1 = await getTmpDir()
290
+ const tmpDir2 = await getTmpDir()
291
+ await mkdir(join(tmpDir1, 'test-extension-1'))
292
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), '{}')
293
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
294
+ Platform.state.getExtensionsPath = () => tmpDir2
295
+ expect(await ExtensionManagement.getExtensions()).toEqual([
296
+ {
297
+ status: 'fulfilled',
298
+ path: join(tmpDir1, '/test-extension-1'),
299
+ },
300
+ ])
301
+ })
302
+
303
+ test.skip('getExtensions - error - invalid value - empty array', async () => {
304
+ const tmpDir1 = await getTmpDir()
305
+ const tmpDir2 = await getTmpDir()
306
+ await mkdir(join(tmpDir1, 'test-extension-1'))
307
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), '[]')
308
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
309
+ Platform.state.getExtensionsPath = () => tmpDir2
310
+ expect(await ExtensionManagement.getExtensions()).toEqual([
311
+ {
312
+ status: 'fulfilled',
313
+
314
+ path: join(tmpDir1, 'test-extension-1'),
315
+ },
316
+ ])
317
+ })
318
+
319
+ test('getExtensions - error - invalid value - null', async () => {
320
+ const tmpDir1 = await getTmpDir()
321
+ const tmpDir2 = await getTmpDir()
322
+ await mkdir(join(tmpDir1, 'test-extension-1'))
323
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), 'null')
324
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
325
+ Platform.state.getExtensionsPath = () => tmpDir2
326
+ expect(await ExtensionManagement.getExtensions()).toEqual([
327
+ {
328
+ status: 'rejected',
329
+ reason: new VError(
330
+ 'Failed to load extension "test-extension-1": Failed to load extension manifest: Invalid manifest file: Not an JSON object.'
331
+ ),
332
+ path: join(tmpDir1, 'test-extension-1'),
333
+ },
334
+ ])
335
+ })
336
+
337
+ test('getExtensions - error - invalid value - string', async () => {
338
+ const tmpDir1 = await getTmpDir()
339
+ const tmpDir2 = await getTmpDir()
340
+ await mkdir(join(tmpDir1, 'test-extension-1'))
341
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), '""')
342
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
343
+ Platform.state.getExtensionsPath = () => tmpDir2
344
+ expect(await ExtensionManagement.getExtensions()).toEqual([
345
+ {
346
+ status: 'rejected',
347
+ reason: new VError(
348
+ 'Failed to load extension "test-extension-1": Failed to load extension manifest: Invalid manifest file: Not an JSON object.'
349
+ ),
350
+ path: join(tmpDir1, 'test-extension-1'),
351
+ },
352
+ ])
353
+ })
354
+
355
+ test('getExtensions - error - invalid value - number', async () => {
356
+ const tmpDir1 = await getTmpDir()
357
+ const tmpDir2 = await getTmpDir()
358
+ await mkdir(join(tmpDir1, 'test-extension-1'))
359
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), '42')
360
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
361
+ Platform.state.getExtensionsPath = () => tmpDir2
362
+ expect(await ExtensionManagement.getExtensions()).toEqual([
363
+ {
364
+ status: 'rejected',
365
+ reason: new VError(
366
+ 'Failed to load extension "test-extension-1": Failed to load extension manifest: Invalid manifest file: Not an JSON object.'
367
+ ),
368
+ path: join(tmpDir1, 'test-extension-1'),
369
+ },
370
+ ])
371
+ })
372
+
373
+ test('getExtensions - error - invalid value - boolean', async () => {
374
+ const tmpDir1 = await getTmpDir()
375
+ const tmpDir2 = await getTmpDir()
376
+ await mkdir(join(tmpDir1, 'test-extension-1'))
377
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), 'true')
378
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
379
+ Platform.state.getExtensionsPath = () => tmpDir2
380
+ expect(await ExtensionManagement.getExtensions()).toEqual([
381
+ {
382
+ status: 'rejected',
383
+ reason: new VError(
384
+ 'Failed to load extension "test-extension-1": Failed to load extension manifest: Invalid manifest file: Not an JSON object.'
385
+ ),
386
+ path: join(tmpDir1, 'test-extension-1'),
387
+ },
388
+ ])
389
+ })
390
+
391
+ // TODO should have better error message here and also stack
392
+ test('getExtensions - error - invalid json', async () => {
393
+ const tmpDir1 = await getTmpDir()
394
+ const tmpDir2 = await getTmpDir()
395
+ await mkdir(join(tmpDir1, 'test-extension-1'))
396
+ await writeFile(join(tmpDir1, 'test-extension-1', 'extension.json'), '{')
397
+ Platform.state.getBuiltinExtensionsPath = () => tmpDir1
398
+ Platform.state.getExtensionsPath = () => tmpDir2
399
+ expect(await ExtensionManagement.getExtensions()).toEqual([
400
+ {
401
+ reason: new VError(
402
+ 'Failed to load extension "test-extension-1": Failed to load extension manifest: Unexpected end of JSON input while parsing "{"'
403
+ ),
404
+ status: 'rejected',
405
+ path: join(tmpDir1, 'test-extension-1'),
406
+ },
407
+ ])
408
+ })