diff --git a/README.md b/README.md index d6880f6..1e582fa 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ about a page than the **UI library** underneath it, which says more than the | | Recognised by | Version | | --- | --- | --- | | **Next.js** | `window.next`, `__NEXT_DATA__`, `self.__next_f`, `/_next/static/`, the `webpackChunk_N_E` registry | **exact**, off `window.next.version` | +| **Fresh** | Fresh 1's `/_frsh/` runtime assets and `__FRSH_STATE`; Fresh 2's `f-client-nav` / `f-partial` attributes | `≤ 1` or `≥ 2`, from the runtime marker | | **Nuxt** | `useNuxtApp`, `__NUXT__`, `__buildAssetsURL`, `/_nuxt/` | none | | **SvelteKit** | `__sveltekit_…`, `/_app/immutable/`, `data-sveltekit-preload-data` | none | | **Astro** | `` and its runtime, `astro:page-load`, `/_astro/`, `data-astro-cid-…` | **exact** from the generator meta tag | @@ -187,10 +188,10 @@ property called `version`, being reported as a release. ### Absorbed evidence -Next.js *is* React and ships webpack or Turbopack. Reporting all three as -separate findings would be technically true and useless, so a meta-framework -absorbs what is intrinsic to it and the popup shows those traces as chips under -the card, evidence and all. +Next.js *is* React and ships webpack or Turbopack; Fresh is built on Preact. +Reporting those implementation details as separate findings would be technically +true and useless, so a meta-framework absorbs what is intrinsic to it and the +popup shows those traces as chips under the card, evidence and all. Only intrinsic relationships are folded in. React on webpack is a *choice*, not a fact about React, so those stay two findings — and Astro keeps its islands @@ -332,6 +333,7 @@ Every icon is the project's own artwork. | rspack | the site favicon, `https://assets.rspack.rs/rspack/favicon-128x128.png` | | turbopack | the Turbo mark from the [vercel/turborepo](https://github.com/vercel/turborepo) README | | astro | the mark from `https://astro.build/favicon.svg` | +| fresh | the [official Fresh SVG](https://github.com/freshframework/fresh/blob/main/www/static/logo.svg) | | knockout | the [Knockout organisation](https://github.com/knockout) avatar on GitHub — the same script K as their favicon, at 460 px instead of 16 | | sveltekit | the Svelte mark, which is SvelteKit's own branding too — the toolbar cannot tell the two apart, the popup can | diff --git a/icons/fresh.png b/icons/fresh.png new file mode 100644 index 0000000..b6fc8e8 Binary files /dev/null and b/icons/fresh.png differ diff --git a/icons/src/fresh.svg b/icons/src/fresh.svg new file mode 100644 index 0000000..a3ed230 --- /dev/null +++ b/icons/src/fresh.svg @@ -0,0 +1 @@ + diff --git a/src/signatures/meta.js b/src/signatures/meta.js index ce6d544..c729711 100644 --- a/src/signatures/meta.js +++ b/src/signatures/meta.js @@ -17,6 +17,46 @@ const relations = (root.StackRelations = root.StackRelations || []); list.push( + { + id: 'fresh', + name: 'Fresh', + category: 'meta-framework', + color: '#f7c948', + home: 'https://usefresh.dev', + rules: [ + // Fresh 1 serves its client runtime and island chunks from this + // reserved directory. seen: Fresh 1 documentation and starter output + { + id: 'fresh1-asset-url', + where: ['url', 'html'], + re: /\/_frsh\/(?:js\/|refresh\.js)/, + weight: STRONG, + max: 1, + desc: '/_frsh/ Fresh 1 runtime asset directory', + }, + // Fresh 1 serializes island props into this uniquely named script. + // seen: Fresh 1 counter starter output + { + id: 'fresh1-state', + where: ['dom', 'html'], + str: '__FRSH_STATE', + weight: STRONG, + max: 1, + desc: '__FRSH_STATE island hydration payload (Fresh 1)', + }, + // Fresh 2's built-in partial navigation is expressed directly in + // markup, including on the framework's own site. seen: usefresh.dev + { + id: 'fresh2-navigation-attrs', + where: ['dom', 'html'], + re: /\bf-(?:client-nav|partial|partial-mode|partial-target)\b/, + weight: STRONG, + min: 2, + desc: 'f-client-nav / f-partial Fresh 2 navigation attribute', + }, + ], + }, + { id: 'next', name: 'Next.js', @@ -498,6 +538,11 @@ * finding and stays its own card. */ relations.push( + { + id: 'fresh', + builtOn: ['preact'], + note: 'Fresh is built on Preact, so Preact runtime markers are attributed to Fresh.', + }, { id: 'next', builtOn: ['react', 'turbopack', 'webpack'], diff --git a/test/fixtures/fresh/page.json b/test/fixtures/fresh/page.json new file mode 100644 index 0000000..7dace38 --- /dev/null +++ b/test/fixtures/fresh/page.json @@ -0,0 +1,7 @@ +{ + "markup": "
", + "dom": "script#__FRSH_STATE\nlink[href=\"/_frsh/js/8da4be105acc5e866d96dbd1367699fea082142b/main.js\"]", + "props": [], + "urls": ["https://example.test/_frsh/js/8da4be105acc5e866d96dbd1367699fea082142b/main.js"], + "globals": [] +} diff --git a/test/run.mjs b/test/run.mjs index 1e8d547..6ad3f6e 100644 --- a/test/run.mjs +++ b/test/run.mjs @@ -90,6 +90,9 @@ const CASES = [ { dir: 'knockout', primary: 'knockout', version: '3.5.3', also: ['jquery'], only: true }, { dir: 'ember', primary: 'ember', version: null, also: ['webpack'], only: true }, { dir: 'stimulus', primary: 'stimulus', version: null, only: true }, + // Fresh 1's island state and runtime asset path are server-rendered page + // markers; Preact, when present, is intrinsic and gets folded in. + { dir: 'fresh', primary: 'fresh', version: '≤ 1', only: true }, // ---- meta-frameworks ------------------------------------------------ // The UI framework and the bundler are intrinsic to each of these, so they @@ -250,6 +253,23 @@ describe('signature engine', () => { assert.deepStrictEqual(detections.map((d) => d.id), []); }); + it('unrelated f-prefixed attributes are not Fresh', () => { + const { detections } = analyze({ + sources: [{ kind: 'dom', label: 'page DOM', text: '
' }], + globals: [], + }); + assert.deepStrictEqual(detections.map((d) => d.id), []); + }); + + it('Fresh 2 navigation attributes', () => { + const { detections } = analyze({ + sources: [{ kind: 'dom', label: 'page DOM', text: '