enso/app/ide-desktop/lib/ts-plugin-namespace-auto-import/index.js
somebody1234 cbf6d41e4c
More E2E tests; export default classes from modules (#8730)
This is a prerequisite for adding a CI action for E2E tests.

- Fix E2E tests
- Remove visual regression testing (VRT) and associated fixtures (screenshots) for now
- Switch dashboard almost fully to Vite, from ESBuild, to match GUI2's build tooling.
- Add some new E2E tests:
- Creating assets
- Deleting assets
- Creating assets from the samples on the home page
- Sort assets
- Includes fixes for sorting:
- Group sorted assets by type again (regression) (see https://github.com/enso-org/cloud-v2/issues/554)
- Make sorting by title, case insensitive. This is because it is more intuitive for non-programmers if all items with uppercase names *aren't* separated from those with lowercase names - especially since the Windows FS is case-insensitive.
- Normalization of Unicode letters is *not* currently being done. It can potentially be added later.
- Double-clicking *anywhere* on a directory row now expands it. Previously it was only being expanded when double clicking
- Add recursive label adding/removal to mirror backend
- Note: The current implementation is not exactly the same as the backend's implementation.
- Fix https://github.com/enso-org/cloud-v2/issues/872
- Unset "saved project details" (for opening the last open project) if fetching it produces an error.

# Important Notes
- All tests pass. (run `npm run test:e2e` in `app/ide-desktop/lib/dashboard`)
- All `npm` commands should be run in `app/ide-desktop/lib/dashboard`. `dashboard:*` npm scripts have been removed from `app/ide-desktop` to prevent a mess.
- `npm run dev` confirmed to still work. Note that it has not been changed as it was already using Vite.
- `npm run build` now uses `vite build`. This has been tested using a local HTTP server that supports `404.html`.
- Other cases have been tested:
- `npm run test:e2e` works (all tests pass)
- `./run ide build` works
- `./run ide watch` works
- `./run ide2 build` works
- `./run gui watch` works
2024-01-31 11:35:41 +00:00

160 lines
5.3 KiB
JavaScript

/** @file A plugin to change auto-imports to use `import * as module` instead of `import {}`. */
/** A TypeScript compiler plugin. */
module.exports = function init() {
/** Turn a module name into a valid identifier.
* @param {string} name - module name */
const normalizeModuleName = name => {
const intermediate = name.length === 0 ? '' : name[0].toLowerCase() + name.slice(1)
const result = intermediate.replace(/^\W+|\W+(.?)/g, (_, a) =>
String(a ?? '').toUpperCase()
)
return NON_CONTEXTUAL_KEYWORDS.has(result) ? '_' + result : result
}
/** Create the plugin.
* @param {import('typescript/lib/tsserverlibrary').server.PluginCreateInfo} info - Plugin utilities. */
const create = info => {
/** @type {import('typescript/lib/tsserverlibrary').LanguageService} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const proxy = Object.create(null)
const oldLS = info.languageService
for (const [k, v] of Object.entries(oldLS)) {
// @ts-expect-error Runtime reflection is not type-safe.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
proxy[k] = (...args) => v.apply(oldLS, args)
}
proxy.getCompletionsAtPosition = (fileName, position, options) => {
const result = info.languageService.getCompletionsAtPosition(
fileName,
position,
options
)
for (const completion of result?.entries ?? []) {
if (
completion.hasAction &&
/\bexport\b/.test(completion.kindModifiers ?? '') &&
completion.data?.exportName !== 'default'
) {
const moduleName = completion.data?.moduleSpecifier?.match(/[^/]+$/)?.[0] ?? ''
if (moduleName) {
completion.insertText = `${normalizeModuleName(moduleName)}.${
completion.name
}`
}
}
}
return result
}
proxy.getCompletionEntryDetails = (
fileName,
position,
entryName,
formatOptions,
source,
preferences,
data
) => {
const result = info.languageService.getCompletionEntryDetails(
fileName,
position,
entryName,
formatOptions,
source,
preferences,
data
)
for (const action of result?.codeActions ?? []) {
if (action.description.startsWith('Add import from ')) {
for (const change of action.changes) {
for (const textChange of change.textChanges) {
textChange.newText = textChange.newText.replace(
/^(import ){.*}( from (['"])(?:.*[/])?(.*)\3)/m,
(_, prefix, suffix, _quote, moduleName) =>
`${prefix}* as ${normalizeModuleName(
String(moduleName)
)}${suffix}`
)
}
}
} else if (action.description.startsWith('Update import from ')) {
const moduleName =
action.description.match(/(['"])(?:.*[/])?(?<moduleName>.*)\1/)?.groups
?.moduleName ?? ''
const replacement = `, * as ${normalizeModuleName(moduleName)}`
for (const change of action.changes) {
for (const textChange of change.textChanges) {
textChange.newText = textChange.newText.replace(
/^, {.*}$/m,
replacement
)
}
}
// "Change 'foo' to 'module.foo'"
} else if (/^Change '(.*)' to '.*[.]\1'$/.test(action.description)) {
for (const change of action.changes) {
for (const textChange of change.textChanges) {
textChange.newText = ''
}
}
}
}
return result
}
return proxy
}
return { create }
}
const NON_CONTEXTUAL_KEYWORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'implements',
'interface',
'let',
'package',
'private',
'protected',
'public',
'static',
'yield',
])