([\s\S]*?)<\/code><\/pre>/gm;
const code = regexForWrapper.exec(html);
- return code[2];
+ return code ? code[2] : "";
}
diff --git a/build/lib/componentPartyShikiTheme.js b/build/lib/componentPartyShikiTheme.ts
similarity index 98%
rename from build/lib/componentPartyShikiTheme.js
rename to build/lib/componentPartyShikiTheme.ts
index 8c57958..e8aff1e 100644
--- a/build/lib/componentPartyShikiTheme.js
+++ b/build/lib/componentPartyShikiTheme.ts
@@ -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;
diff --git a/build/lib/generateContent.js b/build/lib/generateContent.js
deleted file mode 100644
index d9be044..0000000
--- a/build/lib/generateContent.js
+++ /dev/null
@@ -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;
-}
diff --git a/build/lib/generateContent.ts b/build/lib/generateContent.ts
new file mode 100644
index 0000000..891ed36
--- /dev/null
+++ b/build/lib/generateContent.ts
@@ -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 {
+ try {
+ await fs.access(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function shouldRegenerateFiles(
+ contentPath: string,
+ generatedContentDirPath: string,
+ frameworkDirPath: string,
+): Promise {
+ // 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 {
+ 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 = {};
+
+ 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;
+ };
+
+ 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 {
+ const codeFormatted = await prettier.format(jsCode, { parser: "babel" });
+ await fs.writeFile(filepath, codeFormatted);
+}
+
+async function writeDtsFile(filepath: string, dtsCode: string): Promise {
+ const codeFormatted = await prettier.format(dtsCode, {
+ parser: "typescript",
+ });
+ await fs.writeFile(filepath, codeFormatted);
+}
+
+async function generatePlaygroundURL(
+ frameworkId: string,
+ files: File[],
+ title: string,
+): Promise {
+ 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, 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 {
+ // 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`,
+ );
+}
diff --git a/build/lib/highlighter.d.ts b/build/lib/highlighter.d.ts
new file mode 100644
index 0000000..0f6dbf1
--- /dev/null
+++ b/build/lib/highlighter.d.ts
@@ -0,0 +1,8 @@
+export function codeToHighlightCodeHtml(
+ code: string,
+ language: string,
+): Promise;
+
+export function markdownToHighlightedHtml(markdown: string): Promise;
+
+export function disposeHighlighter(): Promise;
diff --git a/build/lib/highlighter.js b/build/lib/highlighter.js
deleted file mode 100644
index 2ce6336..0000000
--- a/build/lib/highlighter.js
+++ /dev/null
@@ -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;
-}
diff --git a/build/lib/highlighter.ts b/build/lib/highlighter.ts
new file mode 100644
index 0000000..5b66fca
--- /dev/null
+++ b/build/lib/highlighter.ts
@@ -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 | null =
+ null;
+let md: MarkdownIt | null = null;
+let highlighterPromise: Promise<
+ HighlighterGeneric
+> | null = null;
+
+async function getHighlighter(): Promise<
+ HighlighterGeneric
+> {
+ 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 {
+ 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 {
+ const highlighterInstance = await getHighlighter();
+ const html = await highlighterInstance.codeToHtml(code, {
+ lang,
+ theme: componentPartyShikiTheme.name,
+ });
+
+ return html;
+}
+
+export async function markdownToHighlightedHtml(
+ markdownText: string,
+): Promise {
+ 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 {
+ if (highlighter) {
+ await highlighter.dispose();
+ highlighter = null;
+ }
+ highlighterPromise = null;
+ md = null;
+}
diff --git a/build/lib/playgroundUrlByFramework.js b/build/lib/playgroundUrlByFramework.ts
similarity index 68%
rename from build/lib/playgroundUrlByFramework.js
rename to build/lib/playgroundUrlByFramework.ts
index cfdd5dd..b7098c6 100644
--- a/build/lib/playgroundUrlByFramework.js
+++ b/build/lib/playgroundUrlByFramework.ts
@@ -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,
+ title?: string,
+ ): string | Promise;
+}
+
+interface SveltePlaygroundOptions {
+ version: number;
+ contentByFilename: Record;
+ 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 = {
+ vue3: (contentByFilename: Record) => {
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,
+ title?: string,
+ ) => {
return generateSveltePlaygroundURL({
version: 4,
contentByFilename,
title,
});
},
- svelte5: async (contentByFilename, title) => {
+ svelte5: async (
+ contentByFilename: Record,
+ title?: string,
+ ) => {
return generateSveltePlaygroundURL({
version: 5,
contentByFilename,
title,
});
},
- alpine: (contentByFilename) => {
+ alpine: (contentByFilename: Record) => {
const BASE_URL =
"https://codesandbox.io/api/v1/sandboxes/define?embed=1¶meters=";
const BASE_PREFIX = `\n\n \n \n \n \n Alpine.js Playground \n \n \n \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) => {
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) => {
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 {
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 {
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 {
const stream = new CompressionStream("gzip");
const writer = stream.writable.getWriter();
writer.write(new TextEncoder().encode(value));
diff --git a/build/template/footer.html b/build/template/footer.html
index 7e4c930..7f04f70 100644
--- a/build/template/footer.html
+++ b/build/template/footer.html
@@ -7,7 +7,7 @@
Component party
-
+
Web component JavaScript frameworks overview by their syntax and
features
diff --git a/content/2-templating/2-styling/mithril/CssStyle.js b/content/2-templating/2-styling/mithril/CssStyle.js
index 4cc1ad4..1483cf6 100644
--- a/content/2-templating/2-styling/mithril/CssStyle.js
+++ b/content/2-templating/2-styling/mithril/CssStyle.js
@@ -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"),
),
};
}
diff --git a/content/2-templating/3-loop/lit/colors-list.js b/content/2-templating/3-loop/lit/colors-list.js
index dc083d5..7692ec1 100644
--- a/content/2-templating/3-loop/lit/colors-list.js
+++ b/content/2-templating/3-loop/lit/colors-list.js
@@ -12,7 +12,7 @@ export class ColorsList extends LitElement {
${repeat(
this.colors,
(color) => color,
- (color) => html`${color} `
+ (color) => html`${color} `,
)}
`;
diff --git a/content/2-templating/3-loop/mithril/Colors.js b/content/2-templating/3-loop/mithril/Colors.js
index 40e522d..ec86fcc 100644
--- a/content/2-templating/3-loop/mithril/Colors.js
+++ b/content/2-templating/3-loop/mithril/Colors.js
@@ -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)),
),
};
}
diff --git a/content/2-templating/3-loop/vue2/Colors.vue b/content/2-templating/3-loop/vue2/Colors.vue
index 193cbe2..cdcf916 100644
--- a/content/2-templating/3-loop/vue2/Colors.vue
+++ b/content/2-templating/3-loop/vue2/Colors.vue
@@ -10,10 +10,7 @@ export default {
- -
+
-
{{ color }}
diff --git a/content/2-templating/3-loop/vue3/Colors.vue b/content/2-templating/3-loop/vue3/Colors.vue
index 19dab14..62ad119 100644
--- a/content/2-templating/3-loop/vue3/Colors.vue
+++ b/content/2-templating/3-loop/vue3/Colors.vue
@@ -4,10 +4,7 @@ const colors = ["red", "green", "blue"];
- -
+
-
{{ color }}
diff --git a/content/2-templating/4-event-click/mithril/Counter.js b/content/2-templating/4-event-click/mithril/Counter.js
index fda54d1..7708c9e 100644
--- a/content/2-templating/4-event-click/mithril/Counter.js
+++ b/content/2-templating/4-event-click/mithril/Counter.js
@@ -8,7 +8,7 @@ export default function Counter() {
m(
"div",
m("p", `Counter: ${count}`),
- m("button", { onclick: incrementCount }, "+1")
+ m("button", { onclick: incrementCount }, "+1"),
),
};
}
diff --git a/content/2-templating/5-dom-ref/angularRenaissance/inputfocused.component.ts b/content/2-templating/5-dom-ref/angularRenaissance/inputfocused.component.ts
index 04091f6..d62cba9 100644
--- a/content/2-templating/5-dom-ref/angularRenaissance/inputfocused.component.ts
+++ b/content/2-templating/5-dom-ref/angularRenaissance/inputfocused.component.ts
@@ -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",
diff --git a/content/2-templating/5-dom-ref/vue2/InputFocused.vue b/content/2-templating/5-dom-ref/vue2/InputFocused.vue
index 596e344..c006b77 100644
--- a/content/2-templating/5-dom-ref/vue2/InputFocused.vue
+++ b/content/2-templating/5-dom-ref/vue2/InputFocused.vue
@@ -7,5 +7,5 @@ export default {
-
+
diff --git a/content/2-templating/5-dom-ref/vue3/InputFocused.vue b/content/2-templating/5-dom-ref/vue3/InputFocused.vue
index 55398e3..1fdffb3 100644
--- a/content/2-templating/5-dom-ref/vue3/InputFocused.vue
+++ b/content/2-templating/5-dom-ref/vue3/InputFocused.vue
@@ -9,5 +9,5 @@ onMounted(() => {
-
+
diff --git a/content/2-templating/6-conditional/mithril/TrafficLight.js b/content/2-templating/6-conditional/mithril/TrafficLight.js
index df1fc88..ff8dd93 100644
--- a/content/2-templating/6-conditional/mithril/TrafficLight.js
+++ b/content/2-templating/6-conditional/mithril/TrafficLight.js
@@ -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())),
),
};
}
diff --git a/content/3-lifecycle/2-on-unmount/angularRenaissance/time.component.ts b/content/3-lifecycle/2-on-unmount/angularRenaissance/time.component.ts
index 69360dd..3f3b994 100644
--- a/content/3-lifecycle/2-on-unmount/angularRenaissance/time.component.ts
+++ b/content/3-lifecycle/2-on-unmount/angularRenaissance/time.component.ts
@@ -9,7 +9,7 @@ export class TimeComponent implements OnDestroy {
timer = setInterval(
() => this.time.set(new Date().toLocaleTimeString()),
- 1000
+ 1000,
);
ngOnDestroy() {
diff --git a/content/4-component-composition/1-props/mithril/UserProfile.js b/content/4-component-composition/1-props/mithril/UserProfile.js
index b7d0028..a2e9e00 100644
--- a/content/4-component-composition/1-props/mithril/UserProfile.js
+++ b/content/4-component-composition/1-props/mithril/UserProfile.js
@@ -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"}`),
),
});
diff --git a/content/4-component-composition/1-props/solid/UserProfile.jsx b/content/4-component-composition/1-props/solid/UserProfile.jsx
index 6b9fa0d..cd24673 100644
--- a/content/4-component-composition/1-props/solid/UserProfile.jsx
+++ b/content/4-component-composition/1-props/solid/UserProfile.jsx
@@ -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 (
diff --git a/content/4-component-composition/2-emit-to-parent/mithril/AnswerButton.js b/content/4-component-composition/2-emit-to-parent/mithril/AnswerButton.js
index 111a286..95e661a 100644
--- a/content/4-component-composition/2-emit-to-parent/mithril/AnswerButton.js
+++ b/content/4-component-composition/2-emit-to-parent/mithril/AnswerButton.js
@@ -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"),
),
});
diff --git a/content/4-component-composition/2-emit-to-parent/mithril/App.js b/content/4-component-composition/2-emit-to-parent/mithril/App.js
index 7d1ea02..4471d67 100644
--- a/content/4-component-composition/2-emit-to-parent/mithril/App.js
+++ b/content/4-component-composition/2-emit-to-parent/mithril/App.js
@@ -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 }),
),
};
}
diff --git a/content/4-component-composition/2-emit-to-parent/vue2/App.vue b/content/4-component-composition/2-emit-to-parent/vue2/App.vue
index 99125c1..69781de 100644
--- a/content/4-component-composition/2-emit-to-parent/vue2/App.vue
+++ b/content/4-component-composition/2-emit-to-parent/vue2/App.vue
@@ -23,10 +23,7 @@ export default {
Are you happy?
-
+
{{ isHappy ? "😀" : "😥" }}
diff --git a/content/4-component-composition/2-emit-to-parent/vue3/App.vue b/content/4-component-composition/2-emit-to-parent/vue3/App.vue
index 43575fe..8e22042 100644
--- a/content/4-component-composition/2-emit-to-parent/vue3/App.vue
+++ b/content/4-component-composition/2-emit-to-parent/vue3/App.vue
@@ -15,10 +15,7 @@ function onAnswerYes() {
Are you happy?
-
+
{{ isHappy ? "😀" : "😥" }}
diff --git a/content/4-component-composition/3-slot/mithril/FunnyButton.js b/content/4-component-composition/3-slot/mithril/FunnyButton.js
index 03cef6c..e0070ca 100644
--- a/content/4-component-composition/3-slot/mithril/FunnyButton.js
+++ b/content/4-component-composition/3-slot/mithril/FunnyButton.js
@@ -18,6 +18,6 @@ export const FunnyButton = () => ({
outline: "0",
},
},
- m(children)
+ m(children),
),
});
diff --git a/content/4-component-composition/4-slot-fallback/mithril/FunnyButton.js b/content/4-component-composition/4-slot-fallback/mithril/FunnyButton.js
index ffa2520..140c63c 100644
--- a/content/4-component-composition/4-slot-fallback/mithril/FunnyButton.js
+++ b/content/4-component-composition/4-slot-fallback/mithril/FunnyButton.js
@@ -17,6 +17,6 @@ export const FunnyButton = ({ children }) => ({
outline: "0",
},
},
- children || m("span", "No content found")
+ children || m("span", "No content found"),
),
});
diff --git a/content/4-component-composition/5-context/mithril/App.js b/content/4-component-composition/5-context/mithril/App.js
index 68226ac..f0d80c8 100644
--- a/content/4-component-composition/5-context/mithril/App.js
+++ b/content/4-component-composition/5-context/mithril/App.js
@@ -15,7 +15,7 @@ export default function App() {
m(
"",
m("h1", `Welcome Back, ${user.username}`),
- m(UserProfile, { user, updateUsername })
+ m(UserProfile, { user, updateUsername }),
),
};
}
diff --git a/content/4-component-composition/5-context/mithril/UserProfile.js b/content/4-component-composition/5-context/mithril/UserProfile.js
index 8c13499..a4a2d35 100644
--- a/content/4-component-composition/5-context/mithril/UserProfile.js
+++ b/content/4-component-composition/5-context/mithril/UserProfile.js
@@ -15,8 +15,8 @@ export default function UserProfile() {
m(
"button",
{ onclick: () => updateUsername("Jane") },
- "Update username to Jane"
- )
+ "Update username to Jane",
+ ),
),
};
}
diff --git a/content/6-form-input/1-input-text/vue2/InputHello.vue b/content/6-form-input/1-input-text/vue2/InputHello.vue
index 329d244..44b2d59 100644
--- a/content/6-form-input/1-input-text/vue2/InputHello.vue
+++ b/content/6-form-input/1-input-text/vue2/InputHello.vue
@@ -11,6 +11,6 @@ export default {
{{ text }}
-
+
diff --git a/content/6-form-input/1-input-text/vue3/InputHello.vue b/content/6-form-input/1-input-text/vue3/InputHello.vue
index 8c1fa4d..60dd2b1 100644
--- a/content/6-form-input/1-input-text/vue3/InputHello.vue
+++ b/content/6-form-input/1-input-text/vue3/InputHello.vue
@@ -5,5 +5,5 @@ const text = ref("Hello World");
{{ text }}
-
+
diff --git a/content/6-form-input/2-checkbox/mithril/IsAvailable.js b/content/6-form-input/2-checkbox/mithril/IsAvailable.js
index 65df2bd..442d0c0 100644
--- a/content/6-form-input/2-checkbox/mithril/IsAvailable.js
+++ b/content/6-form-input/2-checkbox/mithril/IsAvailable.js
@@ -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"),
),
};
}
diff --git a/content/6-form-input/2-checkbox/vue2/IsAvailable.vue b/content/6-form-input/2-checkbox/vue2/IsAvailable.vue
index 7c34a1e..997b3c7 100644
--- a/content/6-form-input/2-checkbox/vue2/IsAvailable.vue
+++ b/content/6-form-input/2-checkbox/vue2/IsAvailable.vue
@@ -10,11 +10,7 @@ export default {
-
+
diff --git a/content/6-form-input/2-checkbox/vue3/IsAvailable.vue b/content/6-form-input/2-checkbox/vue3/IsAvailable.vue
index 96e9124..7583b54 100644
--- a/content/6-form-input/2-checkbox/vue3/IsAvailable.vue
+++ b/content/6-form-input/2-checkbox/vue3/IsAvailable.vue
@@ -5,10 +5,6 @@ const isAvailable = ref(true);
-
+
diff --git a/content/6-form-input/3-radio/mithril/PickPill.js b/content/6-form-input/3-radio/mithril/PickPill.js
index a303117..d158389 100644
--- a/content/6-form-input/3-radio/mithril/PickPill.js
+++ b/content/6-form-input/3-radio/mithril/PickPill.js
@@ -20,9 +20,9 @@ export default function PickPill() {
value: pill,
onchange: handleChange,
}),
- m("label", { for: pill }, pill)
- )
- )
+ m("label", { for: pill }, pill),
+ ),
+ ),
),
};
}
diff --git a/content/6-form-input/3-radio/vue2/PickPill.vue b/content/6-form-input/3-radio/vue2/PickPill.vue
index 18a19b5..9351f02 100644
--- a/content/6-form-input/3-radio/vue2/PickPill.vue
+++ b/content/6-form-input/3-radio/vue2/PickPill.vue
@@ -12,20 +12,10 @@ export default {
Picked: {{ picked }}
-
+
-
+
diff --git a/content/6-form-input/3-radio/vue3/PickPill.vue b/content/6-form-input/3-radio/vue3/PickPill.vue
index 4c533dd..8769f52 100644
--- a/content/6-form-input/3-radio/vue3/PickPill.vue
+++ b/content/6-form-input/3-radio/vue3/PickPill.vue
@@ -7,19 +7,9 @@ const picked = ref("red");
Picked: {{ picked }}
-
+
-
+
diff --git a/content/6-form-input/4-select/angularRenaissance/color-select.component.ts b/content/6-form-input/4-select/angularRenaissance/color-select.component.ts
index c1d073e..ba5eae6 100644
--- a/content/6-form-input/4-select/angularRenaissance/color-select.component.ts
+++ b/content/6-form-input/4-select/angularRenaissance/color-select.component.ts
@@ -6,7 +6,7 @@ import { FormsModule } from "@angular/forms";
selector: "app-color-select",
template: `
`;
diff --git a/content/6-form-input/4-select/mithril/ColorSelect.js b/content/6-form-input/4-select/mithril/ColorSelect.js
index c28d843..5d8179e 100644
--- a/content/6-form-input/4-select/mithril/ColorSelect.js
+++ b/content/6-form-input/4-select/mithril/ColorSelect.js
@@ -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),
+ ),
),
};
}
diff --git a/content/7-webapp-features/1-render-app/react/main.jsx b/content/7-webapp-features/1-render-app/react/main.jsx
index 0bd4746..65821b6 100644
--- a/content/7-webapp-features/1-render-app/react/main.jsx
+++ b/content/7-webapp-features/1-render-app/react/main.jsx
@@ -5,5 +5,5 @@ import App from "./App";
ReactDOM.createRoot(document.getElementById("app")).render(
-
+ ,
);
diff --git a/content/7-webapp-features/2-fetch-data/aurelia2/app.ts b/content/7-webapp-features/2-fetch-data/aurelia2/app.ts
index 580d9f1..5d6080a 100644
--- a/content/7-webapp-features/2-fetch-data/aurelia2/app.ts
+++ b/content/7-webapp-features/2-fetch-data/aurelia2/app.ts
@@ -1,6 +1,5 @@
import { UseFetchUsers } from "./UseFetchUsers";
export class App {
- // eslint-disable-next-line no-unused-vars
constructor(private useFetchUsers: UseFetchUsers) {}
}
diff --git a/content/7-webapp-features/2-fetch-data/lit/x-app.js b/content/7-webapp-features/2-fetch-data/lit/x-app.js
index 0ed628e..1e023cb 100644
--- a/content/7-webapp-features/2-fetch-data/lit/x-app.js
+++ b/content/7-webapp-features/2-fetch-data/lit/x-app.js
@@ -18,7 +18,7 @@ export class XApp extends LitElement {
${user.name.first} ${user.name.last}
- `
+ `,
)}
`,
diff --git a/content/7-webapp-features/2-fetch-data/mithril/App.js b/content/7-webapp-features/2-fetch-data/mithril/App.js
index c07ee25..edb5cba 100644
--- a/content/7-webapp-features/2-fetch-data/mithril/App.js
+++ b/content/7-webapp-features/2-fetch-data/mithril/App.js
@@ -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}`),
+ ),
);
},
};
diff --git a/content/7-webapp-features/2-fetch-data/vue2/App.vue b/content/7-webapp-features/2-fetch-data/vue2/App.vue
index 5d2ccb6..3771eef 100644
--- a/content/7-webapp-features/2-fetch-data/vue2/App.vue
+++ b/content/7-webapp-features/2-fetch-data/vue2/App.vue
@@ -33,14 +33,8 @@ export default {
Fetching users...
An error ocurred while fetching users
- -
-
+ -
+
{{ user.name.first }}
{{ user.name.last }}
diff --git a/content/7-webapp-features/2-fetch-data/vue3/App.vue b/content/7-webapp-features/2-fetch-data/vue3/App.vue
index 8f078e4..ba94919 100644
--- a/content/7-webapp-features/2-fetch-data/vue3/App.vue
+++ b/content/7-webapp-features/2-fetch-data/vue3/App.vue
@@ -8,14 +8,8 @@ const { isLoading, error, data: users } = useFetchUsers();
Fetching users...
An error ocurred while fetching users
- -
-
+ -
+
{{ user.name.first }}
{{ user.name.last }}
diff --git a/cypress.config.ts b/cypress.config.ts
deleted file mode 100644
index 7d03d4f..0000000
--- a/cypress.config.ts
+++ /dev/null
@@ -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",
- },
- },
-});
diff --git a/cypress/e2e/spec.cy.ts b/cypress/e2e/spec.cy.ts
deleted file mode 100644
index eec67b2..0000000
--- a/cypress/e2e/spec.cy.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-///
-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"
-// );
-// });
-// })
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
deleted file mode 100644
index 02e4254..0000000
--- a/cypress/fixtures/example.json
+++ /dev/null
@@ -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"
-}
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
deleted file mode 100644
index 95857ae..0000000
--- a/cypress/support/commands.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-///
-// ***********************************************
-// 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
-// drag(subject: string, options?: Partial): Chainable
-// dismiss(subject: string, options?: Partial): Chainable
-// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
-// }
-// }
-// }
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
deleted file mode 100644
index 6a173d6..0000000
--- a/cypress/support/e2e.ts
+++ /dev/null
@@ -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')
diff --git a/eslint.config.ts b/eslint.config.ts
new file mode 100644
index 0000000..4b6f290
--- /dev/null
+++ b/eslint.config.ts
@@ -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"),
+];
diff --git a/frameworks.mjs b/frameworks.ts
similarity index 52%
rename from frameworks.mjs
rename to frameworks.ts
index 962485f..53db0b0 100644
--- a/frameworks.mjs
+++ b/frameworks.ts
@@ -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),
+ };
+}
diff --git a/index.html b/index.html
index a99a1a6..8c7cd4b 100644
--- a/index.html
+++ b/index.html
@@ -1,9 +1,12 @@
-
+
+
<%= it.title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -22,6 +50,14 @@
+
+
+
+
+
@@ -29,6 +65,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Router.svelte b/src/Router.svelte
deleted file mode 100644
index 7c2adec..0000000
--- a/src/Router.svelte
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-{#if $currentRoute.component}
- {@render $currentRoute.component()}
-{/if}
diff --git a/src/app.css b/src/app.css
index 7bccd01..15a3254 100644
--- a/src/app.css
+++ b/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);
diff --git a/src/components/AppNotificationCenter.svelte b/src/components/AppNotificationCenter.svelte
deleted file mode 100644
index 9b3dae1..0000000
--- a/src/components/AppNotificationCenter.svelte
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- {#snippet notificationContainer({ title, close })}
-
-
-
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
- {/snippet}
-
diff --git a/src/components/Aside.svelte b/src/components/Aside.svelte
index f461363..4e13314 100644
--- a/src/components/Aside.svelte
+++ b/src/components/Aside.svelte
@@ -1,14 +1,14 @@
-