From 7dde6153a1c82c82e9f44ec70238d6f05c2c0ffd Mon Sep 17 00:00:00 2001 From: swwind Date: Sun, 23 Aug 2026 16:33:02 +0800 Subject: [PATCH 1/2] feat: detect Fresh --- icons/fresh.png | Bin 0 -> 3834 bytes icons/src/fresh.svg | 1 + src/signatures/meta.js | 45 ++++++++++++++++++++++++++++++++++ test/fixtures/fresh/page.json | 7 ++++++ test/run.mjs | 20 +++++++++++++++ 5 files changed, 73 insertions(+) create mode 100644 icons/fresh.png create mode 100644 icons/src/fresh.svg create mode 100644 test/fixtures/fresh/page.json diff --git a/icons/fresh.png b/icons/fresh.png new file mode 100644 index 0000000000000000000000000000000000000000..b6fc8e891d8c803f7833cdb2bf96a2ef0d62afe0 GIT binary patch literal 3834 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 literal 0 HcmV?d00001 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 a705fde..b5c1444 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 ba33f42..705a55a 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: '