дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Revert "feat(website): add responsive homepage and GitHub Pages deployment (#…" (#1872)
This reverts commit bb508d4118.
Цей коміт міститься в:
1 джерело
bb508d4118
коміт
4dd51a1101
43 files changed
-3363
No files matched your search
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.tanstack/
|
||||
.output/
|
||||
.nitro/
|
||||
src/routeTree.gen.ts
|
||||
*.tsbuildinfo
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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
@@ -1,2 +0,0 @@
|
||||
# Keep this standalone package independent of the desktop workspace above it.
|
||||
packages: []
|
||||
Whitespace-only changes.
@@ -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 +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 |
@@ -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}`);
|
||||
}
|
||||
@@ -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`);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
@@ -1,2 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module "virtual:stylex:css-only";
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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}` }] : [],
|
||||
};
|
||||
}
|
||||
@@ -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(/^\/+/, "")}`;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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" />,
|
||||
});
|
||||
@@ -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" />,
|
||||
});
|
||||
@@ -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" />,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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(),
|
||||
],
|
||||
}));
|
||||
Reference in new issue
Заблокувати користувача