From 9af4e4c476a812bd1627c40adaede9c32cd21dc7 Mon Sep 17 00:00:00 2001 From: sebthom Date: Fri, 30 May 2025 16:28:21 +0200 Subject: [PATCH] Add Astro web framework support --- .github/dependabot.yml | 34 +++ .gitignore | 4 +- README.md | 5 +- org.eclipse.wildwebdeveloper.tests/.project | 6 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../wildwebdeveloper/tests/TestAstro.java | 194 ++++++++++++++++++ .../testProjects/astro-app/.gitignore | 5 + .../testProjects/astro-app/.project | 11 + .../testProjects/astro-app/astro.config.mjs | 5 + .../testProjects/astro-app/package.json | 14 ++ .../astro-app/src/layouts/base.astro | 17 ++ .../astro-app/src/pages/index.astro | 7 + .../testProjects/astro-app/tsconfig.json | 5 + org.eclipse.wildwebdeveloper/package.json | 1 + org.eclipse.wildwebdeveloper/plugin.xml | 33 ++- .../astro/AstroLanguageServer.java | 80 ++++++++ 16 files changed, 421 insertions(+), 4 deletions(-) create mode 100644 org.eclipse.wildwebdeveloper.tests/.settings/org.eclipse.m2e.core.prefs create mode 100644 org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestAstro.java create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.gitignore create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.project create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/astro.config.mjs create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/package.json create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/layouts/base.astro create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/pages/index.astro create mode 100644 org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/tsconfig.json create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/astro/AstroLanguageServer.java diff --git a/.github/dependabot.yml b/.github/dependabot.yml index baf9ca8713..57837ec9f8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,17 @@ updates: interval: daily open-pull-requests-limit: 10 groups: + astro: + patterns: + - "astro-*" + - "@astrojs/*" + babel: + patterns: + - "@babel/*" + volar: + patterns: + - "volar-*" + - "@volar/*" vue: patterns: - "@vue/*" @@ -45,6 +56,29 @@ updates: typescript-eslint: patterns: - "@typescript-eslint/*" +- package-ecosystem: npm + directory: "/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + astro: + patterns: + - "astro-*" + - "@astrojs/*" + babel: + patterns: + - "@babel/*" + micromark: + patterns: + - "micromark*" + syntax-tree: + patterns: + - "mdast-*" + - "hast*" + - "unist*" + - "rehype" + - "remark" - package-ecosystem: npm directory: "/org.eclipse.wildwebdeveloper.tests/testProjects/vue-app" schedule: diff --git a/.gitignore b/.gitignore index 9f970225ad..e52386e74a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -target/ \ No newline at end of file +# Maven +target/ +.polyglot.META-INF diff --git a/README.md b/README.md index da796d1340..27627f973d 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ and * Kubernetes * Angular (Components & Templates, in TypeScript and HTML files) -* React (JSX, TSX, embedded HTML) +* [Astro](https://astro.build) +* [React](https://react.dev/) (JSX, TSX, embedded HTML) * ESLint (for JavaScript and TypeScript) -* Vue.js +* [Vue.js](https://vuejs.org/) Supported features for edition are diff --git a/org.eclipse.wildwebdeveloper.tests/.project b/org.eclipse.wildwebdeveloper.tests/.project index 2e9b6d5ba7..87dd4fa3b3 100644 --- a/org.eclipse.wildwebdeveloper.tests/.project +++ b/org.eclipse.wildwebdeveloper.tests/.project @@ -20,8 +20,14 @@ + + org.eclipse.m2e.core.maven2Builder + + + + org.eclipse.m2e.core.maven2Nature org.eclipse.pde.PluginNature org.eclipse.jdt.core.javanature diff --git a/org.eclipse.wildwebdeveloper.tests/.settings/org.eclipse.m2e.core.prefs b/org.eclipse.wildwebdeveloper.tests/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000000..f897a7f1cb --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestAstro.java b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestAstro.java new file mode 100644 index 0000000000..30a09c2b63 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestAstro.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.tests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4e.LanguageServiceAccessor; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Event; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.TextEditor; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.tests.harness.util.DisplayHelper; +import org.eclipse.ui.texteditor.AbstractTextEditor; +import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TestAstro { + static IProject project; + static IFolder pagesFolder; + + @BeforeAll + public static void setUp() throws Exception { + AllCleanRule.closeIntro(); + AllCleanRule.enableLogging(); + + project = Utils.provisionTestProject("astro-app"); + ProcessBuilder builder = NodeJSManager.prepareNPMProcessBuilder("install", "--no-bin-links", "--ignore-scripts").directory(project + .getLocation().toFile()); + Process process = builder.start(); + System.out.println(builder.command().toString()); + String result = process.errorReader().lines().collect(Collectors.joining("\n")); + System.out.println("Error Stream: >>>\n" + result + "\n<<<"); + + result = process.inputReader().lines().collect(Collectors.joining("\n")); + System.out.println("Output Stream: >>>\n" + result + "\n<<<"); + + assertEquals(0, process.waitFor(), "npm install didn't complete property"); + + project.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor()); + assertTrue(project.exists()); + pagesFolder = project.getFolder("src").getFolder("pages"); + assertTrue(pagesFolder.exists()); + } + + @BeforeEach + public void setUpTestCase() { + AllCleanRule.enableLogging(); + } + + @AfterAll + public static void tearDown() throws Exception { + new AllCleanRule().afterEach(null); + } + + @Test + @SuppressWarnings("restriction") + void testAstroPage() throws Exception { + final var indexPageFile = project.getFile("src/pages/index.astro"); + final var indexPageEditor = (TextEditor) IDE.openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), + indexPageFile); + final var display = indexPageEditor.getSite().getShell().getDisplay(); + final var doc = indexPageEditor.getDocumentProvider().getDocument(indexPageEditor.getEditorInput()); + + /* + * ensure Astro Language Server is started and connected + */ + final var astroLS = new AtomicReference(); + DisplayHelper.waitForCondition(display, 10_000, () -> { + astroLS.set(LanguageServiceAccessor.getStartedWrappers(doc, null, false).stream() // + .filter(w -> "org.eclipse.wildwebdeveloper.astro".equals(w.serverDefinition.id)) // + .findFirst().orElse(null)); + return astroLS.get() != null // + && astroLS.get().isActive() // + && astroLS.get().isConnectedTo(LSPEclipseUtils.toUri(doc)); + }); + + /* + * ensure that a task marker is created for the unused node:path import statement + */ + assertTrue(DisplayHelper.waitForCondition(display, 10_000, () -> { + try { + return Arrays.stream(indexPageFile.findMarkers("org.eclipse.lsp4e.diagnostic", true, IResource.DEPTH_ZERO)) // + .anyMatch(marker -> marker.getAttribute(IMarker.MESSAGE, "").contains("'path' is declared but its value is never read")); + } catch (final Exception ex) { + ex.printStackTrace(); + return false; + } + }), "Diagnostic not published in standalone component file"); + + /* + * ensure "Open Declaration" works + */ + final var baseLayoutFile = project.getFile("src/layouts/base.astro"); + final var baseLayoutPath = baseLayoutFile.getLocation().toPath(); + int offset = doc.get().indexOf("BaseLayout"); + + // ensure "Open Definition" link exists + assertTrue(DisplayHelper.waitForCondition(display, 10_000, () -> { + try { + final var params = LSPEclipseUtils.toTextDocumentPosistionParams(offset, doc); + final var baseLayoutDefinitionLink = LanguageServers.forDocument(doc) // + .withCapability(ServerCapabilities::getDefinitionProvider) // + .collectAll(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params))) // + .get(1, TimeUnit.SECONDS) // + .stream().filter(Either::isRight) // + .flatMap(e -> e.getRight().stream()) // + .filter(locationLink -> Paths.get(URI.create(locationLink.getTargetUri())).equals(baseLayoutPath)) // + .findFirst().orElse(null); + return baseLayoutDefinitionLink != null; + } catch (final Exception ex) { + ex.printStackTrace(); + return false; + } + })); + + // simulate pressing F3 "Open Declaration" for BaseLayout + display.syncExec(() -> { + try { + indexPageEditor.selectAndReveal(offset, 0); + + final Method getSourceViewerMethod = AbstractTextEditor.class.getDeclaredMethod("getSourceViewer"); //$NON-NLS-1$ + getSourceViewerMethod.setAccessible(true); + final var viewer = (ITextViewer) getSourceViewerMethod.invoke(indexPageEditor); + final var widget = viewer.getTextWidget(); + + final var keyDown = new Event(); + keyDown.type = SWT.KeyDown; + keyDown.keyCode = SWT.F3; + keyDown.widget = widget; + widget.notifyListeners(SWT.KeyDown, keyDown); + final var keyUp = new Event(); + keyUp.type = SWT.KeyUp; + keyUp.keyCode = SWT.F3; + keyUp.widget = widget; + widget.notifyListeners(SWT.KeyUp, keyUp); + } catch (final Exception ex) { + fail(ex); + } + }); + + // ensure a new editor window was opened for "src/layouts/base.astro" file + assertTrue(DisplayHelper.waitForCondition(display, 10_000, () -> { + final var baseLayoutEditor = (TextEditor) PlatformUI.getWorkbench() // + .getActiveWorkbenchWindow() // + .getActivePage() // + .findEditor(new FileEditorInput(baseLayoutFile)); + + if (baseLayoutEditor != null) { + baseLayoutEditor.close(false); + return true; + } + return false; + })); + + /* + * cleanup + */ + indexPageEditor.close(false); + } +} diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.gitignore b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.gitignore new file mode 100644 index 0000000000..ce4145e439 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.gitignore @@ -0,0 +1,5 @@ +dist/ +.astro/ +node_modules/ +npm-debug.log* +package-lock.json \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.project b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.project new file mode 100644 index 0000000000..eabeda859e --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/.project @@ -0,0 +1,11 @@ + + + wildwebdeveloper-astro-app + + + + + + + + diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/astro.config.mjs b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/astro.config.mjs new file mode 100644 index 0000000000..e762ba5cf6 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/astro.config.mjs @@ -0,0 +1,5 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({}); diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/package.json b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/package.json new file mode 100644 index 0000000000..dc79a4b1c8 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/package.json @@ -0,0 +1,14 @@ +{ + "name": "astro-app", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "astro": "^5.8.0" + } +} \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/layouts/base.astro b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/layouts/base.astro new file mode 100644 index 0000000000..2227a46f27 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/layouts/base.astro @@ -0,0 +1,17 @@ +--- +export interface Props { + title: string; +} +const { title } = Astro.props as Props; +--- + + + + + + {title} + + + + + diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/pages/index.astro b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/pages/index.astro new file mode 100644 index 0000000000..5bd741f54f --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/src/pages/index.astro @@ -0,0 +1,7 @@ +--- +import * as path from "node:path"; // unused import tested by TestAstro.java +import BaseLayout from "../layouts/base.astro"; +--- + +

Hello from Astro!

+
diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/tsconfig.json b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/tsconfig.json new file mode 100644 index 0000000000..8bf91d3bb9 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/astro-app/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/org.eclipse.wildwebdeveloper/package.json b/org.eclipse.wildwebdeveloper/package.json index 0e60118126..dbba2aa36e 100644 --- a/org.eclipse.wildwebdeveloper/package.json +++ b/org.eclipse.wildwebdeveloper/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@angular/language-server": "20.0.1", + "astro-vscode" : "2.15.4", "firefox-debugadapter": "2.15.0", "typescript": "5.8.3", "typescript-language-server": "4.3.4", diff --git a/org.eclipse.wildwebdeveloper/plugin.xml b/org.eclipse.wildwebdeveloper/plugin.xml index 3252c9a95d..5f5f4b600e 100644 --- a/org.eclipse.wildwebdeveloper/plugin.xml +++ b/org.eclipse.wildwebdeveloper/plugin.xml @@ -519,7 +519,38 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/astro/AstroLanguageServer.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/astro/AstroLanguageServer.java new file mode 100644 index 0000000000..d1759d30db --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/astro/AstroLanguageServer.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.astro; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.ILog; +import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; +import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; + +/** + * Launches the embedded Node.js based Astro language server. + * + * See https://github.com/withastro/language-tools/tree/main/packages/language-server + * + * @author Sebastian Thomschke + */ +public final class AstroLanguageServer extends ProcessStreamConnectionProvider { + + private static volatile String astroLanguageServerPath; + private static volatile String typescriptSdkPath; + + private static Path resolveResource(String resourcePath) throws IOException { + try { + URL url = FileLocator.toFileURL(AstroLanguageServer.class.getResource(resourcePath)); + return Paths.get(url.toURI()).toAbsolutePath(); + } catch (URISyntaxException ex) { + throw new IOException("Failed to resolve resource URI: " + resourcePath, ex); + } + } + + public AstroLanguageServer() throws IOException { + try { + if (astroLanguageServerPath == null || typescriptSdkPath == null) { + astroLanguageServerPath = resolveResource("/node_modules/astro-vscode/dist/node/server.js").toString(); + typescriptSdkPath = resolveResource("/node_modules/typescript/lib").toString(); + } + setCommands(List.of( // + NodeJSManager.getNodeJsLocation().getAbsolutePath(), // + astroLanguageServerPath, // + "--stdio" // + )); + setWorkingDirectory(System.getProperty("user.dir")); + } catch (IOException ex) { + ILog.get().error(ex.getMessage(), ex); + } + } + + @Override + public Map getInitializationOptions(final URI projectRootUri) { + final Map options = new HashMap<>(); + setWorkingDirectory(projectRootUri.getRawPath()); + + // see https://github.com/withastro/language-tools/blob/main/packages/vscode/src/client.ts + // see https://github.com/withastro/language-tools/blob/main/packages/language-server/src/nodeServer.ts + options.put("typescript", Collections.singletonMap("tsdk", typescriptSdkPath)); + options.put("contentIntellisense", true); + return options; + } +}