@vesk/adapter 0.2.67 → 0.2.69

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,233 @@
1
+ /**
2
+ * esbuild plugin that keeps server-only packages out of a browser bundle.
3
+ *
4
+ * Why a plugin and not a filter on the chunk's own import list: by the time the
5
+ * bundler runs, the chunk's direct imports have already been merged, and the
6
+ * offender is almost never one of them. The shape that actually leaks is
7
+ * `page.vsk → app/db.ts → slintorm → pg`, and only the bundler's transitive
8
+ * resolution sees `pg` at all. Filtering the import list would have caught
9
+ * nothing.
10
+ *
11
+ * A resolved server-only package is replaced with a generated module whose
12
+ * every export is a Proxy that throws. A stub and not a deletion, because the
13
+ * binding is referenced by code that survives into the chunk: dropping it would
14
+ * produce `X is not defined` at hydration, pointing nowhere near the import that
15
+ * caused it. The export names come from the importing statement, read off the
16
+ * importer's own source, so the generated module declares exactly what the
17
+ * importer asked for and nothing is silently undefined.
18
+ *
19
+ * TCP drivers are NOT stubbed. A browser bundle cannot contain one under any
20
+ * circumstances, so a stub would ship a page that looks fine and fails at the
21
+ * first query; they are reported and left for esbuild to reject.
22
+ */
23
+ import { readFileSync } from 'node:fs';
24
+ import { parse } from '@vesk/compiler/src/parser';
25
+ import { classifySpecifier } from '@vesk/adapter/src/server-only';
26
+ const STUB_NAMESPACE = 'vesk-server-only';
27
+ let stubCounter = 0;
28
+ /**
29
+ * The export names one file asks for from `specifier`.
30
+ *
31
+ * Read from the importer's own source rather than guessed: a stub declaring the
32
+ * wrong set of names is either an unresolvable-import build failure or, worse,
33
+ * a silently undefined binding.
34
+ */
35
+ export function importedNamesFor(code, specifier) {
36
+ let ast;
37
+ try {
38
+ ast = parse(code);
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ const names = [];
44
+ // The vsk parser flattens a module specifier to a bare string, while the TS
45
+ // parser keeps it as `{ value }`. Both shapes occur in practice.
46
+ const sourceOf = (node) => {
47
+ const src = node.source;
48
+ if (typeof src === 'string')
49
+ return src;
50
+ const v = src?.value;
51
+ return typeof v === 'string' ? v : null;
52
+ };
53
+ for (const node of ast.body ?? []) {
54
+ if (node?.type === 'ImportDeclaration') {
55
+ if (sourceOf(node) !== specifier)
56
+ continue;
57
+ for (const spec of node.specifiers ?? []) {
58
+ // `import * as x` has no names to declare — the namespace is made of
59
+ // whatever the importer reaches for, and each of those reads is what
60
+ // throws. Declaring guesses would be worse than declaring none.
61
+ if (spec.type === 'ImportNamespaceSpecifier')
62
+ continue;
63
+ const local = spec.local?.name;
64
+ if (typeof local === 'string' && !names.includes(local))
65
+ names.push(local);
66
+ }
67
+ continue;
68
+ }
69
+ if (node?.type === 'ExportAllDeclaration' || node?.type === 'ExportNamedDeclaration') {
70
+ if (sourceOf(node) !== specifier)
71
+ continue;
72
+ // `export * from 'pg'` names nothing at all, and `export { Client } from
73
+ // 'pg'` names only what this barrel happens to forward. Either way the
74
+ // honest stub declares what is written here.
75
+ for (const spec of node.specifiers ?? []) {
76
+ const imported = spec.imported?.name ?? spec.imported?.value ?? spec.local?.name;
77
+ if (typeof imported === 'string' && !names.includes(imported))
78
+ names.push(imported);
79
+ }
80
+ }
81
+ }
82
+ return names;
83
+ }
84
+ /** The shared throwing-proxy factory, emitted once per stubbed package. */
85
+ export function stubFactorySource(specifier) {
86
+ const template = '"$1" comes from "$0", which is server-only. It reached the browser bundle because a ' +
87
+ 'component (or a module one imports) imported it — component code runs in the browser, ' +
88
+ 'where this module cannot work. Move the server-only work into an API route ' +
89
+ '(app/api/**/route.ts) or a server action and call it over HTTP, or import the ' +
90
+ 'edge/browser-safe entry if the package ships one.';
91
+ return [
92
+ 'function __vesk_stub(binding) {',
93
+ ' var message = ' + JSON.stringify(template) + ';',
94
+ ' message = message.replace("$0", ' + JSON.stringify(specifier) + ').replace("$1", String(binding));',
95
+ ' var thrower = function () { throw Object.assign(new Error(message), { __veskServerOnly: ' + JSON.stringify(specifier) + ' }); };',
96
+ ' return new Proxy(function () {}, {',
97
+ ' get: thrower,',
98
+ ' apply: thrower,',
99
+ ' construct: thrower',
100
+ ' });',
101
+ '}',
102
+ ].join('\n');
103
+ }
104
+ /**
105
+ * Is `name` used at module-evaluation position — i.e. NOT inside a function?
106
+ *
107
+ * This is the difference between a stub that is fine and one that is a trap.
108
+ *
109
+ * export function allRows() { return orm.getAll(); } // never runs in the
110
+ * // browser → stub is fine
111
+ * export const orm = createORM({}); // runs the moment the
112
+ * // chunk evaluates
113
+ *
114
+ * The second form cannot be rescued by any amount of stubbing: the browser
115
+ * evaluates it while loading the chunk, so the page dies before a single
116
+ * component exists. A build error is the honest answer there, and it is
117
+ * decidable from the source alone — no types, no dataflow.
118
+ *
119
+ * Non-computed member keys (`obj.orm`) and object/class property keys are not
120
+ * references, for the same reason the reactivity diagnostics skip them.
121
+ */
122
+ export function referencedAtTopLevel(code, name) {
123
+ let ast;
124
+ try {
125
+ ast = parse(code);
126
+ }
127
+ catch {
128
+ // Cannot prove it either way; assume the dangerous case so the build is
129
+ // never softened by an unparseable file.
130
+ return true;
131
+ }
132
+ let found = false;
133
+ const FUNCTION_TYPES = new Set([
134
+ 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression',
135
+ 'ClassMethod', 'ObjectMethod', 'ClassDeclaration', 'ClassExpression',
136
+ ]);
137
+ /** Never carries a reference: AST bookkeeping and source offsets. */
138
+ const SKIP_KEYS = new Set(['type', 'start', 'end', 'loc', 'range', 'raw', 'regex']);
139
+ /** A re-export's module specifier and specifier list name bindings, not use them. */
140
+ const SKIP_EXPORT_NAMED_KEYS = new Set([...SKIP_KEYS, 'source', 'specifiers', 'exportKind', 'attributes']);
141
+ const visit = (node, parentKey, fnDepth) => {
142
+ if (found || node === null || typeof node !== 'object')
143
+ return;
144
+ if (Array.isArray(node)) {
145
+ for (const child of node)
146
+ visit(child, parentKey, fnDepth);
147
+ return;
148
+ }
149
+ const n = node;
150
+ if (n.type === 'MemberExpression' || n.type === 'Property' || n.type === 'PropertyDefinition' || n.type === 'MethodDefinition') {
151
+ const prop = (n.property ?? n.key);
152
+ // `obj.orm` and `{ orm: 1 }` name a field, not the binding. `[orm]` does.
153
+ if (prop && typeof prop === 'object' && prop.name === name && n.computed !== true)
154
+ return;
155
+ }
156
+ // An import NAMES the binding without USING it. Walking into the specifiers
157
+ // made every imported name look top-level-referenced, which would have
158
+ // turned every server-only import into a build error.
159
+ if (n.type === 'ImportDeclaration' || n.type === 'ExportAllDeclaration' || n.type === 'ImportExpression')
160
+ return;
161
+ if (n.type === 'Identifier' && n.name === name) {
162
+ if (parentKey === 'local' || parentKey === 'id' || parentKey === 'params' || parentKey === 'key')
163
+ return;
164
+ if (fnDepth === 0)
165
+ found = true;
166
+ return;
167
+ }
168
+ // `export const orm = createORM({})` carries its USE inside the export
169
+ // declaration, so only the specifier list and module specifier are skipped.
170
+ const skip = n.type === 'ExportNamedDeclaration' ? SKIP_EXPORT_NAMED_KEYS : SKIP_KEYS;
171
+ const nextDepth = FUNCTION_TYPES.has(String(n.type)) ? fnDepth + 1 : fnDepth;
172
+ for (const [key, value] of Object.entries(n)) {
173
+ if (skip.has(key))
174
+ continue;
175
+ visit(value, key, nextDepth);
176
+ }
177
+ };
178
+ visit(ast.body, undefined, 0);
179
+ return found;
180
+ }
181
+ export function serverOnlyPlugin(options = {}) {
182
+ /** Generated stub id -> module source. */
183
+ const stubs = new Map();
184
+ return {
185
+ name: 'vesk-server-only',
186
+ setup(build) {
187
+ build.onResolve({ filter: /.*/ }, args => {
188
+ const kind = classifySpecifier(args.path);
189
+ if (!kind)
190
+ return null;
191
+ if (kind === 'tcp-driver') {
192
+ options.onDriver?.({ specifier: args.path, importer: args.importer });
193
+ // Returning null lets esbuild resolve it normally, which in a browser
194
+ // bundle fails with its own "Could not resolve" error. Substituting a
195
+ // stub here would ship a page that looks fine and dies on first use.
196
+ return null;
197
+ }
198
+ let names = [];
199
+ let importerCode = '';
200
+ try {
201
+ importerCode = readFileSync(args.importer, 'utf-8');
202
+ names = importedNamesFor(importerCode, args.path);
203
+ }
204
+ catch {
205
+ /* importer unreadable — emit a default-only stub rather than crash */
206
+ }
207
+ // A stub only works if the browser never reaches the binding. A
208
+ // module-evaluation-position use means the browser reaches it while
209
+ // loading the chunk, before any component exists, so no stub can help
210
+ // and the build has to say so rather than ship a page that dies on load.
211
+ const eager = names.filter(n => referencedAtTopLevel(importerCode, n));
212
+ if (eager.length > 0) {
213
+ options.onEager?.({ specifier: args.path, bindings: eager, importer: args.importer });
214
+ return null;
215
+ }
216
+ const id = `vesk-stub-${stubCounter++}`;
217
+ stubs.set(id, [
218
+ stubFactorySource(args.path),
219
+ 'export default __vesk_stub("default");',
220
+ ...names.map(n => `export const ${n} = __vesk_stub(${JSON.stringify(n)});`),
221
+ ].join('\n'));
222
+ for (const binding of names) {
223
+ options.onStub?.({ specifier: args.path, binding, importer: args.importer });
224
+ }
225
+ return { path: id, namespace: STUB_NAMESPACE };
226
+ });
227
+ build.onLoad({ filter: /.*/, namespace: STUB_NAMESPACE }, args => ({
228
+ contents: stubs.get(args.path) ?? stubFactorySource('unknown'),
229
+ loader: 'js',
230
+ }));
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * What may not cross the server/client line, and what may not be deployed to a
3
+ * socketless runtime.
4
+ *
5
+ * Vesk splits a `.vsk` file into two graphs without being told which side a
6
+ * given import belongs to: a page is *also* a client component, so the module
7
+ * a page imports for its `load()` ends up in the browser chunk closure. When
8
+ * that module reaches for a database driver, the browser bundler dutifully
9
+ * inlines the driver's Node implementation — `node:net`, `node:tls`,
10
+ * `node:stream` — and the client bundle grows by hundreds of kilobytes of code
11
+ * that cannot run and is never called.
12
+ *
13
+ * So the separation has to be enforced by the build rather than inferred from
14
+ * usage. Two distinct rules, because the failure modes are different:
15
+ *
16
+ * - **Server-only packages** (`slintorm`, `@vesk/adapter`) may legitimately be
17
+ * imported by a page that only touches them during SSR. The import is removed
18
+ * from the browser chunk and every binding it declared becomes a throwing
19
+ * stub, so a build stays possible but a real client-side use fails loudly and
20
+ * names itself. `--strict` turns the warning into a build failure.
21
+ * - **TCP-only drivers** (`pg`, `mysql2`, `mongodb`, `node:sqlite`) cannot run
22
+ * anywhere a socket cannot be opened — not in a browser, and not on any edge
23
+ * runtime. In an edge-targeted build that is not a degradation, it is a
24
+ * guaranteed crash at the first query, so it is a hard build error with no
25
+ * opt-out: the alternative is discovering it in a deployment log.
26
+ *
27
+ * AST-only throughout. No text scanning — see the module rules.
28
+ */
29
+ export type ServerOnlyKind = 'server-only-package' | 'tcp-driver';
30
+ /**
31
+ * Classify a bare import specifier. Returns null for anything that is fine on
32
+ * the client — including a subpath of a listed package that is itself a
33
+ * browser entry.
34
+ */
35
+ export declare function classifySpecifier(spec: string): ServerOnlyKind | null;
36
+ export declare function isServerOnlySpecifier(spec: string): boolean;
37
+ export declare function isTcpDriverSpecifier(spec: string): boolean;
38
+ /**
39
+ * Throwing stand-ins for the bindings a server-only import declared.
40
+ *
41
+ * A stub, not a removal: the binding is referenced by code that survives into
42
+ * the chunk, so dropping it would produce `X is not defined` at hydration — a
43
+ * ReferenceError pointing nowhere near the import that caused it. A Proxy that
44
+ * throws on any use keeps `typeof` truthful (so a feature check does not blow
45
+ * up during module evaluation) while failing at the exact call site with a
46
+ * message that names the package, the binding and the fix.
47
+ */
48
+ export declare function serverOnlyStubs(notes: Array<{
49
+ specifier: string;
50
+ binding: string;
51
+ }>): string;
52
+ /**
53
+ * Same stubs, discovered by scanning source rather than from a recorded list —
54
+ * used by the edge gate, which sees `.vsk` sources before anything is compiled.
55
+ */
56
+ export declare function serverOnlyImportStubs(code: string, classify?: (spec: string) => ServerOnlyKind | null): string;
57
+ export interface ServerOnlyViolation {
58
+ kind: ServerOnlyKind;
59
+ specifier: string;
60
+ /** Absolute path of the file the import was written in. */
61
+ file: string;
62
+ /** 1-based line number when it could be determined. */
63
+ line: number | null;
64
+ /** What to do about it. */
65
+ remedy: string;
66
+ }
67
+ /**
68
+ * Every server-only / TCP-only import written across a set of files.
69
+ *
70
+ * Walks the whole set rather than a module graph, because the file that names
71
+ * the driver is rarely the file that matters: `app/db.ts` importing `slintorm`
72
+ * is the normal shape, and it is the *component* that transitively pulls it in.
73
+ * Callers pair this with their own reachability information.
74
+ */
75
+ export declare function scanServerOnlyImports(files: Array<{
76
+ path: string;
77
+ code: string;
78
+ }>): ServerOnlyViolation[];
79
+ /**
80
+ * The build-time error for an edge-targeted build that imports a TCP driver.
81
+ *
82
+ * No opt-out by design: there is no configuration of an edge target under which
83
+ * a TCP driver works, so the only question is whether the author finds out here
84
+ * or from a production log.
85
+ */
86
+ export declare function edgeDriverBuildError(violations: ServerOnlyViolation[]): Error;
87
+ export declare function isSocketlessTarget(platform: string | null | undefined, target: string | null | undefined): boolean;
88
+ /** Every app source file under `dir`, recursively. */
89
+ export declare function collectAppSources(dir: string, out?: string[]): string[];
90
+ /**
91
+ * TCP-driver imports anywhere under the app directory.
92
+ *
93
+ * Scans the whole tree rather than the client chunk closure: the driver is named
94
+ * in `app/db.ts`, which is correct, and what is wrong is that anything under it
95
+ * is reachable from a socketless runtime. Either way the build has to stop, and
96
+ * it has to stop before minutes of bundling have been spent.
97
+ */
98
+ export declare function tcpDriverViolations(appDir: string): ServerOnlyViolation[];
99
+ //# sourceMappingURL=server-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../src/server-only.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAsCH,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,YAAY,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMrE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAkB5F;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,GAAG,IAAwB,GAAG,MAAM,CAoBjI;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAWD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3C,mBAAmB,EAAE,CA8BvB;AAYD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,KAAK,CAU7E;AAYD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGlH;AAMD,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAUzE"}
@@ -0,0 +1,283 @@
1
+ /**
2
+ * What may not cross the server/client line, and what may not be deployed to a
3
+ * socketless runtime.
4
+ *
5
+ * Vesk splits a `.vsk` file into two graphs without being told which side a
6
+ * given import belongs to: a page is *also* a client component, so the module
7
+ * a page imports for its `load()` ends up in the browser chunk closure. When
8
+ * that module reaches for a database driver, the browser bundler dutifully
9
+ * inlines the driver's Node implementation — `node:net`, `node:tls`,
10
+ * `node:stream` — and the client bundle grows by hundreds of kilobytes of code
11
+ * that cannot run and is never called.
12
+ *
13
+ * So the separation has to be enforced by the build rather than inferred from
14
+ * usage. Two distinct rules, because the failure modes are different:
15
+ *
16
+ * - **Server-only packages** (`slintorm`, `@vesk/adapter`) may legitimately be
17
+ * imported by a page that only touches them during SSR. The import is removed
18
+ * from the browser chunk and every binding it declared becomes a throwing
19
+ * stub, so a build stays possible but a real client-side use fails loudly and
20
+ * names itself. `--strict` turns the warning into a build failure.
21
+ * - **TCP-only drivers** (`pg`, `mysql2`, `mongodb`, `node:sqlite`) cannot run
22
+ * anywhere a socket cannot be opened — not in a browser, and not on any edge
23
+ * runtime. In an edge-targeted build that is not a degradation, it is a
24
+ * guaranteed crash at the first query, so it is a hard build error with no
25
+ * opt-out: the alternative is discovering it in a deployment log.
26
+ *
27
+ * AST-only throughout. No text scanning — see the module rules.
28
+ */
29
+ import { parse } from '@vesk/compiler/src/parser';
30
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ /**
33
+ * Packages that are server-side by construction.
34
+ *
35
+ * `slintorm` is here for a specific reason, and it is not a slur on the ORM:
36
+ * `slintorm` ships an edge-safe entry (`slintorm/edge` and `slintorm/browser`,
37
+ * which resolve to a bundle with no Node built-ins anywhere in its graph), and
38
+ * that entry is deliberately NOT matched. Only the bare `slintorm` specifier is
39
+ * server-only, because only that one pulls in the driver dispatch and the
40
+ * `node:fs` schema generator.
41
+ */
42
+ const SERVER_ONLY_EXACT = new Set([
43
+ 'slintorm',
44
+ '@vesk/adapter',
45
+ ]);
46
+ /**
47
+ * Drivers that need a real socket. Anything here is a hard error in an
48
+ * edge-targeted build.
49
+ *
50
+ * `node:sqlite` is a Node built-in that appears only as an import specifier, so
51
+ * it is listed here alongside the packages for uniformity; it cannot appear in
52
+ * a browser chunk because the bundler already fails on it.
53
+ */
54
+ const TCP_DRIVERS = new Set([
55
+ 'pg',
56
+ 'mysql2',
57
+ 'mongodb',
58
+ 'node:sqlite',
59
+ 'node:net',
60
+ 'node:tls',
61
+ ]);
62
+ /**
63
+ * Classify a bare import specifier. Returns null for anything that is fine on
64
+ * the client — including a subpath of a listed package that is itself a
65
+ * browser entry.
66
+ */
67
+ export function classifySpecifier(spec) {
68
+ if (typeof spec !== 'string' || spec.length === 0)
69
+ return null;
70
+ if (TCP_DRIVERS.has(spec))
71
+ return 'tcp-driver';
72
+ if (spec.startsWith('node:'))
73
+ return 'tcp-driver';
74
+ if (SERVER_ONLY_EXACT.has(spec))
75
+ return 'server-only-package';
76
+ return null;
77
+ }
78
+ export function isServerOnlySpecifier(spec) {
79
+ return classifySpecifier(spec) === 'server-only-package';
80
+ }
81
+ export function isTcpDriverSpecifier(spec) {
82
+ return classifySpecifier(spec) === 'tcp-driver';
83
+ }
84
+ /**
85
+ * Throwing stand-ins for the bindings a server-only import declared.
86
+ *
87
+ * A stub, not a removal: the binding is referenced by code that survives into
88
+ * the chunk, so dropping it would produce `X is not defined` at hydration — a
89
+ * ReferenceError pointing nowhere near the import that caused it. A Proxy that
90
+ * throws on any use keeps `typeof` truthful (so a feature check does not blow
91
+ * up during module evaluation) while failing at the exact call site with a
92
+ * message that names the package, the binding and the fix.
93
+ */
94
+ export function serverOnlyStubs(notes) {
95
+ if (notes.length === 0)
96
+ return '';
97
+ const lines = notes.map(({ binding, specifier }) => {
98
+ const message = `"${binding}" comes from "${specifier}", which is server-only. It reached the client bundle ` +
99
+ 'because a component (or a module one imports) imported it — component code runs in the ' +
100
+ 'browser, where this module cannot work. Move the server-only work into an API route ' +
101
+ '(app/api/**/route.ts) or a server action and call it over HTTP, or import the ' +
102
+ 'edge/browser-safe entry if the package ships one.';
103
+ const thrower = `throw Object.assign(new Error(${JSON.stringify(message)}), { __veskServerOnly: ${JSON.stringify(specifier)} })`;
104
+ return (`const ${binding} = new Proxy(function () {}, { ` +
105
+ `get: function () { ${thrower}; }, ` +
106
+ `apply: function () { ${thrower}; }, ` +
107
+ `construct: function () { ${thrower}; } });`);
108
+ });
109
+ return lines.join('\n') + '\n\n';
110
+ }
111
+ /**
112
+ * Same stubs, discovered by scanning source rather than from a recorded list —
113
+ * used by the edge gate, which sees `.vsk` sources before anything is compiled.
114
+ */
115
+ export function serverOnlyImportStubs(code, classify = classifySpecifier) {
116
+ let ast;
117
+ try {
118
+ ast = parse(code);
119
+ }
120
+ catch {
121
+ return '';
122
+ }
123
+ const notes = [];
124
+ for (const node of ast.body ?? []) {
125
+ // Only ImportDeclaration here, unlike the scanner above: a re-export binds
126
+ // no local name, so there is nothing to replace — the scan still reports it.
127
+ if (node?.type !== 'ImportDeclaration')
128
+ continue;
129
+ const src = node.source?.value;
130
+ if (typeof src !== 'string' || classify(src) !== 'server-only-package')
131
+ continue;
132
+ for (const spec of node.specifiers ?? []) {
133
+ const local = spec?.local?.name;
134
+ if (typeof local === 'string')
135
+ notes.push({ specifier: src, binding: local });
136
+ }
137
+ }
138
+ return serverOnlyStubs(notes);
139
+ }
140
+ const REMEDY = {
141
+ 'server-only-package': 'import it from an API route (app/api/**/route.ts) or a server action instead of a component, ' +
142
+ 'or use the package\'s browser/edge entry point if it ships one.',
143
+ 'tcp-driver': 'this driver opens a TCP socket, which no edge runtime provides. Use the driver\'s HTTP/HTTPS ' +
144
+ 'transport (for SlintORM: `driver: "postgrest"`), or build for a Node target.',
145
+ };
146
+ /**
147
+ * Every server-only / TCP-only import written across a set of files.
148
+ *
149
+ * Walks the whole set rather than a module graph, because the file that names
150
+ * the driver is rarely the file that matters: `app/db.ts` importing `slintorm`
151
+ * is the normal shape, and it is the *component* that transitively pulls it in.
152
+ * Callers pair this with their own reachability information.
153
+ */
154
+ export function scanServerOnlyImports(files) {
155
+ const out = [];
156
+ for (const file of files) {
157
+ let ast;
158
+ try {
159
+ ast = parse(file.code);
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ for (const node of ast.body ?? []) {
165
+ // Re-exports count. `export { Client } from 'pg'` is how a barrel file
166
+ // forwards a driver, and it is exactly the shape that slips past a scan
167
+ // looking only at imports.
168
+ if (node?.type !== 'ImportDeclaration'
169
+ && node?.type !== 'ExportNamedDeclaration'
170
+ && node?.type !== 'ExportAllDeclaration')
171
+ continue;
172
+ const spec = node.source?.value;
173
+ if (typeof spec !== 'string')
174
+ continue;
175
+ const kind = classifySpecifier(spec);
176
+ if (!kind)
177
+ continue;
178
+ out.push({
179
+ kind,
180
+ specifier: spec,
181
+ file: file.path,
182
+ line: lineOf(file.code, node.start),
183
+ remedy: REMEDY[kind],
184
+ });
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+ /** 1-based line of a character offset. */
190
+ function lineOf(code, offset) {
191
+ if (typeof offset !== 'number' || offset < 0 || offset > code.length)
192
+ return null;
193
+ let line = 1;
194
+ for (let i = 0; i < offset; i++) {
195
+ if (code.charCodeAt(i) === 10)
196
+ line++;
197
+ }
198
+ return line;
199
+ }
200
+ /**
201
+ * The build-time error for an edge-targeted build that imports a TCP driver.
202
+ *
203
+ * No opt-out by design: there is no configuration of an edge target under which
204
+ * a TCP driver works, so the only question is whether the author finds out here
205
+ * or from a production log.
206
+ */
207
+ export function edgeDriverBuildError(violations) {
208
+ const detail = violations
209
+ .map(v => ` ${v.file}${v.line ? `:${v.line}` : ''} — import "${v.specifier}"\n ${v.remedy}`)
210
+ .join('\n');
211
+ return new Error(`[vesk] edge-targeted build cannot include a TCP-only driver (${violations.length} found).\n` +
212
+ `${detail}\n\n` +
213
+ 'This was a build error rather than a runtime one because an edge runtime has no socket: ' +
214
+ 'the deployed function would fail on its first query.');
215
+ }
216
+ /**
217
+ * Platforms whose deployed functions have no socket.
218
+ *
219
+ * Derived from `platform-deploy.ts`'s `nodeBuiltins: false` shells — those are
220
+ * exactly the ones bundled with empty-Node-builtin stubs, which is the same
221
+ * reason they cannot open a TCP connection. `vercel`/`netlify`/`aws` run Node
222
+ * functions and are deliberately absent.
223
+ */
224
+ const SOCKETLESS_PLATFORMS = new Set(['edge', 'cloudflare', 'deno', 'coxmos']);
225
+ export function isSocketlessTarget(platform, target) {
226
+ if (target === 'edge')
227
+ return true;
228
+ return typeof platform === 'string' && SOCKETLESS_PLATFORMS.has(platform);
229
+ }
230
+ const SOURCE_EXTENSIONS = ['.vsk', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
231
+ const SKIPPED_DIRS = new Set(['node_modules', '.vesk', '.git', 'dist', '.next', '.output', 'coverage']);
232
+ /** Every app source file under `dir`, recursively. */
233
+ export function collectAppSources(dir, out = []) {
234
+ let entries;
235
+ try {
236
+ entries = readdirSync(dir);
237
+ }
238
+ catch {
239
+ return out;
240
+ }
241
+ for (const entry of entries) {
242
+ if (SKIPPED_DIRS.has(entry) || entry.startsWith('.'))
243
+ continue;
244
+ const full = join(dir, entry);
245
+ let st;
246
+ try {
247
+ st = statSync(full);
248
+ }
249
+ catch {
250
+ continue;
251
+ }
252
+ if (st.isDirectory())
253
+ collectAppSources(full, out);
254
+ else if (st.isFile()) {
255
+ const dot = entry.lastIndexOf('.');
256
+ if (dot === -1)
257
+ continue;
258
+ if (SOURCE_EXTENSIONS.includes(entry.slice(dot).toLowerCase()))
259
+ out.push(full);
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * TCP-driver imports anywhere under the app directory.
266
+ *
267
+ * Scans the whole tree rather than the client chunk closure: the driver is named
268
+ * in `app/db.ts`, which is correct, and what is wrong is that anything under it
269
+ * is reachable from a socketless runtime. Either way the build has to stop, and
270
+ * it has to stop before minutes of bundling have been spent.
271
+ */
272
+ export function tcpDriverViolations(appDir) {
273
+ const files = [];
274
+ for (const path of collectAppSources(appDir)) {
275
+ try {
276
+ files.push({ path, code: readFileSync(path, 'utf-8') });
277
+ }
278
+ catch {
279
+ /* unreadable file — not a build blocker */
280
+ }
281
+ }
282
+ return scanServerOnlyImports(files).filter(v => v.kind === 'tcp-driver');
283
+ }
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleChunkSpec, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, MdHtmlWarning, VeskLeafComponent, VeskPrimitiveComponent, VeskTaggedLeafComponent, VeskComponentRegistry, VeskScope, } from '@vesk/types';
1
+ export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleChunkSpec, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, CompressionConfig, MdHtmlWarning, VeskLeafComponent, VeskPrimitiveComponent, VeskTaggedLeafComponent, VeskComponentRegistry, VeskScope, } from '@vesk/types';
2
2
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.67",
3
+ "version": "0.2.69",
4
4
  "type": "module",
5
5
  "description": "Vesk adapter — builds deployable .vesk/ output for Deno and Node serverless",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "sharp": "^0.35.3"
24
24
  },
25
25
  "dependencies": {
26
- "@vesk/compiler": "^0.2.67",
27
- "@vesk/runtime": "^0.2.67",
28
- "@vesk/types": "^0.2.67"
26
+ "@vesk/compiler": "^0.2.69",
27
+ "@vesk/runtime": "^0.2.69",
28
+ "@vesk/types": "^0.2.69"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",