Revert "feat(website): add responsive homepage and GitHub Pages deployment (#…" (#1872)

This reverts commit bb508d4118.
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-07 01:29:03 +08:00
1 джерело bb508d4118
коміт 4dd51a1101
43 files changed
-3363

No files matched your search

-86
Переглянути файл
@@ -1,86 +0,0 @@
name: Website
on:
push:
branches: [main]
paths:
- "website/**"
- ".github/workflows/website.yml"
pull_request:
paths:
- "website/**"
- ".github/workflows/website.yml"
workflow_dispatch:
release:
types: [published]
# Releases published by GITHUB_TOKEN do not trigger another release workflow.
workflow_run:
workflows: [Release]
types: [completed]
permissions:
contents: read
concurrency:
group: website-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
pages: read
defaults:
run:
working-directory: website
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: website/package.json
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm test
- name: Refresh official release downloads
env:
GITHUB_TOKEN: ${{ github.token }}
run: pnpm release:refresh
# A PR must be testable before Pages has been enabled in repository settings.
- id: pages
if: github.event_name != 'pull_request'
uses: actions/configure-pages@v5
- name: Build static homepage
env:
BASE_PATH: ${{ github.event_name == 'pull_request' && format('/{0}', github.event.repository.name) || steps.pages.outputs.base_path }}
VITE_SITE_URL: ${{ steps.pages.outputs.base_url }}
run: pnpm build && pnpm typecheck && pnpm check:static
# Also verify a custom-domain / user-site root with no repository prefix.
- name: Check root deployment
if: github.event_name == 'pull_request'
env:
BASE_PATH: /
run: pnpm build && pnpm check:static
- uses: actions/upload-pages-artifact@v4
if: github.event_name != 'pull_request'
with:
path: website/dist/client
deploy:
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
-7
Переглянути файл
@@ -1,7 +0,0 @@
node_modules/
dist/
.tanstack/
.output/
.nitro/
src/routeTree.gen.ts
*.tsbuildinfo
-61
Переглянути файл
@@ -1,61 +0,0 @@
# OpenIDEA website
The dark, responsive product homepage uses TanStack Start, StyleX and Base UI.
It is an independent pnpm package; desktop dependencies and commands are unchanged.
Its local workspace boundary keeps installation independent of the parent workspace.
```sh
cd website
pnpm install --frozen-lockfile
pnpm dev
```
Node 22.12+ and pnpm 11 are required. The image preview uses Base UI's Dialog for
keyboard focus management and Escape dismissal. Navigation and downloads are native
links. The debugger screenshot, logo and Inter font are hosted locally. Inter's license
is retained in `public/fonts/OFL.txt`.
## Static build
```sh
pnpm test
BASE_PATH=/openidea/ pnpm build
pnpm typecheck
BASE_PATH=/openidea/ pnpm check:static
pnpm preview
```
Open the preview at `/openidea/` when using that base path. Use `BASE_PATH=/` for a
custom-domain root or a user/organization Pages site. The deployable directory is
`dist/client/`; the server bundle is only used during prerendering and is not deployed.
No running Node server or API is required on GitHub Pages.
`check:static` validates that the initial HTML contains the homepage, benchmark and
measurement caveat, that component styles have been compiled, and that local media,
fonts and bundles exist under the selected base path.
## GitHub Pages
1. In the target repository, select **Settings → Pages → Build and deployment →
Source: GitHub Actions**. Do this once before the first deployment.
2. Merge the website and `.github/workflows/website.yml` into `main`.
3. The Website workflow builds and deploys relevant main pushes. It can also be
triggered manually from Actions on `main`. PRs build and validate without publishing.
`actions/configure-pages` supplies the repository prefix or custom-domain root and
canonical URL. The workflow uploads only `website/dist/client`. Deployment permissions
are restricted to the deploy job; no personal access token is needed by the workflow.
Check the existing `github-pages` environment's branch rules if GitHub blocks a deploy.
Homepage download buttons open the matching macOS, Windows or Linux page when a desktop
platform can be identified. Mobile and unknown systems open the platform chooser. Each
platform page links directly to actual installer assets, with explicit architecture and
package format choices. All download pages are prerendered and work without JavaScript.
`src/data/release.json` is a checked-in snapshot of the official stable release so local
builds work offline. Run `pnpm release:refresh` to refresh it from GitHub. CI refreshes it
before building; publishing a release or completing the Release workflow also rebuilds
the website. No browser-time GitHub API requests or API tokens are needed.
The benchmark follows the root README; update `src/components/Home.tsx` when the published
measurements change.
-11
Переглянути файл
@@ -1,11 +0,0 @@
# Website screenshots
Full-page desktop captures of the prerendered production website, served as static
files at `/openidea/`. These images show only the web page, without browser chrome.
| Page | Width | Screenshot |
| ---------------- | ------ | --------------------------------- |
| Homepage | 1280px | [Full page](desktop.jpg) |
| macOS download | 1280px | [Full page](download-macos.jpg) |
| Windows download | 1280px | [Full page](download-windows.jpg) |
| Linux download | 1280px | [Full page](download-linux.jpg) |
Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 149 KiB

Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 54 KiB

Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 45 KiB

Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 48 KiB

-37
Переглянути файл
@@ -1,37 +0,0 @@
{
"name": "@openidea/website",
"private": true,
"type": "module",
"license": "Apache-2.0",
"packageManager": "[email protected]",
"engines": {
"node": ">=22.12"
},
"scripts": {
"dev": "vite --host 127.0.0.1 --port 3000",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1 --port 3000",
"typecheck": "tsc --noEmit",
"test": "node --test tests/*.test.mjs",
"release:refresh": "node scripts/update-release.mjs",
"check:static": "node scripts/check-static.mjs"
},
"dependencies": {
"@base-ui/react": "1.9.0",
"@stylexjs/stylex": "0.19.1",
"@tanstack/react-router": "1.170.46",
"@tanstack/react-start": "1.169.4",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@stylexjs/unplugin": "0.19.1",
"@types/node": "^22.0.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.0",
"typescript": "^5.9.0",
"unplugin": "^2.3.11",
"vite": "^8.0.0"
}
}
Створено
-1712
File diff suppressed because it is too large. Load diff
-2
Переглянути файл
@@ -1,2 +0,0 @@
# Keep this standalone package independent of the desktop workspace above it.
packages: []
Переглянути файл
Whitespace-only changes.
-93
Переглянути файл
@@ -1,93 +0,0 @@
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Бінарний файл не відображається.
-1
Переглянути файл
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="22 24 56 56"><title>OpenIDEA</title><rect x="22" y="26" width="56" height="52" rx="12" fill="#D9582C"/><rect x="27" y="26" width="46" height="42" rx="9" fill="#1B1A1A"/><circle cx="44" cy="47.4" r="4.1" fill="none" stroke="#EAE7E0" stroke-width="2.7"/><rect x="54.3" y="42" width="2.8" height="10.5" fill="#EAE7E0"/><rect x="51.2" y="42" width="5.9" height="2.5" fill="#EAE7E0"/><rect x="51.2" y="50" width="9.4" height="2.5" fill="#EAE7E0"/><circle cx="55.7" cy="38.4" r="1.8" fill="#EAE7E0"/></svg>

До

Ширина:  |  Висота:  |  Розмір: 573 B

Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 907 KiB

-67
Переглянути файл
@@ -1,67 +0,0 @@
import assert from "node:assert/strict";
import { access, readFile, readdir, stat } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { normalizeBasePath } from "../src/lib/paths.mjs";
export async function checkStatic(root, basePath = "/") {
const base = normalizeBasePath(basePath);
const html = await readFile(resolve(root, "index.html"), "utf8");
const visibleText = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ");
assert.match(visibleText, /The IDE you know,\s*rebuilt in Rust\./, "Missing prerendered homepage");
for (const text of [
"Keep your way of working.",
"Startup and memory.",
"169 MB",
"Warm caches",
"IDEA analyses Python more deeply",
"Try OpenIDEA.",
]) {
assert.ok(visibleText.includes(text), `Missing prerendered homepage content: ${text}`);
}
const checked = new Set();
async function checkAsset(reference, parent = new URL(base, "https://pages.test")) {
if (/^(?:https?:|data:|mailto:|#|\/\/)/.test(reference)) return;
const url = new URL(reference, parent);
assert.ok(url.pathname.startsWith(base), `Asset escaped base path ${base}: ${reference}`);
const relative = decodeURIComponent(url.pathname.slice(base.length));
if (!relative || checked.has(relative)) return;
checked.add(relative);
const file = resolve(root, relative);
assert.ok(file.startsWith(`${resolve(root)}/`), `Asset escaped output directory: ${reference}`);
await access(file).catch(() => {
throw new Error(`Missing asset: ${reference}`);
});
if ((await stat(file)).isDirectory()) {
await access(resolve(file, "index.html")).catch(() => {
throw new Error(`Missing prerendered index: ${reference}`);
});
}
if (relative.endsWith(".css")) {
const css = await readFile(file, "utf8");
assert.ok(
css.includes("cursor:zoom-in") || css.includes("cursor: zoom-in"),
"Missing compiled StyleX component styles",
);
for (const match of css.matchAll(/url\(["']?([^\s)"']+)["']?\)/g)) await checkAsset(match[1], url);
}
}
const pages = (await readdir(root, { recursive: true })).filter((name) => name.endsWith(".html"));
for (const page of pages) {
const pageHtml = await readFile(resolve(root, page), "utf8");
const pageUrl = new URL(`${base}${page.replace(/index\.html$/, "")}`, "https://pages.test");
for (const match of pageHtml.matchAll(/(?:src|href)="([^"]+)"/g)) await checkAsset(match[1], pageUrl);
}
assert.ok(checked.size > 0, "No local assets in prerendered homepage");
return { assets: checked.size, basePath: base, pages: pages.length };
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const result = await checkStatic(resolve("dist/client"), process.env.BASE_PATH);
for (const route of ["download", "download/macos", "download/windows", "download/linux"]) {
const page = await readFile(resolve("dist/client", route, "index.html"), "utf8");
assert.match(page, /<h1[^>]*>Download/, `Missing prerendered download page: ${route}`);
}
console.log(`Static website verified: ${result.pages} pages, ${result.assets} assets, base path ${result.basePath}`);
}
-24
Переглянути файл
@@ -1,24 +0,0 @@
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { buildDownloadManifest } from "../src/lib/downloads.mjs";
// A saved GitHub API response can be used for a reproducible, offline refresh.
const input = process.argv[2];
let release;
if (input) {
release = JSON.parse(await readFile(input, "utf8"));
} else {
const response = await fetch("https://api.github.com/repos/l0ng-ai/openidea/releases/latest", {
headers: {
Accept: "application/vnd.github+json",
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
},
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`Unable to load the latest release (${response.status})`);
release = await response.json();
}
const manifest = buildDownloadManifest(release);
const destination = new URL("../src/data/", import.meta.url);
await mkdir(destination, { recursive: true });
await writeFile(new URL("release.json", destination), `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`Download manifest: ${manifest.tag}, ${Object.values(manifest.packages).flat().length} installers`);
-13
Переглянути файл
@@ -1,13 +0,0 @@
import { useEffect } from "react";
import { publicAsset } from "../lib/paths.mjs";
// Start has no index.html entry for StyleX's automatic dev-server injection.
// Production CSS is extracted into the normal Vite stylesheet instead.
export function DevStyles() {
useEffect(() => {
if (import.meta.env.DEV) void import("virtual:stylex:css-only");
}, []);
return import.meta.env.DEV ? (
<link rel="stylesheet" href={publicAsset("virtual:stylex.css", import.meta.env.BASE_URL)} />
) : null;
}
-143
Переглянути файл
@@ -1,143 +0,0 @@
import * as stylex from "@stylexjs/stylex";
import manifest from "../data/release.json";
import type { Platform } from "../lib/downloads.mjs";
import { publicAsset } from "../lib/paths.mjs";
import { SiteHeader, SiteFooter, docs } from "./SiteLayout";
import { styles as shared } from "./home.stylex";
import { styles as s } from "./download.stylex";
const platforms = [
{
id: "macos",
name: "macOS",
detail: "Apple Silicon and Intel Macs.",
instruction: "Open the DMG and drag OpenIDEA into Applications. Requires macOS 11 or later.",
},
{
id: "windows",
name: "Windows",
detail: "64-bit Intel and AMD PCs.",
instruction:
"Download the EXE and follow the setup steps. The MSI package is also available for managed installations.",
},
{
id: "linux",
name: "Linux",
detail: "AppImage, DEB and RPM for x86_64.",
instruction:
"Make the AppImage executable, then open it. For Debian or Ubuntu, use the DEB package; for Fedora or RHEL, use the RPM package. This release contains x86_64 Linux packages.",
},
] as const;
function DownloadIntro({ title }: { title: string }) {
return (
<div {...stylex.props(s.intro)}>
<img
{...stylex.props(s.logo)}
src={publicAsset("mark.svg", import.meta.env.BASE_URL)}
width={64}
height={64}
alt=""
/>
<h1 {...stylex.props(s.title)}>{title}</h1>
<div {...stylex.props(s.version)}>
<span>Version {manifest.version}</span>
<a {...stylex.props(s.notes)} href={manifest.releaseUrl}>
Release notes
</a>
</div>
</div>
);
}
export function DownloadsPage() {
return (
<>
<SiteHeader />
<main {...stylex.props(s.main, shared.wide)} id="main">
<DownloadIntro title="Download OpenIDEA" />
<p {...stylex.props(s.indexCopy)}>Choose your desktop platform.</p>
<div {...stylex.props(s.grid)}>
{platforms.map((platform) => (
<section {...stylex.props(s.card)} key={platform.id} aria-labelledby={`platform-${platform.id}`}>
<h2 {...stylex.props(s.cardTitle)} id={`platform-${platform.id}`}>
{platform.name}
</h2>
<p {...stylex.props(s.description)}>{platform.detail}</p>
<a
{...stylex.props(shared.button, s.action)}
href={publicAsset(`download/${platform.id}/`, import.meta.env.BASE_URL)}
>
Download for {platform.name}
</a>
</section>
))}
</div>
</main>
<SiteFooter />
</>
);
}
export function DownloadPlatformPage({ platform }: { platform: Platform }) {
const current = platforms.find((item) => item.id === platform)!;
const packages = manifest.packages[platform];
return (
<>
<SiteHeader />
<main {...stylex.props(s.main, shared.content)} id="main">
<DownloadIntro title={`Download for ${current.name}`} />
<nav {...stylex.props(s.platforms)} aria-label="Download platform">
{platforms.map((item) => (
<a
key={item.id}
{...stylex.props(s.platformLink, item.id === platform && s.selected)}
aria-current={item.id === platform ? "page" : undefined}
href={publicAsset(`download/${item.id}/`, import.meta.env.BASE_URL)}
>
{item.name}
</a>
))}
</nav>
<div {...stylex.props(s.grid)}>
{packages.map((pkg, index) => (
<section {...stylex.props(s.card)} key={pkg.id} aria-labelledby={pkg.id}>
<h2 {...stylex.props(s.cardTitle)} id={pkg.id}>
{pkg.label}
</h2>
<p {...stylex.props(s.description)}>{pkg.detail}</p>
<a {...stylex.props(shared.button, index === 0 && shared.primary, s.action)} href={pkg.url}>
Download {pkg.format}
</a>
<span {...stylex.props(s.size)}>{(pkg.size / 1_000_000).toFixed(1)} MB</span>
</section>
))}
{packages.length === 0 && (
<p {...stylex.props(s.installText)}>
Installation packages for {current.name} are not available in this release. You can build OpenIDEA from
source.
</p>
)}
</div>
{packages.length > 0 && (
<section {...stylex.props(s.install)} aria-labelledby="install-title">
<h2 {...stylex.props(s.installTitle)} id="install-title">
Installation
</h2>
<p {...stylex.props(s.installText)}>{current.instruction}</p>
</section>
)}
<p {...stylex.props(s.other)}>
<a {...stylex.props(s.notes)} href={publicAsset("download/", import.meta.env.BASE_URL)}>
All platforms
</a>
{" · "}
<a {...stylex.props(s.notes)} href={docs}>
Build from source
</a>
</p>
</main>
<SiteFooter />
</>
);
}
-170
Переглянути файл
@@ -1,170 +0,0 @@
import * as stylex from "@stylexjs/stylex";
import { Dialog } from "@base-ui/react/dialog";
import { publicAsset } from "../lib/paths.mjs";
import { styles as s } from "./home.stylex";
import { SiteHeader, SiteFooter, repository, docs } from "./SiteLayout";
import { useDownloadPath } from "./useDownloadPath";
const screenshot = publicAsset("media/debug.jpg", import.meta.env.BASE_URL);
const imageDescription = "A Rust project in OpenIDEA, paused at a breakpoint with variables visible in the debugger.";
function ProductPreview() {
return (
<Dialog.Root>
<Dialog.Trigger {...stylex.props(s.product)} aria-label="Enlarge OpenIDEA debugger screenshot">
<img
{...stylex.props(s.productImage)}
src={screenshot}
width={3200}
height={1996}
alt={imageDescription}
fetchPriority="high"
/>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop {...stylex.props(s.backdrop)} />
<Dialog.Popup {...stylex.props(s.popup)}>
<div {...stylex.props(s.dialogHeader)}>
<Dialog.Title {...stylex.props(s.dialogTitle)}>Rust debugging in OpenIDEA</Dialog.Title>
<Dialog.Close {...stylex.props(s.button)}>Close</Dialog.Close>
</div>
<img {...stylex.props(s.dialogImage)} src={screenshot} width={3200} height={1996} alt={imageDescription} />
<Dialog.Description {...stylex.props(s.dialogDescription)}>
Breakpoints, variable inspection and your project in one window.
</Dialog.Description>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}
export function Home() {
const downloads = useDownloadPath();
return (
<>
<SiteHeader />
<main id="main">
<section {...stylex.props(s.content, s.hero)} aria-labelledby="headline">
<ProductPreview />
<h1 {...stylex.props(s.headline)} id="headline">
The IDE you know,
<br />
rebuilt in Rust.
</h1>
<p {...stylex.props(s.description)}>
An open-source IDE with the keymap, tool windows
<br {...stylex.props(s.desktopBreak)} /> and Search Everywhere you already know.
</p>
<div {...stylex.props(s.actions)}>
<a {...stylex.props(s.button, s.primary)} href={downloads}>
Download OpenIDEA
</a>
<a {...stylex.props(s.button)} href={docs}>
Documentation
</a>
</div>
<div {...stylex.props(s.platforms)}>macOS, Windows and Linux</div>
</section>
<section {...stylex.props(s.section, s.content, s.workflow)} aria-labelledby="workflow-title">
<div>
<h2 {...stylex.props(s.heading)} id="workflow-title">
Keep your way of working.
</h2>
<p {...stylex.props(s.intro)}>
Move between your code and your tools with the habits you have built in IntelliJ IDEA.
</p>
</div>
<dl {...stylex.props(s.workflowList)}>
<div>
<dt {...stylex.props(s.term)}>
Search Everywhere{" "}
<span {...stylex.props(s.keys)} aria-label="Press Shift twice">
<kbd {...stylex.props(s.key)}>Shift</kbd>
<kbd {...stylex.props(s.key)}>Shift</kbd>
</span>
</dt>
<dd {...stylex.props(s.definition)}>Find files, symbols, actions and settings from one place.</dd>
</div>
<div>
<dt {...stylex.props(s.term)}>Edit, run and debug</dt>
<dd {...stylex.props(s.definition)}>
Code insight, breakpoints and variable inspection, with support for 60+ languages.
</dd>
</div>
<div>
<dt {...stylex.props(s.term)}>Your tools in one window</dt>
<dd {...stylex.props(s.definition)}>
Git, terminal, database and HTTP clients built into the workbench.
</dd>
</div>
</dl>
</section>
<section {...stylex.props(s.section, s.content)} aria-labelledby="performance-title">
<h2 {...stylex.props(s.heading)} id="performance-title">
Startup and memory.
</h2>
<p {...stylex.props(s.benchmarkIntro)}>Opening freqtrade, a Python project with about 770 files.</p>
<table {...stylex.props(s.table)} aria-label="Benchmark comparison between OpenIDEA and IntelliJ IDEA">
<thead>
<tr>
<th {...stylex.props(s.cell, s.columnHeading, s.measurement)} scope="col">
Measurement
</th>
<th {...stylex.props(s.cell, s.columnHeading)} scope="col">
OpenIDEA
</th>
<th {...stylex.props(s.cell, s.columnHeading)} scope="col">
IntelliJ IDEA
<br />
2026.2.3
</th>
</tr>
</thead>
<tbody>
{[
["Project window", "0.9 s", "3.1 s"],
["Ready, CPU settled", "1.1 s", "13.2 s"],
["Memory, USS", "169 MB", "2011 MB"],
].map(([name, openidea, idea]) => (
<tr key={name}>
<th {...stylex.props(s.cell, s.measurement)} scope="row">
{name}
</th>
<td {...stylex.props(s.cell, s.result)}>{openidea}</td>
<td {...stylex.props(s.cell, s.muted)}>{idea}</td>
</tr>
))}
</tbody>
</table>
<p {...stylex.props(s.conditions)}>
Warm caches, median of 5 runs. Windows 11, 32-core AMD, 62 GB RAM. OpenIDEA release build from main; IDEA
with default settings and plugins. Memory includes all processes. IDEA analyses Python more deeply, so
“ready” does not mean the same work on both sides.
</p>
<a {...stylex.props(s.textLink)} href={`${repository}/tree/main/perf/compare`}>
Read the methodology and run it yourself
</a>
</section>
<section {...stylex.props(s.section, s.content, s.getStarted)} id="download" aria-labelledby="download-title">
<h2 {...stylex.props(s.heading)} id="download-title">
Try OpenIDEA.
</h2>
<p {...stylex.props(s.downloadCopy)}>
Available for macOS, Windows and Linux.
<br />
Open source under Apache 2.0.
</p>
<div {...stylex.props(s.actions, s.downloadActions)}>
<a {...stylex.props(s.button, s.primary, s.downloadButton)} href={downloads}>
Download OpenIDEA
</a>
</div>
<a {...stylex.props(s.textLink, s.sourceLink)} href={docs}>
Or build from source
</a>
</section>
</main>
<SiteFooter />
</>
);
}
-59
Переглянути файл
@@ -1,59 +0,0 @@
import * as stylex from "@stylexjs/stylex";
import { publicAsset } from "../lib/paths.mjs";
import { useDownloadPath } from "./useDownloadPath";
import { styles as s } from "./home.stylex";
export const repository = "https://github.com/l0ng-ai/openidea";
export const docs = `${repository}#build`;
export function SiteHeader() {
const downloadPath = useDownloadPath();
return (
<>
<a {...stylex.props(s.skipLink)} href="#main">
Skip to content
</a>
<header {...stylex.props(s.wide, s.header)}>
<a {...stylex.props(s.brand)} href={import.meta.env.BASE_URL} aria-label="OpenIDEA home">
<img
{...stylex.props(s.mark)}
src={publicAsset("mark.svg", import.meta.env.BASE_URL)}
width={39}
height={39}
alt=""
/>
OpenIDEA
</a>
<nav {...stylex.props(s.nav)} aria-label="Main navigation">
<a {...stylex.props(s.navLink)} href={docs}>
Docs
</a>
<a {...stylex.props(s.navLink)} href={repository}>
GitHub
</a>
<a {...stylex.props(s.navLink, s.navDownload)} href={downloadPath}>
Download
</a>
</nav>
</header>
</>
);
}
export function SiteFooter() {
return (
<footer {...stylex.props(s.wide, s.footer)}>
<div {...stylex.props(s.footerMain)}>
<span>OpenIDEA</span>
<div {...stylex.props(s.footerLinks)}>
<a href={`${repository}/releases`}>Releases</a>
<a href={repository}>GitHub</a>
<a href={`${repository}/blob/main/LICENSE`}>Apache 2.0</a>
</div>
</div>
<p {...stylex.props(s.trademark)}>
Not affiliated with JetBrains. IntelliJ IDEA is a trademark of JetBrains s.r.o.
</p>
</footer>
);
}
-63
Переглянути файл
@@ -1,63 +0,0 @@
import * as stylex from "@stylexjs/stylex";
export const styles = stylex.create({
main: { minHeight: "calc(100dvh - 220px)", paddingBottom: { default: 92, "@media (max-width: 600px)": 56 } },
intro: { paddingTop: { default: 58, "@media (max-width: 600px)": 34 }, textAlign: "center" },
logo: { width: 64, height: 64, display: "block", marginInline: "auto", marginBottom: 25 },
title: {
fontSize: { default: 42, "@media (max-width: 600px)": 32 },
fontWeight: 550,
letterSpacing: "-1.3px",
lineHeight: 1.18,
},
version: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
gap: "8px 20px",
color: "#a5a8af",
fontSize: 14,
marginTop: 19,
},
notes: { textDecorationLine: "underline", textUnderlineOffset: 4, textDecorationColor: "#777b84" },
platforms: {
display: "flex",
justifyContent: "center",
gap: { default: 28, "@media (max-width: 600px)": 22 },
marginTop: 35,
},
platformLink: {
color: { default: "#a5a8af", ":hover": "#dfe1e5" },
fontSize: 14,
paddingBlock: 10,
borderBottomWidth: 1,
borderBottomStyle: "solid",
borderBottomColor: "transparent",
},
selected: { color: "#dfe1e5", borderBottomColor: "#dfe1e5" },
grid: {
display: "grid",
gridTemplateColumns: { default: "repeat(auto-fit, minmax(230px, 1fr))", "@media (max-width: 600px)": "1fr" },
gap: 20,
marginTop: { default: 50, "@media (max-width: 600px)": 32 },
},
card: {
borderWidth: 1,
borderStyle: "solid",
borderColor: "#34363b",
borderRadius: 8,
padding: { default: 28, "@media (max-width: 600px)": 24 },
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
},
cardTitle: { fontSize: 22, fontWeight: 550, letterSpacing: "-.4px", lineHeight: 1.4 },
description: { color: "#a5a8af", fontSize: 14, lineHeight: 1.7, marginTop: 10, marginBottom: 25 },
action: { marginTop: "auto", minWidth: 150 },
size: { color: "#a5a8af", fontSize: 12, marginTop: 12 },
install: { borderTopWidth: 1, borderTopStyle: "solid", borderTopColor: "#34363b", paddingTop: 28, marginTop: 39 },
installTitle: { fontSize: 17, fontWeight: 550, marginBottom: 10 },
installText: { color: "#a5a8af", fontSize: 14, lineHeight: 1.8 },
other: { textAlign: "center", marginTop: 35, color: "#a5a8af", fontSize: 13 },
indexCopy: { marginTop: 23, color: "#a5a8af", fontSize: 14, textAlign: "center" },
});
-285
Переглянути файл
@@ -1,285 +0,0 @@
import * as stylex from "@stylexjs/stylex";
export const styles = stylex.create({
wide: {
width: { default: "min(1112px, calc(100% - 64px))", "@media (max-width: 600px)": "calc(100% - 40px)" },
marginInline: "auto",
},
content: {
width: { default: "min(840px, calc(100% - 48px))", "@media (max-width: 600px)": "calc(100% - 40px)" },
marginInline: "auto",
},
header: {
height: { default: 88, "@media (max-width: 600px)": 76 },
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
brand: {
display: "flex",
alignItems: "center",
gap: { default: 10, "@media (max-width: 600px)": 8 },
fontSize: { default: 21, "@media (max-width: 600px)": 19 },
fontWeight: 650,
letterSpacing: "-.8px",
lineHeight: 1,
},
mark: {
width: { default: 39, "@media (max-width: 600px)": 33 },
height: { default: 39, "@media (max-width: 600px)": 33 },
},
nav: {
display: "flex",
alignItems: "center",
gap: { default: 29, "@media (max-width: 600px)": 20 },
fontSize: { default: 13, "@media (max-width: 600px)": 12 },
color: "#a5a8af",
},
navLink: { paddingBlock: 12, color: { default: "inherit", ":hover": "#ffffff" } },
navDownload: { display: { default: "block", "@media (max-width: 600px)": "none" }, color: "#dfe1e5" },
hero: {
paddingTop: { default: 34, "@media (max-width: 600px)": 23 },
paddingBottom: { default: 104, "@media (max-width: 600px)": 65 },
textAlign: "center",
},
product: {
display: "block",
width: "100%",
padding: 0,
backgroundColor: "transparent",
borderWidth: 1,
borderStyle: "solid",
borderColor: "#40434a",
borderRadius: { default: 11, "@media (max-width: 600px)": 7 },
overflow: "hidden",
lineHeight: 0,
cursor: "zoom-in",
},
productImage: { display: "block", width: "100%", height: "auto" },
headline: {
marginTop: { default: 32, "@media (max-width: 600px)": 31 },
fontSize: { default: 42, "@media (max-width: 600px)": 34, "@media (max-width: 360px)": 30 },
fontWeight: 550,
letterSpacing: { default: "-1.8px", "@media (max-width: 600px)": "-1.3px" },
lineHeight: 1.12,
},
description: {
color: "#a5a8af",
maxWidth: { default: 570, "@media (max-width: 600px)": 325 },
marginInline: "auto",
marginTop: 18,
fontSize: { default: 15, "@media (max-width: 600px)": 14 },
lineHeight: 1.75,
},
desktopBreak: { display: { default: "inline", "@media (max-width: 600px)": "none" } },
actions: {
display: "flex",
justifyContent: "center",
gap: { default: 12, "@media (max-width: 600px)": 10 },
marginTop: { default: 26, "@media (max-width: 600px)": 24 },
},
button: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
minHeight: { default: 45, "@media (max-width: 600px)": 46 },
paddingBlock: { default: 10, "@media (max-width: 600px)": 12 },
paddingInline: { default: 24, "@media (max-width: 600px)": 17, "@media (max-width: 360px)": 12 },
borderWidth: 1,
borderStyle: "solid",
borderColor: { default: "#34363b", ":hover": "#777b84" },
borderRadius: 6,
backgroundColor: "transparent",
color: "#dfe1e5",
fontSize: { default: 13, "@media (max-width: 600px)": 12 },
fontWeight: 550,
lineHeight: 1.4,
textAlign: "center",
cursor: "pointer",
},
primary: {
backgroundColor: { default: "#f5f4f1", ":hover": "#ffffff" },
color: "#191a1c",
borderColor: { default: "#f5f4f1", ":hover": "#ffffff" },
},
platforms: {
marginTop: { default: 14, "@media (max-width: 600px)": 15 },
fontSize: { default: 12, "@media (max-width: 600px)": 11 },
color: "#a5a8af",
},
section: {
borderTopWidth: 1,
borderTopStyle: "solid",
borderTopColor: "#34363b",
paddingBlock: { default: 62, "@media (max-width: 600px)": 42 },
},
heading: {
fontSize: { default: 27, "@media (max-width: 600px)": 25 },
fontWeight: 550,
letterSpacing: { default: "-.8px", "@media (max-width: 600px)": "-.7px" },
lineHeight: 1.3,
},
workflow: {
display: { default: "grid", "@media (max-width: 600px)": "block" },
gridTemplateColumns: "1fr 1fr",
gap: 76,
},
intro: {
marginTop: { default: 17, "@media (max-width: 600px)": 14 },
maxWidth: { default: 330, "@media (max-width: 600px)": "none" },
color: "#a5a8af",
fontSize: { default: 15, "@media (max-width: 600px)": 14 },
},
workflowList: {
margin: 0,
marginTop: { default: 0, "@media (max-width: 600px)": 29 },
display: "flex",
flexDirection: "column",
gap: { default: 23, "@media (max-width: 600px)": 25 },
},
term: { fontSize: { default: 15, "@media (max-width: 600px)": 14 }, fontWeight: 550, lineHeight: 1.4 },
definition: { margin: 0, marginTop: 6, color: "#a5a8af", fontSize: 14, lineHeight: 1.65 },
keys: {
display: "inline-flex",
gap: 5,
marginLeft: { default: 8, "@media (max-width: 600px)": 7 },
verticalAlign: "middle",
},
key: {
fontFamily: "Inter, Arial, sans-serif",
fontSize: { default: 12, "@media (max-width: 600px)": 11 },
paddingBlock: 3,
paddingInline: { default: 7, "@media (max-width: 600px)": 6 },
borderWidth: 1,
borderStyle: "solid",
borderColor: "#4b4e55",
borderRadius: 4,
whiteSpace: "nowrap",
color: "#dfe1e5",
},
benchmarkIntro: {
marginTop: { default: 13, "@media (max-width: 600px)": 14 },
maxWidth: 650,
color: "#a5a8af",
fontSize: { default: 15, "@media (max-width: 600px)": 14 },
},
table: {
width: "100%",
borderCollapse: "collapse",
marginTop: { default: 28, "@media (max-width: 600px)": 23 },
tableLayout: "fixed",
fontSize: { default: 14, "@media (max-width: 600px)": 13 },
},
cell: {
paddingBlock: { default: 14, "@media (max-width: 600px)": 15 },
paddingInline: 0,
borderBottomWidth: 1,
borderBottomStyle: "solid",
borderBottomColor: "#34363b",
textAlign: "right",
fontWeight: 400,
fontVariantNumeric: "tabular-nums",
},
columnHeading: {
fontSize: { default: 12, "@media (max-width: 600px)": 11 },
color: "#a5a8af",
verticalAlign: "bottom",
lineHeight: 1.5,
},
measurement: {
width: { default: "44%", "@media (max-width: 600px)": "41%" },
textAlign: "left",
paddingRight: { default: 0, "@media (max-width: 600px)": 12 },
},
result: { fontWeight: 550 },
muted: { color: "#a5a8af" },
conditions: {
maxWidth: 760,
marginTop: { default: 23, "@media (max-width: 600px)": 22 },
fontSize: { default: 12, "@media (max-width: 600px)": 11 },
lineHeight: { default: 1.75, "@media (max-width: 600px)": 1.85 },
color: "#a5a8af",
},
textLink: {
display: "inline-block",
marginTop: { default: 15, "@media (max-width: 600px)": 16 },
fontSize: { default: 13, "@media (max-width: 600px)": 12 },
textDecorationLine: "underline",
textUnderlineOffset: 4,
textDecorationColor: "#777b84",
color: { default: "inherit", ":hover": "#ffffff" },
},
getStarted: {
textAlign: "center",
paddingTop: { default: 66, "@media (max-width: 600px)": 48 },
paddingBottom: { default: 76, "@media (max-width: 600px)": 52 },
},
downloadCopy: {
maxWidth: { default: 480, "@media (max-width: 600px)": 300 },
marginInline: "auto",
marginTop: 16,
color: "#a5a8af",
fontSize: { default: 15, "@media (max-width: 600px)": 14 },
},
downloadActions: {
marginTop: { default: 23, "@media (max-width: 600px)": 24 },
flexDirection: { default: "row", "@media (max-width: 600px)": "column" },
alignItems: "center",
},
downloadButton: { minWidth: { default: "auto", "@media (max-width: 600px)": 200 } },
sourceLink: { color: "#a5a8af", marginTop: 18 },
footer: {
borderTopWidth: 1,
borderTopStyle: "solid",
borderTopColor: "#34363b",
paddingTop: { default: 25, "@media (max-width: 600px)": 23 },
paddingBottom: { default: 32, "@media (max-width: 600px)": 28 },
color: "#a5a8af",
fontSize: 11,
lineHeight: 1.7,
},
footerMain: {
display: { default: "flex", "@media (max-width: 600px)": "block" },
justifyContent: "space-between",
gap: 20,
},
footerLinks: { display: "flex", gap: 23, marginTop: { default: 0, "@media (max-width: 600px)": 15 } },
trademark: {
marginTop: { default: 14, "@media (max-width: 600px)": 18 },
fontSize: 10,
color: "#9498a1",
maxWidth: { default: "none", "@media (max-width: 600px)": 310 },
},
skipLink: {
position: "fixed",
top: 8,
left: 8,
padding: 12,
backgroundColor: "#f5f4f1",
color: "#191a1c",
borderRadius: 4,
zIndex: 100,
transform: { default: "translateY(-150%)", ":focus": "translateY(0)" },
},
backdrop: { position: "fixed", inset: 0, backgroundColor: "rgb(0 0 0 / .85)", zIndex: 10 },
popup: {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "min(1400px, calc(100vw - 32px))",
maxHeight: "calc(100dvh - 32px)",
overflow: "auto",
backgroundColor: "#191a1c",
borderWidth: 1,
borderStyle: "solid",
borderColor: "#40434a",
borderRadius: 8,
padding: 16,
zIndex: 11,
},
dialogHeader: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, marginBottom: 16 },
dialogTitle: { fontSize: 15, fontWeight: 550, lineHeight: 1.4 },
dialogImage: { display: "block", width: "100%", height: "auto", borderRadius: 4 },
dialogDescription: { fontSize: 12, color: "#a5a8af", marginTop: 12 },
});
-12
Переглянути файл
@@ -1,12 +0,0 @@
import { useSyncExternalStore } from "react";
import { detectDesktopPlatform } from "../lib/downloads.mjs";
import { publicAsset } from "../lib/paths.mjs";
const subscribe = () => () => undefined;
const serverPlatform = () => null;
const browserPlatform = () => detectDesktopPlatform(navigator.userAgent, navigator.maxTouchPoints);
export function useDownloadPath() {
const platform = useSyncExternalStore(subscribe, browserPlatform, serverPlatform);
return publicAsset(platform ? `download/${platform}/` : "download/", import.meta.env.BASE_URL);
}
-76
Переглянути файл
@@ -1,76 +0,0 @@
{
"version": "0.1.24",
"tag": "v0.1.24",
"releaseUrl": "https://github.com/l0ng-ai/openidea/releases/tag/v0.1.24",
"packages": {
"macos": [
{
"id": "mac-arm",
"label": "Apple Silicon",
"detail": "Macs with an Apple Silicon processor",
"format": "DMG",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_aarch64.dmg",
"name": "OpenIDEA_0.1.24_aarch64.dmg",
"size": 43591698
},
{
"id": "mac-intel",
"label": "Intel",
"detail": "Macs with an Intel processor",
"format": "DMG",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_x64.dmg",
"name": "OpenIDEA_0.1.24_x64.dmg",
"size": 47708074
}
],
"windows": [
{
"id": "windows-exe",
"label": "Windows installer",
"detail": "64-bit Intel and AMD PCs",
"format": "EXE",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_x64-setup.exe",
"name": "OpenIDEA_0.1.24_x64-setup.exe",
"size": 36126071
},
{
"id": "windows-msi",
"label": "MSI package",
"detail": "64-bit installer for managed deployments",
"format": "MSI",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_x64_en-US.msi",
"name": "OpenIDEA_0.1.24_x64_en-US.msi",
"size": 48439296
}
],
"linux": [
{
"id": "linux-appimage",
"label": "AppImage",
"detail": "Standalone app for x86_64 Linux",
"format": "AppImage",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_amd64.AppImage",
"name": "OpenIDEA_0.1.24_amd64.AppImage",
"size": 122984952
},
{
"id": "linux-deb",
"label": "Debian / Ubuntu",
"detail": "DEB package for x86_64",
"format": "DEB",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA_0.1.24_amd64.deb",
"name": "OpenIDEA_0.1.24_amd64.deb",
"size": 48842078
},
{
"id": "linux-rpm",
"label": "Fedora / RHEL",
"detail": "RPM package for x86_64",
"format": "RPM",
"url": "https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/OpenIDEA-0.1.24-1.x86_64.rpm",
"name": "OpenIDEA-0.1.24-1.x86_64.rpm",
"size": 48845523
}
]
}
}
-2
Переглянути файл
@@ -1,2 +0,0 @@
/// <reference types="vite/client" />
declare module "virtual:stylex:css-only";
-103
Переглянути файл
@@ -1,103 +0,0 @@
/** @typedef {"macos" | "windows" | "linux"} Platform */
/** @typedef {{id:string,label:string,detail:string,format:string,url:string,name:string,size:number}} DownloadPackage */
/** @typedef {{tag_name:string,html_url:string,draft?:boolean,prerelease?:boolean,assets:{name:string,browser_download_url:string,size:number}[]}} Release */
/** @param {string} userAgent @param {number} [touchPoints] @returns {Platform | null} */
export function detectDesktopPlatform(userAgent, touchPoints = 0) {
if (/Android|iPhone|iPad|iPod|Windows Phone|CrOS/i.test(userAgent)) return null;
if (/Macintosh|Mac OS X/i.test(userAgent)) return touchPoints > 1 ? null : "macos";
if (/Windows NT/i.test(userAgent)) return "windows";
if (/Linux/i.test(userAgent)) return "linux";
return null;
}
/** @type {{platform:Platform,id:string,label:string,detail:string,format:string,pattern:RegExp}[]} */
const installers = [
{
platform: "macos",
id: "mac-arm",
label: "Apple Silicon",
detail: "Macs with an Apple Silicon processor",
format: "DMG",
pattern: /^OpenIDEA.*_(?:aarch64|arm64)\.dmg$/i,
},
{
platform: "macos",
id: "mac-intel",
label: "Intel",
detail: "Macs with an Intel processor",
format: "DMG",
pattern: /^OpenIDEA.*_x64\.dmg$/i,
},
{
platform: "windows",
id: "windows-exe",
label: "Windows installer",
detail: "64-bit Intel and AMD PCs",
format: "EXE",
pattern: /^OpenIDEA.*_x64-setup\.exe$/i,
},
{
platform: "windows",
id: "windows-msi",
label: "MSI package",
detail: "64-bit installer for managed deployments",
format: "MSI",
pattern: /^OpenIDEA.*_x64(?:_[\w-]+)?\.msi$/i,
},
{
platform: "linux",
id: "linux-appimage",
label: "AppImage",
detail: "Standalone app for x86_64 Linux",
format: "AppImage",
pattern: /^OpenIDEA.*_amd64\.AppImage$/i,
},
{
platform: "linux",
id: "linux-deb",
label: "Debian / Ubuntu",
detail: "DEB package for x86_64",
format: "DEB",
pattern: /^OpenIDEA.*_amd64\.deb$/i,
},
{
platform: "linux",
id: "linux-rpm",
label: "Fedora / RHEL",
detail: "RPM package for x86_64",
format: "RPM",
pattern: /^OpenIDEA.*\.x86_64\.rpm$/i,
},
];
/** @param {Release} release */
export function buildDownloadManifest(release) {
if (release.draft || release.prerelease || !/^v?\d+\.\d+\.\d+$/.test(release.tag_name)) {
throw new Error("Expected a published stable release");
}
const prefix = `https://github.com/l0ng-ai/openidea/releases/download/${release.tag_name}/`;
/** @type {Record<Platform, DownloadPackage[]>} */
const packages = { macos: [], windows: [], linux: [] };
for (const installer of installers) {
const asset = release.assets.find((item) => installer.pattern.test(item.name));
if (!asset) continue;
if (
!asset.browser_download_url.startsWith(prefix) ||
decodeURIComponent(new URL(asset.browser_download_url).pathname.split("/").pop() ?? "") !== asset.name
) {
throw new Error(`Unexpected download URL for ${asset.name}`);
}
packages[installer.platform].push({
id: installer.id,
label: installer.label,
detail: installer.detail,
format: installer.format,
url: asset.browser_download_url,
name: asset.name,
size: asset.size,
});
}
if (!Object.values(packages).some((items) => items.length)) throw new Error("No desktop installers in release");
return { version: release.tag_name.replace(/^v/, ""), tag: release.tag_name, releaseUrl: release.html_url, packages };
}
-7
Переглянути файл
@@ -1,7 +0,0 @@
export function pageHead(title: string, path = "") {
const siteUrl = import.meta.env.VITE_SITE_URL?.replace(/\/$/, "");
return {
meta: [{ title: `${title} — OpenIDEA` }],
links: siteUrl ? [{ rel: "canonical", href: `${siteUrl}/${path}` }] : [],
};
}
-10
Переглянути файл
@@ -1,10 +0,0 @@
/** @param {string} [path] */
export function normalizeBasePath(path = "/") {
const clean = path.replace(/^\/+|\/+$/g, "");
return clean ? `/${clean}/` : "/";
}
/** @param {string} asset @param {string} [basePath] */
export function publicAsset(asset, basePath = "/") {
return `${normalizeBasePath(basePath)}${asset.replace(/^\/+/, "")}`;
}
-16
Переглянути файл
@@ -1,16 +0,0 @@
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
return createRouter({
routeTree,
basepath: import.meta.env.BASE_URL,
scrollRestoration: true,
});
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
-37
Переглянути файл
@@ -1,37 +0,0 @@
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import { publicAsset } from "../lib/paths.mjs";
import { DevStyles } from "../components/DevStyles";
import "../styles/global.css";
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "OpenIDEA — The IDE you know, rebuilt in Rust." },
{
name: "description",
content:
"An open-source IDE with the keymap, tool windows and Search Everywhere you already know. Built with a Rust core for macOS, Windows and Linux.",
},
{ name: "theme-color", content: "#191a1c" },
],
links: [{ rel: "icon", type: "image/svg+xml", href: publicAsset("mark.svg", import.meta.env.BASE_URL) }],
}),
component: Root,
});
function Root() {
return (
<html lang="en">
<head>
<HeadContent />
<DevStyles />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
-8
Переглянути файл
@@ -1,8 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DownloadsPage } from "../components/Downloads";
import { pageHead } from "../lib/metadata";
export const Route = createFileRoute("/download/")({
head: () => pageHead("Download", "download/"),
component: DownloadsPage,
});
-8
Переглянути файл
@@ -1,8 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DownloadPlatformPage } from "../components/Downloads";
import { pageHead } from "../lib/metadata";
export const Route = createFileRoute("/download/linux")({
head: () => pageHead("Download for Linux", "download/linux/"),
component: () => <DownloadPlatformPage platform="linux" />,
});
-8
Переглянути файл
@@ -1,8 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DownloadPlatformPage } from "../components/Downloads";
import { pageHead } from "../lib/metadata";
export const Route = createFileRoute("/download/macos")({
head: () => pageHead("Download for macOS", "download/macos/"),
component: () => <DownloadPlatformPage platform="macos" />,
});
-8
Переглянути файл
@@ -1,8 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DownloadPlatformPage } from "../components/Downloads";
import { pageHead } from "../lib/metadata";
export const Route = createFileRoute("/download/windows")({
head: () => pageHead("Download for Windows", "download/windows/"),
component: () => <DownloadPlatformPage platform="windows" />,
});
-8
Переглянути файл
@@ -1,8 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { Home } from "../components/Home";
import { pageHead } from "../lib/metadata";
export const Route = createFileRoute("/")({
head: () => pageHead("The IDE you know, rebuilt in Rust."),
component: Home,
});
-45
Переглянути файл
@@ -1,45 +0,0 @@
@font-face {
font-family: Inter;
src: url("/fonts/inter-latin-wght-normal.woff2") format("woff2");
font-style: normal;
font-weight: 100 900;
font-display: swap;
}
@layer reset {
* {
box-sizing: border-box;
}
html {
color-scheme: dark;
background: #191a1c;
}
body {
margin: 0;
color: #dfe1e5;
font:
15px/1.7 Inter,
Arial,
sans-serif;
-webkit-font-smoothing: antialiased;
}
p,
h1,
h2,
h3,
figure {
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
color: inherit;
}
:focus-visible {
outline: 2px solid #dfe1e5;
outline-offset: 5px;
}
}
-73
Переглянути файл
@@ -1,73 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import { detectDesktopPlatform, buildDownloadManifest } from "../src/lib/downloads.mjs";
test("detects desktop platforms without guessing mobile devices are desktops", () => {
assert.equal(detectDesktopPlatform("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"), "macos");
assert.equal(detectDesktopPlatform("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"), "windows");
assert.equal(detectDesktopPlatform("Mozilla/5.0 (X11; Linux x86_64)"), "linux");
assert.equal(detectDesktopPlatform("Mozilla/5.0 (Linux; Android 15)"), null);
assert.equal(detectDesktopPlatform("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)"), null);
assert.equal(detectDesktopPlatform("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", 5), null);
assert.equal(detectDesktopPlatform("Mozilla/5.0 (X11; CrOS x86_64)"), null);
assert.equal(detectDesktopPlatform("Mozilla/5.0 (X11; FreeBSD amd64)"), null);
assert.equal(detectDesktopPlatform(""), null);
});
function release(names) {
return {
tag_name: "v0.1.24",
html_url: "https://github.com/l0ng-ai/openidea/releases/tag/v0.1.24",
assets: names.map((name) => ({
name,
size: 1234,
browser_download_url: `https://github.com/l0ng-ai/openidea/releases/download/v0.1.24/${name}`,
})),
};
}
test("maps real installer assets to platform-specific direct downloads", () => {
const result = buildDownloadManifest(
release([
"OpenIDEA_0.1.24_aarch64.dmg",
"OpenIDEA_0.1.24_x64.dmg",
"OpenIDEA_0.1.24_x64-setup.exe",
"OpenIDEA_0.1.24_x64_en-US.msi",
"OpenIDEA_0.1.24_amd64.AppImage",
"OpenIDEA_0.1.24_amd64.deb",
"OpenIDEA-0.1.24-1.x86_64.rpm",
"OpenIDEA_0.1.24_amd64.AppImage.sig",
"openidea-server-linux-aarch64.gz",
"latest.json",
]),
);
assert.equal(result.version, "0.1.24");
assert.deepEqual(
result.packages.macos.map((p) => p.label),
["Apple Silicon", "Intel"],
);
assert.deepEqual(
result.packages.windows.map((p) => p.format),
["EXE", "MSI"],
);
assert.deepEqual(
result.packages.linux.map((p) => p.format),
["AppImage", "DEB", "RPM"],
);
assert.ok(result.packages.macos[0].url.endsWith("/OpenIDEA_0.1.24_aarch64.dmg"));
assert.equal(Object.values(result.packages).flat().length, 7);
});
test("does not invent download links for missing platform packages", () => {
const result = buildDownloadManifest(release(["OpenIDEA_0.1.24_aarch64.dmg"]));
assert.deepEqual(result.packages.windows, []);
assert.deepEqual(result.packages.linux, []);
});
test("rejects draft or prerelease data and untrusted installer destinations", () => {
const data = release(["OpenIDEA_0.1.24_aarch64.dmg"]);
assert.throws(() => buildDownloadManifest({ ...data, draft: true }), /stable/i);
assert.throws(() => buildDownloadManifest({ ...data, prerelease: true }), /stable/i);
data.assets[0].browser_download_url = "https://example.com/installer.dmg";
assert.throws(() => buildDownloadManifest(data), /download URL/i);
});
-16
Переглянути файл
@@ -1,16 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import { normalizeBasePath, publicAsset } from "../src/lib/paths.mjs";
test("normalizes both repository Pages and custom-domain roots", () => {
assert.equal(normalizeBasePath(), "/");
assert.equal(normalizeBasePath(""), "/");
assert.equal(normalizeBasePath("/openidea"), "/openidea/");
assert.equal(normalizeBasePath("/openidea/"), "/openidea/");
});
test("keeps public media under the deployed repository prefix", () => {
assert.equal(publicAsset("media/debug.jpg", "/openidea/"), "/openidea/media/debug.jpg");
assert.equal(publicAsset("/media/debug.jpg", "/openidea"), "/openidea/media/debug.jpg");
assert.equal(publicAsset("media/debug.jpg", "/"), "/media/debug.jpg");
});
-53
Переглянути файл
@@ -1,53 +0,0 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { checkStatic } from "../scripts/check-static.mjs";
async function fixture(t, body, css = ".x{cursor:zoom-in}") {
const root = await mkdtemp(join(tmpdir(), "openidea-pages-"));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(join(root, "assets"));
await writeFile(join(root, "assets/site.css"), css);
await writeFile(
join(root, "index.html"),
`<!doctype html><html><head><title>OpenIDEA</title><link rel="stylesheet" href="/openidea/assets/site.css"></head><body>${body}</body></html>`,
);
return root;
}
const content = `<h1>The IDE you know,<br>rebuilt in Rust.</h1><h2>Keep your way of working.</h2><h2>Startup and memory.</h2><p>169 MB. Warm caches, median of 5 runs. IDEA analyses Python more deeply.</p><h2>Try OpenIDEA.</h2>`;
test("rejects a client-only shell instead of publishing an empty homepage", async (t) => {
const root = await fixture(t, '<div id="root"></div>');
await assert.rejects(checkStatic(root, "/openidea/"), /prerendered homepage/i);
});
test("rejects public media that escaped the GitHub Pages prefix", async (t) => {
const root = await fixture(t, `${content}<img src="/media/debug.jpg" alt="IDE">`);
await assert.rejects(checkStatic(root, "/openidea/"), /base path/i);
});
test("rejects missing deployment assets", async (t) => {
const root = await fixture(t, `${content}<img src="/openidea/media/debug.jpg" alt="IDE">`);
await assert.rejects(checkStatic(root, "/openidea/"), /missing asset/i);
});
test("checks a complete static page without needing a server", async (t) => {
const root = await fixture(t, content);
assert.equal((await checkStatic(root, "/openidea/")).assets, 1);
});
test("rejects a linked download directory without a prerendered index", async (t) => {
const root = await fixture(t, `${content}<a href="/openidea/download/macos/">Download</a>`);
await mkdir(join(root, "download/macos"), { recursive: true });
await assert.rejects(checkStatic(root, "/openidea/"), /missing.*index/i);
});
test("also checks local assets referenced from platform download pages", async (t) => {
const root = await fixture(t, `${content}<a href="/openidea/download/">Download</a>`);
await mkdir(join(root, "download"));
await writeFile(join(root, "download/index.html"), '<h1>Download OpenIDEA</h1><img src="/openidea/missing.png">');
await assert.rejects(checkStatic(root, "/openidea/"), /missing asset/i);
});
-17
Переглянути файл
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowJs": true,
"resolveJsonModule": true,
"checkJs": true,
"types": ["vite/client", "node"]
},
"include": ["src", "vite.config.ts"]
}
-22
Переглянути файл
@@ -1,22 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import stylex from "@stylexjs/unplugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { normalizeBasePath } from "./src/lib/paths.mjs";
export default defineConfig(({ mode }) => ({
base: normalizeBasePath(process.env.BASE_PATH),
plugins: [
stylex.vite({
devMode: "css-only",
dev: mode === "development",
// Keep atomic styles above the reset layer regardless of dev stylesheet load order.
useCSSLayers: false,
runtimeInjection: false,
}),
tanstackStart({
prerender: { enabled: true, crawlLinks: false, failOnError: true },
}),
react(),
],
}));