Skip to content

Commit 7707137

Browse files
committed
refactor(frameworks): split framework families from versions
- Model families ({ id, name, latestStable }) separately from versions (FrameworkVersion), making a "multiple latest-stable" state unrepresentable - Generic names now resolve to the current stable: svelte -> Svelte 5, ember -> Ember Polaris, angular -> Angular Renaissance - matchFrameworkId resolves family ids before version ids - Validate config at module load (unique ids + latestStable references exist) - Migrate all consumers to frameworkVersions / FrameworkVersion
1 parent 90f7a0f commit 7707137

9 files changed

Lines changed: 100 additions & 124 deletions

build/generateRedirectsVitePlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
33
import type { Plugin, ResolvedConfig } from "vite";
4-
import { frameworks } from "../frameworks.ts";
4+
import { frameworkVersions } from "../frameworks.ts";
55

66
// canonicalPairs() uses the @frameworks alias which resolves at runtime via
77
// the alias configured in svelte.config.js. For the Vite plugin (Node context)
88
// we resolve it directly from the raw frameworks import, mirroring the same logic.
99
function buildCanonicalPairs(): [string, string][] {
10-
const ids = frameworks.map((f) => f.id);
10+
const ids = frameworkVersions.map((f) => f.id);
1111
const pairs: [string, string][] = [];
1212
for (let i = 0; i < ids.length; i++) {
1313
for (let j = i + 1; j < ids.length; j++) {

build/lib/generateContent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs/promises";
22
import { packageDirectory } from "package-directory";
33
import path from "node:path";
4-
import { frameworks } from "../../frameworks.ts";
4+
import { frameworkVersions } from "../../frameworks.ts";
55
import playgroundUrlByFramework from "./playgroundUrlByFramework.ts";
66
import prettier from "prettier";
77
import { mustUseAngularHighlighter } from "./angularHighlighter.ts";
@@ -165,7 +165,7 @@ export default async function generateContent(options: { noCache?: boolean } = {
165165
});
166166

167167
const frameworksDirPath = path.join(snippetsDirPath, snippetDirName);
168-
const frameworkIds = frameworks.map(({ id }) => id);
168+
const frameworkIds = frameworkVersions.map(({ id }) => id);
169169

170170
await Promise.all(
171171
frameworkIds.map(async (frameworkId: string) => {
@@ -217,7 +217,7 @@ export default async function generateContent(options: { noCache?: boolean } = {
217217
}
218218

219219
if (frameworkSnippet.files.length > 0) {
220-
const frameworkConfig = frameworks.find((f) => f.id === frameworkId);
220+
const frameworkConfig = frameworkVersions.find((f) => f.id === frameworkId);
221221
if (frameworkConfig) {
222222
frameworkSnippet.files = frameworkConfig.filesSorter(
223223
frameworkSnippet.files as unknown as FrameworkFile[],
@@ -362,7 +362,7 @@ async function generatePlaygroundURL(
362362
return;
363363
}
364364

365-
const frameworkConfig = frameworks.find((f) => f.id === frameworkId);
365+
const frameworkConfig = frameworkVersions.find((f) => f.id === frameworkId);
366366
if (!frameworkConfig) {
367367
return;
368368
}

frameworks.ts

Lines changed: 79 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ function sortAllFilenames(files: File[], filenamesSorted: string[]): File[] {
1010
].filter(Boolean) as File[];
1111
}
1212

13-
export interface Framework {
13+
/**
14+
* A specific released version of a framework, e.g. "Svelte 5" or "Vue 3".
15+
*/
16+
export interface FrameworkVersion {
1417
id: string;
1518
title: string;
16-
frameworkName: string;
17-
frameworkNameId: string;
18-
isLatestStable: boolean;
1919
img: string;
2020
playgroundURL: string;
2121
documentationURL: string;
@@ -25,13 +25,22 @@ export interface Framework {
2525
releaseDate: string;
2626
}
2727

28-
export const frameworks: Framework[] = [
28+
/**
29+
* A framework family, e.g. "Svelte". The generic name (`id`) resolves to the
30+
* version referenced by `latestStable`, which is the single source of truth for
31+
* "which version is the current stable one" — making multiple-stable states
32+
* unrepresentable.
33+
*/
34+
export interface Framework {
35+
id: string;
36+
name: string;
37+
latestStable: string;
38+
}
39+
40+
export const frameworkVersions: FrameworkVersion[] = [
2941
{
3042
id: "svelte5",
3143
title: "Svelte 5",
32-
frameworkName: "Svelte",
33-
frameworkNameId: "svelte",
34-
isLatestStable: false,
3544
img: "framework/svelte.svg",
3645
playgroundURL: "https://svelte.dev/playground",
3746
documentationURL: "https://svelte.dev",
@@ -45,9 +54,6 @@ export const frameworks: Framework[] = [
4554
{
4655
id: "react",
4756
title: "React",
48-
frameworkName: "React",
49-
frameworkNameId: "react",
50-
isLatestStable: true,
5157
img: "framework/react.svg",
5258
playgroundURL: "https://codesandbox.io/s/mystifying-goldberg-6wx04b",
5359
documentationURL: "https://reactjs.org",
@@ -61,9 +67,6 @@ export const frameworks: Framework[] = [
6167
{
6268
id: "vue3",
6369
title: "Vue 3",
64-
frameworkName: "Vue",
65-
frameworkNameId: "vue",
66-
isLatestStable: true,
6770
img: "framework/vue.svg",
6871
playgroundURL: "https://play.vuejs.org/",
6972
documentationURL: "https://vuejs.org/guide",
@@ -77,9 +80,6 @@ export const frameworks: Framework[] = [
7780
{
7881
id: "angularRenaissance",
7982
title: "Angular Renaissance",
80-
frameworkName: "Angular",
81-
frameworkNameId: "angular",
82-
isLatestStable: true,
8383
img: "framework/angular-renaissance.svg",
8484
playgroundURL: "https://codesandbox.io/s/angular",
8585
documentationURL: "https://angular.io/docs",
@@ -98,9 +98,6 @@ export const frameworks: Framework[] = [
9898
{
9999
id: "angular",
100100
title: "Angular",
101-
frameworkName: "Angular",
102-
frameworkNameId: "angular",
103-
isLatestStable: false,
104101
img: "framework/angular.svg",
105102
playgroundURL: "https://codesandbox.io/s/angular",
106103
documentationURL: "https://angular.io/docs",
@@ -119,9 +116,6 @@ export const frameworks: Framework[] = [
119116
{
120117
id: "lit",
121118
title: "Lit",
122-
frameworkName: "Lit",
123-
frameworkNameId: "lit",
124-
isLatestStable: true,
125119
img: "framework/lit.svg",
126120
playgroundURL: "https://lit.dev/playground",
127121
documentationURL: "https://lit.dev",
@@ -135,9 +129,6 @@ export const frameworks: Framework[] = [
135129
{
136130
id: "emberPolaris",
137131
title: "Ember Polaris",
138-
frameworkName: "Ember",
139-
frameworkNameId: "ember",
140-
isLatestStable: false,
141132
img: "framework/ember.svg",
142133
playgroundURL: "http://new.emberjs.com",
143134
documentationURL: "https://emberjs.com",
@@ -151,9 +142,6 @@ export const frameworks: Framework[] = [
151142
{
152143
id: "emberOctane",
153144
title: "Ember Octane",
154-
frameworkName: "Ember",
155-
frameworkNameId: "ember",
156-
isLatestStable: true,
157145
img: "framework/ember.svg",
158146
playgroundURL: "https://ember-twiddle.com",
159147
documentationURL: "https://emberjs.com",
@@ -167,9 +155,6 @@ export const frameworks: Framework[] = [
167155
{
168156
id: "solid",
169157
title: "Solid.js",
170-
frameworkName: "Solid",
171-
frameworkNameId: "solid",
172-
isLatestStable: true,
173158
img: "framework/solid.svg",
174159
playgroundURL: "https://playground.solidjs.com/",
175160
documentationURL: "https://www.solidjs.com/",
@@ -183,9 +168,6 @@ export const frameworks: Framework[] = [
183168
{
184169
id: "svelte4",
185170
title: "Svelte 4",
186-
frameworkName: "Svelte",
187-
frameworkNameId: "svelte",
188-
isLatestStable: true,
189171
img: "framework/svelte.svg",
190172
playgroundURL: "https://svelte.dev/repl",
191173
documentationURL: "https://svelte.dev/",
@@ -199,9 +181,6 @@ export const frameworks: Framework[] = [
199181
{
200182
id: "vue2",
201183
title: "Vue 2",
202-
frameworkName: "Vue",
203-
frameworkNameId: "vue",
204-
isLatestStable: false,
205184
img: "framework/vue.svg",
206185
playgroundURL: "",
207186
documentationURL: "https://v2.vuejs.org",
@@ -215,9 +194,6 @@ export const frameworks: Framework[] = [
215194
{
216195
id: "alpine",
217196
title: "Alpine",
218-
frameworkName: "Alpine",
219-
frameworkNameId: "alpine",
220-
isLatestStable: true,
221197
img: "framework/alpine.svg",
222198
playgroundURL: "https://codesandbox.io/s/7br3q8",
223199
documentationURL: "https://alpinejs.dev/start-here",
@@ -231,9 +207,6 @@ export const frameworks: Framework[] = [
231207
{
232208
id: "mithril",
233209
title: "Mithril",
234-
frameworkName: "Mithril",
235-
frameworkNameId: "mithril",
236-
isLatestStable: true,
237210
img: "framework/mithril.svg",
238211
playgroundURL: "https://codesandbox.io/s/q99qzov66",
239212
documentationURL: "https://mithril.js.org/",
@@ -247,9 +220,6 @@ export const frameworks: Framework[] = [
247220
{
248221
id: "aurelia2",
249222
title: "Aurelia 2",
250-
frameworkName: "Aurelia",
251-
frameworkNameId: "aurelia",
252-
isLatestStable: true,
253223
img: "framework/aurelia.svg",
254224
playgroundURL: "https://stackblitz.com/edit/au2-conventions?file=src%2Fmy-app.html",
255225
documentationURL: "http://docs.aurelia.io",
@@ -263,9 +233,6 @@ export const frameworks: Framework[] = [
263233
{
264234
id: "qwik",
265235
title: "Qwik",
266-
frameworkName: "Qwik",
267-
frameworkNameId: "qwik",
268-
isLatestStable: true,
269236
img: "framework/qwik.svg",
270237
playgroundURL: "https://qwik.builder.io/playground",
271238
documentationURL: "https://qwik.builder.io/docs/overview",
@@ -279,9 +246,6 @@ export const frameworks: Framework[] = [
279246
{
280247
id: "marko",
281248
title: "Marko",
282-
frameworkName: "Marko",
283-
frameworkNameId: "marko",
284-
isLatestStable: true,
285249
img: "framework/marko.svg",
286250
playgroundURL: "https://markojs.com/playground/",
287251
documentationURL: "https://markojs.com/docs/getting-started/",
@@ -295,9 +259,6 @@ export const frameworks: Framework[] = [
295259
{
296260
id: "aurelia1",
297261
title: "Aurelia 1",
298-
frameworkName: "Aurelia",
299-
frameworkNameId: "aurelia",
300-
isLatestStable: false,
301262
img: "framework/aurelia.svg",
302263
playgroundURL: "https://codesandbox.io/s/ppmy26opw7",
303264
documentationURL: "http://aurelia.io/docs/",
@@ -311,9 +272,6 @@ export const frameworks: Framework[] = [
311272
{
312273
id: "ripple",
313274
title: "Ripple",
314-
frameworkName: "Ripple",
315-
frameworkNameId: "ripple",
316-
isLatestStable: true,
317275
img: "framework/ripple.svg",
318276
playgroundURL: "https://www.ripple-ts.com/playground",
319277
documentationURL: "https://www.ripple-ts.com/",
@@ -326,62 +284,80 @@ export const frameworks: Framework[] = [
326284
},
327285
];
328286

329-
export function matchFrameworkId(id: string): Framework | undefined {
330-
// First try to find by exact ID
331-
let framework = frameworks.find((f) => f.id === id);
287+
export const frameworks: Framework[] = [
288+
{ id: "svelte", name: "Svelte", latestStable: "svelte5" },
289+
{ id: "react", name: "React", latestStable: "react" },
290+
{ id: "vue", name: "Vue", latestStable: "vue3" },
291+
{ id: "angular", name: "Angular", latestStable: "angularRenaissance" },
292+
{ id: "lit", name: "Lit", latestStable: "lit" },
293+
{ id: "ember", name: "Ember", latestStable: "emberPolaris" },
294+
{ id: "solid", name: "Solid", latestStable: "solid" },
295+
{ id: "alpine", name: "Alpine", latestStable: "alpine" },
296+
{ id: "mithril", name: "Mithril", latestStable: "mithril" },
297+
{ id: "aurelia", name: "Aurelia", latestStable: "aurelia2" },
298+
{ id: "qwik", name: "Qwik", latestStable: "qwik" },
299+
{ id: "marko", name: "Marko", latestStable: "marko" },
300+
{ id: "ripple", name: "Ripple", latestStable: "ripple" },
301+
];
302+
303+
/**
304+
* Invariants, validated at module load so a misconfiguration fails fast instead
305+
* of silently resolving to the wrong version:
306+
* - version ids are unique,
307+
* - framework (family) ids are unique,
308+
* - each family's `latestStable` references an existing version.
309+
*/
310+
function assertValidFrameworksConfig(versions: FrameworkVersion[], families: Framework[]): void {
311+
const errors: string[] = [];
312+
313+
const versionIds = new Set<string>();
314+
for (const { id } of versions) {
315+
if (versionIds.has(id)) errors.push(`duplicate version id "${id}"`);
316+
versionIds.add(id);
317+
}
332318

333-
// If not found, try to find by framework name ID and return the latest stable version
334-
if (!framework) {
335-
const latestStable = getLatestStableFrameworkByFrameworkName(id);
336-
if (latestStable) {
337-
framework = latestStable;
319+
const familyIds = new Set<string>();
320+
for (const family of families) {
321+
if (familyIds.has(family.id)) errors.push(`duplicate framework id "${family.id}"`);
322+
familyIds.add(family.id);
323+
if (!versionIds.has(family.latestStable)) {
324+
errors.push(
325+
`framework "${family.id}".latestStable points to unknown version "${family.latestStable}"`,
326+
);
338327
}
339328
}
340329

341-
return framework;
330+
if (errors.length > 0) {
331+
throw new Error(`Invalid frameworks configuration:\n - ${errors.join("\n - ")}`);
332+
}
342333
}
343334

344-
/**
345-
* Get all frameworks that belong to a specific framework name
346-
*/
347-
export function getFrameworksByFrameworkName(frameworkNameId: string): Framework[] {
348-
return frameworks.filter((framework) => framework.frameworkNameId === frameworkNameId);
349-
}
335+
assertValidFrameworksConfig(frameworkVersions, frameworks);
350336

351337
/**
352-
* Get the latest stable framework for a given framework name
338+
* Resolve an id to a concrete framework version. Accepts either a framework
339+
* family id (e.g. "svelte" or "angular") or a version id (e.g. "svelte5").
340+
*
341+
* Family resolution takes precedence over version resolution: a generic name
342+
* always maps to its latest stable version, even when a (legacy) version shares
343+
* that id — e.g. "angular" resolves to Angular Renaissance, not the legacy
344+
* "angular" version.
353345
*/
354-
export function getLatestStableFrameworkByFrameworkName(
355-
frameworkNameId: string,
356-
): Framework | undefined {
357-
return frameworks.find(
358-
(framework) => framework.frameworkNameId === frameworkNameId && framework.isLatestStable,
359-
);
360-
}
346+
export function matchFrameworkId(id: string): FrameworkVersion | undefined {
347+
const family = frameworks.find((f) => f.id === id);
348+
if (family) {
349+
const stable = frameworkVersions.find((v) => v.id === family.latestStable);
350+
if (stable) return stable;
351+
}
361352

362-
/**
363-
* Get all unique framework name IDs
364-
*/
365-
export function getFrameworkNameIds(): string[] {
366-
return [...new Set(frameworks.map((framework) => framework.frameworkNameId))];
353+
return frameworkVersions.find((v) => v.id === id);
367354
}
368355

369356
/**
370-
* Get framework name information including all versions and latest stable
357+
* Get the latest stable version of a framework family, e.g. "svelte" -> Svelte 5.
371358
*/
372-
export function getFrameworkNameInfo(frameworkNameId: string): {
373-
frameworkNameId: string;
374-
frameworks: Framework[];
375-
latestStable: Framework | undefined;
376-
allVersions: string[];
377-
} {
378-
const familyFrameworks = getFrameworksByFrameworkName(frameworkNameId);
379-
const latestStable = getLatestStableFrameworkByFrameworkName(frameworkNameId);
380-
381-
return {
382-
frameworkNameId,
383-
frameworks: familyFrameworks,
384-
latestStable,
385-
allVersions: familyFrameworks.map((f) => f.id),
386-
};
359+
export function getLatestStableVersion(frameworkId: string): FrameworkVersion | undefined {
360+
const family = frameworks.find((f) => f.id === frameworkId);
361+
if (!family) return undefined;
362+
return frameworkVersions.find((v) => v.id === family.latestStable);
387363
}

0 commit comments

Comments
 (0)