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 +2 -0
- package/package.json +1 -1
- package/src/create.mjs +9 -24
- package/src/security/headers.mjs +24 -0
- package/src/server/server.mjs +14 -2
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.
|
|
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(
|
|
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(
|
|
153
|
-
|
|
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 }) {
|
|
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';
|
package/src/security/headers.mjs
CHANGED
|
@@ -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': [],
|
package/src/server/server.mjs
CHANGED
|
@@ -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
|
|