@timber-js/app 0.2.0-alpha.87 → 0.2.0-alpha.89

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.
Files changed (57) hide show
  1. package/LICENSE +8 -0
  2. package/dist/client/index.d.ts +44 -1
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/link.d.ts +7 -44
  6. package/dist/client/link.d.ts.map +1 -1
  7. package/dist/config-types.d.ts +39 -0
  8. package/dist/config-types.d.ts.map +1 -1
  9. package/dist/config-validation.d.ts.map +1 -1
  10. package/dist/fonts/bundle.d.ts +48 -0
  11. package/dist/fonts/bundle.d.ts.map +1 -0
  12. package/dist/fonts/dev-middleware.d.ts +22 -0
  13. package/dist/fonts/dev-middleware.d.ts.map +1 -0
  14. package/dist/fonts/pipeline.d.ts +138 -0
  15. package/dist/fonts/pipeline.d.ts.map +1 -0
  16. package/dist/fonts/transform.d.ts +72 -0
  17. package/dist/fonts/transform.d.ts.map +1 -0
  18. package/dist/fonts/types.d.ts +45 -1
  19. package/dist/fonts/types.d.ts.map +1 -1
  20. package/dist/fonts/virtual-modules.d.ts +59 -0
  21. package/dist/fonts/virtual-modules.d.ts.map +1 -0
  22. package/dist/index.js +753 -575
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/entries.d.ts.map +1 -1
  25. package/dist/plugins/fonts.d.ts +16 -83
  26. package/dist/plugins/fonts.d.ts.map +1 -1
  27. package/dist/server/action-client.d.ts +8 -0
  28. package/dist/server/action-client.d.ts.map +1 -1
  29. package/dist/server/action-handler.d.ts +7 -0
  30. package/dist/server/action-handler.d.ts.map +1 -1
  31. package/dist/server/index.js +158 -2
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/route-matcher.d.ts +7 -0
  34. package/dist/server/route-matcher.d.ts.map +1 -1
  35. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  36. package/dist/server/sensitive-fields.d.ts +74 -0
  37. package/dist/server/sensitive-fields.d.ts.map +1 -0
  38. package/package.json +6 -7
  39. package/src/cli.ts +0 -0
  40. package/src/client/index.ts +77 -1
  41. package/src/client/link.tsx +15 -65
  42. package/src/config-types.ts +39 -0
  43. package/src/config-validation.ts +7 -3
  44. package/src/fonts/bundle.ts +142 -0
  45. package/src/fonts/dev-middleware.ts +74 -0
  46. package/src/fonts/pipeline.ts +275 -0
  47. package/src/fonts/transform.ts +353 -0
  48. package/src/fonts/types.ts +50 -1
  49. package/src/fonts/virtual-modules.ts +159 -0
  50. package/src/plugins/entries.ts +37 -0
  51. package/src/plugins/fonts.ts +102 -704
  52. package/src/plugins/routing.ts +6 -5
  53. package/src/server/action-client.ts +34 -4
  54. package/src/server/action-handler.ts +32 -2
  55. package/src/server/route-matcher.ts +7 -0
  56. package/src/server/rsc-entry/index.ts +19 -3
  57. package/src/server/sensitive-fields.ts +230 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Virtual module source generators for the timber-fonts plugin.
3
+ *
4
+ * The plugin exposes three virtual modules:
5
+ *
6
+ * - `@timber/fonts/google` — exports Google font loader functions. The
7
+ * transform hook replaces actual call sites with static `FontResult`
8
+ * objects, so this module is only loaded as a fallback (e.g. when the
9
+ * transform hook hasn't run yet, or when an unknown font is referenced
10
+ * via the default export proxy).
11
+ * - `@timber/fonts/local` — exports `localFont()` as a fallback default.
12
+ * - `virtual:timber-font-css-register` — side-effect module that sets the
13
+ * combined `@font-face` CSS on `globalThis.__timber_font_css`. The RSC
14
+ * entry reads this at render time to inline a `<style>` tag.
15
+ *
16
+ * Design doc: 24-fonts.md
17
+ */
18
+
19
+ import type { ExtractedFont, FontFaceDescriptor } from './types.js';
20
+ import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from './css.js';
21
+ import { generateFallbackCss } from './fallbacks.js';
22
+ import { generateLocalFontFaces } from './local.js';
23
+
24
+ export const VIRTUAL_GOOGLE = '@timber/fonts/google';
25
+ export const VIRTUAL_LOCAL = '@timber/fonts/local';
26
+ export const RESOLVED_GOOGLE = '\0@timber/fonts/google';
27
+ export const RESOLVED_LOCAL = '\0@timber/fonts/local';
28
+
29
+ export const VIRTUAL_FONT_CSS_REGISTER = 'virtual:timber-font-css-register';
30
+ export const RESOLVED_FONT_CSS_REGISTER = '\0virtual:timber-font-css-register';
31
+
32
+ /**
33
+ * Convert a font family name to a PascalCase export name.
34
+ * e.g. "JetBrains Mono" → "JetBrains_Mono"
35
+ */
36
+ function familyToExportName(family: string): string {
37
+ return family.replace(/\s+/g, '_');
38
+ }
39
+
40
+ /**
41
+ * Generate the virtual module source for `@timber/fonts/google`.
42
+ *
43
+ * The transform hook replaces real call sites at build time, so this
44
+ * module only matters as a runtime fallback. We export a Proxy default
45
+ * that handles any font name plus named exports for known families
46
+ * (for tree-shaking).
47
+ */
48
+ export function generateGoogleVirtualModule(fonts: Iterable<ExtractedFont>): string {
49
+ const families = new Set<string>();
50
+ for (const font of fonts) {
51
+ if (font.provider === 'google') families.add(font.family);
52
+ }
53
+
54
+ const lines = [
55
+ '// Auto-generated virtual module: @timber/fonts/google',
56
+ '// Each export is a font loader function that returns a FontResult.',
57
+ '',
58
+ 'function createFontResult(family, config) {',
59
+ ' return {',
60
+ ' className: `timber-font-${family.toLowerCase().replace(/\\s+/g, "-")}`,',
61
+ ' style: { fontFamily: family },',
62
+ ' variable: config?.variable,',
63
+ ' };',
64
+ '}',
65
+ '',
66
+ 'export default new Proxy({}, {',
67
+ ' get(_, prop) {',
68
+ ' if (typeof prop === "string") {',
69
+ ' return (config) => createFontResult(prop.replace(/_/g, " "), config);',
70
+ ' }',
71
+ ' }',
72
+ '});',
73
+ ];
74
+
75
+ for (const family of families) {
76
+ const exportName = familyToExportName(family);
77
+ lines.push('');
78
+ lines.push(`export function ${exportName}(config) {`);
79
+ lines.push(` return createFontResult('${family}', config);`);
80
+ lines.push('}');
81
+ }
82
+
83
+ return lines.join('\n');
84
+ }
85
+
86
+ /**
87
+ * Generate the virtual module source for `@timber/fonts/local`.
88
+ *
89
+ * Like the google virtual module, this is a runtime fallback. The
90
+ * transform hook normally replaces `localFont(...)` calls with static
91
+ * `FontResult` objects at build time.
92
+ */
93
+ export function generateLocalVirtualModule(): string {
94
+ return [
95
+ '// Auto-generated virtual module: @timber/fonts/local',
96
+ '',
97
+ 'export default function localFont(config) {',
98
+ ' const family = config?.family || "Local Font";',
99
+ ' return {',
100
+ ' className: `timber-font-${family.toLowerCase().replace(/\\s+/g, "-")}`,',
101
+ ' style: { fontFamily: family },',
102
+ ' variable: config?.variable,',
103
+ ' };',
104
+ '}',
105
+ ].join('\n');
106
+ }
107
+
108
+ /**
109
+ * Generate combined CSS for a single extracted font.
110
+ *
111
+ * Includes `@font-face` rules (local or Google), the size-adjusted fallback
112
+ * `@font-face`, and the scoped class rule. For Google fonts the resolved
113
+ * `FontFaceDescriptor[]` must be passed in (from `generateProductionFontFaces`
114
+ * in production or `resolveDevFontFaces` in dev).
115
+ */
116
+ export function generateFontCss(font: ExtractedFont, googleFaces?: FontFaceDescriptor[]): string {
117
+ const cssParts: string[] = [];
118
+
119
+ if (font.provider === 'local' && font.localSources) {
120
+ const faces = generateLocalFontFaces(font.family, font.localSources, font.display);
121
+ const faceCss = generateFontFaces(faces);
122
+ if (faceCss) cssParts.push(faceCss);
123
+ }
124
+
125
+ if (font.provider === 'google' && googleFaces && googleFaces.length > 0) {
126
+ const faceCss = generateFontFaces(googleFaces);
127
+ if (faceCss) cssParts.push(faceCss);
128
+ }
129
+
130
+ const fallbackCss = generateFallbackCss(font.family);
131
+ if (fallbackCss) cssParts.push(fallbackCss);
132
+
133
+ if (font.variable) {
134
+ cssParts.push(generateVariableClass(font.className, font.variable, font.fontFamily));
135
+ } else {
136
+ cssParts.push(generateFontFamilyClass(font.className, font.fontFamily));
137
+ }
138
+
139
+ return cssParts.join('\n\n');
140
+ }
141
+
142
+ /**
143
+ * Generate the CSS output for all extracted fonts.
144
+ *
145
+ * Includes `@font-face` rules for local and Google fonts, fallback rules,
146
+ * and scoped classes. `googleFontFacesMap` provides pre-resolved
147
+ * `FontFaceDescriptor[]` for each Google font (keyed by `ExtractedFont.id`).
148
+ */
149
+ export function generateAllFontCss(
150
+ registry: Map<string, ExtractedFont>,
151
+ googleFontFacesMap?: Map<string, FontFaceDescriptor[]>
152
+ ): string {
153
+ const cssParts: string[] = [];
154
+ for (const font of registry.values()) {
155
+ const googleFaces = googleFontFacesMap?.get(font.id);
156
+ cssParts.push(generateFontCss(font, googleFaces));
157
+ }
158
+ return cssParts.join('\n\n');
159
+ }
@@ -98,6 +98,38 @@ function stripRootPrefix(id: string, root: string): string {
98
98
  *
99
99
  * Serializes output mode and feature flags for runtime consumption.
100
100
  */
101
+ /**
102
+ * Extract the JSON-serializable subset of `forms` config.
103
+ *
104
+ * Drops function-valued `stripSensitiveFields` with a build-time warning —
105
+ * functions cannot cross the JSON boundary to the runtime, so users must
106
+ * configure function predicates per-action via `createActionClient` instead.
107
+ */
108
+ function serializeFormsConfig(
109
+ forms: { stripSensitiveFields?: unknown } | undefined
110
+ ): { stripSensitiveFields?: boolean | readonly string[] } | undefined {
111
+ if (!forms) return undefined;
112
+ const opt = forms.stripSensitiveFields;
113
+ if (opt === undefined) return {};
114
+ if (typeof opt === 'boolean') return { stripSensitiveFields: opt };
115
+ if (Array.isArray(opt)) {
116
+ // Coerce to a string array and drop any non-string entries.
117
+ const names = opt.filter((v): v is string => typeof v === 'string');
118
+ return { stripSensitiveFields: names };
119
+ }
120
+ if (typeof opt === 'function') {
121
+ console.warn(
122
+ '[timber] forms.stripSensitiveFields was set to a function in timber.config.ts — ' +
123
+ 'this is not supported at the global level (functions cannot be serialized ' +
124
+ 'into the runtime config). Use a per-action override via ' +
125
+ '`createActionClient({ stripSensitiveFields: (name) => ... })` instead. ' +
126
+ 'The built-in deny-list will be used globally.'
127
+ );
128
+ return {};
129
+ }
130
+ return {};
131
+ }
132
+
101
133
  function generateConfigModule(ctx: PluginContext): string {
102
134
  const runtimeConfig = {
103
135
  output: ctx.config.output ?? 'server',
@@ -114,6 +146,11 @@ function generateConfigModule(ctx: PluginContext): string {
114
146
  // Auto-generated sitemap config — opt-in via timber.config.ts.
115
147
  // See design/16-metadata.md §"Auto-generated Sitemap"
116
148
  sitemap: ctx.config.sitemap,
149
+ // Forms config — only the serializable subset. `stripSensitiveFields`
150
+ // accepts boolean | string[] at the global level. Functions must be
151
+ // configured per-action via `createActionClient({ stripSensitiveFields })`
152
+ // because JSON.stringify would silently drop them here. See TIM-816.
153
+ forms: serializeFormsConfig(ctx.config.forms),
117
154
  // Per-build deployment ID for version skew detection (TIM-446).
118
155
  // Null in dev mode — HMR handles code updates without full reloads.
119
156
  deploymentId: ctx.deploymentId ?? null,