Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** | `<astro-island>` and its runtime, `astro:page-load`, `/_astro/`, `data-astro-cid-…` | **exact** from the generator meta tag |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
Binary file added icons/fresh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions icons/src/fresh.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions src/signatures/meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'],
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/fresh/page.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"markup": "<!doctype html><html><head><link rel=\"modulepreload\" href=\"/_frsh/js/8da4be105acc5e866d96dbd1367699fea082142b/main.js\"></head><body><!--frsh-counter:0--><div><button>+1</button></div><!--/frsh-counter:0--><script id=\"__FRSH_STATE\" type=\"application/json\">{\"v\":[]}</script></body></html>",
"dom": "script#__FRSH_STATE\nlink[href=\"/_frsh/js/8da4be105acc5e866d96dbd1367699fea082142b/main.js\"]",
"props": [],
"urls": ["https://example.test/_frsh/js/8da4be105acc5e866d96dbd1367699fea082142b/main.js"],
"globals": []
}
20 changes: 20 additions & 0 deletions test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: '<form f-action="save" f-state="dirty">' }],
globals: [],
});
assert.deepStrictEqual(detections.map((d) => d.id), []);
});

it('Fresh 2 navigation attributes', () => {
const { detections } = analyze({
sources: [{ kind: 'dom', label: 'page DOM', text: '<main f-client-nav><button f-partial="/recipes/lemonade">' }],
globals: [],
});
assert.deepStrictEqual(detections.map((d) => d.id), ['fresh']);
assert.strictEqual(detections[0].version.text, '≥ 2');
});

// window globals alone should be enough, with no script bodies at all.
it('globals-only', () => {
const { detections } = analyze({ sources: [], globals: ['webpackChunkmyapp'] });
Expand Down
Loading