stuff script, ci, component, data

This commit is contained in:
Ayres Vitor
2025-12-09 13:23:03 -03:00
parent 9a436ac777
commit 921b2efd47
12 changed files with 4706 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
name: 'Sync Community Resources'
on:
schedule:
# weekly on Mondays at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
sync-community-resources:
name: Sync Community Resources
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm i
- name: sync-community-resources
run: pnpm build:community-resources
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: pnpm prettier src/data -w
# tauri-docs PR
- name: Git config
run: |
git config --global user.name "tauri-bot"
git config --global user.email "tauri-bot@tauri.app"
- name: Create pull request for updated docs
id: cpr
# soft fork of https://github.com/peter-evans/create-pull-request for security purposes
uses: tauri-apps/create-pull-request@v3.4.1
if: github.event_name != 'pull_request' && github.event_name != 'push'
with:
token: ${{ secrets.ORG_TAURI_BOT_PAT }}
commit-message: 'chore(docs): Update Community Resources'
branch: ci/v2/update-community-resources
title: Update Community Resources
labels: 'bot'

View File

@@ -14,6 +14,7 @@
"dev": "astro dev",
"format": "prettier -w --cache --plugin prettier-plugin-astro .",
"format:check": "prettier -c --cache --plugin prettier-plugin-astro .",
"build:community-resources": "pnpm --filter community-resources run build",
"build:compatibility-table": "pnpm --filter compatibility-table run build",
"build:references": "pnpm --filter js-api-generator run build",
"build:releases": "pnpm --filter releases-generator run build",

View File

@@ -0,0 +1,22 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"overrides": [
{
"files": ["*.json", "*.md", "*.toml", "*.yml"],
"options": {
"useTabs": false
}
},
{
"files": ["*.md", "*.mdx"],
"options": {
"printWidth": 80
}
}
]
}

View File

@@ -0,0 +1,218 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_FILE = path.resolve(__dirname, '../../src/data/communityResources.json');
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || null;
const query = 'tauri-plugin-';
const npmBaseUrl = 'https://registry.npmjs.org';
const cratesBaseUrl = 'https://crates.io/';
const githubBaseUrl = 'https://api.github.com/repos';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
function cleanRepoUrl(url: string) {
if (!url) {
return null;
}
// hmm
return url.replace(/^git\+/, '').replace(/\.git$/, '');
}
async function fetchJson(url: string, headers?: Headers) {
if (!headers) {
headers = new Headers();
}
if (!headers.has('User-Agent')) {
headers.set(
'User-Agent',
'tauri-docs-plugins-discover (https://github.com/tauri-apps/tauri-docs - @vasfvitor)'
);
}
const res = await fetch(url, { headers });
if (!res.ok) {
throw new Error(`Failed ${url}: ${res.status} ${res.statusText}`);
}
return res.json();
}
async function fetchCrates() {
const results = [];
let page = 1;
const per_page = 100;
while (true) {
const url = `https://crates.io/api/v1/crates?page=${page}&per_page=${per_page}&q=${query}`;
const j = await fetchJson(url);
if (!j.crates || j.crates.length === 0) {
break;
}
for (const c of j.crates) {
if (!c.name || !c.name.startsWith(query)) {
continue;
}
results.push({
source: 'crates',
name: c.name,
description: c.description || '',
version: c.max_version || c.newest_version || '',
downloads: c.downloads || 0,
repository: cleanRepoUrl(c.repository || c.homepage || ''),
license: c.license || '',
homepage: c.homepage || '',
crates_io: `https://crates.io/crates/${c.name}`,
});
}
if (j.meta && j.meta.total <= page * per_page) break;
page++;
await sleep(1001);
}
return results;
}
interface ResultsItem {
source: string;
name: string;
description: string;
version: string;
version_npm?: string;
date?: string;
repository: string | null;
npm?: string;
crates_io?: string;
downloads?: number;
github_stars?: number | null;
}
async function fetchNpm() {
const results: ResultsItem[] = [];
const size = 250;
const url = `https://registry.npmjs.org/-/v1/search?text=tauri-plugin-&size=${size}`;
const j = await fetchJson(url);
if (!j.objects) return results;
for (const obj of j.objects) {
const p = obj.package;
const name = p.name;
if (!name) {
continue;
}
if (!/(^|\/)tauri-plugin-/.test(name)) {
continue;
}
const repo = p.links && p.links.repository ? p.links.repository : p.repository;
results.push({
source: 'npm',
name,
description: p.description || '',
version: p.version || '',
date: p.date || '',
repository: cleanRepoUrl(repo || p.links?.homepage || ''),
npm: `https://www.npmjs.com/package/${encodeURIComponent(name)}`,
});
}
return results;
}
function extractGithubRepo(url: string) {
if (!url) {
return null;
}
try {
const u = new URL(url);
if (u.hostname !== 'github.com') {
return null;
}
const parts = u.pathname.replace(/^\//, '').split('/');
if (parts.length < 2) {
return null;
}
return `${parts[0]}/${parts[1]}`;
} catch (e) {
return null;
}
}
async function fetchGithubStars(ownerRepo: string) {
if (!ownerRepo) {
return null;
}
const url = `https://api.github.com/repos/${ownerRepo}`;
const headers = new Headers();
headers.append('Accept', 'application/vnd.github+json');
if (GITHUB_TOKEN) {
headers.append('Authorization', `token ${GITHUB_TOKEN}`);
}
try {
const j = await fetchJson(url, headers);
return j.stargazers_count ?? null;
} catch (e) {
return null;
}
}
async function run() {
console.log('Fetching crates.io packages...');
const crates = await fetchCrates();
console.log(`Found ${crates.length} crates matching prefix.`);
console.log('Fetching npm packages...');
const npm = await fetchNpm();
console.log(`Found ${npm.length} npm packages matching prefix.`);
const map = new Map();
for (const c of crates) {
map.set(c.name, { ...c });
}
for (const n of npm) {
const existing = map.get(n.name);
if (existing) {
existing.npm = n.npm;
existing.version_npm = n.version;
existing.description = existing.description || n.description;
existing.repository = existing.repository || n.repository;
} else {
map.set(n.name, { ...n });
}
}
// TODO: fetch GitHub stars
let count = 0;
for (const [name, item] of map) {
const ownerRepo = extractGithubRepo(item.repository);
if (ownerRepo) {
// eslint-disable-next-line no-await-in-loop
item.github_stars = await fetchGithubStars(ownerRepo);
count++;
// Rate limit for GitHub API (especially without token): ~60 req/hour unauthenticated
// Add small delay to avoid hitting rate limits
if (!GITHUB_TOKEN && count % 10 === 0) {
// eslint-disable-next-line no-await-in-loop
await sleep(1000);
}
} else {
item.github_stars = null;
}
}
const items = Array.from(map.values()).sort(
(a, b) => (b.github_stars || 0) - (a.github_stars || 0)
);
const outputData = {
generated: new Date().toISOString(),
count: items.length,
resources: items,
};
await fs.mkdir(path.dirname(OUTPUT_FILE), { recursive: true });
await fs.writeFile(OUTPUT_FILE, JSON.stringify(outputData, null, 2), 'utf8');
console.log(`Wrote ${items.length} resources to ${OUTPUT_FILE}`);
}
run().catch((e) => {
console.error('Error generating resources:', e);
process.exit(1);
});

View File

@@ -0,0 +1,19 @@
{
"name": "community-resources",
"version": "1.0.0",
"private": "true",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsm ./build.ts"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@types/node": "^22.0.0",
"tsm": "^2.3.0",
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"skipLibCheck": true
}
}

12
pnpm-lock.yaml generated
View File

@@ -87,6 +87,18 @@ importers:
specifier: ^5.3.3
version: 5.9.3
packages/community-resources:
dependencies:
'@types/node':
specifier: ^22.0.0
version: 22.15.21
tsm:
specifier: ^2.3.0
version: 2.3.0
typescript:
specifier: ^5.3.3
version: 5.9.3
packages/compatibility-table:
dependencies:
'@iarna/toml':

View File

@@ -6,6 +6,7 @@ packages:
- packages/releases-generator
- packages/compatibility-table
- packages/fetch-sponsors
- packages/community-resources
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild

View File

@@ -0,0 +1,372 @@
---
import communityResourcesData from '../data/communityResources.json';
const { resources, count, generated } = communityResourcesData;
---
<community-resources>
<div class="search-container">
<input
type="text"
id="resource-search"
placeholder="Search plugins by name or description..."
class="search-input"
autocomplete="off"
/>
<div class="filters">
<label>
<span>Source:</span>
<select id="filter-source" class="filter-select">
<option value="">All</option>
<option value="both">Both (npm & crates)</option>
<option value="npm">npm only</option>
<option value="crates">crates only</option>
</select>
</label>
<label>
<span>Sort by:</span>
<select id="sort-by" class="filter-select">
<option value="stars">GitHub Stars</option>
<option value="name">Name</option>
<option value="downloads">Crate Downloads</option>
</select>
</label>
</div>
<div class="summary" id="resource-summary">
Showing <strong id="visible-count">{count}</strong> of <strong>{count}</strong> community plugins
</div>
</div>
<div class="table-wrapper">
<table class="resources-table">
<thead>
<tr>
<th>Name & Links</th>
<th>Description</th>
<th class="text-right">Crate Version</th>
<th class="text-right">Crate Downloads</th>
<th class="text-right">NPM Version</th>
<th class="text-right">GitHub Stars</th>
</tr>
</thead>
<tbody id="resources-tbody">
{
resources.map((item) => (
<tr
class="resource-row"
data-name={item.name}
data-description={item.description}
data-has-npm={item.npm ? 'true' : 'false'}
data-has-crates={item.crates_io ? 'true' : 'false'}
data-stars={item.github_stars || 0}
data-downloads={item.downloads || 0}
>
<td class="name-cell">
<div class="name-with-links">
<code>{item.name}</code>
<div class="links">
{item.repository && (
<a
href={item.repository}
target="_blank"
rel="noopener noreferrer"
title="Repository"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
</a>
)}
{item.crates_io && (
<a
href={item.crates_io}
target="_blank"
rel="noopener noreferrer"
title="crates.io"
>
<span class="link-text">crates</span>
</a>
)}
{item.npm && (
<a href={item.npm} target="_blank" rel="noopener noreferrer" title="npm">
<span class="link-text">npm</span>
</a>
)}
</div>
</div>
</td>
<td class="description-cell">{item.description}</td>
<td class="text-right">{item.version || '-'}</td>
<td class="text-right">{item.downloads ? item.downloads.toLocaleString() : '-'}</td>
<td class="text-right">{item.version_npm || '-'}</td>
<td class="text-right">{item.github_stars ?? '-'}</td>
</tr>
))
}
</tbody>
</table>
</div>
<div class="no-results hidden" id="no-results">No plugins match your search criteria.</div>
</community-resources>
<style>
.search-container {
margin-bottom: 1.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
background: var(--sl-color-black);
color: var(--sl-color-white);
margin-bottom: 1rem;
}
.search-input:focus {
outline: 2px solid var(--sl-color-text-accent);
outline-offset: 0;
}
.filters {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.filters label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.filter-select {
padding: 0.5rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.375rem;
background: var(--sl-color-black);
color: var(--sl-color-white);
cursor: pointer;
}
.summary {
font-size: 0.9rem;
color: var(--sl-color-gray-2);
margin-bottom: 1rem;
}
.table-wrapper {
overflow-x: auto;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
}
.resources-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.resources-table th,
.resources-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--sl-color-gray-6);
}
.resources-table thead {
background: var(--sl-color-gray-6);
position: sticky;
top: 0;
}
.resources-table th {
font-weight: 600;
white-space: nowrap;
}
.resources-table tbody tr:last-child td {
border-bottom: none;
}
.resources-table tbody tr:hover {
background: var(--sl-color-gray-7);
}
.text-right {
text-align: right;
}
.name-cell {
white-space: nowrap;
}
.name-with-links {
display: flex;
align-items: center;
gap: 0.75rem;
}
.name-cell code {
font-size: 0.85rem;
color: var(--sl-color-text-accent);
background: transparent;
padding: 0;
}
.description-cell {
max-width: 400px;
line-height: 1.4;
}
.links {
display: flex;
gap: 0.75rem;
align-items: center;
}
.links a {
color: var(--sl-color-text-accent);
text-decoration: none;
display: inline-flex;
align-items: center;
transition: opacity 0.2s;
}
.links a:hover {
opacity: 0.7;
}
.link-text {
font-size: 0.85rem;
}
.no-results {
padding: 3rem;
text-align: center;
color: var(--sl-color-gray-3);
font-size: 1.1rem;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.resources-table {
font-size: 0.8rem;
}
.resources-table th,
.resources-table td {
padding: 0.5rem;
}
.description-cell {
max-width: 200px;
font-size: 0.75rem;
}
}
</style>
<script>
class CommunityResources extends HTMLElement {
searchInput: HTMLInputElement;
sourceFilter: HTMLSelectElement;
sortSelect: HTMLSelectElement;
tbody: HTMLElement;
summaryEl: HTMLElement;
visibleCountEl: HTMLElement;
noResultsEl: HTMLElement;
rows: HTMLTableRowElement[];
totalCount: number;
constructor() {
super();
this.searchInput = this.querySelector('#resource-search') as HTMLInputElement;
this.sourceFilter = this.querySelector('#filter-source') as HTMLSelectElement;
this.sortSelect = this.querySelector('#sort-by') as HTMLSelectElement;
this.tbody = this.querySelector('#resources-tbody') as HTMLElement;
this.summaryEl = this.querySelector('#resource-summary') as HTMLElement;
this.visibleCountEl = this.querySelector('#visible-count') as HTMLElement;
this.noResultsEl = this.querySelector('#no-results') as HTMLElement;
this.rows = Array.from(this.tbody.querySelectorAll('.resource-row'));
this.totalCount = this.rows.length;
this.searchInput.addEventListener('input', () => this.filterAndSort());
this.sourceFilter.addEventListener('change', () => this.filterAndSort());
this.sortSelect.addEventListener('change', () => this.filterAndSort());
}
filterAndSort() {
const query = this.searchInput.value.toLowerCase();
const sourceFilter = this.sourceFilter.value;
let visibleRows = this.rows.filter((row) => {
const name = row.dataset.name?.toLowerCase() || '';
const description = row.dataset.description?.toLowerCase() || '';
const hasNpm = row.dataset.hasNpm === 'true';
const hasCrates = row.dataset.hasCrates === 'true';
const matchesQuery = !query || name.includes(query) || description.includes(query);
let matchesSource = true;
if (sourceFilter === 'both') {
matchesSource = hasNpm && hasCrates;
} else if (sourceFilter === 'npm') {
matchesSource = hasNpm;
} else if (sourceFilter === 'crates') {
matchesSource = hasCrates;
}
return matchesQuery && matchesSource;
});
const sortBy = this.sortSelect.value;
visibleRows.sort((a, b) => {
if (sortBy === 'stars') {
return (parseInt(b.dataset.stars || '0') || 0) - (parseInt(a.dataset.stars || '0') || 0);
} else if (sortBy === 'name') {
return (a.dataset.name || '').localeCompare(b.dataset.name || '');
} else if (sortBy === 'downloads') {
return (
(parseInt(b.dataset.downloads || '0') || 0) -
(parseInt(a.dataset.downloads || '0') || 0)
);
}
return 0;
});
this.rows.forEach((row) => row.classList.add('hidden'));
visibleRows.forEach((row) => {
row.classList.remove('hidden');
this.tbody.appendChild(row);
});
const visibleCount = visibleRows.length;
this.visibleCountEl.textContent = visibleCount.toString();
if (visibleCount === 0) {
this.noResultsEl.classList.remove('hidden');
this.querySelector('.table-wrapper')?.classList.add('hidden');
} else {
this.noResultsEl.classList.add('hidden');
this.querySelector('.table-wrapper')?.classList.remove('hidden');
}
}
}
customElements.define('community-resources', CommunityResources);
</script>

View File

@@ -0,0 +1,12 @@
---
title: Community Plugins
i18nReady: true
sidebar:
label: Overview
tableOfContents: false
template: splash
---
import CommunityResources from '@components/CommunityResources.astro';
<CommunityResources />

View File

@@ -12,6 +12,7 @@ import CommunityList from '@components/list/Community.astro';
import Search from '@components/CardGridSearch.astro';
import AwesomeTauri from '@components/AwesomeTauri.astro';
import TableCompatibility from '@components/plugins/TableCompatibility.astro';
import CommunityResources from '@components/CommunityResources.astro';
import { platformOptions } from 'src/types';
import { platformFilters } from 'src/api/search.ts';
@@ -21,6 +22,8 @@ Tauri comes with extensibility in mind. On this page you'll find:
- **[Community Resources](#community-plugins)**: More plugins and recipes built by the Tauri community. You can also contribute your own on [Awesome Tauri](https://github.com/tauri-apps/awesome-tauri)
- **[Support Table](#support-table)**: A compatibility table showing which platforms are supported by each official plugin
Also we have a dedicated page for listing discovered community tauri-plugin-* [Crates and NPM packages](/plugin/index-external-resources/)
<br />
**Use the search and filter functionality to find features or community resources:**
@@ -34,6 +37,8 @@ Tauri comes with extensibility in mind. On this page you'll find:
<AwesomeTauri section="integrations" />
</Search>
## Support Table
Hover "\*" to see notes. For more details visit the plugin page

File diff suppressed because it is too large Load Diff