glashjs 0.14.2 → 0.15.0

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/bin/glash.mjs CHANGED
@@ -124,6 +124,8 @@ async function main() {
124
124
  auth: optionBool('--no-auth', undefined),
125
125
  sqlRunner: optionBool('--no-sql-runner', undefined),
126
126
  aiPrompts: optionBool('--no-ai-prompts', undefined),
127
+ translate: optionBool('--no-translate', undefined),
128
+ animation: flag('--animation') ? true : undefined,
127
129
  });
128
130
  break;
129
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glashjs",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "glashjs — The Postgres-native full-stack framework for builders who want to ship without DevOps. Framework, hosting, database, auth, and deploy in one GlashDB-native runtime.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/create.mjs CHANGED
@@ -111,7 +111,7 @@ function projectFiles(answers) {
111
111
  '.env.local': envLocal(answers),
112
112
  'glash.config.mjs': configFile(answers, hasCss),
113
113
  'README.md': projectReadme(answers),
114
- 'routes/_layout.jsx': layoutRoute(answers),
114
+ 'routes/_layout.jsx': layoutRoute(),
115
115
  'routes/index.jsx': indexRoute(answers),
116
116
  'routes/api/health.mjs': healthRoute(),
117
117
  'db/schema.sql': schemaSql(answers),
@@ -119,7 +119,6 @@ function projectFiles(answers) {
119
119
  ...(answers.css === 'tailwind' ? { 'styles/input.css': tailwindInput() } : {}),
120
120
  ...(answers.auth ? authRoutes() : {}),
121
121
  ...(answers.sqlRunner ? sqlRunnerRoutes() : {}),
122
- ...(answers.translate ? { 'routes/api/_glash/geo.mjs': geoRoute() } : {}),
123
122
  ...(answers.animation ? { 'routes/demo/motion.jsx': motionDemoRoute() } : {}),
124
123
  ...(answers.aiPrompts ? { '.glash/prompts/deploy.md': aiDeployPrompt(answers) } : {}),
125
124
  'public/favicon.svg': faviconSvg(),
@@ -136,6 +135,11 @@ export default defineConfig({
136
135
  publicDir: 'public',
137
136
  outDir: '.glash/out',
138
137
  offline: true,
138
+ // Built-in IP auto-translation: detect the visitor's country and offer a
139
+ // one-tap in-place translation into their native language. The framework
140
+ // serves the runtime + geo endpoint and relaxes the CSP for the translate
141
+ // widget automatically — no route or layout wiring needed.
142
+ i18n: ${answers.translate ? 'true' : 'false'},
139
143
  favicon: 'node_modules/glashjs/templates/glash_favicon.svg',
140
144
  animatedFavicon: false,
141
145
  stylesheets: ${hasCss ? "['/app.css']" : '[]'},
@@ -149,19 +153,10 @@ export default defineConfig({
149
153
  `;
150
154
  }
151
155
 
152
- function layoutRoute(answers = {}) {
153
- const t = answers.translate;
154
- const imports = [`import { Link } from 'glashjs/link';`];
155
- if (t) {
156
- imports.push(`import { useEffect } from 'preact/hooks';`);
157
- imports.push(`import { glashAutoTranslate } from 'glashjs/i18n';`);
158
- }
159
- return `${imports.join('\n')}
156
+ function layoutRoute() {
157
+ return `import { Link } from 'glashjs/link';
160
158
 
161
- export default function RootLayout({ children }) {${t ? `
162
- // Built-in IP auto-translation: detect the visitor's country and offer to
163
- // translate the page into their native language. Reads /api/_glash/geo.
164
- useEffect(() => { glashAutoTranslate(); }, []);` : ''}
159
+ export default function RootLayout({ children }) {
165
160
  return (
166
161
  <div className="shell">
167
162
  <header className="nav">
@@ -178,16 +173,6 @@ export default function RootLayout({ children }) {${t ? `
178
173
  `;
179
174
  }
180
175
 
181
- function geoRoute() {
182
- return `// IP -> country -> native language, read from the edge geo header
183
- // (CF-IPCountry / X-Glash-Country on glashDB hosting). The client i18n runtime
184
- // calls this to decide whether to offer auto-translation.
185
- import { geoRouteHandler } from 'glashjs/i18n';
186
-
187
- export const GET = geoRouteHandler;
188
- `;
189
- }
190
-
191
176
  function motionDemoRoute() {
192
177
  return `import { useEffect, useRef } from 'preact/hooks';
193
178
  import { animate } from 'motion';
@@ -11,14 +11,37 @@ import { createHash } from 'node:crypto';
11
11
  * builds hash/nonce-based, so XSS via injected <script> is blocked by default.
12
12
  * Pass `connectSrc` to allow your API/realtime origins.
13
13
  */
14
+ // Google Translate widget origins used by the built-in i18n auto-translation.
15
+ // Scripts stay nonce-based (no 'unsafe-inline'): GT's element.js loads as an
16
+ // EXTERNAL script from these origins and fetches translations over XHR, so the
17
+ // framework's strict, nonce-based script CSP is preserved. Only style-src gains
18
+ // 'unsafe-inline' (low-risk) for the inline styles GT applies to translated text.
19
+ const I18N_CSP = {
20
+ script: ['https://translate.google.com', 'https://translate.googleapis.com', 'https://www.gstatic.com'],
21
+ style: ["'unsafe-inline'", 'https://www.gstatic.com'],
22
+ img: ['https://translate.google.com', 'https://translate.googleapis.com', 'https://www.gstatic.com'],
23
+ connect: ['https://translate.googleapis.com', 'https://ipapi.co'],
24
+ frame: ['https://translate.google.com'],
25
+ };
26
+ const merge = (base, extra) => [...new Set([...base, ...extra])];
27
+
14
28
  export function buildCsp({
15
29
  connectSrc = ["'self'"],
16
30
  imgSrc = ["'self'", 'data:', 'blob:'],
17
31
  mediaSrc = ["'self'", 'blob:'],
18
32
  styleSrc = ["'self'"],
19
33
  scriptSrc = ["'self'"],
34
+ frameSrc = ["'self'"],
20
35
  nonce,
36
+ i18n = false,
21
37
  } = {}) {
38
+ if (i18n) {
39
+ scriptSrc = merge(scriptSrc, I18N_CSP.script);
40
+ styleSrc = merge(styleSrc, I18N_CSP.style);
41
+ imgSrc = merge(imgSrc, I18N_CSP.img);
42
+ connectSrc = merge(connectSrc, I18N_CSP.connect);
43
+ frameSrc = merge(frameSrc, I18N_CSP.frame);
44
+ }
22
45
  const script = nonce ? [...scriptSrc, `'nonce-${nonce}'`] : scriptSrc;
23
46
  const directives = {
24
47
  'default-src': ["'self'"],
@@ -31,6 +54,7 @@ export function buildCsp({
31
54
  'img-src': imgSrc,
32
55
  'media-src': mediaSrc,
33
56
  'connect-src': connectSrc,
57
+ 'frame-src': frameSrc,
34
58
  'worker-src': ["'self'"],
35
59
  'manifest-src': ["'self'"],
36
60
  'upgrade-insecure-requests': [],
@@ -76,6 +76,17 @@ export async function createGlashServer({ root = process.cwd(), dev = false } =
76
76
  if (pathname === '/_glash/nav.js') {
77
77
  return send(res, 200, 'text/javascript; charset=utf-8', NAV_CLIENT, { ...secHeaders, 'cache-control': dev ? 'no-store' : 'public, max-age=31536000, immutable' });
78
78
  }
79
+ // Built-in IP auto-translation runtime + geo endpoint (config: i18n).
80
+ if (cfg.i18n) {
81
+ if (pathname === '/_glash/i18n.js') {
82
+ return send(res, 200, 'text/javascript; charset=utf-8', I18N_CLIENT, { ...secHeaders, 'cache-control': dev ? 'no-store' : 'public, max-age=31536000, immutable' });
83
+ }
84
+ // Geo endpoint: an app-defined routes/api/_glash/geo.mjs wins; otherwise
85
+ // the framework answers from the edge country header.
86
+ if (pathname === '/api/_glash/geo' && !matchRoute(routes, pathname)) {
87
+ return send(res, 200, 'application/json', JSON.stringify(geoRouteHandler({ headers: req.headers })), { ...secHeaders, 'cache-control': 'no-store' });
88
+ }
89
+ }
79
90
  // Static first: in production this serves prebuilt /_glash/<id>.js bundles
80
91
  // (written by `glash build`) — no runtime esbuild needed.
81
92
  if (await serveStatic(res, outDir, pathname, req, secHeaders)) return;
@@ -182,6 +193,7 @@ async function handlePage(res, mod, ctx, cfg, secHeaders, dev) {
182
193
  body: page.body ?? '',
183
194
  offline: cfg.offline,
184
195
  animatedFavicon: !!cfg.animatedFavicon,
196
+ i18n: cfg.i18n,
185
197
  nonce, dev,
186
198
  });
187
199
  send(res, page.status || 200, 'text/html; charset=utf-8', docHtml, { ...pageHeaders(cfg, secHeaders, nonce), ...(page.headers || {}) });
@@ -207,7 +219,7 @@ async function handleComponentPage(res, route, ctx, cfg, secHeaders, root, route
207
219
  // external 'self' module — both pass the strict CSP without 'unsafe-inline'.
208
220
  const head = renderStylesheets(cfg) + renderMeta(meta) + `<script type="application/json" id="glash-props">${safeJson(props)}</script>`;
209
221
  const { open, tail } = documentParts({
210
- title, head, offline: cfg.offline, animatedFavicon: !!cfg.animatedFavicon, nonce, dev,
222
+ title, head, offline: cfg.offline, animatedFavicon: !!cfg.animatedFavicon, i18n: cfg.i18n, nonce, dev,
211
223
  });
212
224
  const bundleTag = `</div><script type="module" src="/_glash/${id}.js"></script>`;
213
225
  res.writeHead(200, { ...pageHeaders(cfg, secHeaders, nonce), 'content-type': 'text/html; charset=utf-8' });
@@ -256,7 +268,7 @@ async function resolveMeta(metadata, ctx) {
256
268
  // Per-request page headers: a fresh CSP carrying this request's script nonce, so
257
269
  // the framework's own inline <script>s run while injected scripts stay blocked.
258
270
  function pageHeaders(cfg, secHeaders, nonce) {
259
- const csp = securityHeaders({ ...(cfg.security || {}), csp: { ...((cfg.security || {}).csp || {}), nonce } })['Content-Security-Policy'];
271
+ const csp = securityHeaders({ ...(cfg.security || {}), csp: { ...((cfg.security || {}).csp || {}), nonce, i18n: !!cfg.i18n } })['Content-Security-Policy'];
260
272
  return { ...secHeaders, 'Content-Security-Policy': csp };
261
273
  }
262
274