@teamscala/service-boot 0.23.455 → 0.23.456

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.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.455",
9
+ "version": "0.23.456",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @system server-handlers
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Behavioural pins for the stylesheet resolver (operator ruling 2026-09-03:
7
+ * the fallback chain IS the drift — a bare id resolved via Bun.resolve
8
+ * returned whatever the package's main was (a .js for a normal package),
9
+ * the resolve(base, id) tail invented nonexistent paths whose ENOENT
10
+ * surfaced as misleading downstream CSS errors, and inlineCssImports
11
+ * SWALLOWED unresolved imports, leaving literals that failed downstream
12
+ * instead of at the build. Every case below pins the ruled contract:
13
+ * resolve or THROW naming the specifier; remote https imports are skipped
14
+ * explicitly, never resolved and never thrown on.
15
+ */
16
+ import { describe, expect, test } from "bun:test";
17
+ import { mkdir, rm, writeFile } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { tmpdir } from "node:os";
20
+ import { bunResolveStylesheet } from "./styles-build.ts";
21
+
22
+ async function makeTree(tag: string, files: Record<string, string>) {
23
+ const root = join(tmpdir(), `styles-build-${tag}-${process.pid}`);
24
+ await rm(root, { recursive: true, force: true });
25
+ for (const [rel, content] of Object.entries(files)) {
26
+ const p = join(root, rel);
27
+ await mkdir(p.slice(0, p.lastIndexOf("/")), { recursive: true });
28
+ await writeFile(p, content);
29
+ }
30
+ return root;
31
+ }
32
+
33
+ describe("bunResolveStylesheet — relative ids", () => {
34
+ test("a relative import that exists resolves and inlines its nested @imports", async () => {
35
+ const root = await makeTree("rel-ok", {
36
+ "src/entry.css": '@import "./leaf.css";\n.body { color: red; }\n',
37
+ "src/leaf.css": ".leaf { color: blue; }\n",
38
+ });
39
+ const r = await bunResolveStylesheet("./entry.css", join(root, "src"), root);
40
+ expect(r.content).toContain(".body");
41
+ expect(r.content).toContain(".leaf");
42
+ await rm(root, { recursive: true, force: true });
43
+ });
44
+
45
+ test("a relative import that does NOT exist throws naming the specifier and the path", async () => {
46
+ const root = await makeTree("rel-missing", {
47
+ "src/entry.css": ".body { color: red; }\n",
48
+ });
49
+ const err = await bunResolveStylesheet(
50
+ "./nope.css",
51
+ join(root, "src"),
52
+ root,
53
+ ).catch((e) => e);
54
+ expect(err).toBeInstanceOf(Error);
55
+ expect((err as Error).message).toContain('"./nope.css"');
56
+ expect((err as Error).message).toContain("not found");
57
+ await rm(root, { recursive: true, force: true });
58
+ });
59
+ });
60
+
61
+ describe("bunResolveStylesheet — bare ids", () => {
62
+ test("a CSS-only package resolves via its declared style entry", async () => {
63
+ const root = await makeTree("bare-style", {
64
+ "node_modules/tw-animate-css/package.json": JSON.stringify({
65
+ name: "tw-animate-css",
66
+ exports: { ".": { style: "./dist/tw-animate.css" } },
67
+ }),
68
+ "node_modules/tw-animate-css/dist/tw-animate.css":
69
+ ".tw-animate { animation: none; }\n",
70
+ "src/app.css": '.x { color: red; }\n',
71
+ });
72
+ const r = await bunResolveStylesheet(
73
+ "tw-animate-css",
74
+ join(root, "src"),
75
+ root,
76
+ );
77
+ expect(r.content).toContain(".tw-animate");
78
+ await rm(root, { recursive: true, force: true });
79
+ });
80
+
81
+ test("a normal JS package (no style entry) THROWS naming the specifier — never its .js main", async () => {
82
+ const root = await makeTree("bare-js-main", {
83
+ "node_modules/lodash/package.json": JSON.stringify({
84
+ name: "lodash",
85
+ main: "lodash.js",
86
+ }),
87
+ "node_modules/lodash/lodash.js": "module.exports = {};\n",
88
+ "src/app.css": ".x { color: red; }\n",
89
+ });
90
+ const err = await bunResolveStylesheet(
91
+ "lodash",
92
+ join(root, "src"),
93
+ root,
94
+ ).catch((e) => e);
95
+ expect(err).toBeInstanceOf(Error);
96
+ expect((err as Error).message).toContain('"lodash"');
97
+ expect((err as Error).message).toContain("no style entry");
98
+ expect((err as Error).message).not.toContain("lodash.js");
99
+ await rm(root, { recursive: true, force: true });
100
+ });
101
+
102
+ test("a bare id with no package anywhere THROWS naming the specifier and both roots", async () => {
103
+ const root = await makeTree("bare-missing", {
104
+ "src/app.css": ".x { color: red; }\n",
105
+ });
106
+ const src = join(root, "src");
107
+ const err = await bunResolveStylesheet("no-such-pkg", src, root).catch(
108
+ (e) => e,
109
+ );
110
+ expect(err).toBeInstanceOf(Error);
111
+ expect((err as Error).message).toContain('"no-such-pkg"');
112
+ expect((err as Error).message).toContain("unresolved");
113
+ expect((err as Error).message).toContain(src);
114
+ await rm(root, { recursive: true, force: true });
115
+ });
116
+ });
117
+
118
+ describe("bunResolveStylesheet — remote @imports", () => {
119
+ test("a remote https @import is kept verbatim (stripped later by stripRemoteImports), never resolved", async () => {
120
+ const root = await makeTree("remote-skip", {
121
+ "node_modules/cdn-fonts/package.json": JSON.stringify({
122
+ name: "cdn-fonts",
123
+ exports: { ".": { style: "./index.css" } },
124
+ }),
125
+ "node_modules/cdn-fonts/index.css":
126
+ '@import "https://rsms.me/inter/inter.css";\n.cdn { font-family: inter; }\n',
127
+ "src/app.css": ".x { color: red; }\n",
128
+ });
129
+ const r = await bunResolveStylesheet(
130
+ "cdn-fonts",
131
+ join(root, "src"),
132
+ root,
133
+ );
134
+ expect(r.content).toContain("https://rsms.me/inter/inter.css");
135
+ expect(r.content).toContain(".cdn");
136
+ await rm(root, { recursive: true, force: true });
137
+ });
138
+
139
+ test("a LOCAL @import of a missing file THROWS at the build instead of leaving a literal", async () => {
140
+ const root = await makeTree("local-missing-nested", {
141
+ "node_modules/broken-pkg/package.json": JSON.stringify({
142
+ name: "broken-pkg",
143
+ exports: { ".": { style: "./index.css" } },
144
+ }),
145
+ "node_modules/broken-pkg/index.css": '@import "./gone.css";\n.real { color: red; }\n',
146
+ "src/app.css": ".x { color: red; }\n",
147
+ });
148
+ const err = await bunResolveStylesheet(
149
+ "broken-pkg",
150
+ join(root, "src"),
151
+ root,
152
+ ).catch((e) => e);
153
+ expect(err).toBeInstanceOf(Error);
154
+ expect((err as Error).message).toContain('"./gone.css"');
155
+ await rm(root, { recursive: true, force: true });
156
+ });
157
+ });
@@ -31,31 +31,52 @@ async function resolveStylesheetRaw(
31
31
  const r = await Bun.resolve("tailwindcss/index.css", cwd);
32
32
  return { path: r, base: dirname(r), content: await Bun.file(r).text() };
33
33
  }
34
- try {
35
- const r = await Bun.resolve(id, base);
36
- return { path: r, base: dirname(r), content: await Bun.file(r).text() };
37
- } catch (error) {
38
- getAppLogger().warn("style import resolve failed — trying package fallback", error);
34
+ if (id.startsWith(".")) {
35
+ // Relative import: resolve against the importing file's directory and
36
+ // REQUIRE existence — a missing leaf is a build-breaking error naming
37
+ // the specifier, never a silently-dropped subtree.
38
+ const p = resolve(base, id);
39
+ if (!(await Bun.file(p).exists())) {
40
+ throw new Error(
41
+ `style import "${id}" not found at ${p} (resolved relative to ${base})`,
42
+ );
43
+ }
44
+ return { path: p, base: dirname(p), content: await Bun.file(p).text() };
39
45
  }
40
- if (!id.startsWith(".")) {
41
- try {
42
- const pkgPath = await Bun.resolve(id + "/package.json", cwd);
43
- const pkg = JSON.parse(await Bun.file(pkgPath).text());
44
- const entry =
45
- pkg.exports?.["."]?.style || pkg.style || pkg.main;
46
- if (entry) {
46
+ // Bare specifier: find node_modules/<id>/package.json by walking upward
47
+ // from the importing file's dir then cwd, and read the DECLARED STYLE
48
+ // entry (exports["."].style / style). Bun.resolve is not used here: it
49
+ // hands back whatever the package's main is — for a normal JS package
50
+ // that is a .js entry, not the declared stylesheet. A package with no
51
+ // style entry is not a stylesheet import: that is a build-breaking
52
+ // error naming the specifier, never an invented path whose ENOENT
53
+ // surfaces as a misleading downstream CSS error.
54
+ const tried: string[] = [];
55
+ for (const root of [base, cwd]) {
56
+ let dir = root;
57
+ for (;;) {
58
+ const pkgPath = join(dir, "node_modules", id, "package.json");
59
+ if (await Bun.file(pkgPath).exists()) {
60
+ const pkg = await Bun.file(pkgPath).json();
61
+ const entry: string | undefined =
62
+ pkg.exports?.["."]?.style ?? pkg.style;
63
+ if (!entry) {
64
+ throw new Error(
65
+ `style import "${id}" resolved to ${pkgPath} but the package declares no style entry (exports["."].style / style) — not a stylesheet package`,
66
+ );
67
+ }
47
68
  const r = resolve(dirname(pkgPath), entry);
48
69
  return { path: r, base: dirname(r), content: await Bun.file(r).text() };
49
70
  }
50
- } catch (error) {
51
- getAppLogger().warn(
52
- "package style entry resolve failed — falling back to bare resolve",
53
- error,
54
- );
71
+ tried.push(pkgPath);
72
+ const parent = dirname(dir);
73
+ if (parent === dir) break;
74
+ dir = parent;
55
75
  }
56
76
  }
57
- const r = resolve(base, id);
58
- return { path: r, base: dirname(r), content: await Bun.file(r).text() };
77
+ throw new Error(
78
+ `style import "${id}" unresolved — no node_modules/${id}/package.json found walking up from ${base} or ${cwd} (${tried.length} location(s) tried)`,
79
+ );
59
80
  }
60
81
 
61
82
  // Tailwind v4's compile() invokes `loadStylesheet` for DIRECT @imports in the entry (app.css) but does NOT re-invoke it for @imports WITHIN the loaded
@@ -87,14 +108,18 @@ async function inlineCssImports(
87
108
  result += m[0];
88
109
  continue;
89
110
  }
111
+ // Remote @imports are CSP-forbidden and stripped by
112
+ // stripRemoteImports AFTER the compile: skip them here EXPLICITLY —
113
+ // they must never reach resolveStylesheetRaw, and an unresolved
114
+ // LOCAL import below is a throw, not a swallow.
115
+ if (spec.startsWith("http://") || spec.startsWith("https://")) {
116
+ result += m[0];
117
+ continue;
118
+ }
90
119
  if (seen.has(spec)) continue; // cycle guard
91
120
  seen.add(spec);
92
- try {
93
- const resolved = await resolveStylesheetRaw(spec, base, cwd);
94
- result += await inlineCssImports(resolved.content, resolved.base, cwd, seen);
95
- } catch {
96
- result += m[0]; // unresolved — keep literal; CSS/Tailwind will surface it
97
- }
121
+ const resolved = await resolveStylesheetRaw(spec, base, cwd);
122
+ result += await inlineCssImports(resolved.content, resolved.base, cwd, seen);
98
123
  }
99
124
  result += css.slice(lastIdx);
100
125
  return result;
@@ -111,6 +136,8 @@ async function bunResolveStylesheet(
111
136
  return { ...resolved, content };
112
137
  }
113
138
 
139
+ export { bunResolveStylesheet };
140
+
114
141
  // The app owns its fonts via an explicit, CSP-allowlisted <link> in index.html (Google Fonts — `style-src ... fonts.googleapis.com`, `font-src ...
115
142
  // fonts.gstatic.com`). Third-party package CSS MUST NOT inject its own remote `@import url(http…)`: @puckeditor/core/dist/index.css opens with
116
143
  // `@import "https://rsms.me/inter/inter.css"`, an arbitrary CDN our CSP does NOT allow. The Tailwind compile inlines that mid-output, where it was either
@@ -33,8 +33,6 @@ export async function resolveProposalAuthorizedEmails(
33
33
  proposalId: string,
34
34
  ): Promise<Set<string>> {
35
35
  try {
36
- const { getPrisma } = await import("@teamscala/db/prisma-registry");
37
- const prisma = getPrisma();
38
36
  const rows = await queryRows`
39
37
  SELECT lower(u.email) AS email
40
38
  FROM better_auth_member m