scale-framework
v0.1.2
Published
SSR-first full-stack framework starter for the Scale language
Readme
Scale Framework (SSR-first)
Scale is an SSR-first full-stack framework prototype with:
- File-system routing from
app/**/index.scale - Parser-based
.scaletemplates (AST, no regex rendering) - Server functions in
server.jswithfunc:Namebindings - Server-driven component rendering from
components/*.scale - Secure event flow (session token + CSRF + server-owned function map/state)
Quick Start
npm install
npm run devOpen http://localhost:3000.
CLI
The package now ships first-party CLI commands:
create-scale-app my-app
cd my-app
npm install
npm run devBy default this uses a local file: dependency pointing to the framework source (good for local development).
Use --registry if you want package.json to use a semver dependency instead.
create-scale-app my-app --registryGenerate routes/components inside an existing app:
scale generate route /weather/[city]
scale generate component weather-itemRouting
Static routes
app/index.scale->/app/about/index.scale->/about
Dynamic routes
app/city/[city]/index.scale->/city/seattle- Catch-all supported with
[...parts]
Route params are injected into state as both:
paramsobject ({{params.city}})- top-level fields (
{{city}})
Layouts
Use layout.scale in app/ or nested route folders.
- Parent and child layouts compose automatically.
- Layouts can render
<slot />,<slot></slot>, or{{children}}.
Error pages
Optional:
app/404.scalefor unmatched routesapp/500.scalefor server errors
Middleware
Optional middleware files:
app/middleware.js(global)app/<route>/middleware.js(route-specific)
Middleware can export either:
module.exports = async (ctx) => { ... }module.exports = { onRequest: async (ctx) => { ... } }
ctx.phase is "load" or "event". Return { state } to merge state, { redirect, statusCode } to redirect, or { statusCode, body } to block.
.scale Syntax
<div func:Waitlist>
<button func:SetCounter var:{type: "plus"}>Add</button>
<p>{{number}}</p>
</div>Features:
func:Namebinds an element to a server function.- Interactive
funcdefaults toclickon non-form elements andsubmiton<form>elements;onload-only funcs do not bind click handlers. var:{...}passes static element metadata to handlers asself.attrs:path.to.attrsapplies server-defined HTML attributes from state.{{key}}and{{nested.key}}interpolate server state.{{static:key}}reads from an immutable snapshot captured after route onload.{{comp.slot}}can render component descriptors.{{comp.list}}can render arrays of component descriptors.- Interpolations inside
<script>tags render raw text (no HTML escaping or<span>wrappers).
Example:
<a attrs:ui.cityLink>{{city}}</a>server.return({
ui: {
cityLink: {
href: "/weather/new-york",
title: "Open New York",
"data-temp": 72,
},
},
});Components
components/form.scale becomes components.form() in handlers.
components/weather-item.scale becomes components["weather-item"]().
External component libraries (installable)
You can install a component package and register it in createScaleServer:
const path = require("node:path");
const { createScaleServer } = require("scale-framework");
const server = createScaleServer({
appDir: path.join(__dirname, "app"),
componentsDir: path.join(__dirname, "components"),
componentSources: [
{ package: "@acme/scale-ui", prefix: "ui" },
],
});Then call package components via the prefix:
components["ui/button"]({ label: "Save" });Notes:
componentSourcesentries can be string paths or objects.{ package: "name" }resolves from installednode_modules.- Optional package metadata:
package.json->scale.componentsDir(defaults to"components"). - Local
componentsDiris still loaded automatically and can override package components with the same names.
Returning one component
server.return({
comp: {
menu: components.form(),
},
});Returning an array of components
server.return({
comp: {
weather: cities.map((city) => components["weather-item"](city)),
},
});Server Handler API
In server.js, each exported function gets:
element: event registrationserver: response helperscomponents: component factoriesdata: form/data validation helpers
const FormSubmit = ({ element, server, components, data }) => {
element.onsubmit((props) => {
const validation = data.validate(props, {
name: { type: "string", required: true, minLength: 2 },
email: { type: "string", required: true, email: true },
});
if (!validation.ok) {
server.validation(validation.errors, {
...props,
comp: { menu: components.form({ error: "Fix the fields" }) },
});
return;
}
server.return({
...props,
...validation.values,
comp: {
menu: components.thanks(validation.values),
},
});
});
};element methods
element.onload(handler, options?)element.onclick(handler)element.onsubmit(handler)element.on("event", handler, options?)
Handler signature is (props, self).
Deferred load is supported:
element.onload(async (props) => {
const data = await fetchSomething();
server.return({ ...props, data });
}, { defer: true });With defer: true, the initial page HTML is sent first, then the deferred load patch is applied once the handler finishes.
TypeScript/Typed JS support
Type definitions are bundled for handler context and server APIs (element, server, components, data).
For typed handler exports, use:
const { defineHandlers } = require("scale-framework");
module.exports = defineHandlers({
HomePage: ({ element, server }) => {
element.onload((props = {}) => {
server.return(props);
});
},
});server methods
server.return(statePatch)server.update(props, statePatch)merges previousprops+statePatchand mergescompmaps automaticallyserver.component(name, props)server.redirect(location, statusCode = 303)server.validation(errors, statePatch)server.invalid(errors, props, statePatch)same asvalidation, but with automaticprops+compmerge
Forms/Data Layer
Built-in data helpers (data):
data.asString(source, key, options)data.asNumber(source, key, options)data.asBoolean(source, key, options)data.pick(source, keys)data.requireFields(source, keys)data.validate(source, schema)
Form submit events send form fields automatically to the server.
Security Model
State is intentionally ephemeral/in-memory (not permanent), but the event pipeline is server-authoritative:
- Client sends only
sessionId,csrfToken,elementId,event,form. - Server resolves
func+selffrom the rendered element map in session state. - Client cannot choose arbitrary server function names.
Implemented protections:
- CSRF token validation per session
- Same-origin
Originvalidation for event POSTs - Event rate limiting
- JSON payload size limits
- Security headers (
CSP,X-Frame-Options,nosniff, etc.)
Rendering Model
- SSR first render on
GET. onloadhandlers produce initial state.onload(..., { defer: true })runs after first paint and patches the page when finished.- Events (
click,submit, custom) call server handlers. - Server re-renders route view and returns HTML fragment.
- Client swaps
[data-scale-root]content with the new server-rendered view.
Dev/Build Pipeline
npm run dev # Node watch mode
npm run test # Node test runner
npm run build # outputs .scale-build/
npm run preview # run built outputDev Error Overlay + Template Mapping
In dev mode (NODE_ENV !== "production"), server/template failures return structured errors and the browser shows an overlay with:
- message and stack
.scaletemplate file path- mapped line/column
- source frame snippet
Template/Component Live Reload
Dev mode exposes /_scale/hmr (SSE). The client runtime listens for .scale / route server.js changes and performs a live page reload.
Build output includes a route manifest: .scale-build/route-manifest.json.
VS Code Language Support + Linter
A local extension is included at:
vscode/scale-language
It provides:
.scalesyntax highlighting- snippets
- go to definition from
func:Nameto siblingserver.jsexports - inline diagnostics for tags/interpolations/directives
func:toserver.jsexport checks (whenserver.jsis in the same folder)
Run extension tests:
node --test vscode/scale-language/test/*.test.jsUse in VS Code:
- Open
vscode/scale-languageas a project in VS Code. - Press
F5to launch an Extension Development Host. - Open any
.scalefile in the dev host.
Current Example Routes
/waitlist form + validation + component swap/weatherarray-mapped weather components/city/seattledynamic route param demo
Notes
- Session/state is in-memory by design (ephemeral, not persistent storage).
- This is still a framework prototype; production hardening and ecosystem tooling can be expanded further.
