mirror of
https://github.com/lainbo/component-party.git
synced 2026-04-04 20:39:02 +08:00
v3: Technical refactor (#302)
This commit is contained in:
committed by
GitHub
parent
f9cf2c60a5
commit
54eeee51b4
5
.babelrc
5
.babelrc
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }]
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
const _require = require("esm")(module);
|
||||
module.exports = _require("./.eslintrc.esm.mjs").default;
|
||||
@@ -1,24 +0,0 @@
|
||||
import FRAMEWORKS from "./frameworks.mjs";
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.Config}
|
||||
*/
|
||||
export default {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
plugins: ["prettier"],
|
||||
overrides: FRAMEWORKS.reduce((acc, { eslint }) => {
|
||||
if (Array.isArray(eslint)) {
|
||||
acc.push(...eslint);
|
||||
} else {
|
||||
acc.push(eslint);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
30
.github/actions/setup-node-pnpm/action.yml
vendored
Normal file
30
.github/actions/setup-node-pnpm/action.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Setup Node.js and pnpm"
|
||||
description: "Setup Node.js and pnpm with caching"
|
||||
inputs:
|
||||
node-version:
|
||||
description: "Node.js version to use"
|
||||
required: false
|
||||
default: "22.18.0"
|
||||
pnpm-version:
|
||||
description: "pnpm version to use"
|
||||
required: false
|
||||
default: "10.14.0"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Node.js Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Read Node version from .nvmrc
|
||||
- name: Read Node version from .nvmrc
|
||||
id: nvm
|
||||
run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Setup Node.js with the version from .nvmrc
|
||||
- name: Use Node.js version from .nvmrc
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# - run: pnpm run lint # uncomment when eslint is fixed
|
||||
- run: pnpm run build
|
||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js and pnpm
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
|
||||
- name: Build application
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run checks
|
||||
run: pnpm check:ci
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm run test:e2e:install
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run test:e2e
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
16
.github/workflows/e2e.yml
vendored
16
.github/workflows/e2e.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: End-to-end tests
|
||||
on: push
|
||||
jobs:
|
||||
cypress-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: pnpm build
|
||||
start: pnpm preview --port 5173
|
||||
browser: chrome
|
||||
36
.github/workflows/setup-node-pnpm.yml
vendored
Normal file
36
.github/workflows/setup-node-pnpm.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Setup Node.js and pnpm
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node-version:
|
||||
description: "Node.js version to use"
|
||||
required: false
|
||||
type: string
|
||||
default: "22.18.0"
|
||||
pnpm-version:
|
||||
description: "pnpm version to use"
|
||||
required: false
|
||||
type: string
|
||||
default: "10.14.0"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
test-results
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -7,13 +8,21 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Package manager files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.npm
|
||||
.yarn
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Prettier cache
|
||||
.cache
|
||||
|
||||
# Lint
|
||||
.eslintcache
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -30,4 +39,9 @@ src/generatedContent
|
||||
|
||||
archive
|
||||
|
||||
vite.config.js.timestamp*
|
||||
vite.config.js.timestamp*
|
||||
|
||||
playwright-report
|
||||
test-results
|
||||
|
||||
public/_redirects
|
||||
@@ -1 +0,0 @@
|
||||
pnpm lint-staged
|
||||
156
.oxlintrc.json
Normal file
156
.oxlintrc.json
Normal file
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"node": true,
|
||||
"shared-node-browser": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/logs",
|
||||
"**/*.log",
|
||||
"**/npm-debug.log*",
|
||||
"**/yarn-debug.log*",
|
||||
"**/yarn-error.log*",
|
||||
"**/pnpm-debug.log*",
|
||||
"**/lerna-debug.log*",
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/dist-ssr",
|
||||
"**/*.local",
|
||||
".vscode/*",
|
||||
"!.vscode/extensions.json",
|
||||
"**/.idea",
|
||||
"**/.DS_Store",
|
||||
"**/*.suo",
|
||||
"**/*.ntvs*",
|
||||
"**/*.njsproj",
|
||||
"**/*.sln",
|
||||
"**/*.sw?",
|
||||
"src/generatedContent",
|
||||
"**/archive",
|
||||
"**/vite.config.js.timestamp*",
|
||||
"content/**"
|
||||
],
|
||||
"rules": {
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-empty-static-block": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-import-assign": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-new-native-nonconstructor": "error",
|
||||
"no-nonoctal-decimal-escape": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-setter-return": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-unexpected-multiline": "off",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-with": "error",
|
||||
"require-yield": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"no-array-constructor": "error",
|
||||
"@typescript-eslint/no-duplicate-enum-values": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extra-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/no-this-alias": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "error",
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"curly": "off",
|
||||
"unicorn/empty-brace-spaces": "off",
|
||||
"unicorn/no-nested-ternary": "off",
|
||||
"unicorn/number-literal-case": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
||||
"rules": {
|
||||
"no-class-assign": "off",
|
||||
"no-const-assign": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-dupe-keys": "off",
|
||||
"no-func-assign": "off",
|
||||
"no-import-assign": "off",
|
||||
"no-new-native-nonconstructor": "off",
|
||||
"no-obj-calls": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-setter-return": "off",
|
||||
"no-this-before-super": "off",
|
||||
"no-unsafe-negation": "off",
|
||||
"no-var": "error",
|
||||
"no-with": "off",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.svelte", "**/*.svelte"],
|
||||
"rules": {
|
||||
"no-inner-declarations": "off",
|
||||
"no-self-assign": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.svelte", "**/*.svelte"],
|
||||
"rules": {
|
||||
"no-inner-declarations": "off",
|
||||
"no-self-assign": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
.prettierrc
11
.prettierrc
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"svelte.svelte-vscode",
|
||||
"antfu.unocss"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
// Runs Prettier, then ESLint
|
||||
"editor.codeActionsOnSave": ["source.formatDocument", "source.fixAll.eslint"],
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,9 @@ This site is built with [Vite](https://vitejs.dev) and [Svelte](https://svelte.d
|
||||
|
||||
1. Fork the project and create a new branch
|
||||
2. Add the new framework SVG logo in `public/framework`
|
||||
3. Install the ESLint plugin associated to the framework
|
||||
4. In `frameworks.mjs`, add a new entry with SVG link and ESLint configuration
|
||||
5. If the framework needs a language syntax highlight, add it to the call to `getHighlighter`’s `langs` argument in `build/lib/generateContent.js`
|
||||
6. To make a playground link:
|
||||
1. Add a `create${FRAMEWORK}Playground.js` file in `build/lib/playground`.
|
||||
2. That file should export a function that returns an object with a `fromContentByFilename` method that accepts an object of filepath keys and file content values, then returns an absolute URL to a framework’s online REPL with those files loaded.
|
||||
3. Register its export in `build/lib/playground/index.js`
|
||||
3. In `frameworks.ts`, add a new entry with SVG link
|
||||
4. If the framework needs a language syntax highlight, add it to the call to `getHighlighter`’s `langs` argument in `build/lib/generateContent.ts`
|
||||
5. To make a playground link in `build/lib/playgroundUrlByFramework.ts`.
|
||||
|
||||
## Improve website
|
||||
|
||||
@@ -23,4 +19,4 @@ pnpm i
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
This project requires Node.js to be `v14.0.0` or higher, because we use new JavaScript features in our code, such as optional chaining.
|
||||
This project requires Node.js to be `v22.18.0` or higher, because we use new JavaScript features in our code, such as optional chaining.
|
||||
|
||||
53
README.md
53
README.md
@@ -433,7 +433,7 @@ How do we solve this ? Developers love having framework overview by examples. It
|
||||
<details>
|
||||
<summary>
|
||||
<img width="18" height="18" src="public/framework/ember.svg" />
|
||||
<b>Ember Polaris (preview)</b>
|
||||
<b>Ember Polaris</b>
|
||||
<img src="https://us-central1-progress-markdown.cloudfunctions.net/progress/91" />
|
||||
</summary>
|
||||
|
||||
@@ -657,6 +657,44 @@ How do we solve this ? Developers love having framework overview by examples. It
|
||||
- [x] Fetch data
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<img width="18" height="18" src="public/framework/ripple.svg" />
|
||||
<b>Ripple</b>
|
||||
<img src="https://us-central1-progress-markdown.cloudfunctions.net/progress/91" />
|
||||
</summary>
|
||||
|
||||
- [x] Reactivity
|
||||
- [x] Declare state
|
||||
- [x] Update state
|
||||
- [x] Computed state
|
||||
- [x] Templating
|
||||
- [x] Minimal template
|
||||
- [x] Styling
|
||||
- [x] Loop
|
||||
- [x] Event click
|
||||
- [x] Dom ref
|
||||
- [x] Conditional
|
||||
- [x] Lifecycle
|
||||
- [x] On mount
|
||||
- [x] On unmount
|
||||
- [ ] Component composition
|
||||
- [x] Props
|
||||
- [x] Emit to parent
|
||||
- [x] Slot
|
||||
- [x] Slot fallback
|
||||
- [ ] Context
|
||||
- [x] Form input
|
||||
- [x] Input text
|
||||
- [x] Checkbox
|
||||
- [x] Radio
|
||||
- [x] Select
|
||||
- [ ] Webapp features
|
||||
- [x] Render app
|
||||
- [ ] Fetch data
|
||||
|
||||
</details>
|
||||
|
||||
<!-- progression end -->
|
||||
|
||||
@@ -671,7 +709,7 @@ pnpm i
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
This project requires Node.js to be `v20` or higher.
|
||||
This project requires Node.js to be `v22.18.0` or higher.
|
||||
|
||||
### Principle when add/edit a framework snippet
|
||||
|
||||
@@ -684,16 +722,13 @@ We believe that deep understanding should precede optimization, enabling learner
|
||||
|
||||
1. Fork the project and create a new branch
|
||||
2. Add the new framework SVG logo in `public/framework`
|
||||
3. Install the ESLint plugin associated to the framework
|
||||
4. In `frameworks.mjs`, add a new entry with SVG link and ESLint configuration
|
||||
5. If the framework needs a language syntax highlight, add it to the call to `getHighlighter`’s `langs` argument in `build/lib/generateContent.js`
|
||||
6. To make a playground link:
|
||||
1. In file `build/lib/playgroundUrlByFramework.js`, add your framework id.
|
||||
2. The method accepts an object of filepath keys and file content values, then returns a playground URL to the framework’s online REPL with those files loaded.
|
||||
3. In `frameworks.ts`, add a new entry with SVG link
|
||||
4. If the framework needs a language syntax highlight, add it to the call to `getHighlighter`’s `langs` argument in `build/lib/generateContent.ts`
|
||||
5. To make a playground link in `build/lib/playgroundUrlByFramework.ts`.
|
||||
|
||||
## 🧑💻 Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. \[[Contribute](CONTRIBUTING.md)].
|
||||
This project exists thanks to all the people who contribute. [Contribute](./CONTRIBUTING.md)
|
||||
[](https://github.com/matschik/component-party/graphs/contributors)
|
||||
|
||||
## ⚖️ License
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import generateContent from "./lib/generateContent.js";
|
||||
import generateContent from "./lib/generateContent";
|
||||
import { createFsCache } from "micache";
|
||||
import { hashElement } from "folder-hash";
|
||||
import chokidar from "chokidar";
|
||||
import chokidar, { type FSWatcher } from "chokidar";
|
||||
import { disposeHighlighter } from "./lib/highlighter.ts";
|
||||
|
||||
const contentDirFsCache = await createFsCache("pluginGenerateFrameworkContent");
|
||||
|
||||
export default function pluginGenerateFrameworkContent() {
|
||||
const name = "generateFrameworkContent";
|
||||
|
||||
function logInfo(...args) {
|
||||
function logInfo(...args: unknown[]) {
|
||||
console.info(`[${name}]`, ...args);
|
||||
}
|
||||
|
||||
let buildIsRunning = false;
|
||||
|
||||
async function build() {
|
||||
async function build(): Promise<void> {
|
||||
if (buildIsRunning) {
|
||||
return;
|
||||
}
|
||||
@@ -23,7 +24,7 @@ export default function pluginGenerateFrameworkContent() {
|
||||
const contentDirHash =
|
||||
(await hashElement("content")).hash +
|
||||
(await hashElement("build")).hash +
|
||||
(await hashElement("frameworks.mjs")).hash;
|
||||
(await hashElement("frameworks.ts")).hash;
|
||||
|
||||
const contentDirLastHash = await contentDirFsCache.get("contentDirHash");
|
||||
if (contentDirHash !== contentDirLastHash) {
|
||||
@@ -36,14 +37,14 @@ export default function pluginGenerateFrameworkContent() {
|
||||
buildIsRunning = false;
|
||||
}
|
||||
|
||||
let fsContentWatcher;
|
||||
let fsContentWatcher: FSWatcher | undefined;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
fsContentWatcher = chokidar.watch(["content"]).on("change", build);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
async buildStart() {
|
||||
async buildStart(): Promise<void> {
|
||||
try {
|
||||
await build();
|
||||
} catch (error) {
|
||||
@@ -51,8 +52,10 @@ export default function pluginGenerateFrameworkContent() {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async buildEnd() {
|
||||
fsContentWatcher && (await fsContentWatcher.close());
|
||||
async buildEnd(): Promise<void> {
|
||||
await fsContentWatcher?.close();
|
||||
// Dispose of highlighter instances to prevent memory leaks
|
||||
await disposeHighlighter();
|
||||
},
|
||||
};
|
||||
}
|
||||
6
build/lib/angularHighlighter.d.ts
vendored
Normal file
6
build/lib/angularHighlighter.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export function mustUseAngularHighlighter(fileContent: string): boolean;
|
||||
|
||||
export function highlightAngularComponent(
|
||||
fileContent: string,
|
||||
fileExt: string,
|
||||
): Promise<string>;
|
||||
@@ -1,12 +1,15 @@
|
||||
import { codeToHighlightCodeHtml } from "./highlighter.js";
|
||||
import { codeToHighlightCodeHtml } from "./highlighter.ts";
|
||||
|
||||
export function mustUseAngularHighlighter(fileContent) {
|
||||
export function mustUseAngularHighlighter(fileContent: string): boolean {
|
||||
return (
|
||||
fileContent.includes("@angular/core") && fileContent.includes("template")
|
||||
);
|
||||
}
|
||||
|
||||
export async function highlightAngularComponent(fileContent, fileExt) {
|
||||
export async function highlightAngularComponent(
|
||||
fileContent: string,
|
||||
fileExt: string,
|
||||
): Promise<string> {
|
||||
const templateCode = getAngularTemplateCode(fileContent);
|
||||
|
||||
let codeHighlighted = "";
|
||||
@@ -15,26 +18,26 @@ export async function highlightAngularComponent(fileContent, fileExt) {
|
||||
removeAngularTemplateContent(fileContent);
|
||||
const templateCodeHighlighted = await codeToHighlightCodeHtml(
|
||||
templateCode,
|
||||
"html"
|
||||
"html",
|
||||
);
|
||||
|
||||
const componentWithoutTemplateHighlighted = await codeToHighlightCodeHtml(
|
||||
componentWithEmptyTemplate,
|
||||
fileExt
|
||||
fileExt,
|
||||
);
|
||||
|
||||
codeHighlighted = componentWithoutTemplateHighlighted.replace(
|
||||
"template",
|
||||
"template: `" + removeCodeWrapper(templateCodeHighlighted) + "`,"
|
||||
"template: `" + removeCodeWrapper(templateCodeHighlighted) + "`,",
|
||||
);
|
||||
} else {
|
||||
codeHighlighted = codeToHighlightCodeHtml(fileContent, fileExt);
|
||||
codeHighlighted = await codeToHighlightCodeHtml(fileContent, fileExt);
|
||||
}
|
||||
|
||||
return codeHighlighted;
|
||||
}
|
||||
|
||||
function getAngularTemplateCode(fileContent) {
|
||||
function getAngularTemplateCode(fileContent: string): string {
|
||||
// regex to grab what is inside angular component template inside backticks
|
||||
const regex = /template:\s*`([\s\S]*?)`/gm;
|
||||
|
||||
@@ -46,17 +49,17 @@ function getAngularTemplateCode(fileContent) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function removeAngularTemplateContent(fileContent) {
|
||||
function removeAngularTemplateContent(fileContent: string): string {
|
||||
const componentWithoutContentInsideTemplate = fileContent.replace(
|
||||
/template:\s*`([\s\S]*?)([^*])`,?/gm,
|
||||
"template"
|
||||
"template",
|
||||
);
|
||||
|
||||
return componentWithoutContentInsideTemplate;
|
||||
}
|
||||
|
||||
function removeCodeWrapper(html) {
|
||||
function removeCodeWrapper(html: string): string {
|
||||
const regexForWrapper = /<pre([\s\S]*?)><code>([\s\S]*?)<\/code><\/pre>/gm;
|
||||
const code = regexForWrapper.exec(html);
|
||||
return code[2];
|
||||
return code ? code[2] : "";
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
export default {
|
||||
import { type ThemeRegistration } from "shiki";
|
||||
|
||||
export const componentPartyShikiTheme = {
|
||||
name: "one-dark-pro-for-component-party",
|
||||
type: "dark",
|
||||
semanticHighlighting: true,
|
||||
semanticTokenColors: {
|
||||
enumMember: {
|
||||
foreground: "#56b6c2",
|
||||
},
|
||||
"variable.constant": {
|
||||
foreground: "#d19a66",
|
||||
},
|
||||
"variable.defaultLibrary": {
|
||||
foreground: "#e5c07b",
|
||||
},
|
||||
"variable:dart": {
|
||||
foreground: "#d19a66",
|
||||
},
|
||||
"property:dart": {
|
||||
foreground: "#d19a66",
|
||||
},
|
||||
"annotation:dart": {
|
||||
foreground: "#d19a66",
|
||||
},
|
||||
"parameter.label:dart": {
|
||||
foreground: "#abb2bf",
|
||||
},
|
||||
macro: {
|
||||
foreground: "#d19a66",
|
||||
},
|
||||
tomlArrayKey: {
|
||||
foreground: "#e5c07b",
|
||||
},
|
||||
enumMember: "#56b6c2",
|
||||
"variable.constant": "#d19a66",
|
||||
"variable.defaultLibrary": "#e5c07b",
|
||||
"variable:dart": "#d19a66",
|
||||
"property:dart": "#d19a66",
|
||||
"annotation:dart": "#d19a66",
|
||||
"parameter.label:dart": "#abb2bf",
|
||||
macro: "#d19a66",
|
||||
tomlArrayKey: "#e5c07b",
|
||||
},
|
||||
tokenColors: [
|
||||
{
|
||||
@@ -2110,4 +2094,4 @@ export default {
|
||||
"walkThrough.embeddedEditorBackground": "#2e3440",
|
||||
"welcomePage.buttonHoverBackground": "#404754",
|
||||
},
|
||||
};
|
||||
} as const satisfies ThemeRegistration;
|
||||
@@ -1,227 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { packageDirectory } from "pkg-dir";
|
||||
import path from "node:path";
|
||||
import kebabCase from "lodash.kebabcase";
|
||||
import FRAMEWORKS from "../../frameworks.mjs";
|
||||
import playgroundUrlByFramework from "./playgroundUrlByFramework.js";
|
||||
import prettier from "prettier";
|
||||
import {
|
||||
highlightAngularComponent,
|
||||
mustUseAngularHighlighter,
|
||||
} from "./angularHighlighter.js";
|
||||
import {
|
||||
codeToHighlightCodeHtml,
|
||||
markdownToHighlightedHtml,
|
||||
} from "./highlighter.js";
|
||||
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function generateContent() {
|
||||
const rootDir = await packageDirectory();
|
||||
const contentPath = path.join(rootDir, "content");
|
||||
const sectionDirNames = await fs.readdir(contentPath);
|
||||
|
||||
const treePayload = {
|
||||
sections: [],
|
||||
snippets: [],
|
||||
};
|
||||
|
||||
const byFrameworkId = {};
|
||||
|
||||
for (const sectionDirName of sectionDirNames) {
|
||||
const sectionTitle = dirNameToTitle(sectionDirName);
|
||||
const sectionId = kebabCase(sectionTitle);
|
||||
|
||||
treePayload.sections.push({
|
||||
sectionId,
|
||||
sectionDirName,
|
||||
title: sectionTitle,
|
||||
});
|
||||
|
||||
const snippetsDirPath = path.join(contentPath, sectionDirName);
|
||||
const snippetDirNames = await fs.readdir(snippetsDirPath);
|
||||
|
||||
for (const snippetDirName of snippetDirNames) {
|
||||
const title = dirNameToTitle(snippetDirName);
|
||||
const snippetId = kebabCase(title);
|
||||
|
||||
treePayload.snippets.push({
|
||||
sectionId,
|
||||
snippetId,
|
||||
snippetDirName,
|
||||
sectionDirName,
|
||||
title,
|
||||
});
|
||||
|
||||
const frameworksDirPath = path.join(snippetsDirPath, snippetDirName);
|
||||
const frameworkIds = FRAMEWORKS.map(({ id }) => id);
|
||||
|
||||
await Promise.all(
|
||||
frameworkIds.map(async (frameworkId) => {
|
||||
const frameworkSnippet = {
|
||||
frameworkId,
|
||||
snippetId,
|
||||
files: [],
|
||||
playgroundURL: "",
|
||||
markdownFiles: [],
|
||||
snippetEditHref: `https://github.com/matschik/component-party/tree/main/content/${sectionDirName}/${snippetDirName}/${frameworkId}`,
|
||||
};
|
||||
|
||||
const codeFilesDirPath = path.join(frameworksDirPath, frameworkId);
|
||||
if (!(await pathExists(codeFilesDirPath))) {
|
||||
byFrameworkId[frameworkId].push(frameworkSnippet);
|
||||
return;
|
||||
}
|
||||
const codeFileNames = await fs.readdir(codeFilesDirPath);
|
||||
|
||||
for (const codeFileName of codeFileNames) {
|
||||
const codeFilePath = path.join(codeFilesDirPath, codeFileName);
|
||||
const ext = path.parse(codeFilePath).ext.split(".").pop();
|
||||
const content = await fs.readFile(codeFilePath, "utf-8");
|
||||
|
||||
const file = {
|
||||
fileName: codeFileName,
|
||||
ext,
|
||||
content,
|
||||
contentHtml: "",
|
||||
};
|
||||
|
||||
if (ext === "md") {
|
||||
file.contentHtml = await markdownToHighlightedHtml(content);
|
||||
frameworkSnippet.markdownFiles.push(file);
|
||||
} else {
|
||||
file.contentHtml = mustUseAngularHighlighter(content)
|
||||
? await highlightAngularComponent(content, ext)
|
||||
: await codeToHighlightCodeHtml(content, ext);
|
||||
|
||||
frameworkSnippet.files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (frameworkSnippet.files.length > 0) {
|
||||
const { filesSorter } = FRAMEWORKS.find(
|
||||
(f) => f.id === frameworkId
|
||||
);
|
||||
frameworkSnippet.files = filesSorter(frameworkSnippet.files);
|
||||
const playgroundURL = await generatePlaygroundURL(
|
||||
frameworkId,
|
||||
frameworkSnippet.files,
|
||||
title
|
||||
);
|
||||
|
||||
if (playgroundURL) {
|
||||
frameworkSnippet.playgroundURL = playgroundURL;
|
||||
}
|
||||
|
||||
// Remove content key, not used anymore
|
||||
frameworkSnippet.files = frameworkSnippet.files.map((file) => ({
|
||||
...file,
|
||||
content: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!byFrameworkId[frameworkId]) {
|
||||
byFrameworkId[frameworkId] = [];
|
||||
}
|
||||
|
||||
byFrameworkId[frameworkId].push(frameworkSnippet);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const generatedContentDirPath = path.join(rootDir, "src/generatedContent");
|
||||
const frameworkDirPath = path.join(generatedContentDirPath, "framework");
|
||||
const treeFilePath = path.join(generatedContentDirPath, "tree.js");
|
||||
const frameworkIndexPath = path.join(frameworkDirPath, "index.js");
|
||||
const commentDisclaimer = `// File generated from "node scripts/generateContent.js", DO NOT EDIT/COMMIT`;
|
||||
|
||||
if (!(await pathExists(generatedContentDirPath))) {
|
||||
await fs.mkdir(generatedContentDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await writeJsFile(
|
||||
treeFilePath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export const sections = ${JSON.stringify(treePayload.sections, null, 2)};
|
||||
export const snippets = ${JSON.stringify(treePayload.snippets, null, 2)};
|
||||
`
|
||||
);
|
||||
|
||||
if (!(await pathExists(frameworkDirPath))) {
|
||||
await fs.mkdir(frameworkDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(byFrameworkId).map((frameworkId) => {
|
||||
const frameworkFilePath = path.join(
|
||||
frameworkDirPath,
|
||||
`${frameworkId}.js`
|
||||
);
|
||||
return writeJsFile(
|
||||
frameworkFilePath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export default ${JSON.stringify(byFrameworkId[frameworkId], null, 2)}
|
||||
`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await writeJsFile(
|
||||
frameworkIndexPath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export default {
|
||||
${Object.keys(byFrameworkId)
|
||||
.map(
|
||||
(frameworkId) =>
|
||||
`${frameworkId}: () => import("./${frameworkId}.js")`
|
||||
)
|
||||
.join(",\n")}
|
||||
|
||||
};
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
function dirNameToTitle(dirName) {
|
||||
return capitalize(dirName.split("-").splice(1).join(" "));
|
||||
}
|
||||
|
||||
function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
async function writeJsFile(filepath, jsCode) {
|
||||
const codeFormatted = await prettier.format(jsCode, { parser: "babel" });
|
||||
await fs.writeFile(filepath, codeFormatted);
|
||||
}
|
||||
|
||||
async function generatePlaygroundURL(frameworkId, files, title) {
|
||||
const frameworkIdPlayground = playgroundUrlByFramework[frameworkId];
|
||||
if (!frameworkIdPlayground) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameworkConfig = FRAMEWORKS.find((f) => f.id === frameworkId);
|
||||
|
||||
const contentByFilename = frameworkConfig
|
||||
.filesSorter(files)
|
||||
.reduce((acc, file) => {
|
||||
acc[file.fileName] = file.content;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const playgroundURL = await frameworkIdPlayground(contentByFilename, title);
|
||||
|
||||
return playgroundURL;
|
||||
}
|
||||
446
build/lib/generateContent.ts
Normal file
446
build/lib/generateContent.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { packageDirectory } from "package-directory";
|
||||
import path from "node:path";
|
||||
import { frameworks } from "../../frameworks.ts";
|
||||
import playgroundUrlByFramework from "./playgroundUrlByFramework.ts";
|
||||
import prettier from "prettier";
|
||||
import {
|
||||
highlightAngularComponent,
|
||||
mustUseAngularHighlighter,
|
||||
} from "./angularHighlighter.ts";
|
||||
import {
|
||||
codeToHighlightCodeHtml,
|
||||
markdownToHighlightedHtml,
|
||||
} from "./highlighter.ts";
|
||||
import kebabCase from "just-kebab-case";
|
||||
|
||||
interface File {
|
||||
fileName: string;
|
||||
ext: string;
|
||||
content?: string;
|
||||
contentHtml: string;
|
||||
}
|
||||
|
||||
interface FrameworkFile {
|
||||
fileName: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface FrameworkSnippet {
|
||||
frameworkId: string;
|
||||
snippetId: string;
|
||||
files: File[];
|
||||
playgroundURL: string;
|
||||
markdownFiles: File[];
|
||||
snippetEditHref: string;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
sectionId: string;
|
||||
sectionDirName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Snippet {
|
||||
sectionId: string;
|
||||
snippetId: string;
|
||||
snippetDirName: string;
|
||||
sectionDirName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface TreePayload {
|
||||
sections: Section[];
|
||||
snippets: Snippet[];
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldRegenerateFiles(
|
||||
contentPath: string,
|
||||
generatedContentDirPath: string,
|
||||
frameworkDirPath: string,
|
||||
): Promise<boolean> {
|
||||
// Check if any generated files exist
|
||||
const treeFilePath = path.join(generatedContentDirPath, "tree.js");
|
||||
const treeDtsFilePath = path.join(generatedContentDirPath, "tree.d.ts");
|
||||
const frameworkIndexPath = path.join(frameworkDirPath, "index.js");
|
||||
const frameworkIndexDtsPath = path.join(frameworkDirPath, "index.d.ts");
|
||||
|
||||
const generatedFilesExist = await Promise.all([
|
||||
pathExists(treeFilePath),
|
||||
pathExists(treeDtsFilePath),
|
||||
pathExists(frameworkIndexPath),
|
||||
pathExists(frameworkIndexDtsPath),
|
||||
]);
|
||||
|
||||
// If any generated file is missing, regenerate
|
||||
if (!generatedFilesExist.every(Boolean)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the modification time of the content directory
|
||||
const contentStats = await fs.stat(contentPath);
|
||||
const contentModTime = contentStats.mtime;
|
||||
|
||||
// Check if any generated file is older than the content directory
|
||||
const generatedFilePaths = [
|
||||
treeFilePath,
|
||||
treeDtsFilePath,
|
||||
frameworkIndexPath,
|
||||
frameworkIndexDtsPath,
|
||||
];
|
||||
|
||||
for (const filePath of generatedFilePaths) {
|
||||
try {
|
||||
const fileStats = await fs.stat(filePath);
|
||||
if (fileStats.mtime < contentModTime) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// If we can't stat a file, regenerate to be safe
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default async function generateContent(
|
||||
options: { noCache?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const rootDir = await packageDirectory();
|
||||
if (!rootDir) {
|
||||
throw new Error("Could not find package directory");
|
||||
}
|
||||
const contentPath = path.join(rootDir, "content");
|
||||
const generatedContentDirPath = path.join(rootDir, "src/generatedContent");
|
||||
const frameworkDirPath = path.join(generatedContentDirPath, "framework");
|
||||
|
||||
// Check if we should skip generation due to cache
|
||||
if (!options.noCache) {
|
||||
const shouldRegenerate = await shouldRegenerateFiles(
|
||||
contentPath,
|
||||
generatedContentDirPath,
|
||||
frameworkDirPath,
|
||||
);
|
||||
|
||||
if (!shouldRegenerate) {
|
||||
console.info("Generated content is up to date, skipping generation.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sectionDirNames = await fs.readdir(contentPath);
|
||||
|
||||
const treePayload: TreePayload = {
|
||||
sections: [],
|
||||
snippets: [],
|
||||
};
|
||||
|
||||
const byFrameworkId: Record<string, FrameworkSnippet[]> = {};
|
||||
|
||||
for (const sectionDirName of sectionDirNames) {
|
||||
const sectionTitle = dirNameToTitle(sectionDirName);
|
||||
const sectionId = kebabCase(sectionTitle);
|
||||
|
||||
treePayload.sections.push({
|
||||
sectionId,
|
||||
sectionDirName,
|
||||
title: sectionTitle,
|
||||
});
|
||||
|
||||
const snippetsDirPath = path.join(contentPath, sectionDirName);
|
||||
const snippetDirNames = await fs.readdir(snippetsDirPath);
|
||||
|
||||
for (const snippetDirName of snippetDirNames) {
|
||||
const title = dirNameToTitle(snippetDirName);
|
||||
const snippetId = kebabCase(title);
|
||||
|
||||
treePayload.snippets.push({
|
||||
sectionId,
|
||||
snippetId,
|
||||
snippetDirName,
|
||||
sectionDirName,
|
||||
title,
|
||||
});
|
||||
|
||||
const frameworksDirPath = path.join(snippetsDirPath, snippetDirName);
|
||||
const frameworkIds = frameworks.map(({ id }) => id);
|
||||
|
||||
await Promise.all(
|
||||
frameworkIds.map(async (frameworkId: string) => {
|
||||
const frameworkSnippet: FrameworkSnippet = {
|
||||
frameworkId,
|
||||
snippetId,
|
||||
files: [],
|
||||
playgroundURL: "",
|
||||
markdownFiles: [],
|
||||
snippetEditHref: `https://github.com/matschik/component-party/tree/main/content/${sectionDirName}/${snippetDirName}/${frameworkId}`,
|
||||
};
|
||||
|
||||
const codeFilesDirPath = path.join(frameworksDirPath, frameworkId);
|
||||
if (!(await pathExists(codeFilesDirPath))) {
|
||||
byFrameworkId[frameworkId].push(frameworkSnippet);
|
||||
return;
|
||||
}
|
||||
const codeFileNames = await fs.readdir(codeFilesDirPath);
|
||||
|
||||
for (const codeFileName of codeFileNames) {
|
||||
const codeFilePath = path.join(codeFilesDirPath, codeFileName);
|
||||
const ext = path.parse(codeFilePath).ext.split(".").pop() || "";
|
||||
const content = await fs.readFile(codeFilePath, "utf-8");
|
||||
|
||||
const file: File = {
|
||||
fileName: codeFileName,
|
||||
ext,
|
||||
content,
|
||||
contentHtml: "",
|
||||
};
|
||||
|
||||
if (ext === "md") {
|
||||
file.contentHtml = await markdownToHighlightedHtml(content);
|
||||
frameworkSnippet.markdownFiles.push(file);
|
||||
} else {
|
||||
file.contentHtml = mustUseAngularHighlighter(content)
|
||||
? await highlightAngularComponent(content, ext)
|
||||
: await codeToHighlightCodeHtml(content, ext);
|
||||
|
||||
frameworkSnippet.files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (frameworkSnippet.files.length > 0) {
|
||||
const frameworkConfig = frameworks.find(
|
||||
(f) => f.id === frameworkId,
|
||||
);
|
||||
if (frameworkConfig) {
|
||||
frameworkSnippet.files = frameworkConfig.filesSorter(
|
||||
frameworkSnippet.files as unknown as FrameworkFile[],
|
||||
) as unknown as File[];
|
||||
}
|
||||
const playgroundURL = await generatePlaygroundURL(
|
||||
frameworkId,
|
||||
frameworkSnippet.files,
|
||||
title,
|
||||
);
|
||||
|
||||
if (playgroundURL) {
|
||||
frameworkSnippet.playgroundURL = playgroundURL;
|
||||
}
|
||||
|
||||
// Remove content key, not used anymore
|
||||
frameworkSnippet.files = frameworkSnippet.files.map((file) => ({
|
||||
fileName: file.fileName,
|
||||
ext: file.ext,
|
||||
contentHtml: file.contentHtml,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!byFrameworkId[frameworkId]) {
|
||||
byFrameworkId[frameworkId] = [];
|
||||
}
|
||||
|
||||
byFrameworkId[frameworkId].push(frameworkSnippet);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const treeFilePath = path.join(generatedContentDirPath, "tree.js");
|
||||
const treeDtsFilePath = path.join(generatedContentDirPath, "tree.d.ts");
|
||||
const frameworkIndexPath = path.join(frameworkDirPath, "index.js");
|
||||
const frameworkIndexDtsPath = path.join(frameworkDirPath, "index.d.ts");
|
||||
const commentDisclaimer = `// File generated from "node scripts/generateContent.js", DO NOT EDIT/COMMIT`;
|
||||
|
||||
if (!(await pathExists(generatedContentDirPath))) {
|
||||
await fs.mkdir(generatedContentDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await writeJsFile(
|
||||
treeFilePath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export const sections = ${JSON.stringify(treePayload.sections, null, 2)};
|
||||
export const snippets = ${JSON.stringify(treePayload.snippets, null, 2)};
|
||||
`,
|
||||
);
|
||||
|
||||
await writeDtsFile(
|
||||
treeDtsFilePath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export interface Section {
|
||||
sectionId: string;
|
||||
sectionDirName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Snippet {
|
||||
sectionId: string;
|
||||
snippetId: string;
|
||||
snippetDirName: string;
|
||||
sectionDirName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export declare const sections: Section[];
|
||||
export declare const snippets: Snippet[];
|
||||
`,
|
||||
);
|
||||
|
||||
if (!(await pathExists(frameworkDirPath))) {
|
||||
await fs.mkdir(frameworkDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(byFrameworkId).map((frameworkId) => {
|
||||
const frameworkFilePath = path.join(
|
||||
frameworkDirPath,
|
||||
`${frameworkId}.js`,
|
||||
);
|
||||
return writeJsFile(
|
||||
frameworkFilePath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export default ${JSON.stringify(byFrameworkId[frameworkId], null, 2)}
|
||||
`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await writeJsFile(
|
||||
frameworkIndexPath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
export default {
|
||||
${Object.keys(byFrameworkId)
|
||||
.map(
|
||||
(frameworkId) =>
|
||||
`${frameworkId}: () => import("./${frameworkId}.js")`,
|
||||
)
|
||||
.join(",\n")}
|
||||
|
||||
};
|
||||
`,
|
||||
);
|
||||
|
||||
await writeDtsFile(
|
||||
frameworkIndexDtsPath,
|
||||
`
|
||||
${commentDisclaimer}
|
||||
declare const snippetsImporterByFrameworkId: {
|
||||
[key: string]: () => Promise<any>;
|
||||
};
|
||||
|
||||
export default snippetsImporterByFrameworkId;
|
||||
`,
|
||||
);
|
||||
|
||||
// Generate _redirects file for Cloudflare Pages
|
||||
await generateRedirectsFile(rootDir);
|
||||
}
|
||||
|
||||
function dirNameToTitle(dirName: string): string {
|
||||
return capitalize(dirName.split("-").splice(1).join(" "));
|
||||
}
|
||||
|
||||
function capitalize(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
async function writeJsFile(filepath: string, jsCode: string): Promise<void> {
|
||||
const codeFormatted = await prettier.format(jsCode, { parser: "babel" });
|
||||
await fs.writeFile(filepath, codeFormatted);
|
||||
}
|
||||
|
||||
async function writeDtsFile(filepath: string, dtsCode: string): Promise<void> {
|
||||
const codeFormatted = await prettier.format(dtsCode, {
|
||||
parser: "typescript",
|
||||
});
|
||||
await fs.writeFile(filepath, codeFormatted);
|
||||
}
|
||||
|
||||
async function generatePlaygroundURL(
|
||||
frameworkId: string,
|
||||
files: File[],
|
||||
title: string,
|
||||
): Promise<string | undefined> {
|
||||
const frameworkIdPlayground = playgroundUrlByFramework[frameworkId];
|
||||
if (!frameworkIdPlayground) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameworkConfig = frameworks.find((f) => f.id === frameworkId);
|
||||
if (!frameworkConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentByFilename = frameworkConfig
|
||||
.filesSorter(files as unknown as FrameworkFile[])
|
||||
.reduce((acc: Record<string, string>, file) => {
|
||||
if ((file as { content?: string }).content) {
|
||||
acc[(file as { fileName: string }).fileName] = (
|
||||
file as unknown as { content: string }
|
||||
).content;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const playgroundURL = await frameworkIdPlayground(contentByFilename, title);
|
||||
|
||||
return playgroundURL;
|
||||
}
|
||||
|
||||
async function generateRedirectsFile(rootDir: string): Promise<void> {
|
||||
// Generate all possible framework combinations
|
||||
const frameworkIds = frameworks.map((f) => f.id);
|
||||
const redirects: string[] = [];
|
||||
|
||||
// Generate redirects for all framework pairs (both directions)
|
||||
for (let i = 0; i < frameworkIds.length; i++) {
|
||||
for (let j = 0; j < frameworkIds.length; j++) {
|
||||
if (i !== j) {
|
||||
const framework1 = frameworkIds[i];
|
||||
const framework2 = frameworkIds[j];
|
||||
const redirectPath = `/compare/${framework1}-vs-${framework2}`;
|
||||
const targetUrl = `/?f=${framework1}-${framework2}`;
|
||||
redirects.push(`${redirectPath} ${targetUrl} 301`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dynamic redirects for comma-separated patterns using placeholders
|
||||
const dynamicRedirects = [
|
||||
// Single framework redirects (static for better performance)
|
||||
...frameworkIds.map((id) => `/?f=${id} / 301`),
|
||||
// Dynamic redirects for comma-separated patterns - matches any comma-separated values
|
||||
// This will catch patterns like /?f=react,vue3,angular or /?f=mithril,alpine,etc
|
||||
"/?f=:frameworks / 301",
|
||||
];
|
||||
|
||||
redirects.push(...dynamicRedirects);
|
||||
|
||||
// Add specific compare patterns that don't match our framework pairs
|
||||
const specificCompareRedirects = ["/compare/emberPolaris-vs-angular / 301"];
|
||||
redirects.push(...specificCompareRedirects);
|
||||
|
||||
const redirectsContent = `# File generated from "node scripts/generateContent.js", DO NOT EDIT/COMMIT
|
||||
${redirects.join("\n")}
|
||||
`;
|
||||
|
||||
const publicDir = path.join(rootDir, "public");
|
||||
const redirectsFilePath = path.join(publicDir, "_redirects");
|
||||
|
||||
await fs.writeFile(redirectsFilePath, redirectsContent);
|
||||
console.info(
|
||||
`Generated _redirects file for Cloudflare Pages with ${redirects.length} redirects`,
|
||||
);
|
||||
}
|
||||
8
build/lib/highlighter.d.ts
vendored
Normal file
8
build/lib/highlighter.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export function codeToHighlightCodeHtml(
|
||||
code: string,
|
||||
language: string,
|
||||
): Promise<string>;
|
||||
|
||||
export function markdownToHighlightedHtml(markdown: string): Promise<string>;
|
||||
|
||||
export function disposeHighlighter(): Promise<void>;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { createHighlighter } from "shiki";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import Shiki from "@shikijs/markdown-it";
|
||||
import componentPartyShikiTheme from "./componentPartyShikiTheme.js";
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
theme: componentPartyShikiTheme,
|
||||
langs: [
|
||||
"javascript",
|
||||
"svelte",
|
||||
"html",
|
||||
"hbs",
|
||||
"gjs",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"vue",
|
||||
"marko",
|
||||
],
|
||||
langAlias: {
|
||||
ripple: "jsx", // until ripple is supported by shiki
|
||||
},
|
||||
});
|
||||
|
||||
const md = MarkdownIt({
|
||||
html: true,
|
||||
});
|
||||
|
||||
md.use(
|
||||
await Shiki({
|
||||
theme: componentPartyShikiTheme,
|
||||
})
|
||||
);
|
||||
|
||||
export async function codeToHighlightCodeHtml(code, lang) {
|
||||
const html = await highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
theme: componentPartyShikiTheme,
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export async function markdownToHighlightedHtml(markdownText) {
|
||||
const html = md.render(markdownText);
|
||||
return html;
|
||||
}
|
||||
94
build/lib/highlighter.ts
Normal file
94
build/lib/highlighter.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
createHighlighter,
|
||||
type HighlighterGeneric,
|
||||
type BundledLanguage,
|
||||
type BundledTheme,
|
||||
} from "shiki";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { fromHighlighter } from "@shikijs/markdown-it/core";
|
||||
import { componentPartyShikiTheme } from "./componentPartyShikiTheme.ts";
|
||||
|
||||
// Singleton instances
|
||||
let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
|
||||
null;
|
||||
let md: MarkdownIt | null = null;
|
||||
let highlighterPromise: Promise<
|
||||
HighlighterGeneric<BundledLanguage, BundledTheme>
|
||||
> | null = null;
|
||||
|
||||
async function getHighlighter(): Promise<
|
||||
HighlighterGeneric<BundledLanguage, BundledTheme>
|
||||
> {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighter({
|
||||
themes: [componentPartyShikiTheme],
|
||||
langs: [
|
||||
"javascript",
|
||||
"svelte",
|
||||
"html",
|
||||
"hbs",
|
||||
"gjs",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"vue",
|
||||
"marko",
|
||||
],
|
||||
langAlias: {
|
||||
ripple: "jsx", // until ripple is supported by shiki
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!highlighter) {
|
||||
highlighter = await highlighterPromise;
|
||||
}
|
||||
|
||||
return highlighter;
|
||||
}
|
||||
|
||||
async function getMarkdownIt(): Promise<MarkdownIt> {
|
||||
if (!md) {
|
||||
md = MarkdownIt({
|
||||
html: true,
|
||||
});
|
||||
|
||||
const highlighterInstance = await getHighlighter();
|
||||
md.use(
|
||||
fromHighlighter(highlighterInstance, {
|
||||
theme: componentPartyShikiTheme,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
export async function codeToHighlightCodeHtml(
|
||||
code: string,
|
||||
lang: string,
|
||||
): Promise<string> {
|
||||
const highlighterInstance = await getHighlighter();
|
||||
const html = await highlighterInstance.codeToHtml(code, {
|
||||
lang,
|
||||
theme: componentPartyShikiTheme.name,
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export async function markdownToHighlightedHtml(
|
||||
markdownText: string,
|
||||
): Promise<string> {
|
||||
const mdInstance = await getMarkdownIt();
|
||||
const html = mdInstance.render(markdownText);
|
||||
return html;
|
||||
}
|
||||
|
||||
// Function to dispose of instances when no longer needed
|
||||
export async function disposeHighlighter(): Promise<void> {
|
||||
if (highlighter) {
|
||||
await highlighter.dispose();
|
||||
highlighter = null;
|
||||
}
|
||||
highlighterPromise = null;
|
||||
md = null;
|
||||
}
|
||||
@@ -1,16 +1,49 @@
|
||||
import nodePath from "node:path";
|
||||
import { compressToURL } from "@matschik/lz-string";
|
||||
import { getParameters } from "codesandbox/lib/api/define.js";
|
||||
import LZString from "lz-string";
|
||||
|
||||
export default {
|
||||
vue3: (contentByFilename) => {
|
||||
interface PlaygroundFunction {
|
||||
(
|
||||
contentByFilename: Record<string, string>,
|
||||
title?: string,
|
||||
): string | Promise<string | undefined>;
|
||||
}
|
||||
|
||||
interface SveltePlaygroundOptions {
|
||||
version: number;
|
||||
contentByFilename: Record<string, string>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface File {
|
||||
name: string;
|
||||
basename: string;
|
||||
contents: string;
|
||||
text: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface PlaygroundData {
|
||||
title: string;
|
||||
files: File[];
|
||||
}
|
||||
|
||||
// Replacement for codesandbox's getParameters function
|
||||
function getParameters(parameters: unknown): string {
|
||||
return LZString.compressToBase64(JSON.stringify(parameters))
|
||||
.replace(/\+/g, "-") // Convert '+' to '-'
|
||||
.replace(/\//g, "_") // Convert '/' to '_'
|
||||
.replace(/=+$/, ""); // Remove ending '='
|
||||
}
|
||||
|
||||
const playgroundUrlByFramework: Record<string, PlaygroundFunction> = {
|
||||
vue3: (contentByFilename: Record<string, string>) => {
|
||||
const BASE_URL = "https://sfc.vuejs.org/#";
|
||||
|
||||
function utoa(data) {
|
||||
function utoa(data: string): string {
|
||||
return btoa(unescape(encodeURIComponent(data)));
|
||||
}
|
||||
|
||||
function generateURLFromData(data) {
|
||||
function generateURLFromData(data: unknown): string {
|
||||
return `${BASE_URL}${utoa(JSON.stringify(data))}`;
|
||||
}
|
||||
const data = Object.assign({}, contentByFilename, {
|
||||
@@ -21,21 +54,27 @@ export default {
|
||||
const url = generateURLFromData(data);
|
||||
return url;
|
||||
},
|
||||
svelte4: async (contentByFilename, title) => {
|
||||
svelte4: async (
|
||||
contentByFilename: Record<string, string>,
|
||||
title?: string,
|
||||
) => {
|
||||
return generateSveltePlaygroundURL({
|
||||
version: 4,
|
||||
contentByFilename,
|
||||
title,
|
||||
});
|
||||
},
|
||||
svelte5: async (contentByFilename, title) => {
|
||||
svelte5: async (
|
||||
contentByFilename: Record<string, string>,
|
||||
title?: string,
|
||||
) => {
|
||||
return generateSveltePlaygroundURL({
|
||||
version: 5,
|
||||
contentByFilename,
|
||||
title,
|
||||
});
|
||||
},
|
||||
alpine: (contentByFilename) => {
|
||||
alpine: (contentByFilename: Record<string, string>) => {
|
||||
const BASE_URL =
|
||||
"https://codesandbox.io/api/v1/sandboxes/define?embed=1¶meters=";
|
||||
const BASE_PREFIX = `<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <meta http-equiv="X-UA-Compatible" content="ie=edge" />\n <title>Alpine.js Playground</title>\n <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>\n </head>\n <body>\n\n`;
|
||||
@@ -49,9 +88,7 @@ export default {
|
||||
},
|
||||
"index.html": {
|
||||
content:
|
||||
BASE_PREFIX +
|
||||
(contentByFilename["index.html"]?.content || "") +
|
||||
BASE_SUFFIX,
|
||||
BASE_PREFIX + (contentByFilename["index.html"] || "") + BASE_SUFFIX,
|
||||
},
|
||||
"sandbox.config.json": {
|
||||
content: '{\n "template": "static"\n}',
|
||||
@@ -61,14 +98,14 @@ export default {
|
||||
|
||||
return `${BASE_URL}${parameters}`;
|
||||
},
|
||||
solid: (contentByFilename) => {
|
||||
solid: (contentByFilename: Record<string, string>) => {
|
||||
const BASE_URL = "https://playground.solidjs.com/#";
|
||||
const SOURCE_PREFIX = `import { render } from "solid-js/web";\n`;
|
||||
const getSourceSuffix = (componentName) =>
|
||||
const getSourceSuffix = (componentName: string) =>
|
||||
`\n\nrender(() => <${componentName} />, document.getElementById("app"));\n`;
|
||||
|
||||
function generateURLFromData(data) {
|
||||
return `${BASE_URL}${compressToURL(JSON.stringify(data))}`;
|
||||
function generateURLFromData(data: unknown): string {
|
||||
return `${BASE_URL}${LZString.compressToEncodedURIComponent(JSON.stringify(data))}`;
|
||||
}
|
||||
|
||||
const data = Object.keys(contentByFilename).map((filename) => {
|
||||
@@ -92,10 +129,10 @@ export default {
|
||||
|
||||
return generateURLFromData(data);
|
||||
},
|
||||
marko: async (contentByFilename) => {
|
||||
marko: async (contentByFilename: Record<string, string>) => {
|
||||
let firstFile = true;
|
||||
const data = Object.entries(contentByFilename).map(([path, content]) => ({
|
||||
path: firstFile ? (firstFile = false) || "index.marko" : path,
|
||||
path: firstFile ? ((firstFile = false), "index.marko") : path,
|
||||
content,
|
||||
}));
|
||||
|
||||
@@ -106,11 +143,13 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
export default playgroundUrlByFramework;
|
||||
|
||||
async function generateSveltePlaygroundURL({
|
||||
version,
|
||||
contentByFilename,
|
||||
title,
|
||||
}) {
|
||||
}: SveltePlaygroundOptions): Promise<string | undefined> {
|
||||
const BASE_URL = `https://svelte.dev/playground/untitled?version=${version}#`;
|
||||
|
||||
const filenames = Object.keys(contentByFilename);
|
||||
@@ -118,7 +157,7 @@ async function generateSveltePlaygroundURL({
|
||||
return;
|
||||
}
|
||||
|
||||
const files = filenames.map((filename, index) => {
|
||||
const files: File[] = filenames.map((filename, index) => {
|
||||
const contents = contentByFilename[filename];
|
||||
const name = index === 0 ? "App.svelte" : nodePath.parse(filename).base;
|
||||
return {
|
||||
@@ -130,9 +169,12 @@ async function generateSveltePlaygroundURL({
|
||||
};
|
||||
});
|
||||
|
||||
const payload = { title, files };
|
||||
const payload: PlaygroundData = { title: title || "", files };
|
||||
|
||||
const hash = await compress_and_encode_text(JSON.stringify(payload));
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}${hash}`;
|
||||
|
||||
@@ -140,7 +182,7 @@ async function generateSveltePlaygroundURL({
|
||||
}
|
||||
|
||||
// method `compress_and_encode_text` from https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/src/routes/(authed)/playground/%5Bid%5D/gzip.js
|
||||
async function compress_and_encode_text(input) {
|
||||
async function compress_and_encode_text(input: string): Promise<string> {
|
||||
const reader = new Blob([input])
|
||||
.stream()
|
||||
.pipeThrough(new CompressionStream("gzip"))
|
||||
@@ -161,7 +203,7 @@ async function compress_and_encode_text(input) {
|
||||
}
|
||||
|
||||
// method `compress` from https://github.com/marko-js/website/blob/main/src/util/hasher.ts#L8-L25
|
||||
export async function markoCompress(value) {
|
||||
export async function markoCompress(value: string): Promise<string> {
|
||||
const stream = new CompressionStream("gzip");
|
||||
const writer = stream.writable.getWriter();
|
||||
writer.write(new TextEncoder().encode(value));
|
||||
@@ -7,7 +7,7 @@
|
||||
<img src="/popper.svg" alt="logo" class="size-7" />
|
||||
<span>Component party</span>
|
||||
</a>
|
||||
<p class="text-sm leading-6 text-gray-300">
|
||||
<p class="text-gray-300">
|
||||
Web component JavaScript frameworks overview by their syntax and
|
||||
features
|
||||
</p>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function CssStyle() {
|
||||
m(
|
||||
"div",
|
||||
m("h1.title", "I am red"),
|
||||
m("button", { style: { fontSize: "10rem" } }, "I am a button")
|
||||
m("button", { style: { fontSize: "10rem" } }, "I am a button"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class ColorsList extends LitElement {
|
||||
${repeat(
|
||||
this.colors,
|
||||
(color) => color,
|
||||
(color) => html`<li>${color}</li>`
|
||||
(color) => html`<li>${color}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function Colors() {
|
||||
view: () =>
|
||||
m(
|
||||
"ul",
|
||||
colors.map((color, idx) => m("li", { key: idx }, color))
|
||||
colors.map((color, idx) => m("li", { key: idx }, color)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
>
|
||||
<li v-for="color in colors" :key="color">
|
||||
{{ color }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -4,10 +4,7 @@ const colors = ["red", "green", "blue"];
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
>
|
||||
<li v-for="color in colors" :key="color">
|
||||
{{ color }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function Counter() {
|
||||
m(
|
||||
"div",
|
||||
m("p", `Counter: ${count}`),
|
||||
m("button", { onclick: incrementCount }, "+1")
|
||||
m("button", { onclick: incrementCount }, "+1"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterNextRender, Component, ElementRef, viewChild } from "@angular/core";
|
||||
import {
|
||||
afterNextRender,
|
||||
Component,
|
||||
ElementRef,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-input-focused",
|
||||
|
||||
@@ -7,5 +7,5 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input ref="inputElement">
|
||||
<input ref="inputElement" />
|
||||
</template>
|
||||
|
||||
@@ -9,5 +9,5 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input ref="inputElement">
|
||||
<input ref="inputElement" />
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function TrafficLight() {
|
||||
"div",
|
||||
m("button", { onclick: nextLight }, "Next light"),
|
||||
m("p", `Light is: ${currentLight()}`),
|
||||
m("p", "You must ", m("span", instructions()))
|
||||
m("p", "You must ", m("span", instructions())),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class TimeComponent implements OnDestroy {
|
||||
|
||||
timer = setInterval(
|
||||
() => this.time.set(new Date().toLocaleTimeString()),
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -7,6 +7,6 @@ export const UserProfile = () => ({
|
||||
m("p", `My name is ${name}!`),
|
||||
m("p", `My age is ${age}!`),
|
||||
m("p", `My favourite colors are ${favouriteColors.join(", ")}!`),
|
||||
m("p", `I am ${isAvailable ? "available" : "not available"}`)
|
||||
m("p", `I am ${isAvailable ? "available" : "not available"}`),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mergeProps } from "solid-js";
|
||||
export default function UserProfile(props) {
|
||||
const merged = mergeProps(
|
||||
{ name: "", age: null, favouriteColors: [], isAvailable: false },
|
||||
props
|
||||
props,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,6 @@ export const AnswerButton = ({ attrs: { onYes, onNo } }) => ({
|
||||
m(
|
||||
"div",
|
||||
m("button", { onclick: onYes }, "YES"),
|
||||
m("button", { onclick: onNo }, "NO")
|
||||
m("button", { onclick: onNo }, "NO"),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function App() {
|
||||
"",
|
||||
m("p", "Are you happy?"),
|
||||
m("p", { style: { fontSize: 50 } }, isHappy ? "😀" : "😥"),
|
||||
m(AnswerButton, { onYes: onAnswerYes, onNo: onAnswerNo })
|
||||
m(AnswerButton, { onYes: onAnswerYes, onNo: onAnswerNo }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<p>Are you happy?</p>
|
||||
<AnswerButton
|
||||
@yes="onAnswerYes"
|
||||
@no="onAnswerNo"
|
||||
/>
|
||||
<AnswerButton @yes="onAnswerYes" @no="onAnswerNo" />
|
||||
<p style="font-size: 50px">
|
||||
{{ isHappy ? "😀" : "😥" }}
|
||||
</p>
|
||||
|
||||
@@ -15,10 +15,7 @@ function onAnswerYes() {
|
||||
|
||||
<template>
|
||||
<p>Are you happy?</p>
|
||||
<AnswerButton
|
||||
@yes="onAnswerYes"
|
||||
@no="onAnswerNo"
|
||||
/>
|
||||
<AnswerButton @yes="onAnswerYes" @no="onAnswerNo" />
|
||||
<p style="font-size: 50px">
|
||||
{{ isHappy ? "😀" : "😥" }}
|
||||
</p>
|
||||
|
||||
@@ -18,6 +18,6 @@ export const FunnyButton = () => ({
|
||||
outline: "0",
|
||||
},
|
||||
},
|
||||
m(children)
|
||||
m(children),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -17,6 +17,6 @@ export const FunnyButton = ({ children }) => ({
|
||||
outline: "0",
|
||||
},
|
||||
},
|
||||
children || m("span", "No content found")
|
||||
children || m("span", "No content found"),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function App() {
|
||||
m(
|
||||
"",
|
||||
m("h1", `Welcome Back, ${user.username}`),
|
||||
m(UserProfile, { user, updateUsername })
|
||||
m(UserProfile, { user, updateUsername }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ export default function UserProfile() {
|
||||
m(
|
||||
"button",
|
||||
{ onclick: () => updateUsername("Jane") },
|
||||
"Update username to Jane"
|
||||
)
|
||||
"Update username to Jane",
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ text }}</p>
|
||||
<input v-model="text">
|
||||
<input v-model="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,5 +5,5 @@ const text = ref("Hello World");
|
||||
|
||||
<template>
|
||||
<p>{{ text }}</p>
|
||||
<input v-model="text">
|
||||
<input v-model="text" />
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function IsAvailable() {
|
||||
checked: isAvailable,
|
||||
onchange: onUpdate,
|
||||
}),
|
||||
m("label", { for: "is-available" }, "Is available")
|
||||
m("label", { for: "is-available" }, "Is available"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,11 +10,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
id="is-available"
|
||||
v-model="isAvailable"
|
||||
type="checkbox"
|
||||
>
|
||||
<input id="is-available" v-model="isAvailable" type="checkbox" />
|
||||
<label for="is-available">Is available</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,10 +5,6 @@ const isAvailable = ref(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
id="is-available"
|
||||
v-model="isAvailable"
|
||||
type="checkbox"
|
||||
>
|
||||
<input id="is-available" v-model="isAvailable" type="checkbox" />
|
||||
<label for="is-available">Is available</label>
|
||||
</template>
|
||||
|
||||
@@ -20,9 +20,9 @@ export default function PickPill() {
|
||||
value: pill,
|
||||
onchange: handleChange,
|
||||
}),
|
||||
m("label", { for: pill }, pill)
|
||||
)
|
||||
)
|
||||
m("label", { for: pill }, pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,20 +12,10 @@ export default {
|
||||
<div>
|
||||
<div>Picked: {{ picked }}</div>
|
||||
|
||||
<input
|
||||
id="blue-pill"
|
||||
v-model="picked"
|
||||
type="radio"
|
||||
value="blue"
|
||||
>
|
||||
<input id="blue-pill" v-model="picked" type="radio" value="blue" />
|
||||
<label for="blue-pill">Blue pill</label>
|
||||
|
||||
<input
|
||||
id="red-pill"
|
||||
v-model="picked"
|
||||
type="radio"
|
||||
value="red"
|
||||
>
|
||||
<input id="red-pill" v-model="picked" type="radio" value="red" />
|
||||
<label for="red-pill">Red pill</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,19 +7,9 @@ const picked = ref("red");
|
||||
<template>
|
||||
<div>Picked: {{ picked }}</div>
|
||||
|
||||
<input
|
||||
id="blue-pill"
|
||||
v-model="picked"
|
||||
type="radio"
|
||||
value="blue"
|
||||
>
|
||||
<input id="blue-pill" v-model="picked" type="radio" value="blue" />
|
||||
<label for="blue-pill">Blue pill</label>
|
||||
|
||||
<input
|
||||
id="red-pill"
|
||||
v-model="picked"
|
||||
type="radio"
|
||||
value="red"
|
||||
>
|
||||
<input id="red-pill" v-model="picked" type="radio" value="red" />
|
||||
<label for="red-pill">Red pill</label>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FormsModule } from "@angular/forms";
|
||||
selector: "app-color-select",
|
||||
template: `
|
||||
<select [(ngModel)]="selectedColorId">
|
||||
@for (let color of colors; track: color) {
|
||||
@for (let color of colors; track color) {
|
||||
<option [value]="color.id" [disabled]="color.isDisabled">
|
||||
{{ color.text }}
|
||||
</option>
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ColorSelect extends LitElement {
|
||||
?disabled=${color.isDisabled}
|
||||
>
|
||||
${color.text}
|
||||
</option>`
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
`;
|
||||
|
||||
@@ -17,8 +17,8 @@ export default function ColorSelect() {
|
||||
"select",
|
||||
{ value: selectedColorId, onchange: handleSelect },
|
||||
colors.map(({ id, text, isDisabled }) =>
|
||||
m("option", { key: id, id, disabled: isDisabled, value: id }, text)
|
||||
)
|
||||
m("option", { key: id, id, disabled: isDisabled, value: id }, text),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ import App from "./App";
|
||||
ReactDOM.createRoot(document.getElementById("app")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { UseFetchUsers } from "./UseFetchUsers";
|
||||
|
||||
export class App {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
constructor(private useFetchUsers: UseFetchUsers) {}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class XApp extends LitElement {
|
||||
<img src=${user.picture.thumbnail} alt="user" />
|
||||
<p>${user.name.first} ${user.name.last}</p>
|
||||
</li>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</ul>
|
||||
`,
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function App() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const { results } = await m.request(
|
||||
"https://randomuser.me/api/?results=3"
|
||||
"https://randomuser.me/api/?results=3",
|
||||
);
|
||||
users = results;
|
||||
} catch (err) {
|
||||
@@ -28,8 +28,8 @@ export default function App() {
|
||||
"li",
|
||||
{ key: user.login.uuid },
|
||||
m("img", { src: user.picture.thumbnail, alt: "user" }),
|
||||
m("p", `${user.name.first} ${user.name.last}`)
|
||||
)
|
||||
m("p", `${user.name.first} ${user.name.last}`),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,14 +33,8 @@ export default {
|
||||
<p v-if="isLoading">Fetching users...</p>
|
||||
<p v-else-if="error">An error ocurred while fetching users</p>
|
||||
<ul v-else-if="users">
|
||||
<li
|
||||
v-for="user in users"
|
||||
:key="user.login.uuid"
|
||||
>
|
||||
<img
|
||||
:src="user.picture.thumbnail"
|
||||
alt="user"
|
||||
>
|
||||
<li v-for="user in users" :key="user.login.uuid">
|
||||
<img :src="user.picture.thumbnail" alt="user" />
|
||||
<p>
|
||||
{{ user.name.first }}
|
||||
{{ user.name.last }}
|
||||
|
||||
@@ -8,14 +8,8 @@ const { isLoading, error, data: users } = useFetchUsers();
|
||||
<p v-if="isLoading">Fetching users...</p>
|
||||
<p v-else-if="error">An error ocurred while fetching users</p>
|
||||
<ul v-else-if="users">
|
||||
<li
|
||||
v-for="user in users"
|
||||
:key="user.login.uuid"
|
||||
>
|
||||
<img
|
||||
:src="user.picture.thumbnail"
|
||||
alt="user"
|
||||
>
|
||||
<li v-for="user in users" :key="user.login.uuid">
|
||||
<img :src="user.picture.thumbnail" alt="user" />
|
||||
<p>
|
||||
{{ user.name.first }}
|
||||
{{ user.name.last }}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: "http://localhost:5173",
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "svelte",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
describe("Initial frameworks", () => {
|
||||
it("initial frameworks from scratch", () => {
|
||||
cy.visit("/");
|
||||
cy.get("[data-framework-id-selected-list]").should(
|
||||
"have.attr",
|
||||
"data-framework-id-selected-list",
|
||||
"react,svelte5"
|
||||
);
|
||||
});
|
||||
|
||||
it("initial frameworks from local storage", () => {
|
||||
cy.visit("/", {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem("framework_display", '["vue3","svelte4"]');
|
||||
},
|
||||
});
|
||||
cy.get("[data-framework-id-selected-list]").should(
|
||||
"have.attr",
|
||||
"data-framework-id-selected-list",
|
||||
"vue3,svelte4"
|
||||
);
|
||||
});
|
||||
|
||||
it("initial frameworks from query param 'f'", () => {
|
||||
cy.visit("/?f=react,vue3");
|
||||
cy.get("[data-framework-id-selected-list]").should(
|
||||
"have.attr",
|
||||
"data-framework-id-selected-list",
|
||||
"react,vue3"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pages", () => {
|
||||
it("compare page", () => {
|
||||
cy.visit("/compare/vue2-vs-vue3");
|
||||
cy.get("[data-framework-id-selected-list]").should(
|
||||
"have.attr",
|
||||
"data-framework-id-selected-list",
|
||||
"vue2,vue3"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// describe("sidenav links", () => {
|
||||
// it("clicking sidenav links", () => {
|
||||
// cy.visit("/?f=react");
|
||||
// cy.get("main a[href='#reactivity']").click();
|
||||
// cy.get("[data-framework-id-selected-list]").should(
|
||||
// "have.attr",
|
||||
// "data-framework-id-selected-list",
|
||||
// "react"
|
||||
// );
|
||||
// });
|
||||
// })
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
45
eslint.config.ts
Normal file
45
eslint.config.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import prettier from "eslint-config-prettier";
|
||||
import js from "@eslint/js";
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import globals from "globals";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript-eslint";
|
||||
import svelteConfig from "./svelte.config";
|
||||
import oxlint from "eslint-plugin-oxlint";
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["content/**"],
|
||||
},
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs["flat/prettier"],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
|
||||
ignores: ["eslint.config.js", "svelte.config.js"],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: [".svelte"],
|
||||
parser: ts.parser,
|
||||
svelteConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
...oxlint.buildFromOxlintConfigFile("./.oxlintrc.json"),
|
||||
];
|
||||
@@ -1,128 +1,89 @@
|
||||
function sortAllFilenames(files, filenamesSorted) {
|
||||
interface File {
|
||||
fileName: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function sortAllFilenames(files: File[], filenamesSorted: string[]): File[] {
|
||||
return [
|
||||
...filenamesSorted.map((filename) =>
|
||||
files.find(({ fileName }) => fileName === filename)
|
||||
files.find(({ fileName }) => fileName === filename),
|
||||
),
|
||||
...(files.filter(({ fileName }) => !filenamesSorted.includes(fileName)) ||
|
||||
[]),
|
||||
].filter(Boolean);
|
||||
].filter(Boolean) as File[];
|
||||
}
|
||||
|
||||
const frameworks = [
|
||||
export interface Framework {
|
||||
id: string;
|
||||
title: string;
|
||||
frameworkName: string;
|
||||
frameworkNameId: string;
|
||||
isLatestStable: boolean;
|
||||
img: string;
|
||||
playgroundURL: string;
|
||||
documentationURL: string;
|
||||
filesSorter: (files: File[]) => File[];
|
||||
repositoryLink: string;
|
||||
mainPackageName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
export const frameworks: Framework[] = [
|
||||
{
|
||||
id: "svelte5",
|
||||
title: "Svelte 5",
|
||||
frameworkName: "Svelte",
|
||||
isCurrentVersion: false,
|
||||
frameworkNameId: "svelte",
|
||||
isLatestStable: false,
|
||||
img: "framework/svelte.svg",
|
||||
eslint: {
|
||||
files: ["**/TODO-THIS-IS-DISABLED-svelte5/*.svelte"],
|
||||
parser: "svelte-eslint-parser",
|
||||
},
|
||||
playgroundURL: "https://svelte-5-preview.vercel.app/",
|
||||
documentationURL: "https://svelte-5-preview.vercel.app/docs",
|
||||
playgroundURL: "https://svelte.dev/playground",
|
||||
documentationURL: "https://svelte.dev",
|
||||
filesSorter(files) {
|
||||
return sortAllFilenames(files, ["index.html", "app.js", "App.svelte"]);
|
||||
},
|
||||
repositoryLink: "https://github.com/sveltejs/svelte",
|
||||
mainPackageName: "svelte",
|
||||
releaseDate: "2024-10-01",
|
||||
},
|
||||
{
|
||||
id: "react",
|
||||
title: "React",
|
||||
frameworkName: "React",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "react",
|
||||
isLatestStable: true,
|
||||
img: "framework/react.svg",
|
||||
eslint: {
|
||||
files: ["**/react/*.jsx", "**/react/*.tsx"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
playgroundURL: "https://codesandbox.io/s/mystifying-goldberg-6wx04b",
|
||||
documentationURL: "https://reactjs.org/docs/getting-started.html",
|
||||
documentationURL: "https://reactjs.org",
|
||||
filesSorter(files) {
|
||||
return sortAllFilenames(files, ["index.html", "main.jsx", "App.jsx"]);
|
||||
},
|
||||
repositoryLink: "https://github.com/facebook/react",
|
||||
mainPackageName: "react",
|
||||
releaseDate: "2013-05-29",
|
||||
},
|
||||
{
|
||||
id: "vue3",
|
||||
title: "Vue 3",
|
||||
frameworkName: "Vue",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "vue",
|
||||
isLatestStable: true,
|
||||
img: "framework/vue.svg",
|
||||
eslint: {
|
||||
files: ["**/vue3/*.vue"],
|
||||
env: {
|
||||
"vue/setup-compiler-macros": true,
|
||||
},
|
||||
extends: ["eslint:recommended", "plugin:vue/vue3-recommended"],
|
||||
rules: {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
},
|
||||
},
|
||||
playgroundURL: "https://sfc.vuejs.org",
|
||||
playgroundURL: "https://play.vuejs.org/",
|
||||
documentationURL: "https://vuejs.org/guide",
|
||||
filesSorter(files) {
|
||||
return sortAllFilenames(files, ["index.html", "main.js", "App.vue"]);
|
||||
},
|
||||
repositoryLink: "https://github.com/vuejs/core",
|
||||
mainPackageName: "vue",
|
||||
releaseDate: "2020-09-18",
|
||||
},
|
||||
{
|
||||
id: "angularRenaissance",
|
||||
title: "Angular Renaissance",
|
||||
frameworkName: "Angular",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "angular",
|
||||
isLatestStable: true,
|
||||
img: "framework/angular-renaissance.svg",
|
||||
eslint: [
|
||||
{
|
||||
files: ["**/angular-renaissance/**"],
|
||||
parserOptions: {
|
||||
project: ["tsconfig.app.json"],
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
// This is required if you use inline templates in Components
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
],
|
||||
rules: {
|
||||
/**
|
||||
* Any TypeScript source code (NOT TEMPLATE) related rules you wish to use/reconfigure over and above the
|
||||
* recommended set provided by the @angular-eslint project would go here.
|
||||
*/
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{ type: "attribute", prefix: "app", style: "camelCase" },
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{ type: "element", prefix: "app", style: "kebab-case" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/angular-renaissance/*.html"],
|
||||
extends: ["plugin:@angular-eslint/template/recommended"],
|
||||
rules: {
|
||||
/**
|
||||
* Any template/HTML related rules you wish to use/reconfigure over and above the
|
||||
* recommended set provided by the @angular-eslint project would go here.
|
||||
*/
|
||||
},
|
||||
},
|
||||
],
|
||||
playgroundURL: "https://codesandbox.io/s/angular",
|
||||
documentationURL: "https://angular.io/docs",
|
||||
filesSorter(files) {
|
||||
@@ -135,51 +96,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/angular/angular",
|
||||
mainPackageName: "@angular/core",
|
||||
releaseDate: "2024-11-01",
|
||||
},
|
||||
{
|
||||
id: "angular",
|
||||
title: "Angular",
|
||||
frameworkName: "Angular",
|
||||
isCurrentVersion: false,
|
||||
frameworkNameId: "angular",
|
||||
isLatestStable: false,
|
||||
img: "framework/angular.svg",
|
||||
eslint: [
|
||||
{
|
||||
files: ["**/angular/**"],
|
||||
parserOptions: {
|
||||
project: ["tsconfig.app.json"],
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
// This is required if you use inline templates in Components
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
],
|
||||
rules: {
|
||||
/**
|
||||
* Any TypeScript source code (NOT TEMPLATE) related rules you wish to use/reconfigure over and above the
|
||||
* recommended set provided by the @angular-eslint project would go here.
|
||||
*/
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{ type: "attribute", prefix: "app", style: "camelCase" },
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{ type: "element", prefix: "app", style: "kebab-case" },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/angular/*.html"],
|
||||
extends: ["plugin:@angular-eslint/template/recommended"],
|
||||
rules: {
|
||||
/**
|
||||
* Any template/HTML related rules you wish to use/reconfigure over and above the
|
||||
* recommended set provided by the @angular-eslint project would go here.
|
||||
*/
|
||||
},
|
||||
},
|
||||
],
|
||||
playgroundURL: "https://codesandbox.io/s/angular",
|
||||
documentationURL: "https://angular.io/docs",
|
||||
filesSorter(files) {
|
||||
@@ -192,19 +117,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/angular/angular",
|
||||
mainPackageName: "@angular/core",
|
||||
releaseDate: "2010-10-20",
|
||||
},
|
||||
{
|
||||
id: "lit",
|
||||
title: "Lit",
|
||||
frameworkName: "Lit",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "lit",
|
||||
isLatestStable: true,
|
||||
img: "framework/lit.svg",
|
||||
eslint: {
|
||||
files: ["**/lit/**"],
|
||||
plugins: ["lit"],
|
||||
parser: "@babel/eslint-parser",
|
||||
extends: ["plugin:lit/recommended"],
|
||||
},
|
||||
playgroundURL: "https://lit.dev/playground",
|
||||
documentationURL: "https://lit.dev",
|
||||
filesSorter(files) {
|
||||
@@ -212,19 +133,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/lit/lit",
|
||||
mainPackageName: "lit",
|
||||
releaseDate: "2021-05-27",
|
||||
},
|
||||
{
|
||||
id: "emberOctane",
|
||||
title: "Ember Octane",
|
||||
frameworkName: "Ember",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "ember",
|
||||
isLatestStable: true,
|
||||
img: "framework/ember.svg",
|
||||
eslint: {
|
||||
files: ["**/emberOctane/**"],
|
||||
plugins: ["ember"],
|
||||
parser: "@babel/eslint-parser",
|
||||
extends: ["plugin:ember/recommended"],
|
||||
},
|
||||
playgroundURL: "https://ember-twiddle.com",
|
||||
documentationURL: "https://emberjs.com",
|
||||
filesSorter(files) {
|
||||
@@ -232,18 +149,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/emberjs/ember.js",
|
||||
mainPackageName: "ember-source",
|
||||
releaseDate: "2019-12-01",
|
||||
},
|
||||
{
|
||||
id: "solid",
|
||||
title: "Solid.js",
|
||||
frameworkName: "Solid",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "solid",
|
||||
isLatestStable: true,
|
||||
img: "framework/solid.svg",
|
||||
eslint: {
|
||||
files: ["**/solid/*.jsx"],
|
||||
plugins: ["solid"],
|
||||
extends: ["eslint:recommended", "plugin:solid/recommended"],
|
||||
},
|
||||
playgroundURL: "https://playground.solidjs.com/",
|
||||
documentationURL: "https://www.solidjs.com/",
|
||||
filesSorter(files) {
|
||||
@@ -251,17 +165,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/solidjs/solid",
|
||||
mainPackageName: "solid-js",
|
||||
releaseDate: "2021-06-28",
|
||||
},
|
||||
{
|
||||
id: "svelte4",
|
||||
title: "Svelte 4",
|
||||
frameworkName: "Svelte",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "svelte",
|
||||
isLatestStable: true,
|
||||
img: "framework/svelte.svg",
|
||||
eslint: {
|
||||
files: ["**/svelte4/*.svelte"],
|
||||
parser: "svelte-eslint-parser",
|
||||
},
|
||||
playgroundURL: "https://svelte.dev/repl",
|
||||
documentationURL: "https://svelte.dev/",
|
||||
filesSorter(files) {
|
||||
@@ -269,21 +181,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/sveltejs/svelte",
|
||||
mainPackageName: "svelte",
|
||||
releaseDate: "2023-06-01",
|
||||
},
|
||||
{
|
||||
id: "vue2",
|
||||
title: "Vue 2",
|
||||
frameworkName: "Vue",
|
||||
isCurrentVersion: false,
|
||||
frameworkNameId: "vue",
|
||||
isLatestStable: false,
|
||||
img: "framework/vue.svg",
|
||||
eslint: {
|
||||
files: ["**/vue2/*.vue"],
|
||||
extends: ["eslint:recommended", "plugin:vue/recommended"],
|
||||
rules: {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
},
|
||||
},
|
||||
playgroundURL: "",
|
||||
documentationURL: "https://v2.vuejs.org",
|
||||
filesSorter(files) {
|
||||
@@ -291,17 +197,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/vuejs/vue",
|
||||
mainPackageName: "vue@^2",
|
||||
releaseDate: "2016-09-30",
|
||||
},
|
||||
{
|
||||
id: "alpine",
|
||||
title: "Alpine",
|
||||
frameworkName: "Alpine",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "alpine",
|
||||
isLatestStable: true,
|
||||
img: "framework/alpine.svg",
|
||||
eslint: {
|
||||
files: ["**/alpine/**"],
|
||||
extends: ["eslint:recommended"],
|
||||
},
|
||||
playgroundURL: "https://codesandbox.io/s/7br3q8",
|
||||
documentationURL: "https://alpinejs.dev/start-here",
|
||||
filesSorter(files) {
|
||||
@@ -309,23 +213,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/alpinejs/alpine",
|
||||
mainPackageName: "alpinejs",
|
||||
releaseDate: "2019-11-06",
|
||||
},
|
||||
{
|
||||
id: "emberPolaris",
|
||||
title: "Ember Polaris",
|
||||
frameworkName: "Ember",
|
||||
isCurrentVersion: false,
|
||||
frameworkNameId: "ember",
|
||||
isLatestStable: false,
|
||||
img: "framework/ember.svg",
|
||||
eslint: {
|
||||
files: ["**/emberPolaris/**"],
|
||||
plugins: ["ember"],
|
||||
parser: "ember-eslint-parser",
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:ember/recommended",
|
||||
"plugin:ember/recommended-gjs",
|
||||
],
|
||||
},
|
||||
playgroundURL: "http://new.emberjs.com",
|
||||
documentationURL: "https://emberjs.com",
|
||||
filesSorter(files) {
|
||||
@@ -333,22 +229,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/emberjs/ember.js",
|
||||
mainPackageName: "ember-source",
|
||||
releaseDate: "2024-12-01",
|
||||
},
|
||||
{
|
||||
id: "mithril",
|
||||
title: "Mithril",
|
||||
frameworkName: "Mithril",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "mithril",
|
||||
isLatestStable: true,
|
||||
img: "framework/mithril.svg",
|
||||
eslint: {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
files: ["**/mithril/**"],
|
||||
extends: ["eslint:recommended"],
|
||||
},
|
||||
playgroundURL: "https://codesandbox.io/s/q99qzov66",
|
||||
documentationURL: "https://mithril.js.org/",
|
||||
filesSorter(files) {
|
||||
@@ -356,28 +245,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/MithrilJS/mithril.js",
|
||||
mainPackageName: "mithril",
|
||||
releaseDate: "2014-03-07",
|
||||
},
|
||||
{
|
||||
id: "aurelia2",
|
||||
title: "Aurelia 2",
|
||||
frameworkName: "Aurelia",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "aurelia",
|
||||
isLatestStable: true,
|
||||
img: "framework/aurelia.svg",
|
||||
eslint: {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
files: ["**/aurelia2/**"],
|
||||
extends: ["eslint:recommended"],
|
||||
},
|
||||
playgroundURL:
|
||||
"https://stackblitz.com/edit/au2-conventions?file=src%2Fmy-app.html",
|
||||
documentationURL: "http://docs.aurelia.io",
|
||||
@@ -391,31 +267,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/aurelia/aurelia",
|
||||
mainPackageName: "aurelia",
|
||||
releaseDate: "2021-01-19",
|
||||
},
|
||||
{
|
||||
id: "qwik",
|
||||
title: "Qwik",
|
||||
frameworkName: "Qwik",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "qwik",
|
||||
isLatestStable: true,
|
||||
img: "framework/qwik.svg",
|
||||
eslint: {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
files: ["**/qwik/**"],
|
||||
extends: ["eslint:recommended", "plugin:qwik/recommended"],
|
||||
rules: {
|
||||
"qwik/valid-lexical-scope": "off",
|
||||
},
|
||||
},
|
||||
playgroundURL: "https://qwik.builder.io/playground",
|
||||
documentationURL: "https://qwik.builder.io/docs/overview",
|
||||
filesSorter(files) {
|
||||
@@ -423,16 +283,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/BuilderIO/qwik",
|
||||
mainPackageName: "@builder.io/qwik",
|
||||
releaseDate: "2022-09-23",
|
||||
},
|
||||
{
|
||||
id: "marko",
|
||||
title: "Marko",
|
||||
frameworkName: "Marko",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "marko",
|
||||
isLatestStable: true,
|
||||
img: "framework/marko.svg",
|
||||
eslint: {
|
||||
files: ["!**"], // Marko’s linter/prettyprinter doesn’t use eslint
|
||||
},
|
||||
playgroundURL: "https://markojs.com/playground/",
|
||||
documentationURL: "https://markojs.com/docs/getting-started/",
|
||||
filesSorter(files) {
|
||||
@@ -440,28 +299,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/marko-js/marko",
|
||||
mainPackageName: "marko",
|
||||
releaseDate: "2014-04-09",
|
||||
},
|
||||
{
|
||||
id: "aurelia1",
|
||||
title: "Aurelia 1",
|
||||
frameworkName: "Aurelia",
|
||||
isCurrentVersion: false,
|
||||
frameworkNameId: "aurelia",
|
||||
isLatestStable: false,
|
||||
img: "framework/aurelia.svg",
|
||||
eslint: {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
files: ["**/aurelia1/**"],
|
||||
extends: ["eslint:recommended"],
|
||||
},
|
||||
playgroundURL: "https://codesandbox.io/s/ppmy26opw7",
|
||||
documentationURL: "http://aurelia.io/docs/",
|
||||
filesSorter(files) {
|
||||
@@ -474,16 +320,15 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/aurelia/framework",
|
||||
mainPackageName: "aurelia-framework",
|
||||
releaseDate: "2016-01-26",
|
||||
},
|
||||
{
|
||||
id: "ripple",
|
||||
title: "Ripple",
|
||||
frameworkName: "Ripple",
|
||||
isCurrentVersion: true,
|
||||
frameworkNameId: "ripple",
|
||||
isLatestStable: true,
|
||||
img: "framework/ripple.svg",
|
||||
eslint: {
|
||||
files: ["!**"],
|
||||
},
|
||||
playgroundURL: "https://www.ripplejs.com/playground",
|
||||
documentationURL: "https://www.ripplejs.com/",
|
||||
filesSorter(files) {
|
||||
@@ -491,15 +336,71 @@ const frameworks = [
|
||||
},
|
||||
repositoryLink: "https://github.com/trueadm/ripple",
|
||||
mainPackageName: "ripple",
|
||||
releaseDate: "2023-01-01",
|
||||
},
|
||||
];
|
||||
|
||||
export function matchFrameworkId(id) {
|
||||
return frameworks.find(
|
||||
(framework) => framework.id === id
|
||||
// ||(framework.isCurrentVersion &&
|
||||
// framework.frameworkName.toLowerCase() === id)
|
||||
export function matchFrameworkId(id: string): Framework | undefined {
|
||||
// First try to find by exact ID
|
||||
let framework = frameworks.find((f) => f.id === id);
|
||||
|
||||
// If not found, try to find by framework name ID and return the latest stable version
|
||||
if (!framework) {
|
||||
const latestStable = getLatestStableFrameworkByFrameworkName(id);
|
||||
if (latestStable) {
|
||||
framework = latestStable;
|
||||
}
|
||||
}
|
||||
|
||||
return framework;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all frameworks that belong to a specific framework name
|
||||
*/
|
||||
export function getFrameworksByFrameworkName(
|
||||
frameworkNameId: string,
|
||||
): Framework[] {
|
||||
return frameworks.filter(
|
||||
(framework) => framework.frameworkNameId === frameworkNameId,
|
||||
);
|
||||
}
|
||||
|
||||
export default frameworks;
|
||||
/**
|
||||
* Get the latest stable framework for a given framework name
|
||||
*/
|
||||
export function getLatestStableFrameworkByFrameworkName(
|
||||
frameworkNameId: string,
|
||||
): Framework | undefined {
|
||||
return frameworks.find(
|
||||
(framework) =>
|
||||
framework.frameworkNameId === frameworkNameId && framework.isLatestStable,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique framework name IDs
|
||||
*/
|
||||
export function getFrameworkNameIds(): string[] {
|
||||
return [...new Set(frameworks.map((framework) => framework.frameworkNameId))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get framework name information including all versions and latest stable
|
||||
*/
|
||||
export function getFrameworkNameInfo(frameworkNameId: string): {
|
||||
frameworkNameId: string;
|
||||
frameworks: Framework[];
|
||||
latestStable: Framework | undefined;
|
||||
allVersions: string[];
|
||||
} {
|
||||
const familyFrameworks = getFrameworksByFrameworkName(frameworkNameId);
|
||||
const latestStable = getLatestStableFrameworkByFrameworkName(frameworkNameId);
|
||||
|
||||
return {
|
||||
frameworkNameId,
|
||||
frameworks: familyFrameworks,
|
||||
latestStable,
|
||||
allVersions: familyFrameworks.map((f) => f.id),
|
||||
};
|
||||
}
|
||||
138
index.html
138
index.html
@@ -1,9 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><%= it.title %></title>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="canonical" href="<%= it.url %>" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/font/Mona-Sans.woff2"
|
||||
@@ -12,9 +15,34 @@
|
||||
crossorigin=""
|
||||
/>
|
||||
|
||||
<!-- DNS Prefetch for external resources -->
|
||||
<link rel="dns-prefetch" href="//github.com" />
|
||||
<link rel="dns-prefetch" href="//codesandbox.io" />
|
||||
<link rel="dns-prefetch" href="//play.vuejs.org" />
|
||||
<link rel="dns-prefetch" href="//svelte.dev" />
|
||||
<link rel="dns-prefetch" href="//playground.solidjs.com" />
|
||||
<link rel="dns-prefetch" href="//qwik.builder.io" />
|
||||
<link rel="dns-prefetch" href="//lit.dev" />
|
||||
<link rel="dns-prefetch" href="//ember-twiddle.com" />
|
||||
<link rel="dns-prefetch" href="//markojs.com" />
|
||||
<link rel="dns-prefetch" href="//stackblitz.com" />
|
||||
<link rel="dns-prefetch" href="//www.ripplejs.com" />
|
||||
|
||||
<!-- Preconnect to critical external domains -->
|
||||
<link rel="preconnect" href="https://github.com" crossorigin />
|
||||
<link rel="preconnect" href="https://codesandbox.io" crossorigin />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<meta name="title" content="<%= it.title %>" />
|
||||
<meta name="description" content="<%= it.description %>" />
|
||||
<meta name="keywords" content="<%= it.keywords %>" />
|
||||
<meta name="author" content="Component Party" />
|
||||
<meta
|
||||
name="robots"
|
||||
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
|
||||
/>
|
||||
<meta name="googlebot" content="index, follow" />
|
||||
<meta name="bingbot" content="index, follow" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -22,6 +50,14 @@
|
||||
<meta property="og:title" content="<%= it.title %>" />
|
||||
<meta property="og:description" content="<%= it.description %>" />
|
||||
<meta property="og:image" content="<%= it.image %>" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="<%= it.title %> - JavaScript Framework Comparison"
|
||||
/>
|
||||
<meta property="og:site_name" content="Component Party" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
@@ -29,6 +65,101 @@
|
||||
<meta property="twitter:title" content="<%= it.title %>" />
|
||||
<meta property="twitter:description" content="<%= it.description %>" />
|
||||
<meta property="twitter:image" content="<%= it.image %>" />
|
||||
<meta
|
||||
property="twitter:image:alt"
|
||||
content="<%= it.title %> - JavaScript Framework Comparison"
|
||||
/>
|
||||
<meta property="twitter:creator" content="@componentparty" />
|
||||
<meta property="twitter:site" content="@componentparty" />
|
||||
|
||||
<!-- Additional SEO Meta Tags -->
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta name="msapplication-TileColor" content="#111827" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Component Party" />
|
||||
<meta name="application-name" content="Component Party" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Component Party",
|
||||
"description": "<%= it.description %>",
|
||||
"url": "<%= it.url %>",
|
||||
"applicationCategory": "DeveloperApplication",
|
||||
"operatingSystem": "Web Browser",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "Component Party",
|
||||
"url": "https://component-party.dev"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Component Party",
|
||||
"url": "https://component-party.dev",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://component-party.dev/popper.svg"
|
||||
}
|
||||
},
|
||||
"keywords": "<%= it.keywords %>",
|
||||
"inLanguage": "en-US",
|
||||
"isAccessibleForFree": true,
|
||||
"browserRequirements": "Requires JavaScript. Requires HTML5.",
|
||||
"softwareVersion": "2.0.0",
|
||||
"datePublished": "2024-01-01",
|
||||
"dateModified": "2024-12-01",
|
||||
"mainEntity": {
|
||||
"@type": "ItemList",
|
||||
"name": "JavaScript Frameworks Comparison",
|
||||
"description": "A comprehensive comparison of popular JavaScript frameworks and libraries",
|
||||
"numberOfItems": "<%= it.frameworkCount || '20+' %>",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "React",
|
||||
"description": "A JavaScript library for building user interfaces",
|
||||
"applicationCategory": "WebApplication",
|
||||
"operatingSystem": "Web Browser"
|
||||
},
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Vue.js",
|
||||
"description": "A progressive JavaScript framework for building user interfaces",
|
||||
"applicationCategory": "WebApplication",
|
||||
"operatingSystem": "Web Browser"
|
||||
},
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Angular",
|
||||
"description": "A platform and framework for building single-page client applications",
|
||||
"applicationCategory": "WebApplication",
|
||||
"operatingSystem": "Web Browser"
|
||||
},
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Svelte",
|
||||
"description": "A radical new approach to building user interfaces",
|
||||
"applicationCategory": "WebApplication",
|
||||
"operatingSystem": "Web Browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Mona Sans";
|
||||
@@ -58,10 +189,5 @@
|
||||
<div id="app" class="min-h-screen"></div>
|
||||
<!--template:footer-->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<!-- 100% privacy-first analytics -->
|
||||
<script
|
||||
async
|
||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
10
lefthook.yml
Normal file
10
lefthook.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
pre-commit:
|
||||
commands:
|
||||
format:
|
||||
run: pnpm _format --write {staged_files}
|
||||
glob: "*.{js,ts,svelte,html,md,css}"
|
||||
stage_fixed: true
|
||||
update-readme-progress:
|
||||
run: node scripts/generateReadMeProgress.ts && git add README.md
|
||||
glob: "content/**/*"
|
||||
fail_text: "Failed to update README progress"
|
||||
131
package.json
131
package.json
@@ -3,98 +3,85 @@
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.6.5",
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"repository": "github:matschik/component-party.dev",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix",
|
||||
"lint:check": "eslint .",
|
||||
"format": "prettier --cache --ignore-path .gitignore --plugin-search-dir=. --write .",
|
||||
"format:check": "prettier --ignore-path .gitignore --plugin-search-dir=. . --check",
|
||||
"build:content": "node scripts/generateContent.js",
|
||||
"build:progress": "node scripts/generateReadMeProgress.js",
|
||||
"prepare": "husky",
|
||||
"cy:open-e2e": "cypress open --e2e --browser chrome",
|
||||
"cy:open-unit": "cypress open --component --browser chrome",
|
||||
"cy:run-e2e": "cypress run --e2e",
|
||||
"cy:e2e": "start-server-and-test dev http-get://localhost:5173 cy:open-e2e"
|
||||
"_format": "prettier --cache --cache-location .cache/prettier",
|
||||
"format": "pnpm _format --write .",
|
||||
"lint": "oxlint && eslint",
|
||||
"check": "pnpm format && svelte-check --tsconfig ./tsconfig.json && pnpm lint",
|
||||
"check:ci": "svelte-check --tsconfig ./tsconfig.json && pnpm lint",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"build:content": "node scripts/generateContent.ts --no-cache",
|
||||
"build:progress": "node scripts/generateReadMeProgress.ts",
|
||||
"build:sitemap": "node scripts/generateSitemap.ts",
|
||||
"prepare": "lefthook install",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:install": "playwright install chromium",
|
||||
"test": "pnpm run test:e2e",
|
||||
"check:package-manager": "node scripts/check-package-manager.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/heroicons": "^1.2.2",
|
||||
"classnames": "^2.5.1",
|
||||
"radix3": "^1.1.2"
|
||||
"@iconify-json/heroicons": "^1.2.3",
|
||||
"just-throttle": "^4.2.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"runed": "^0.32.0",
|
||||
"sv-router": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/eslint-plugin": "^19.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^19.3.0",
|
||||
"@angular-eslint/template-parser": "^19.3.0",
|
||||
"@angular/core": "^19.2.7",
|
||||
"@aurelia/router": "2.0.0-beta.23",
|
||||
"@babel/core": "^7.26.10",
|
||||
"@babel/eslint-parser": "^7.27.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
||||
"@builder.io/qwik": "^1.13.0",
|
||||
"@lit/context": "^1.1.5",
|
||||
"@matschik/lz-string": "^0.0.2",
|
||||
"@shikijs/markdown-it": "^3.3.0",
|
||||
"@sveltejs/vite-plugin-svelte": "5.0.3",
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "4.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"@unocss/preset-icons": "66.1.0-beta.12",
|
||||
"@unocss/preset-typography": "66.1.0-beta.12",
|
||||
"@unocss/preset-wind": "66.1.0-beta.12",
|
||||
"@unocss/reset": "66.1.0-beta.12",
|
||||
"aurelia": "2.0.0-beta.23",
|
||||
"aurelia-framework": "^1.4.1",
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"eslint": "^9.36.0",
|
||||
"@iconify/json": "^2.2.386",
|
||||
"@iconify/tailwind4": "^1.0.6",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@shikijs/markdown-it": "^3.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "6.2.0",
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "5.0.1",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/folder-hash": "^4.0.4",
|
||||
"@types/html-minifier-terser": "^7.0.2",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"chokidar": "^4.0.3",
|
||||
"codesandbox": "^2.2.3",
|
||||
"cypress": "^14.3.2",
|
||||
"ember-eslint-parser": "^0.5.9",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-plugin-ember": "^12.5.0",
|
||||
"eslint-plugin-lit": "^2.1.1",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-qwik": "^1.13.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-solid": "^0.14.5",
|
||||
"eslint-plugin-svelte": "^3.5.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"eta": "^3.5.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-oxlint": "^1.16.0",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"eta": "^4.0.1",
|
||||
"folder-hash": "^4.1.1",
|
||||
"globals": "^16.4.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.1",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"lefthook": "^1.13.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"micache": "^2.4.1",
|
||||
"pkg-dir": "^8.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"shiki": "^3.3.0",
|
||||
"solid-js": "^1.9.5",
|
||||
"start-server-and-test": "^2.0.11",
|
||||
"svelte": "5.28.2",
|
||||
"oxlint": "^1.16.0",
|
||||
"package-directory": "^8.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"shiki": "^3.13.0",
|
||||
"svelte": "5.39.3",
|
||||
"svelte-check": "^4.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "66.1.0-beta.12",
|
||||
"vite": "^6.3.2",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,svelte,vue,html,md,css,hbs}": "prettier --cache --write"
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"vite": "^7.1.6",
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"cypress",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
31
playwright.config.ts
Normal file
31
playwright.config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "test/e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
["html", { open: "never" }],
|
||||
["json", { outputFile: "test-results/results.json" }],
|
||||
["junit", { outputFile: "test-results/results.xml" }],
|
||||
],
|
||||
use: {
|
||||
baseURL: "http://localhost:4144",
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "pnpm run build && pnpm run preview --port 4144",
|
||||
port: 4144,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
8539
pnpm-lock.yaml
generated
8539
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
74
public/manifest.json
Normal file
74
public/manifest.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "Component Party - JavaScript Framework Comparison",
|
||||
"short_name": "Component Party",
|
||||
"description": "Compare JavaScript frameworks side-by-side: React, Vue, Angular, Svelte, Solid.js, and more. See syntax differences, features, and code examples for web development frameworks.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#111827",
|
||||
"theme_color": "#111827",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"lang": "en-US",
|
||||
"categories": ["developer", "productivity", "utilities"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/banner2.png",
|
||||
"sizes": "1200x630",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "Component Party Framework Comparison Interface"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "React vs Vue",
|
||||
"short_name": "React vs Vue",
|
||||
"description": "Compare React and Vue.js frameworks",
|
||||
"url": "/?f=react,vue3",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "React vs Angular",
|
||||
"short_name": "React vs Angular",
|
||||
"description": "Compare React and Angular frameworks",
|
||||
"url": "/?f=react,angularRenaissance",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Vue vs Svelte",
|
||||
"short_name": "Vue vs Svelte",
|
||||
"description": "Compare Vue.js and Svelte frameworks",
|
||||
"url": "/?f=vue3,svelte5",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,3 +2,9 @@
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://component-party.dev/sitemap.xml
|
||||
|
||||
# Crawl-delay for respectful crawling
|
||||
Crawl-delay: 1
|
||||
|
||||
231
public/sitemap.xml
Normal file
231
public/sitemap.xml
Normal file
@@ -0,0 +1,231 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://component-party.dev/</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue3</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=angularRenaissance</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=angular</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=lit</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=emberOctane</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=svelte4</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue2</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=alpine</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=emberPolaris</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=mithril</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=aurelia2</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=qwik</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=marko</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=aurelia1</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=ripple</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,vue3</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,angularRenaissance</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue3,angularRenaissance</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue3,svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue3,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=angularRenaissance,svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=angularRenaissance,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=svelte5,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,vue3,angularRenaissance</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,svelte5,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue3,svelte5,solid</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=react,vue3,svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=svelte4,svelte5</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=vue2,vue3</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=angular,angularRenaissance</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=aurelia1,aurelia2</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://component-party.dev/?f=emberOctane,emberPolaris</loc>
|
||||
<lastmod>2025-09-21</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -1,3 +0,0 @@
|
||||
import generateContent from "../build/lib/generateContent.js";
|
||||
|
||||
generateContent();
|
||||
16
scripts/generateContent.ts
Normal file
16
scripts/generateContent.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import generateContent from "../build/lib/generateContent.ts";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const noCache = args.includes("--no-cache") || args.includes("--force");
|
||||
|
||||
console.log(`Generating content${noCache ? " (no cache)" : ""}...`);
|
||||
|
||||
try {
|
||||
await generateContent({ noCache });
|
||||
console.log("Content generation completed successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error generating content:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,11 +1,36 @@
|
||||
import fs from "fs/promises";
|
||||
import { packageDirectory } from "pkg-dir";
|
||||
import { packageDirectory } from "package-directory";
|
||||
import path from "node:path";
|
||||
import kebabCase from "lodash.kebabcase";
|
||||
import FRAMEWORKS from "../frameworks.mjs";
|
||||
import { frameworks } from "../frameworks.ts";
|
||||
import prettier from "prettier";
|
||||
import kebabCase from "just-kebab-case";
|
||||
|
||||
async function main() {
|
||||
interface File {
|
||||
path: string;
|
||||
fileName: string;
|
||||
ext: string;
|
||||
}
|
||||
|
||||
interface FrameworkChild {
|
||||
dirName: string;
|
||||
path: string;
|
||||
files: File[];
|
||||
}
|
||||
|
||||
interface SubSection {
|
||||
id: string;
|
||||
path: string;
|
||||
dirName: string;
|
||||
title: string;
|
||||
children: FrameworkChild[];
|
||||
}
|
||||
|
||||
interface Section {
|
||||
title: string;
|
||||
children: SubSection[];
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const contentTree = await parseContentDir();
|
||||
const readmeContent = await fs.readFile("README.md", "utf8");
|
||||
|
||||
@@ -14,12 +39,12 @@ async function main() {
|
||||
const MARKER_START = "<!-- progression start -->";
|
||||
const MARKER_END = "<!-- progression end -->";
|
||||
const progressionContentRegex = new RegExp(
|
||||
`${MARKER_START}([\\s\\S]*?)${MARKER_END}`
|
||||
`${MARKER_START}([\\s\\S]*?)${MARKER_END}`,
|
||||
);
|
||||
|
||||
const newReadmeContent = readmeContent.replace(
|
||||
progressionContentRegex,
|
||||
`${MARKER_START}\n${progressionContent}\n${MARKER_END}`
|
||||
`${MARKER_START}\n${progressionContent}\n${MARKER_END}`,
|
||||
);
|
||||
|
||||
await fs.writeFile("README.md", newReadmeContent);
|
||||
@@ -27,35 +52,38 @@ async function main() {
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
async function parseContentDir() {
|
||||
async function parseContentDir(): Promise<Section[]> {
|
||||
const rootDir = await packageDirectory();
|
||||
if (!rootDir) {
|
||||
throw new Error("Could not find package directory");
|
||||
}
|
||||
const contentPath = path.join(rootDir, "content");
|
||||
const rootDirs = await fs.readdir(contentPath);
|
||||
|
||||
function dirNameToTitle(dirName) {
|
||||
function dirNameToTitle(dirName: string): string {
|
||||
return capitalize(dirName.split("-").slice(1).join(" "));
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
const sections = [];
|
||||
const sections: Section[] = [];
|
||||
|
||||
for (const rootDirName of rootDirs) {
|
||||
const rootSectionPath = path.join(contentPath, rootDirName);
|
||||
const subDirs = await fs.readdir(rootSectionPath).catch(() => []);
|
||||
const children = [];
|
||||
const children: SubSection[] = [];
|
||||
|
||||
for (const subDir of subDirs) {
|
||||
const subDirPath = path.join(rootSectionPath, subDir);
|
||||
const frameworks = await fs.readdir(subDirPath).catch(() => []);
|
||||
const frameworkChildren = [];
|
||||
const frameworkChildren: FrameworkChild[] = [];
|
||||
|
||||
for (const fw of frameworks) {
|
||||
const fwPath = path.join(subDirPath, fw);
|
||||
const fileNames = await fs.readdir(fwPath).catch(() => []);
|
||||
const files = fileNames.map((fileName) => ({
|
||||
const files: File[] = fileNames.map((fileName) => ({
|
||||
path: path.join(fwPath, fileName),
|
||||
fileName,
|
||||
ext: path.extname(fileName).slice(1),
|
||||
@@ -81,37 +109,39 @@ async function parseContentDir() {
|
||||
return sections;
|
||||
}
|
||||
|
||||
function mdCheck(b) {
|
||||
function mdCheck(b: boolean): string {
|
||||
return b ? "x" : " ";
|
||||
}
|
||||
|
||||
async function generateProgressionMarkdown(contentTree) {
|
||||
async function generateProgressionMarkdown(
|
||||
contentTree: Section[],
|
||||
): Promise<string> {
|
||||
let output = "";
|
||||
|
||||
for (const framework of FRAMEWORKS) {
|
||||
const frameworkLines = [];
|
||||
const allChecks = [];
|
||||
for (const framework of frameworks) {
|
||||
const frameworkLines: string[] = [];
|
||||
const allChecks: boolean[] = [];
|
||||
|
||||
for (const root of contentTree) {
|
||||
const sectionChecks = [];
|
||||
const subLines = [];
|
||||
const sectionChecks: boolean[] = [];
|
||||
const subLines: string[] = [];
|
||||
|
||||
for (const sub of root.children) {
|
||||
const fwEntry = sub.children.find((c) => c.dirName === framework.id);
|
||||
const hasFiles = fwEntry?.files?.length > 0;
|
||||
const hasFiles = (fwEntry?.files?.length ?? 0) > 0;
|
||||
sectionChecks.push(!!hasFiles);
|
||||
subLines.push(` * [${mdCheck(hasFiles)}] ${sub.title}`);
|
||||
}
|
||||
|
||||
frameworkLines.push(
|
||||
`* [${mdCheck(sectionChecks.every(Boolean))}] ${root.title}`
|
||||
`* [${mdCheck(sectionChecks.every(Boolean))}] ${root.title}`,
|
||||
);
|
||||
frameworkLines.push(...subLines);
|
||||
allChecks.push(...sectionChecks);
|
||||
}
|
||||
|
||||
const percent = Math.ceil(
|
||||
(allChecks.filter(Boolean).length / allChecks.length) * 100
|
||||
(allChecks.filter(Boolean).length / allChecks.length) * 100,
|
||||
);
|
||||
|
||||
const markdown = `
|
||||
120
scripts/generateSitemap.ts
Normal file
120
scripts/generateSitemap.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { frameworks } from "../frameworks.ts";
|
||||
|
||||
interface SitemapUrl {
|
||||
loc: string;
|
||||
lastmod: string;
|
||||
changefreq:
|
||||
| "always"
|
||||
| "hourly"
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "yearly"
|
||||
| "never";
|
||||
priority: number;
|
||||
}
|
||||
|
||||
function generateSitemapXml(urls: SitemapUrl[]): string {
|
||||
const header = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
|
||||
|
||||
const footer = `</urlset>`;
|
||||
|
||||
const urlEntries = urls
|
||||
.map(
|
||||
(url) => ` <url>
|
||||
<loc>${url.loc}</loc>
|
||||
<lastmod>${url.lastmod}</lastmod>
|
||||
<changefreq>${url.changefreq}</changefreq>
|
||||
<priority>${url.priority}</priority>
|
||||
</url>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return `${header}\n${urlEntries}\n${footer}`;
|
||||
}
|
||||
|
||||
function generateFrameworkCombinations(): string[] {
|
||||
const combinations: string[] = [];
|
||||
|
||||
// Add main page
|
||||
combinations.push("https://component-party.dev/");
|
||||
|
||||
// Add individual framework pages
|
||||
for (const framework of frameworks) {
|
||||
combinations.push(`https://component-party.dev/?f=${framework.id}`);
|
||||
}
|
||||
|
||||
// Add popular two-framework combinations
|
||||
const popularFrameworks = [
|
||||
"react",
|
||||
"vue3",
|
||||
"angularRenaissance",
|
||||
"svelte5",
|
||||
"solid",
|
||||
];
|
||||
for (let i = 0; i < popularFrameworks.length; i++) {
|
||||
for (let j = i + 1; j < popularFrameworks.length; j++) {
|
||||
combinations.push(
|
||||
`https://component-party.dev/?f=${popularFrameworks[i]},${popularFrameworks[j]}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add some three-framework combinations
|
||||
const threeFrameworkCombos = [
|
||||
["react", "vue3", "angularRenaissance"],
|
||||
["react", "svelte5", "solid"],
|
||||
["vue3", "svelte5", "solid"],
|
||||
["react", "vue3", "svelte5"],
|
||||
];
|
||||
|
||||
for (const combo of threeFrameworkCombos) {
|
||||
combinations.push(`https://component-party.dev/?f=${combo.join(",")}`);
|
||||
}
|
||||
|
||||
// Add version comparison pages
|
||||
const versionComparisons = [
|
||||
["svelte4", "svelte5"],
|
||||
["vue2", "vue3"],
|
||||
["angular", "angularRenaissance"],
|
||||
["aurelia1", "aurelia2"],
|
||||
["emberOctane", "emberPolaris"],
|
||||
];
|
||||
|
||||
for (const [v1, v2] of versionComparisons) {
|
||||
combinations.push(`https://component-party.dev/?f=${v1},${v2}`);
|
||||
}
|
||||
|
||||
return combinations;
|
||||
}
|
||||
|
||||
async function generateSitemap(): Promise<void> {
|
||||
const baseUrl = "https://component-party.dev";
|
||||
const currentDate = new Date().toISOString().split("T")[0];
|
||||
|
||||
const urls: SitemapUrl[] = generateFrameworkCombinations().map((loc) => ({
|
||||
loc,
|
||||
lastmod: currentDate,
|
||||
changefreq: "weekly" as const,
|
||||
priority: loc === `${baseUrl}/` ? 1.0 : 0.8,
|
||||
}));
|
||||
|
||||
const sitemapXml = generateSitemapXml(urls);
|
||||
|
||||
// Write to public directory
|
||||
const publicDir = path.join(import.meta.dirname, "..", "public");
|
||||
const sitemapPath = path.join(publicDir, "sitemap.xml");
|
||||
|
||||
await fs.writeFile(sitemapPath, sitemapXml, "utf8");
|
||||
console.log(`Generated sitemap with ${urls.length} URLs at ${sitemapPath}`);
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
generateSitemap().catch(console.error);
|
||||
}
|
||||
|
||||
export default generateSitemap;
|
||||
@@ -1,24 +1,33 @@
|
||||
import frameworks from "../frameworks.mjs";
|
||||
import { frameworks } from "../frameworks";
|
||||
|
||||
const mainPackageNames = frameworks.map((f) => f.mainPackageName);
|
||||
|
||||
async function getPackageDownloads(packageName) {
|
||||
interface PackageDownloadStats {
|
||||
packageName: string;
|
||||
downloads: number;
|
||||
}
|
||||
|
||||
async function getPackageDownloads(
|
||||
packageName: string,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.npmjs.org/downloads/point/last-month/${packageName}`
|
||||
`https://api.npmjs.org/downloads/point/last-month/${packageName}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.downloads;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to fetch download stats for package ${packageName}: `,
|
||||
error
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sortPackagesByDownloads(packages) {
|
||||
async function sortPackagesByDownloads(
|
||||
packages: string[],
|
||||
): Promise<PackageDownloadStats[] | null> {
|
||||
try {
|
||||
const downloadStats = await Promise.all(packages.map(getPackageDownloads));
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<script>
|
||||
import Router from "./Router.svelte";
|
||||
import Index from "./Index.svelte";
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: Index },
|
||||
{ path: "/compare/:versus", component: Index },
|
||||
];
|
||||
<script lang="ts">
|
||||
import { Router } from "sv-router";
|
||||
import "./router.ts";
|
||||
</script>
|
||||
|
||||
<Router {routes} />
|
||||
<Router />
|
||||
|
||||
683
src/Index.svelte
683
src/Index.svelte
@@ -1,186 +1,141 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
||||
import c from "classnames";
|
||||
|
||||
import FRAMEWORKS, { matchFrameworkId } from "../frameworks.mjs";
|
||||
import { frameworks, matchFrameworkId } from "@frameworks";
|
||||
import FrameworkLabel from "./components/FrameworkLabel.svelte";
|
||||
import { sections, snippets } from "./generatedContent/tree.js";
|
||||
import snippetsImporterByFrameworkId from "./generatedContent/framework/index.js";
|
||||
import CodeEditor from "./components/CodeEditor.svelte";
|
||||
import AppNotificationCenter from "./components/AppNotificationCenter.svelte";
|
||||
import createLocaleStorage from "./lib/createLocaleStorage.js";
|
||||
import { getContext, onDestroy, onMount } from "svelte";
|
||||
import createLocaleStorage from "./lib/createLocaleStorage.ts";
|
||||
import { watch } from "runed";
|
||||
import Header from "./components/Header.svelte";
|
||||
import Aside from "./components/Aside.svelte";
|
||||
import GithubIcon from "./components/GithubIcon.svelte";
|
||||
import {
|
||||
FRAMEWORK_IDS_FROM_URL_KEY,
|
||||
FRAMEWORK_SEPARATOR,
|
||||
} from "./constants.ts";
|
||||
import { searchParams } from "sv-router";
|
||||
import { navigate } from "./router.ts";
|
||||
|
||||
const { currentRoute, navigate } = getContext("router");
|
||||
|
||||
const frameworkIdsStorage = createLocaleStorage("framework_display");
|
||||
|
||||
function removeSearchParamKeyFromURL(k) {
|
||||
// Get the current search params as an object
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (!searchParams.has(k)) {
|
||||
// The key doesn't exist, so don't do anything
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the parameter you want to remove
|
||||
searchParams.delete(k);
|
||||
|
||||
let newUrl = window.location.pathname;
|
||||
if (searchParams.toString().length > 0) {
|
||||
// There are still search params, so include the `?` character
|
||||
newUrl += `?${searchParams}`;
|
||||
}
|
||||
|
||||
// Update the URL without reloading the page
|
||||
history.replaceState({}, "", newUrl);
|
||||
interface File {
|
||||
fileName: string;
|
||||
contentHtml: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const FRAMEWORK_IDS_FROM_URL_KEY = "f";
|
||||
const SITE_TITLE = "Component Party";
|
||||
const MAX_FRAMEWORK_NB_INITIAL_DISPLAYED = 9;
|
||||
const FRAMEWORKS_BONUS = FRAMEWORKS.slice(MAX_FRAMEWORK_NB_INITIAL_DISPLAYED);
|
||||
interface FrameworkSnippet {
|
||||
frameworkId: string;
|
||||
snippetId: string;
|
||||
files: File[];
|
||||
playgroundURL: string;
|
||||
markdownFiles: File[];
|
||||
snippetEditHref: string;
|
||||
}
|
||||
|
||||
let frameworkIdsSelected = $state(new SvelteSet());
|
||||
let snippetsByFrameworkId = $state(new SvelteMap());
|
||||
const MAX_FRAMEWORK_NOBONUS = 9;
|
||||
const DEFAULT_FRAMEWORKS = ["react", "svelte5"];
|
||||
const FRAMEWORKS_BONUS = frameworks.slice(MAX_FRAMEWORK_NOBONUS);
|
||||
const frameworkIdsStorage = createLocaleStorage<string[]>(
|
||||
"framework_display",
|
||||
[],
|
||||
);
|
||||
|
||||
const frameworkIdsSelected = new SvelteSet<string>();
|
||||
const frameworkIdsSelectedArr = $derived([...frameworkIdsSelected]);
|
||||
const frameworksSelected = $derived(
|
||||
frameworkIdsSelectedArr.map((id: string) => matchFrameworkId(id)),
|
||||
);
|
||||
const snippetsByFrameworkId = new SvelteMap<string, FrameworkSnippet[]>();
|
||||
let frameworkIdsSelectedInitialized = $state(false);
|
||||
let isVersusFrameworks = $state(false);
|
||||
let onMountCallbacks = $state(new SvelteSet());
|
||||
let isMounted = $state(false);
|
||||
|
||||
function handleVersus(versus) {
|
||||
const fids = versus.split("-vs-");
|
||||
|
||||
if (fids.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameworks = fids.map(matchFrameworkId);
|
||||
|
||||
if (frameworks.some((f) => !f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return frameworks;
|
||||
}
|
||||
|
||||
const unsubscribeCurrentRoute = currentRoute.subscribe(($currentRoute) => {
|
||||
window.scrollTo(0, 0);
|
||||
isVersusFrameworks = false;
|
||||
document.title = SITE_TITLE;
|
||||
|
||||
if ($currentRoute.path === "/") {
|
||||
if (isMounted) {
|
||||
handleInitialFrameworkIdsSelectedFromStorage({ useDefaults: false });
|
||||
} else {
|
||||
onMountCallbacks.add(() =>
|
||||
handleInitialFrameworkIdsSelectedFromStorage({ useDefaults: true })
|
||||
);
|
||||
}
|
||||
} else if ($currentRoute.params?.versus) {
|
||||
const versusFrameworks = handleVersus($currentRoute.params.versus);
|
||||
if (versusFrameworks) {
|
||||
isVersusFrameworks = true;
|
||||
for (const versusFramework of versusFrameworks) {
|
||||
frameworkIdsSelected.add(versusFramework.id);
|
||||
}
|
||||
frameworkIdsSelectedInitialized = true;
|
||||
document.title = `${versusFrameworks
|
||||
.map((f) => f.title)
|
||||
.join(" vs ")} - ${SITE_TITLE}`;
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
} else {
|
||||
navigate("/");
|
||||
const isVersusFrameworks = $derived(frameworksSelected.length === 2);
|
||||
const siteTitle = $derived(
|
||||
isVersusFrameworks
|
||||
? `${frameworksSelected.map((f) => f!.title).join(" vs ")} - Component Party`
|
||||
: "Component Party",
|
||||
);
|
||||
const frameworkIdsFromSearchParam = $derived.by(() => {
|
||||
const value = searchParams.get(FRAMEWORK_IDS_FROM_URL_KEY);
|
||||
if (typeof value === "string") {
|
||||
return value.split(FRAMEWORK_SEPARATOR).filter(matchFrameworkId);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
onDestroy(unsubscribeCurrentRoute);
|
||||
// handle on link click
|
||||
watch(
|
||||
[() => frameworkIdsFromSearchParam],
|
||||
() => {
|
||||
selectFrameworks(frameworkIdsFromSearchParam);
|
||||
},
|
||||
{ lazy: true },
|
||||
);
|
||||
|
||||
function handleInitialFrameworkIdsSelectedFromStorage({ useDefaults }) {
|
||||
if (frameworkIdsSelectedInitialized) {
|
||||
return;
|
||||
function selectFrameworks(frameworkIds: string[]) {
|
||||
frameworkIdsSelected.clear();
|
||||
for (const frameworkId of frameworkIds) {
|
||||
frameworkIdsSelected.add(frameworkId);
|
||||
}
|
||||
let frameworkIdsSelectedOnInit = [];
|
||||
navigateWithFrameworkSelection();
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
function onInit() {
|
||||
const frameworkIdsFromStorage = frameworkIdsStorage
|
||||
.getJSON()
|
||||
.filter((id) => matchFrameworkId(id));
|
||||
|
||||
const frameworkIdsFromURLStr = url.searchParams.get(
|
||||
FRAMEWORK_IDS_FROM_URL_KEY
|
||||
);
|
||||
|
||||
if (frameworkIdsFromURLStr) {
|
||||
const frameworkIdsFromURL = frameworkIdsFromURLStr
|
||||
.split(",")
|
||||
.filter(matchFrameworkId);
|
||||
if (frameworkIdsFromURL.length > 0) {
|
||||
frameworkIdsSelectedOnInit = frameworkIdsFromURL;
|
||||
} else {
|
||||
removeSearchParamKeyFromURL(FRAMEWORK_IDS_FROM_URL_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
if (frameworkIdsSelectedOnInit.length === 0) {
|
||||
const frameworkIdsFromStorage = frameworkIdsStorage
|
||||
.getJSON()
|
||||
?.filter(matchFrameworkId);
|
||||
if (frameworkIdsFromStorage?.length > 0) {
|
||||
frameworkIdsSelectedOnInit = frameworkIdsFromStorage;
|
||||
}
|
||||
}
|
||||
|
||||
if (useDefaults && frameworkIdsSelectedOnInit.length === 0) {
|
||||
frameworkIdsSelectedOnInit = ["react", "svelte5"];
|
||||
}
|
||||
|
||||
for (const fid of frameworkIdsSelectedOnInit) {
|
||||
frameworkIdsSelected.add(fid);
|
||||
// From search param
|
||||
if (frameworkIdsFromSearchParam.length > 0) {
|
||||
selectFrameworks(frameworkIdsFromSearchParam);
|
||||
} else if (frameworkIdsFromStorage.length > 0) {
|
||||
selectFrameworks(frameworkIdsFromStorage);
|
||||
} else {
|
||||
// Default frameworks
|
||||
selectFrameworks(DEFAULT_FRAMEWORKS);
|
||||
}
|
||||
|
||||
frameworkIdsSelectedInitialized = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
isMounted = true;
|
||||
for (const callback of onMountCallbacks) {
|
||||
callback();
|
||||
}
|
||||
onMountCallbacks.clear();
|
||||
});
|
||||
onInit();
|
||||
|
||||
function saveFrameworkIdsSelectedOnStorage() {
|
||||
frameworkIdsStorage.setJSON([...frameworkIdsSelected]);
|
||||
removeSearchParamKeyFromURL(FRAMEWORK_IDS_FROM_URL_KEY);
|
||||
}
|
||||
|
||||
function toggleFrameworkId(frameworkId) {
|
||||
function toggleFrameworkId(frameworkId: string) {
|
||||
if (frameworkIdsSelected.has(frameworkId)) {
|
||||
frameworkIdsSelected.delete(frameworkId);
|
||||
} else {
|
||||
frameworkIdsSelected.add(frameworkId);
|
||||
}
|
||||
frameworkIdsSelected = frameworkIdsSelected;
|
||||
saveFrameworkIdsSelectedOnStorage();
|
||||
navigateWithFrameworkSelection();
|
||||
}
|
||||
|
||||
let snippetsByFrameworkIdLoading = $state(new SvelteSet());
|
||||
let snippetsByFrameworkIdError = $state(new SvelteSet());
|
||||
async function navigateWithFrameworkSelection() {
|
||||
if (frameworkIdsSelected.size === 0) {
|
||||
searchParams.delete(FRAMEWORK_IDS_FROM_URL_KEY);
|
||||
} else {
|
||||
searchParams.set(
|
||||
FRAMEWORK_IDS_FROM_URL_KEY,
|
||||
frameworkIdsSelectedArr.join(FRAMEWORK_SEPARATOR),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
[...frameworkIdsSelected].map((frameworkId) => {
|
||||
let snippetsByFrameworkIdLoading = new SvelteSet<string>();
|
||||
let snippetsByFrameworkIdError = new SvelteSet<string>();
|
||||
|
||||
watch([() => frameworkIdsSelected.entries()], () => {
|
||||
for (const frameworkId of frameworkIdsSelectedArr) {
|
||||
if (!snippetsByFrameworkId.has(frameworkId)) {
|
||||
snippetsByFrameworkIdError.delete(frameworkId);
|
||||
snippetsByFrameworkIdLoading.add(frameworkId);
|
||||
|
||||
snippetsImporterByFrameworkId[frameworkId]()
|
||||
.then(({ default: frameworkSnippets }) => {
|
||||
snippetsByFrameworkId.set(frameworkId, frameworkSnippets);
|
||||
})
|
||||
.then(
|
||||
({
|
||||
default: frameworkSnippets,
|
||||
}: {
|
||||
default: FrameworkSnippet[];
|
||||
}) => {
|
||||
snippetsByFrameworkId.set(frameworkId, frameworkSnippets);
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
snippetsByFrameworkIdError.add(frameworkId);
|
||||
})
|
||||
@@ -188,87 +143,137 @@
|
||||
snippetsByFrameworkIdLoading.delete(frameworkId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (frameworkIdsSelected.size === 0) {
|
||||
navigate("/");
|
||||
}
|
||||
});
|
||||
|
||||
let showBonusFrameworks = $state(false);
|
||||
|
||||
const frameworksSelected = $derived(
|
||||
[...frameworkIdsSelected].map(matchFrameworkId)
|
||||
);
|
||||
|
||||
const bonusFrameworks = $derived(
|
||||
FRAMEWORKS_BONUS.filter((f) => !frameworkIdsSelected.has(f.id))
|
||||
FRAMEWORKS_BONUS.filter(({ id }) => !frameworkIdsSelected.has(id)),
|
||||
);
|
||||
|
||||
const frameworksNotSelected = $derived(
|
||||
FRAMEWORKS.filter((f) => !frameworkIdsSelected.has(f.id))
|
||||
frameworks.filter(({ id }) => id && !frameworkIdsSelected.has(id)),
|
||||
);
|
||||
|
||||
const headerFrameworks = $derived([
|
||||
...frameworksSelected,
|
||||
...frameworksNotSelected.filter(
|
||||
(f) => !bonusFrameworks.find((bf) => bf.id === f.id)
|
||||
),
|
||||
...(showBonusFrameworks ? bonusFrameworks : []),
|
||||
]);
|
||||
const headerFrameworks = $derived(
|
||||
[
|
||||
...frameworksSelected.filter((f) => f),
|
||||
...frameworksNotSelected.filter(
|
||||
(f) => f && !bonusFrameworks.find((bf) => bf.id === f.id),
|
||||
),
|
||||
...(showBonusFrameworks ? bonusFrameworks : []),
|
||||
].filter((f): f is NonNullable<typeof f> => !!f),
|
||||
);
|
||||
</script>
|
||||
|
||||
<AppNotificationCenter />
|
||||
<svelte:head>
|
||||
<title>{siteTitle}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={isVersusFrameworks
|
||||
? `Compare ${frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
" vs ",
|
||||
)} frameworks side-by-side. See syntax differences, features, and code examples for ${frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(" and ")}.`
|
||||
: "Compare JavaScript frameworks side-by-side: React, Vue, Angular, Svelte, Solid.js, and more. See syntax differences, features, and code examples for web development frameworks."}
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content={isVersusFrameworks
|
||||
? frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(", ") +
|
||||
", framework comparison, JavaScript frameworks, web development, frontend development, code comparison"
|
||||
: "JavaScript frameworks, React, Vue, Angular, Svelte, Solid.js, framework comparison, web development, frontend frameworks, component libraries, JavaScript libraries, code comparison, programming tools, developer tools, web components, JSX, TypeScript, modern JavaScript"}
|
||||
/>
|
||||
{#if isVersusFrameworks}
|
||||
<meta property="og:title" content={siteTitle} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Compare {frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
' vs ',
|
||||
)} frameworks side-by-side. See syntax differences, features, and code examples for {frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(' and ')}."
|
||||
/>
|
||||
<meta property="twitter:title" content={siteTitle} />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Compare {frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
' vs ',
|
||||
)} frameworks side-by-side. See syntax differences, features, and code examples for {frameworksSelected
|
||||
.map((f) => f?.title)
|
||||
.filter(Boolean)
|
||||
.join(' and ')}."
|
||||
/>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<Header {frameworksSelected} />
|
||||
<Header />
|
||||
|
||||
<div class="flex border-b border-gray-700">
|
||||
<Aside />
|
||||
<div class="pb-8 w-10 grow">
|
||||
<div
|
||||
class="flex px-6 lg:px-20 py-2 sticky top-0 z-20 w-full backdrop-blur bg-gray-900/80 border-b border-gray-700 whitespace-nowrap overflow-x-auto"
|
||||
data-framework-id-selected-list={[...frameworkIdsSelected].join(",")}
|
||||
data-framework-id-selected-list={frameworkIdsSelectedArr.join(",")}
|
||||
data-testid="framework-selection-bar"
|
||||
>
|
||||
{#each headerFrameworks as framework}
|
||||
<button
|
||||
title={frameworkIdsSelected.has(framework.id)
|
||||
? `Hide ${framework.title}`
|
||||
: `Display ${framework.title}`}
|
||||
class={c(
|
||||
"text-sm flex-shrink-0 rounded border px-3 py-1 bg-gray-900 hover:bg-gray-800 transition-all mr-2",
|
||||
frameworkIdsSelected.has(framework.id)
|
||||
? "border-blue-900"
|
||||
: "opacity-70 border-opacity-50 border-gray-700"
|
||||
)}
|
||||
onclick={() => {
|
||||
toggleFrameworkId(framework.id);
|
||||
if (isVersusFrameworks && $currentRoute.path !== "/") {
|
||||
navigate("/");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FrameworkLabel id={framework.id} size={15} />
|
||||
</button>
|
||||
{#each headerFrameworks as framework (framework.id)}
|
||||
{#if framework}
|
||||
<button
|
||||
title={frameworkIdsSelected.has(framework.id)
|
||||
? `Hide ${framework.title}`
|
||||
: `Display ${framework.title}`}
|
||||
class={[
|
||||
"text-sm flex-shrink-0 rounded border px-3 py-1 bg-gray-900 hover:bg-gray-800 transition-all mr-2",
|
||||
frameworkIdsSelected.has(framework.id)
|
||||
? "border-blue-900"
|
||||
: "opacity-70 border-opacity-50 border-gray-700",
|
||||
]}
|
||||
data-testid={`framework-button-${framework.id}`}
|
||||
onclick={() => {
|
||||
toggleFrameworkId(framework.id);
|
||||
if (frameworkIdsSelectedArr.length === 0) {
|
||||
frameworkIdsStorage.remove();
|
||||
} else {
|
||||
frameworkIdsStorage.setJSON(frameworkIdsSelectedArr);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FrameworkLabel id={framework.id} size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if bonusFrameworks.length > 0 && !showBonusFrameworks}
|
||||
<button
|
||||
title="show more frameworks"
|
||||
class="opacity-70 text-sm flex-shrink-0 rounded border border-gray-700 px-3 py-1 border-opacity-50 bg-gray-900 hover:bg-gray-800 transition-all mr-2"
|
||||
class="opacity-70 text-sm flex-shrink-0 rounded border border-gray-700 px-3 py-1 border-opacity-50 bg-gray-900 hover:bg-gray-800 transition-all mr-2 flex items-center justify-center"
|
||||
data-testid="show-more-frameworks-button"
|
||||
onclick={() => {
|
||||
showBonusFrameworks = !showBonusFrameworks;
|
||||
}}
|
||||
aria-label="show more frameworks"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="iconify ph--dots-three size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -276,29 +281,49 @@
|
||||
<main class="relative pt-6">
|
||||
<div>
|
||||
{#if frameworkIdsSelected.size === 0}
|
||||
<div class="space-y-4">
|
||||
<section
|
||||
class="space-y-4"
|
||||
data-testid="empty-state"
|
||||
aria-labelledby="empty-state-heading"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<div class="i-heroicons:arrow-up size-6 animate-bounce"></div>
|
||||
<span
|
||||
class="iconify ph--arrow-up size-6 animate-bounce"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<h1 id="empty-state-heading" class="sr-only">
|
||||
Select Frameworks to Compare
|
||||
</h1>
|
||||
<p
|
||||
class="text-lg opacity-80 flex items-center text-center space-x-3"
|
||||
data-testid="empty-state-message"
|
||||
>
|
||||
<img src="/popper.svg" alt="logo" class="size-6" />
|
||||
<img
|
||||
src="/popper.svg"
|
||||
alt="Component Party logo"
|
||||
class="size-6"
|
||||
/>
|
||||
<span>
|
||||
Please select a framework to view framework's snippets
|
||||
</span>
|
||||
<img src="/popper.svg" alt="logo" class="size-6" />
|
||||
<img
|
||||
src="/popper.svg"
|
||||
alt="Component Party logo"
|
||||
class="size-6"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="space-y-20">
|
||||
{#each sections as section}
|
||||
{#each sections as section (section.sectionId)}
|
||||
<div class="px-6 md:px-14 lg:px-20 max-w-full">
|
||||
<h1
|
||||
<h2
|
||||
id={section.sectionId}
|
||||
class="header-anchor text-2xl font-bold"
|
||||
data-testid={`section-${section.sectionId}`}
|
||||
>
|
||||
{section.title}
|
||||
<a
|
||||
@@ -308,15 +333,20 @@
|
||||
>
|
||||
#
|
||||
</a>
|
||||
</h1>
|
||||
</h2>
|
||||
|
||||
<div class="space-y-8 mt-2">
|
||||
{#each snippets.filter((s) => s.sectionId === section.sectionId) as snippet}
|
||||
{#each snippets.filter((s) => s.sectionId === section.sectionId) as snippet (snippet.snippetId)}
|
||||
{@const snippetPathId =
|
||||
section.sectionId + "." + snippet.snippetId}
|
||||
<div id={snippetPathId} data-snippet-id={snippetPathId}>
|
||||
<h2
|
||||
<div
|
||||
id={snippetPathId}
|
||||
data-snippet-id={snippetPathId}
|
||||
data-testid={`snippet-${snippetPathId}`}
|
||||
>
|
||||
<h3
|
||||
class="header-anchor sticky py-2 top-[2.94rem] z-10 bg-[var(--bg-color)] font-semibold text-xl"
|
||||
data-testid={`snippet-title-${snippetPathId}`}
|
||||
>
|
||||
{snippet.title}
|
||||
<a
|
||||
@@ -326,145 +356,170 @@
|
||||
>
|
||||
#
|
||||
</a>
|
||||
</h2>
|
||||
</h3>
|
||||
{#if frameworkIdsSelectedInitialized}
|
||||
<div
|
||||
class="grid grid-cols-1 2xl:grid-cols-2 gap-10 mt-4"
|
||||
class="grid grid-cols-1 xl:grid-cols-2 gap-y-4 xl:gap-y-8 gap-x-10 mt-2"
|
||||
>
|
||||
{#each [...frameworkIdsSelected] as frameworkId (frameworkId)}
|
||||
{#each frameworkIdsSelectedArr as frameworkId (frameworkId)}
|
||||
{@const framework = matchFrameworkId(frameworkId)}
|
||||
{@const frameworkSnippet = snippetsByFrameworkId
|
||||
.get(frameworkId)
|
||||
?.find((s) => s.snippetId === snippet.snippetId)}
|
||||
?.find(
|
||||
(s: FrameworkSnippet) =>
|
||||
s.snippetId === snippet.snippetId,
|
||||
)}
|
||||
{@const frameworkSnippetIsLoading =
|
||||
snippetsByFrameworkIdLoading.has(frameworkId)}
|
||||
{@const frameworkSnippetIsError =
|
||||
snippetsByFrameworkIdError.has(frameworkId)}
|
||||
|
||||
<div>
|
||||
{#if framework}
|
||||
<div
|
||||
class="flex justify-between items-center space-x-3"
|
||||
data-testid={`framework-snippet-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<h3
|
||||
style="margin-top: 0rem; margin-bottom: 0rem;"
|
||||
<div
|
||||
class="flex justify-between items-center space-x-3"
|
||||
>
|
||||
<FrameworkLabel id={framework.id} />
|
||||
</h3>
|
||||
{#if frameworkSnippet}
|
||||
<div class="flex items-center space-x-3">
|
||||
{#if frameworkSnippet.playgroundURL}
|
||||
<a
|
||||
href={frameworkSnippet.playgroundURL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open playground for ${framework.title}`}
|
||||
>
|
||||
<button
|
||||
<h3
|
||||
class="m-0"
|
||||
data-testid={`framework-title-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<FrameworkLabel id={framework.id} />
|
||||
</h3>
|
||||
{#if frameworkSnippet}
|
||||
<div class="flex items-center space-x-3">
|
||||
{#if frameworkSnippet.playgroundURL}
|
||||
<a
|
||||
href={frameworkSnippet.playgroundURL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="opacity-50 hover:opacity-100 bg-gray-800 hover:bg-gray-700 py-1 px-2 rounded transition-all flex items-center gap-x-2"
|
||||
title={`Open playground for ${framework.title}`}
|
||||
aria-label={`Open playground for ${framework.title}`}
|
||||
tabindex="-1"
|
||||
data-testid={`playground-button-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<div
|
||||
class="i-heroicons:play size-4"
|
||||
></div>
|
||||
</button>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{#if frameworkSnippet}
|
||||
{#if frameworkSnippet.files.length > 0}
|
||||
<CodeEditor
|
||||
files={frameworkSnippet.files}
|
||||
snippetEditHref={frameworkSnippet.snippetEditHref}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-gray-800 text-white rounded-md mx-auto"
|
||||
>
|
||||
<span
|
||||
class="iconify ph--play size-4"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{#if frameworkSnippet}
|
||||
{#if frameworkSnippet.files.length > 0}
|
||||
<CodeEditor
|
||||
files={frameworkSnippet.files}
|
||||
snippetEditHref={frameworkSnippet.snippetEditHref}
|
||||
data-testid={`code-editor-${frameworkId}-${snippet.snippetId}`}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="text-center py-8 px-4 sm:px-6"
|
||||
class="bg-gray-800 text-white rounded-md mx-auto"
|
||||
data-testid={`missing-snippet-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="block text-2xl tracking-tight font-bold"
|
||||
>
|
||||
Missing snippet
|
||||
</span>
|
||||
<span
|
||||
class="block text-lg mt-1 font-semibold space-x-1"
|
||||
>
|
||||
<span>
|
||||
Help us to improve Component Party
|
||||
</span>
|
||||
<img
|
||||
src="/popper.svg"
|
||||
alt="logo"
|
||||
class="size-5 m-0 inline-block"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-center">
|
||||
<div
|
||||
class="inline-flex rounded-md shadow"
|
||||
>
|
||||
<a
|
||||
class="inline-flex space-x-2 items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-[#161b22] hover:bg-[#161b22]/80 no-underline"
|
||||
href={frameworkSnippet.snippetEditHref}
|
||||
<div
|
||||
class="text-center py-8 px-4 sm:px-6"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="block text-2xl tracking-tight font-bold"
|
||||
data-testid="missing-snippet-title"
|
||||
>
|
||||
<button
|
||||
class="flex items-center space-x-3"
|
||||
Missing snippet
|
||||
</span>
|
||||
<span
|
||||
class="block text-lg mt-1 font-semibold space-x-1"
|
||||
data-testid="missing-snippet-message"
|
||||
>
|
||||
<span>
|
||||
Help us to improve Component
|
||||
Party
|
||||
</span>
|
||||
<img
|
||||
src="/popper.svg"
|
||||
alt="logo"
|
||||
class="size-5 m-0 inline-block"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-center">
|
||||
<div
|
||||
class="inline-flex rounded-md shadow"
|
||||
>
|
||||
<a
|
||||
class="inline-flex space-x-2 items-center justify-center px-4 py-2 border border-transparent text-base font-medium rounded-md text-white bg-[#161b22] hover:bg-[#161b22]/80 no-underline"
|
||||
href={frameworkSnippet.snippetEditHref}
|
||||
data-testid={`contribute-link-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<span>Contribute on Github</span
|
||||
<button
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<GithubIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</a>
|
||||
<span
|
||||
>Contribute on Github</span
|
||||
>
|
||||
<span
|
||||
class="iconify simple-icons--github size-5"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if frameworkSnippetIsLoading}
|
||||
<div role="status">
|
||||
{/if}
|
||||
{:else if frameworkSnippetIsLoading}
|
||||
<div
|
||||
class="w-75px h-23px bg-[#0d1117] py-3 px-4 rounded-t"
|
||||
role="status"
|
||||
data-testid={`loading-snippet-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
<div
|
||||
class="h-2.5 rounded-full bg-gray-700 w-10 animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-164px bg-[#0d1117] px-4 py-7"
|
||||
>
|
||||
<div class="max-w-sm animate-pulse">
|
||||
class="w-75px h-23px bg-[#0d1117] py-3 px-4 rounded-t"
|
||||
>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 w-48 mb-4"
|
||||
class="h-2.5 rounded-full bg-gray-700 w-10 animate-pulse"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 max-w-[360px] mb-2.5"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 mb-4"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 max-w-[330px] mb-2.5"
|
||||
></div>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-164px bg-[#0d1117] px-4 py-7"
|
||||
>
|
||||
<div class="max-w-sm animate-pulse">
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 w-48 mb-4"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 max-w-[360px] mb-2.5"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 mb-4"
|
||||
></div>
|
||||
<div
|
||||
class="h-3.5 rounded-full bg-gray-700 max-w-[330px] mb-2.5"
|
||||
></div>
|
||||
<span
|
||||
class="sr-only"
|
||||
data-testid="loading-text"
|
||||
>Loading...</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if frameworkSnippetIsError}
|
||||
<p class="text-orange-500">
|
||||
Error loading snippets. Please reload the
|
||||
page.
|
||||
</p>
|
||||
{/if}
|
||||
{:else if frameworkSnippetIsError}
|
||||
<p
|
||||
class="text-orange-500"
|
||||
data-testid={`error-snippet-${frameworkId}-${snippet.snippetId}`}
|
||||
>
|
||||
Error loading snippets. Please reload the
|
||||
page.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -494,6 +549,6 @@
|
||||
}
|
||||
|
||||
.header-anchor:hover > a {
|
||||
opacity: 100;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<script>
|
||||
import { onMount, onDestroy, setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import { createRouter } from "radix3";
|
||||
|
||||
let { routes = [] } = $props();
|
||||
|
||||
const router = createRouter({
|
||||
routes: routes.reduce((acc, route) => {
|
||||
acc[route.path] = {
|
||||
...route,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
|
||||
const currentRoute = writable({ component: null });
|
||||
|
||||
function navigate(path, state) {
|
||||
state = state || {};
|
||||
const urlParsed = new URL(path, window.location.origin);
|
||||
const routePayload = router.lookup(urlParsed.pathname);
|
||||
|
||||
if (routePayload) {
|
||||
if (routePayload.component.toString().startsWith("class")) {
|
||||
currentRoute.set(routePayload);
|
||||
window.history.pushState(state, "", path);
|
||||
} else if (typeof routePayload.component === "function") {
|
||||
currentRoute.set({
|
||||
...routePayload,
|
||||
component: routePayload.component,
|
||||
});
|
||||
window.history.pushState(state, "", path);
|
||||
} else {
|
||||
console.error("Invalid route component");
|
||||
}
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
|
||||
window.onpopstate = () => {
|
||||
navigate(window.location.href);
|
||||
};
|
||||
|
||||
function handleClick(event) {
|
||||
const target = event.target.closest("a[href]");
|
||||
if (
|
||||
target &&
|
||||
target.getAttribute("href").startsWith("/") &&
|
||||
target.getAttribute("target") !== "_blank"
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigate(target.getAttribute("href"));
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClick);
|
||||
navigate(window.location.href, { isInitialNavigation: true });
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
});
|
||||
|
||||
setContext("router", {
|
||||
currentRoute,
|
||||
navigate,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $currentRoute.component}
|
||||
{@render $currentRoute.component()}
|
||||
{/if}
|
||||
14
src/app.css
14
src/app.css
@@ -1,3 +1,17 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@iconify/tailwind4";
|
||||
@plugin "@iconify/tailwind4" {
|
||||
prefix: "iconify";
|
||||
prefixes: ph, simple-icons;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: blur(10px)) {
|
||||
[class*="bg-"].backdrop-blur {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<script>
|
||||
import NotificationCenter from "./NotificationCenter.svelte";
|
||||
import TransitionWithClass from "./TransitionWithClass.svelte";
|
||||
</script>
|
||||
|
||||
<NotificationCenter zIndex={100}>
|
||||
{#snippet notificationContainer({ title, close })}
|
||||
<TransitionWithClass
|
||||
class="pointer-events-auto overflow-hidden rounded-lg bg-[#181622] border border-[#33323e] shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
enter="transform ease-out duration-200 transition"
|
||||
enterFrom="translate-y-2 opacity-0 translate-y-0 translate-x-2"
|
||||
enterTo="translate-y-0 opacity-100 translate-x-0"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="i-heroicons:check-circle size-6 text-green-400"></div>
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p class="text-sm font-medium">
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button
|
||||
class="inline-flex rounded-md bg-transparent focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
onclick={close}
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<div class="i-heroicons:x-mark size-5"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionWithClass>
|
||||
{/snippet}
|
||||
</NotificationCenter>
|
||||
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
import c from "classnames";
|
||||
<script lang="ts">
|
||||
import { sections, snippets } from "../generatedContent/tree.js";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import throttle from "../lib/throttle.js";
|
||||
import { onMount } from "svelte";
|
||||
import throttle from "just-throttle";
|
||||
|
||||
let largestVisibleSnippetId = $state(null);
|
||||
let largestVisibleSnippetId: string = $state("");
|
||||
|
||||
function getLargestElement(elements) {
|
||||
function getLargestElement(elements: NodeListOf<Element>): Element | null {
|
||||
let largestArea = 0;
|
||||
let largestElement = null;
|
||||
let largestElement: Element | null = null;
|
||||
let firstFullyVisibleElement: Element | null = null;
|
||||
|
||||
for (const element of elements) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -20,6 +20,19 @@
|
||||
if (visibleWidth > 0 && visibleHeight > 0) {
|
||||
const area = visibleWidth * visibleHeight;
|
||||
|
||||
// Check if element is fully visible
|
||||
const isFullyVisible =
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= window.innerHeight &&
|
||||
rect.right <= window.innerWidth;
|
||||
|
||||
// Prioritize first fully visible element
|
||||
if (isFullyVisible && !firstFullyVisibleElement) {
|
||||
firstFullyVisibleElement = element;
|
||||
}
|
||||
|
||||
// Track largest element as fallback
|
||||
if (area > largestArea) {
|
||||
largestArea = area;
|
||||
largestElement = element;
|
||||
@@ -27,18 +40,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
return largestElement;
|
||||
// Return first fully visible element if found, otherwise largest element
|
||||
return firstFullyVisibleElement || largestElement;
|
||||
}
|
||||
|
||||
function listenLargestSnippetOnScroll() {
|
||||
function scrollToElement(elementId: string) {
|
||||
const target = document.getElementById(elementId);
|
||||
if (target) {
|
||||
// Update URL hash
|
||||
window.history.pushState(null, "", `#${elementId}`);
|
||||
// Scroll to target
|
||||
target.scrollIntoView({ block: "start" });
|
||||
}
|
||||
}
|
||||
|
||||
onMount(function listenLargestSnippetOnScroll() {
|
||||
function onScroll() {
|
||||
const largestSnippet = getLargestElement(
|
||||
document.querySelectorAll("[data-snippet-id]")
|
||||
document.querySelectorAll("[data-snippet-id]"),
|
||||
);
|
||||
if (largestSnippet) {
|
||||
largestVisibleSnippetId = largestSnippet.dataset.snippetId;
|
||||
largestVisibleSnippetId =
|
||||
largestSnippet.getAttribute("data-snippet-id") ?? "";
|
||||
} else {
|
||||
largestVisibleSnippetId = null;
|
||||
largestVisibleSnippetId = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,55 +74,45 @@
|
||||
return () => {
|
||||
document.removeEventListener("scroll", throttleOnScroll);
|
||||
};
|
||||
}
|
||||
|
||||
let unlistenLargestSnippetOnScroll;
|
||||
|
||||
onMount(() => {
|
||||
unlistenLargestSnippetOnScroll = listenLargestSnippetOnScroll();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlistenLargestSnippetOnScroll && unlistenLargestSnippetOnScroll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="no-scroll hidden lg:block sticky flex-shrink-0 w-[300px] overflow-y-auto top-0 pr-8 max-h-screen border-r border-gray-700"
|
||||
class="no-scroll hidden lg:block sticky flex-shrink-0 w-[240px] overflow-y-auto top-0 pr-8 max-h-screen border-r border-gray-700"
|
||||
>
|
||||
<nav class="w-full text-base py-2 pl-4 pb-20">
|
||||
<ul class="space-y-6">
|
||||
{#each sections as section}
|
||||
{#each sections as section (section.sectionId)}
|
||||
<li>
|
||||
<a
|
||||
href={`#${section.sectionId}`}
|
||||
class={c(
|
||||
"inline-block w-full py-1.5 px-4 text-white font-semibold opacity-90 hover:opacity-100 hover:bg-gray-800 rounded transition-opacity",
|
||||
<button
|
||||
class={[
|
||||
"inline-block w-full py-1.5 px-4 text-white font-semibold opacity-90 hover:opacity-100 hover:bg-gray-800 rounded transition-opacity text-left",
|
||||
{
|
||||
"bg-gray-800":
|
||||
largestVisibleSnippetId &&
|
||||
largestVisibleSnippetId.startsWith(section.sectionId),
|
||||
}
|
||||
)}
|
||||
},
|
||||
]}
|
||||
onclick={() => scrollToElement(section.sectionId)}
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
</button>
|
||||
<ul>
|
||||
{#each snippets.filter((s) => s.sectionId === section.sectionId) as snippet}
|
||||
{#each snippets.filter((s: any) => s.sectionId === section.sectionId) as snippet (snippet.snippetId)}
|
||||
{@const snippetPathId =
|
||||
section.sectionId + "." + snippet.snippetId}
|
||||
<li>
|
||||
<a
|
||||
href={`#${snippetPathId}`}
|
||||
class={c(
|
||||
"inline-block w-full py-1.5 px-4 text-white font-medium hover:bg-gray-800 rounded hover:opacity-100 transition-opacity",
|
||||
<button
|
||||
class={[
|
||||
"inline-block w-full py-1.5 px-4 text-white font-medium hover:bg-gray-800 rounded hover:opacity-100 transition-opacity text-left",
|
||||
snippetPathId === largestVisibleSnippetId
|
||||
? "bg-gray-800 opacity-70"
|
||||
: "opacity-50"
|
||||
)}
|
||||
: "opacity-50",
|
||||
]}
|
||||
onclick={() => scrollToElement(snippetPathId)}
|
||||
>
|
||||
{snippet.title}
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,35 +1,52 @@
|
||||
<script>
|
||||
import c from "classnames";
|
||||
import { notifications } from "./NotificationCenter.svelte";
|
||||
import copyToClipboard from "../lib/copyToClipboard.js";
|
||||
<script lang="ts">
|
||||
import copyToClipboard from "../lib/copyToClipboard.ts";
|
||||
|
||||
const { files = [], snippetEditHref } = $props();
|
||||
interface File {
|
||||
fileName: string;
|
||||
contentHtml: string;
|
||||
}
|
||||
|
||||
let codeSnippetEl = $state();
|
||||
interface Props {
|
||||
files: File[];
|
||||
snippetEditHref?: string;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
let filenameSelected = $state(files.length > 0 && files[0]?.fileName);
|
||||
const {
|
||||
files = [],
|
||||
snippetEditHref,
|
||||
"data-testid": dataTestId,
|
||||
}: Props = $props();
|
||||
|
||||
const snippet = $derived(
|
||||
filenameSelected && files.find((s) => s.fileName === filenameSelected)
|
||||
let codeSnippetEl: HTMLElement | undefined = $state();
|
||||
|
||||
let filenameSelected: string | undefined = $state(
|
||||
files.length > 0 ? files[0]?.fileName : undefined,
|
||||
);
|
||||
|
||||
function copySnippet() {
|
||||
const snippet: File | undefined = $derived(
|
||||
filenameSelected
|
||||
? files.find((s) => s.fileName === filenameSelected)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
function copySnippet(): void {
|
||||
if (codeSnippetEl) {
|
||||
copyToClipboard(codeSnippetEl.innerText);
|
||||
notifications.show({
|
||||
title: "Snippet copied to clipboard",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 items-center ml-0 overflow-x-auto">
|
||||
<div
|
||||
class="flex space-x-1 items-center ml-0 overflow-x-auto"
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{#each files as file (file.fileName)}
|
||||
<button
|
||||
class={c(
|
||||
"bg-[#0d1117] py-1.5 px-3 flex-shrink-0 text-xs rounded-t inline-block",
|
||||
filenameSelected !== file.fileName && "opacity-60"
|
||||
)}
|
||||
class={[
|
||||
"bg-[#0d1117] py-1.5 px-3 flex-shrink-0 text-xs rounded-t inline-block transition-all duration-200 hover:opacity-100",
|
||||
filenameSelected !== file.fileName && "opacity-60",
|
||||
]}
|
||||
onclick={() => {
|
||||
filenameSelected = file.fileName;
|
||||
}}
|
||||
@@ -42,9 +59,12 @@
|
||||
<div class="relative group">
|
||||
<div
|
||||
bind:this={codeSnippetEl}
|
||||
class="bg-[#0d1117] px-4 py-3 text-sm overflow-auto"
|
||||
class="bg-[#0d1117] px-4 py-3 text-sm overflow-auto rounded-b rounded-tr"
|
||||
>
|
||||
{@html snippet.contentHtml}
|
||||
{#if snippet}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html snippet.contentHtml}
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="absolute hidden group-hover:block transition-all top-0 right-0 mt-2 mr-2"
|
||||
@@ -55,17 +75,17 @@
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Edit on Github"
|
||||
class="px-1.5 bg-[#0d1117] py-1 rounded border opacity-60 hover:opacity-90"
|
||||
class="bg-[#0d1117] rounded border opacity-60 hover:opacity-90 transition-all duration-200 p-1 flex items-center justify-center"
|
||||
>
|
||||
<div class="i-heroicons:pencil size-4"></div>
|
||||
<span class="iconify ph--pencil size-4" aria-hidden="true"></span>
|
||||
</a>
|
||||
<button
|
||||
class="px-1.5 bg-[#0d1117] py-1 rounded border opacity-60 hover:opacity-90"
|
||||
class="bg-[#0d1117] rounded border opacity-60 hover:opacity-90 transition-all duration-200 p-1 flex items-center justify-center"
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
onclick={copySnippet}
|
||||
>
|
||||
<div class="i-heroicons:clipboard-document size-4"></div>
|
||||
<span class="iconify ph--clipboard size-4" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
<script>
|
||||
import FRAMEWORKS from "../../frameworks.mjs";
|
||||
<script lang="ts">
|
||||
import { frameworks } from "@frameworks";
|
||||
import type { Framework } from "@frameworks";
|
||||
|
||||
let { id, size = 20 } = $props();
|
||||
interface Props {
|
||||
id: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const framework = $derived(FRAMEWORKS.find((f) => f.id === id));
|
||||
let { id, size = 20 }: Props = $props();
|
||||
|
||||
const baseURL = import.meta.env.DEV
|
||||
const framework: Framework | undefined = $derived(
|
||||
frameworks.find((f) => f.id === id),
|
||||
);
|
||||
|
||||
const baseURL: string = import.meta.env.DEV
|
||||
? "/"
|
||||
: "https://raw.githubusercontent.com/matschik/component-party/main/public/";
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
{#if framework?.img}
|
||||
<img
|
||||
src={baseURL + framework.img}
|
||||
width={size}
|
||||
height={size}
|
||||
class="inline mr-[5px] mb-0 mt-0"
|
||||
class="flex-shrink-0"
|
||||
alt={`logo of ${framework.title}`}
|
||||
/>
|
||||
{/if}
|
||||
<span class="flex-shrink-0">{framework.title}</span>
|
||||
<span class="flex-shrink-0 inline-block">{framework?.title || id}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<script>
|
||||
const props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,18 +1,33 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
<script lang="ts">
|
||||
import createLocaleStorage from "../lib/createLocaleStorage";
|
||||
import GithubIcon from "./GithubIcon.svelte";
|
||||
|
||||
interface StarCountStorageData {
|
||||
value: string;
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
interface ShieldsApiResponse {
|
||||
schemaVersion: number;
|
||||
label: string;
|
||||
message: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const REPOSITORY_PATH = "matschik/component-party.dev";
|
||||
const STAR_COUNT_EXPIRES_IN_MS = 1000 * 60 * 2;
|
||||
const STAR_COUNT_EXPIRES_IN_MS = 1000 * 60 * 5; // Shields.io caches for 5-15 minutes
|
||||
|
||||
const starCountStorage = createLocaleStorage("github-star-count");
|
||||
const starCountStorage = createLocaleStorage("github-star-count-v2", {
|
||||
value: "0",
|
||||
fetchedAt: 0,
|
||||
});
|
||||
|
||||
let starCount = $state(0);
|
||||
let isFetchingStarCount = $state(false);
|
||||
let starCount: string = $state("0");
|
||||
let isFetchingStarCount: boolean = $state(false);
|
||||
|
||||
async function getRepoStarCount(): Promise<void> {
|
||||
const starCountStorageData: StarCountStorageData | null =
|
||||
starCountStorage.getJSON() as StarCountStorageData | null;
|
||||
|
||||
async function getRepoStarCount() {
|
||||
const starCountStorageData = starCountStorage.getJSON();
|
||||
if (starCountStorageData) {
|
||||
starCount = starCountStorageData.value;
|
||||
if (
|
||||
@@ -25,35 +40,38 @@
|
||||
|
||||
isFetchingStarCount = true;
|
||||
|
||||
// Github public API rate limit: 60 requests per hour
|
||||
const data = await fetch(
|
||||
`https://api.github.com/repos/${REPOSITORY_PATH}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3.star+json",
|
||||
Authorization: "",
|
||||
try {
|
||||
// Using Shields.io JSON endpoint - no rate limits, cached for 5-15 minutes
|
||||
const data: ShieldsApiResponse = await fetch(
|
||||
`https://img.shields.io/github/stars/${REPOSITORY_PATH}.json`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
isFetchingStarCount = false;
|
||||
})
|
||||
.then((r) => r.json());
|
||||
)
|
||||
.finally(() => {
|
||||
isFetchingStarCount = false;
|
||||
})
|
||||
.then((r) => r.json());
|
||||
|
||||
if (data.stargazers_count) {
|
||||
starCount = data.stargazers_count;
|
||||
starCountStorage.setJSON({
|
||||
value: starCount,
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
if (data.message) {
|
||||
// Use the formatted string directly from Shields.io (e.g., "3.1k", "500")
|
||||
starCount = data.message;
|
||||
starCountStorage.setJSON({
|
||||
value: starCount,
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to fetch star count from Shields.io:", error);
|
||||
// Keep the existing cached value if available
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getRepoStarCount();
|
||||
});
|
||||
getRepoStarCount();
|
||||
|
||||
function onButtonClick() {
|
||||
function onButtonClick(): void {
|
||||
starCountStorage.remove();
|
||||
}
|
||||
</script>
|
||||
@@ -66,34 +84,21 @@
|
||||
onclick={onButtonClick}
|
||||
>
|
||||
<span class="flex items-center px-3 sm:space-x-2">
|
||||
<GithubIcon class="size-[1.3rem] sm:size-[1.1rem]" />
|
||||
<span
|
||||
class="iconify simple-icons--github size-[1.3rem] sm:size-[1.1rem]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="hidden sm:inline">Star</span>
|
||||
</span>
|
||||
{#if isFetchingStarCount || starCount !== 0}
|
||||
{#if isFetchingStarCount || starCount !== "0"}
|
||||
<div
|
||||
class="hidden h-full items-center justify-center px-3 sm:flex border-[#373b43] sm:border-l"
|
||||
>
|
||||
{#if isFetchingStarCount && starCount === 0}
|
||||
<svg
|
||||
class="animate-spin size-4 mx-1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
{#if isFetchingStarCount && starCount === "0"}
|
||||
<span
|
||||
class="iconify ph--spinner animate-spin size-4 mx-1"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{:else}
|
||||
<span>{starCount}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<script>
|
||||
import { notifications } from "./NotificationCenter.svelte";
|
||||
<script lang="ts">
|
||||
import GithubStarButton from "./GithubStarButton.svelte";
|
||||
import copyToClipboard from "../lib/copyToClipboard.js";
|
||||
|
||||
let { frameworksSelected = [] } = $props();
|
||||
|
||||
function copyShareLink() {
|
||||
if (frameworksSelected.length === 0) {
|
||||
return;
|
||||
}
|
||||
let shareURL = `${location.origin}`;
|
||||
if (frameworksSelected.length === 2) {
|
||||
shareURL += `/compare/${[...frameworksSelected].map((f) => f.id).join("-vs-")}`;
|
||||
} else {
|
||||
shareURL += `?f=${[...frameworksSelected].map((f) => f.id).join(",")}`;
|
||||
}
|
||||
copyToClipboard(shareURL);
|
||||
notifications.show({
|
||||
title: `Framework selection link copied with ${[...frameworksSelected].map((f) => f.title).join(", ")}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="backdrop-blur bg-gray-900/80 border-b border-gray-700">
|
||||
@@ -27,21 +7,10 @@
|
||||
<div class="flex justify-between items-center py-3">
|
||||
<a class="font-semibold text-lg flex items-center space-x-3" href="/">
|
||||
<img src="/popper.svg" alt="logo" class="size-5" />
|
||||
<span>Component party</span>
|
||||
<h1>Component Party</h1>
|
||||
</a>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
{#if frameworksSelected.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center space-x-2 rounded border border-gray-700 border-opacity-50 bg-gray-900 px-3 py-1 text-sm text-white transition-all hover:bg-gray-800"
|
||||
aria-label="Copy framework selection link"
|
||||
onclick={copyShareLink}
|
||||
>
|
||||
<div class="i-heroicons:link size-[1.3rem] sm:size-[1.1rem]"></div>
|
||||
<span class="hidden sm:inline">Share</span>
|
||||
</button>
|
||||
{/if}
|
||||
<GithubStarButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<script module>
|
||||
import { writable } from "svelte/store";
|
||||
const { subscribe, update } = writable([]);
|
||||
|
||||
const DISMISS_TIMEOUT_DEFAULT = 4 * 1000;
|
||||
|
||||
export const notifications = {
|
||||
subscribe,
|
||||
show(notification) {
|
||||
notification = {
|
||||
...notification,
|
||||
dismissAfter: DISMISS_TIMEOUT_DEFAULT,
|
||||
close() {
|
||||
notifications.dismiss(this);
|
||||
},
|
||||
};
|
||||
update((notifications) => [notification, ...notifications]);
|
||||
},
|
||||
dismiss(notification) {
|
||||
update((notifications) =>
|
||||
notifications.filter((t) => t !== notification)
|
||||
);
|
||||
},
|
||||
dismissAll() {
|
||||
update(() => []);
|
||||
},
|
||||
};
|
||||
|
||||
export function addNotificationsMethod(method, cb) {
|
||||
notifications[method] = (...args) => {
|
||||
const notification = cb(...args);
|
||||
notifications.show(notification);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let { zIndex = 20, notificationContainer } = $props();
|
||||
|
||||
function dismissAfter(_, notification) {
|
||||
notification.dismissAfter &&
|
||||
setTimeout(
|
||||
() => notifications.dismiss(notification),
|
||||
notification.dismissAfter
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="notifications-container" style={`z-index: ${zIndex};`}>
|
||||
<ul class="notifications-list space-y-4">
|
||||
{#each $notifications as notification (notification)}
|
||||
<li use:dismissAfter={notification}>
|
||||
{@render notificationContainer(notification)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.notifications-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 2.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.notifications-container {
|
||||
margin: 1.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.notifications-list {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 20rem;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user