@_linked/server 2.24.1 → 2.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Node's own `import()`, out of reach of any bundler or SSR transform.
3
+ *
4
+ * Under an app's dev SSR runner this package is itself evaluated by Vite, and
5
+ * Vite's SSR transform rewrites every `import()` it can see into its own
6
+ * module loader — `@vite-ignore` only silences the warning, it does not stop
7
+ * the rewrite. An installed package's backend loaded that way becomes a second,
8
+ * Vite-evaluated instance alongside the one Node already loaded for the app's
9
+ * own imports, and both register their shapes (whichever registers last wins).
10
+ *
11
+ * Building the call with the Function constructor keeps it out of the source
12
+ * any transform parses, so it always resolves through Node's ESM loader and
13
+ * its cache: one instance per resolved file.
14
+ *
15
+ * The source carries a per-evaluation nonce. V8 reuses compiled
16
+ * Function-constructor code by source text across vm contexts, and a dynamic
17
+ * import in reused code resolves against the context that compiled it first —
18
+ * under a test runner that runs several files in one worker, that is an
19
+ * earlier, torn-down file. A unique source gives each evaluation of this
20
+ * module its own compilation.
21
+ */
22
+ export declare const nativeImport: (specifier: string) => Promise<any>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Node's own `import()`, out of reach of any bundler or SSR transform.
3
+ *
4
+ * Under an app's dev SSR runner this package is itself evaluated by Vite, and
5
+ * Vite's SSR transform rewrites every `import()` it can see into its own
6
+ * module loader — `@vite-ignore` only silences the warning, it does not stop
7
+ * the rewrite. An installed package's backend loaded that way becomes a second,
8
+ * Vite-evaluated instance alongside the one Node already loaded for the app's
9
+ * own imports, and both register their shapes (whichever registers last wins).
10
+ *
11
+ * Building the call with the Function constructor keeps it out of the source
12
+ * any transform parses, so it always resolves through Node's ESM loader and
13
+ * its cache: one instance per resolved file.
14
+ *
15
+ * The source carries a per-evaluation nonce. V8 reuses compiled
16
+ * Function-constructor code by source text across vm contexts, and a dynamic
17
+ * import in reused code resolves against the context that compiled it first —
18
+ * under a test runner that runs several files in one worker, that is an
19
+ * earlier, torn-down file. A unique source gives each evaluation of this
20
+ * module its own compilation.
21
+ */
22
+ export const nativeImport = new Function('specifier', `return import(specifier); // ${Date.now()}-${Math.random()}`);
23
+ //# sourceMappingURL=nativeImport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeImport.js","sourceRoot":"","sources":["../../../src/utils/nativeImport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,QAAQ,CACtC,WAAW,EACX,gCAAgC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CACvB,CAAC","sourcesContent":["/**\n * Node's own `import()`, out of reach of any bundler or SSR transform.\n *\n * Under an app's dev SSR runner this package is itself evaluated by Vite, and\n * Vite's SSR transform rewrites every `import()` it can see into its own\n * module loader — `@vite-ignore` only silences the warning, it does not stop\n * the rewrite. An installed package's backend loaded that way becomes a second,\n * Vite-evaluated instance alongside the one Node already loaded for the app's\n * own imports, and both register their shapes (whichever registers last wins).\n *\n * Building the call with the Function constructor keeps it out of the source\n * any transform parses, so it always resolves through Node's ESM loader and\n * its cache: one instance per resolved file.\n *\n * The source carries a per-evaluation nonce. V8 reuses compiled\n * Function-constructor code by source text across vm contexts, and a dynamic\n * import in reused code resolves against the context that compiled it first —\n * under a test runner that runs several files in one worker, that is an\n * earlier, torn-down file. A unique source gives each evaluation of this\n * module its own compilation.\n */\nexport const nativeImport = new Function(\n 'specifier',\n `return import(specifier); // ${Date.now()}-${Math.random()}`\n) as (specifier: string) => Promise<any>;\n"]}
package/package.json CHANGED
@@ -10,13 +10,18 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "2.24.1",
13
+ "version": "2.24.3",
14
14
  "packageManager": "npm@11.21.0",
15
15
  "linkedPackage": true,
16
16
  "type": "module",
17
17
  "main": "lib/esm/index.js",
18
18
  "types": "index.d.ts",
19
19
  "license": "ISC",
20
+ "files": [
21
+ "lib",
22
+ "styles",
23
+ "CHANGELOG.md"
24
+ ],
20
25
  "scripts": {
21
26
  "start": "npm exec linked dev",
22
27
  "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --config jest.config.cjs",
@@ -1,20 +0,0 @@
1
- # Changesets
2
-
3
- This package uses [changesets](https://github.com/changesets/changesets) for version management and publishing.
4
-
5
- ## Adding a changeset
6
-
7
- When making a change that should trigger a new release, run:
8
-
9
- ```bash
10
- npx changeset
11
- ```
12
-
13
- This generates a markdown file in `.changeset/` describing the change. Commit it with your PR.
14
-
15
- On merge to `main`, the publish workflow either:
16
-
17
- 1. Creates (or updates) a **Release PR** that bumps the version + updates `CHANGELOG.md`, or
18
- 2. If the Release PR is already merged, publishes the new version to npm.
19
-
20
- Changes that don't need a release (docs, CI config, tooling) can skip the changeset — the check is a warning, not a block.
@@ -1,14 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3
- "changelog": [
4
- "@changesets/changelog-github",
5
- { "repo": "linked-fw/server" }
6
- ],
7
- "commit": false,
8
- "fixed": [],
9
- "linked": [],
10
- "access": "public",
11
- "baseBranch": "main",
12
- "updateInternalDependencies": "patch",
13
- "ignore": []
14
- }
package/.gitattributes DELETED
@@ -1 +0,0 @@
1
- package-lock.json linguist-generated=true -diff merge=ours
@@ -1,18 +0,0 @@
1
- name: PR
2
-
3
- on:
4
- pull_request:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- checks:
10
- uses: linked-fw/.github/.github/workflows/pr.yml@v1
11
- # The changeset reminder posts a PR comment. A reusable workflow only holds what its
12
- # caller granted, and the org default is read-only.
13
- permissions:
14
- contents: read
15
- pull-requests: write
16
- with:
17
- require-tests: true
18
- run-e2e: false
@@ -1,30 +0,0 @@
1
- # Calls the shared workflow in linked-fw/.github.
2
- #
3
- # This FILENAME is what npm's trusted-publisher check validates — not the shared workflow's.
4
- # Renaming this file invalidates this package's trusted publisher, and the only symptom is
5
- # that releases quietly start needing manual approval again.
6
- name: Publish
7
-
8
- on:
9
- push:
10
- branches:
11
- - main
12
-
13
- concurrency: ${{ github.workflow }}-${{ github.ref }}
14
-
15
- jobs:
16
- release:
17
- uses: linked-fw/.github/.github/workflows/publish.yml@v1
18
- permissions:
19
- contents: write
20
- pull-requests: write
21
- # Needed HERE as well as in the shared workflow: a reusable workflow cannot widen the
22
- # permissions its caller was granted, and without this npm never gets an id-token and
23
- # every release silently falls back to the approval queue.
24
- id-token: write
25
- # Named rather than `inherit`: `inherit` forwards everything this repo can see, which is
26
- # how a community repo would end up forwarding a first-party token.
27
- secrets:
28
- NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
29
- RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
30
- RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
@@ -1,58 +0,0 @@
1
- ---
2
- summary: Why this package declares its own server-only surface rather than being named by
3
- consumers, what the declaration means, and the one part of this package that is not server-only
4
- and should not live here.
5
- ---
6
-
7
- # This package's server-only surface
8
-
9
- This package is a **replaceable module**. An application may swap it for another implementation,
10
- and anything that depends on knowing it by name breaks the moment someone does.
11
-
12
- That matters for one specific rule: **a frontend bundle must not contain this package.** Almost
13
- everything here touches a filesystem, a process or a socket, and a `ShapeProvider` extends
14
- `BackendProvider`, which imports node's `path`. Pull any of it into a client graph and the bundler
15
- resolves `path` to the abandoned npm shim of the same name, the shim references `process`, and the
16
- application hydrates into an empty shell with `ReferenceError: process is not defined` — an error
17
- naming nothing that would lead you back to the import.
18
-
19
- ## The declaration
20
-
21
- ```jsonc
22
- "linked": {
23
- "serverOnly": [".", "./shapes/*", "./server/*", "./utils/*"]
24
- }
25
- ```
26
-
27
- A consuming application's client/server boundary check reads this, rather than hardcoding
28
- `@_linked/server`. The difference is not stylistic: a check that names this package protects one
29
- arrangement of the framework, and a replacement inherits none of it. A check that reads
30
- declarations protects whatever is installed.
31
-
32
- It is purely declarative. No code, no exports, and no resolution behaviour depends on it.
33
-
34
- ## The convention it complements
35
-
36
- A consumer also treats anything under `<pkg>/backend/**` as server-only, in **any** package, with
37
- no declaration needed. Between the two, no application has to know a package by name.
38
-
39
- A package whose server-only code all lives under `backend/` therefore needs no `linked.serverOnly`
40
- at all. This package declares one because its server-only surface predates that convention and is
41
- spread across several subpaths.
42
-
43
- ## What is not server-only, and should move
44
-
45
- Three exports are client-safe and are published from here anyway:
46
-
47
- | Export | Who uses it |
48
- |---|---|
49
- | `BackendAPIStore` | the store a **browser** uses to talk to the backend; constructed by an app's frontend storage config |
50
- | `getAccessUrlLocalFileStore` | the same frontend config |
51
- | `LincdWebApp` | constructed in a React hook |
52
-
53
- They belong in `@_linked/server-utils`, which exists for exactly the isomorphic half of the
54
- framework. Until they move, every consuming application must carry a standing exception in its
55
- boundary check — and a boundary with permanent exceptions stops being one.
56
-
57
- Tracked as follow-up work; noted here so the next person to wonder why an app imports
58
- `@_linked/server` from its frontend finds the answer.
@@ -1,104 +0,0 @@
1
- ---
2
- summary: >
3
- DONE. /resized/* no longer fetches anything: both branches read and write
4
- through LinkedFileStorage. The `?src=` parameter still accepts a full URL, so
5
- nothing broke -- the URL is resolved to a store key and the bytes are read
6
- directly, which turned out not to need the major release this item assumed.
7
- Kept as the record of why the route looks the way it does.
8
- ---
9
-
10
- # 001 — Resize by store key instead of fetching a URL
11
-
12
- > **Done.** Delivered without the breaking change this item expected: see *Resolution* at the end.
13
-
14
- ## Where this came from
15
-
16
- `?src=` used to be handed straight to `fetch`, which made every deployment an
17
- open proxy into its own network — and because the fetched bytes were written
18
- into the *public* file store and the caller redirected to them, a reachable
19
- internal resource was persisted rather than merely leaked.
20
-
21
- That was fixed by restricting `src` to the configured file store: parsed-origin
22
- comparison against `LinkedFileStorage.accessURL`, a `fileExists` check on the
23
- derived key, a fetch of a URL rebuilt from that key, and `redirect: 'error'`
24
- plus a timeout and a size cap. See `src/utils/resizeSource.ts`.
25
-
26
- That fix was deliberately the conservative one, because it is backward
27
- compatible. This item is the version that is actually correct.
28
-
29
- ## The problem with what is there now
30
-
31
- Once `src` is guaranteed to name an image in the store, the URL is a store key
32
- with extra steps:
33
-
34
- - **The fetch is pointless.** `resizeImage` calls
35
- `LinkedFileStorage.fileExists(key)` and then performs an HTTP request to
36
- retrieve bytes that `LinkedFileStorage.getFile(key)` would return directly.
37
- For a local store that is a loopback request to read a file already on disk.
38
- - **It keeps a fetch that has to stay correct.** Origin comparison, redirect
39
- refusal, credential rejection, the size cap and the timeout all exist to make
40
- an outbound request safe. None of that code needs to exist if there is no
41
- outbound request.
42
- - **The API lies about what it accepts.** Callers pass a full URL, but only one
43
- origin's URLs are valid. A key says what is meant.
44
-
45
- ## What the change looks like
46
-
47
- Replace the `src`-fetch branch with a key lookup:
48
-
49
- ```ts
50
- const bytes = await LinkedFileStorage.getFile(key);
51
- ```
52
-
53
- `resolveResizeSource` still has a job during a deprecation window — turning a
54
- legacy full URL into a key — but `fetchStoredImage`, and with it the timeout,
55
- the size cap and the redirect handling, can go.
56
-
57
- ## Why it is not done yet
58
-
59
- It is a breaking change to the route's contract. Anything passing
60
- `?src=<full URL>` keeps working only if the URL-to-key translation is retained,
61
- and anything passing a third-party URL — which no longer works anyway after the
62
- SSRF fix — breaks visibly rather than silently.
63
-
64
- ## First open questions
65
-
66
- 1. Is `?src=` as a full URL used by real consumers, or is the local branch
67
- (`/resized/<path>`) the only one anyone uses? That decides whether this needs
68
- a deprecation window at all.
69
- 2. Should the key form be a new parameter (`?key=`) so both can coexist for a
70
- release, or should `src` simply start accepting a key as well as a URL?
71
- 3. ~~The other branch of the same route reads from `<cwd>/data/uploads`
72
- directly~~ — **done.** Both branches now read and write through
73
- `LinkedFileStorage`, the derivative cache has its own purpose
74
- (`resizedImagesPurpose`), and the hand-rolled directory creation is gone
75
- because `LocalFileStore` makes its own parents. That was the larger half of
76
- "the route reads and writes through the store"; what is left here is only
77
- the remote branch's needless HTTP hop.
78
-
79
- ## Related
80
-
81
- - `src/utils/resizeSource.ts` — the guard this would let go.
82
- - `src/tests/resized-images.test.ts` — the behaviour that must keep passing.
83
-
84
- ## Resolution
85
-
86
- Done, and the "why it is not done yet" above turned out to be wrong.
87
-
88
- The assumption was that dropping the fetch meant dropping `?src=<full URL>`,
89
- which would break callers and need a major. It does not. The URL form still
90
- works: `resolveResizeSource` turns it into a store key, and the bytes are then
91
- read with `LinkedFileStorage.getFile(key)` instead of being fetched over HTTP.
92
- The request contract is unchanged, so this shipped as a minor.
93
-
94
- What went with the fetch: `fetchStoredImage`, the 10s timeout, the 25MB
95
- streaming cap and `redirect: 'error'`. All of that existed to make an outbound
96
- request safe, and there is no outbound request any more — the route cannot be
97
- pointed at anything, rather than being guarded against it.
98
-
99
- The local branch had already moved to the store, so both halves now read and
100
- write the same way.
101
-
102
- Open question 2 — whether to add a `?key=` parameter — was not needed and is
103
- dropped. Question 1, whether anyone passes a full URL, no longer matters either
104
- way, since both forms cost the same now.
@@ -1,100 +0,0 @@
1
- ---
2
- summary: Two fixes that stood between a Vite-built application and a release it could actually
3
- serve — the entry tag was rendered as a classic script, and LocalFileStore rewrote the object
4
- keys a release is addressed by. Records what was wrong, why neither surfaced earlier, and the
5
- contracts they establish.
6
- ---
7
-
8
- # Serving a Vite-built release
9
-
10
- Two independent defects, both on the path from "a release exists" to "a browser runs it". Neither
11
- could be seen from inside this package: the first needs a real Vite bundle, the second needs a
12
- release actually published to a local store.
13
-
14
- ## 1. The entry tag was a classic script
15
-
16
- **`LinkedServer.tsx` — [#51](https://github.com/linked-fw/server/pull/51)**
17
-
18
- The SSR render passed the client entry to React as `bootstrapScripts`, which emits
19
-
20
- ```html
21
- <script src="…/assets/index-abc123.js" async></script>
22
- ```
23
-
24
- Vite always emits the client entry as an **ES module**. A browser handed module syntax in a
25
- classic script refuses the file outright:
26
-
27
- ```
28
- Uncaught SyntaxError: Cannot use import statement outside a module
29
- ```
30
-
31
- So **no Vite production build could hydrate at all** — the server rendered correct HTML, every
32
- asset returned 200, and the page stayed inert.
33
-
34
- The dev branch immediately above this code already used `bootstrapModules`, for exactly this
35
- reason. Only the production branch was wrong.
36
-
37
- ### What it does now
38
-
39
- `start()` records whether `assets['main.js']` was resolved from a Vite manifest
40
- (`mainEntryIsModule`), and the SSR render passes `bootstrapModules` when it was.
41
-
42
- This is deliberately **not** unconditional. A legacy webpack bundle is a classic script and a
43
- module tag would break it, so the decision follows the manifest the entry actually came from
44
- rather than a global setting.
45
-
46
- ### Why it survived
47
-
48
- Dev never takes this path — `config.server.vite` routes to `bootstrapScriptContent` with the
49
- React-refresh preamble. And no consuming application had completed a production build, because
50
- the release pipeline itself was still being finished. The first app to get that far hit this
51
- immediately.
52
-
53
- ## 2. `LocalFileStore` rewrote release object keys
54
-
55
- **`shapes/filestores/LocalFileStore.ts` — [#52](https://github.com/linked-fw/server/pull/52)**
56
-
57
- `resolveTarget`'s preserved character class was `[A-Za-z0-9._/]` — no `-` — so a run of dashes
58
- collapsed to one:
59
-
60
- ```
61
- asked for: shapeCodeGenerator--2JmNvrO.js
62
- stored as: shapeCodeGenerator-2JmNvrO.js
63
- ```
64
-
65
- Rollup's default base64 hash alphabet includes `-`, so an entry whose hash begins with one meets
66
- the separator as `--`. The stored key is then neither what the caller asked for nor what the
67
- bundle's own URLs point at.
68
-
69
- `publish-app` requires a release store to keep keys verbatim and caught it — but only after 125 of
70
- 158 objects had uploaded, leaving an incomplete release.
71
-
72
- ### What it does now
73
-
74
- `-` joins the preserved set. The method's own doc comment already stated the intent — not to
75
- corrupt "a key a program chose on purpose, such as a Vite content hash: `main-hwqwrAvA.css`" — so
76
- this makes the character class match what it says. Everything else is unchanged: `My File (2).TXT`
77
- still becomes `My-File-2-.TXT`.
78
-
79
- A regression test in `src/tests/local-file-store.test.ts` covers the doubled dash specifically, and
80
- was confirmed to fail without the change.
81
-
82
- ## The contract both establish
83
-
84
- **A release object must be addressable by exactly the key it was given.** The bundle's own chunk
85
- URLs are baked in at build time and nothing rewrites them at request time, so a store that alters a
86
- key — or a server that renders the entry in a form the browser rejects — breaks the release with no
87
- useful error.
88
-
89
- Anything implementing `IFileStore` for release assets inherits that requirement.
90
-
91
- ## Known limitation, not fixed here
92
-
93
- `LocalFileStore.accessURL` is bare `SITE_ROOT`, while the store writes to `data/uploads/<key>` and
94
- `LinkedServer` serves that tree at `/uploads` — the `/uploads` segment is appended inside
95
- `publicURL` instead. `build-app` bakes the bundle's base URL from `accessURL`, so publishing to the
96
- default store produces chunk URLs missing that segment, and every chunk 404s.
97
-
98
- An application can work around it by registering a dedicated `appAssets` store whose `accessURL`
99
- carries the segment. Moving it into `accessURL` here would be the real fix, but it changes that
100
- field for every existing consumer and warrants its own assessment.
package/jest.config.cjs DELETED
@@ -1,23 +0,0 @@
1
- /** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
2
- module.exports = {
3
- preset: 'ts-jest/presets/default-esm',
4
- testEnvironment: 'node',
5
- rootDir: '.',
6
- roots: ['<rootDir>/src/tests'],
7
- testMatch: ['<rootDir>/src/tests/*.test.ts'],
8
- extensionsToTreatAsEsm: ['.ts', '.tsx'],
9
- transform: {
10
- '^.+\\.(ts|tsx)$': [
11
- 'ts-jest',
12
- {
13
- useESM: true,
14
- tsconfig: '<rootDir>/tsconfig-tests.json',
15
- },
16
- ],
17
- },
18
- // Strip the `.js` extension from relative ESM imports so ts-jest resolves the
19
- // `.ts` source (NodeNext-style specifiers carry `.js`; jest maps back to src).
20
- moduleNameMapper: {
21
- '^(\\.{1,2}/.*)\\.js$': '$1',
22
- },
23
- };
package/renovate.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": ["local>linked-fw/renovate-config"]
4
- }
package/tsconfig-cjs.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "module": "commonjs",
5
- "target": "es6",
6
- "outDir": "lib/cjs"
7
- }
8
- }
package/tsconfig-esm.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "module": "esnext",
5
- "target": "es2018",
6
- "outDir": "lib/esm",
7
- "moduleResolution": "bundler"
8
- }
9
- }
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "module": "esnext",
5
- "moduleResolution": "bundler",
6
- "target": "es2020",
7
- "noEmit": true
8
- },
9
- "files": [],
10
- "include": ["./src/**/*.ts", "./src/**/*.tsx"]
11
- }