From 033bed0186aae36e856b4fab368c5f537f1a49e7 Mon Sep 17 00:00:00 2001 From: sasjs Date: Sat, 24 Oct 2026 13:13:49 +0000 Subject: [PATCH 1/5] docs(build): document the browser build produced by streamConfig.mockWeb sasjs build emits a self-contained static site when mockWeb is set: same services, same webout contract, no server and no SAS. The page covers the required shape of the target, where the site lands (mockWebFolder, sasjsbuild/mock by default), how the request flow works, and how to serve it - a service worker needs a secure context, so localhost and https work and file:// does not. It also states the limits plainly: single user, no authentication, and no SAS engine, so SAS-backed services cannot run in it. --- docs/build.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/build.md b/docs/build.md index adfdb00..08472d6 100644 --- a/docs/build.md +++ b/docs/build.md @@ -49,3 +49,45 @@ Put this access token in a macro variable called `ACCESS_TOKEN` and it will be u !!! Warning Saving security tokens in project repositories, especially if they are committed to source control, is a security risk - as anyone with access can use them to make modifications on the Viya platform. If you are running Viya code in non Viya-enabled SAS environments using the ACCESS_TOKEN approach described above, be sure to use a secure mechanism such as `%inc` from a secure directory, or another approved mechanism for securing these kinds of variables. Avoid writing Access Tokens to log files. + +## Browser build (`streamConfig.mockWeb`) + +Setting [`mockWeb`](https://cli.sasjs.io/sasjsconfig.html#streamConfig_mockWeb) to `true` makes `sasjs build` produce a second artefact: a self-contained static site whose backend runs in the browser. The same services run, the same `webout` and header contract applies, and there is no server, no SAS and no network in the path. It exists so an app whose services are JavaScript can be hosted on any static host - a demo, a review environment, a shared link - and still behave like a deployed SASjs app. + +```json +"streamConfig": { + "webSourcePath": "../client/dist", + "mockWeb": true, + "mockWebFolder": "sasjsbuild/mock" +} +``` + +`mockWeb` is independent of [`streamWeb`](https://cli.sasjs.io/sasjsconfig.html#streamConfig_streamWeb): the same target still deploys for real, so one config serves both the browser build and a normal `sasjs deploy`. + +### Requirements + +* the target's services must be JavaScript - typically the `mocks` project pattern, where `sas/mocks/sasjs/services` holds `.js` programs. A target that compiles `.sas` cannot be built for the browser, and the build says so +* [`webSourcePath`](https://cli.sasjs.io/sasjsconfig.html#streamConfig_webSourcePath) has to point at the built frontend (`dist` by default); the site is assembled from that folder +* the target's `appLoc` has to match the appLoc the frontend was built with, because the services are resolved from it + +### What the build produces + +The site is written to [`mockWebFolder`](https://cli.sasjs.io/sasjsconfig.html#streamConfig_mockWebFolder), `sasjsbuild/mock` by default. It contains the frontend, the compiled JavaScript services, a service worker and the small runtime that executes them. The virtual drive the services see is seeded from the build output - the same files `sasjs compile` wrote, plus anything `syncFolder` copied in - so a service reading `services/...` or a data file finds it without any upload step. + +### How it runs + +A service worker intercepts the calls the [SASjs Adapter](https://adapter.sasjs.io) makes, invokes the matching JavaScript service, and resolves `webout`, log and headers back to the app. Requests appear in the browser's network tab as they normally would. Anything the services write to the drive is kept in IndexedDB, so a page refresh does not lose it. + +### Serving it + +Serve the folder over http or https. A service worker needs a secure context, which means `localhost` and `https` both work but `file://` does not: + +```bash +cd sasjsbuild/mock +python3 -m http.server 8099 +``` + +Deploy it like any static site - it is just files. + +!!! warning + This is a single-user, no-authentication build. Services that read SAS tables, run macros or depend on SAS libraries cannot work in it, because there is no SAS engine in the browser. Treat it as a demo and review artefact, not as a replacement for a real deployment. From ff6ed4a59d0404e80f51e63b8cdbaefb4aae26df Mon Sep 17 00:00:00 2001 From: sasjs Date: Sat, 24 Oct 2026 14:42:11 +0000 Subject: [PATCH 2/5] docs(build): document shipping the browser build with a captured drive Covers what a snapshot is, how to capture one (window.SASjsMock.download()), where to put the file, how to reset or ship the plain seed instead, and the trap that state tied to an origin - a licence, for instance - does not decrypt anywhere else, which is why a snapshot has to be captured on the domain it is deployed to. Also notes that the host assets carry a ?v= hash, so index.html and sw.js have to be uploaded with the rest. --- docs/build.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index 08472d6..90d4368 100644 --- a/docs/build.md +++ b/docs/build.md @@ -87,7 +87,34 @@ cd sasjsbuild/mock python3 -m http.server 8099 ``` -Deploy it like any static site - it is just files. +Deploy it like any static site - it is just files. The runtime files under `host/` are referenced with a `?v=` query that changes with their contents, so a redeploy replaces them for returning visitors too; upload `index.html` and `sw.js` together with everything else, since those two carry the reference. + +### Shipping the site with data + +Out of the box the drive a visitor gets holds the services and whatever else lives in the build folder - enough to run, but empty of the data those services would have produced. A snapshot changes that: if `mock-snapshot.json` sits in the build folder, the build copies it to `host/vfs-snapshot.json` and the runtime prefers it over the plain seed, so every visitor starts from the captured state instead of an empty drive. + +To capture one, open the site, use the app until the drive is in the state you want, and run this in the browser console: + +```js +await window.SASjsMock.download() +``` + +That saves `mock-snapshot.json` to your downloads. Put it beside the compiled output - `sasjsbuild/mock-snapshot.json` for the default build folder - and build again. The build reports it in `host/mock-config.json` as `"snapshot": true`. + +```json +{ + "appLoc": "/Public/app/dc", + "snapshot": true, + "driveEntries": 97, + "jsPrograms": 43 +} +``` + +Three things worth knowing: + +* the snapshot is a plain JSON map of drive path to file contents, and a real app's drive runs to megabytes, so treat it as a build input you generate rather than something to keep in git +* state stays tied to where it was captured. A licence issued for one origin does not decrypt on another, so capture the snapshot on the domain you deploy to - if an app asks to be licensed or configured after deployment while it worked locally, this is the usual reason +* to start over, `window.SASjsMock.reset()` clears the persisted drive and reseeds from the shipped files; to ship the plain seed instead, remove `mock-snapshot.json` from the build folder and build again !!! warning This is a single-user, no-authentication build. Services that read SAS tables, run macros or depend on SAS libraries cannot work in it, because there is no SAS engine in the browser. Treat it as a demo and review artefact, not as a replacement for a real deployment. From ae66c68b602a81524da38ae3121fcd3100f84903 Mon Sep 17 00:00:00 2001 From: "sasjs@4gl.io" Date: Sat, 24 Oct 2026 20:27:45 +0000 Subject: [PATCH 3/5] docs(build): document serving a download from a browser build A mock of a streaming service was the one thing a browser build could not do: the download headers were ignored and a byte body had nowhere to go. Say how a service declares a file response, in text and in bytes, and what the host does with the headers it was given. Pairs with sasjs/cli#1494. --- docs/build.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/build.md b/docs/build.md index 90d4368..e85cb0d 100644 --- a/docs/build.md +++ b/docs/build.md @@ -78,6 +78,39 @@ The site is written to [`mockWebFolder`](https://cli.sasjs.io/sasjsconfig.html#s A service worker intercepts the calls the [SASjs Adapter](https://adapter.sasjs.io) makes, invokes the matching JavaScript service, and resolves `webout`, log and headers back to the app. Requests appear in the browser's network tab as they normally would. Anything the services write to the drive is kept in IndexedDB, so a page refresh does not lose it. +### Downloads and file responses + +A service answers with a file the same way it does on a server: it declares the response in `_SASJS_WEBOUT_HEADERS` and writes the body where the wrapper points it. The generated wrapper already declares `fs`, so a service uses it rather than requiring it again. + +For text - a CSV, a DDL script, a log - assign `_webout` (or write the string to `weboutPath`): + +```js +fs.writeFileSync( + _SASJS_WEBOUT_HEADERS, + 'Content-type: text/csv\nContent-Disposition: attachment;filename="users.csv"' +) +_webout = 'id,name\n1,Ann\n' +``` + +For bytes - a zip, an xlsx, an image - write them to `weboutPath` as a `Buffer` or a `Uint8Array`: + +```js +fs.writeFileSync( + _SASJS_WEBOUT_HEADERS, + 'Content-type: application/zip\nContent-Disposition: attachment;filename="pack.zip"' +) +fs.writeFileSync(weboutPath, Buffer.from(zipBytes)) +``` + +`Buffer.from()` takes bytes, an array of numbers, or a base64 string with `'base64'` as the second argument, so a library that produces base64 needs no extra step. This is the browser counterpart of `%mp_streamfile` - the same two writes - and the browser offers the response under the name in `Content-Disposition`. + +Only `Content-type` and `Content-Disposition` are applied; any other header is ignored, because a static host has no cache to control and no cookies to set. A request made with `_debug` returns the text frame instead, since its body is the webout wrapped in the log. + +Two things to keep in mind: + +* the drive holds text, so a file a service writes as bytes is stored as base64 and reads back as bytes - it is a demo drive, not a filesystem for large binaries +* with no `Content-type` declared, a byte body is served as `application/octet-stream` + ### Serving it Serve the folder over http or https. A service worker needs a secure context, which means `localhost` and `https` both work but `file://` does not: From 207130fdd62f0b0c8774d9e39a3679acc7a2cf88 Mon Sep 17 00:00:00 2001 From: "sasjs@4gl.io" Date: Sat, 24 Oct 2026 23:47:44 +0000 Subject: [PATCH 4/5] docs(build): note the ASCII filename form a download header needs --- docs/build.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/build.md b/docs/build.md index e85cb0d..1748bc5 100644 --- a/docs/build.md +++ b/docs/build.md @@ -110,6 +110,7 @@ Two things to keep in mind: * the drive holds text, so a file a service writes as bytes is stored as base64 and reads back as bytes - it is a demo drive, not a filesystem for large binaries * with no `Content-type` declared, a byte body is served as `application/octet-stream` +* a filename outside ASCII belongs in RFC 6266 form - `Content-Disposition: attachment;filename*=UTF-8''r%C3%A9sum%C3%A9.csv`. Raw non-ASCII bytes in a header are not portable: the browser's own download honours the bytes it is given, and the same header sent by a SASjs Server arrives with node's latin1 encoding, so the two only agree on the ASCII form ### Serving it From 82e45a13f8aeda0b93032b29e860aa28f8299c19 Mon Sep 17 00:00:00 2001 From: Allan Bowe <4420615+allanbowe@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:12:36 +0000 Subject: [PATCH 5/5] Update build.md --- docs/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index 1748bc5..070cee6 100644 --- a/docs/build.md +++ b/docs/build.md @@ -106,7 +106,7 @@ fs.writeFileSync(weboutPath, Buffer.from(zipBytes)) Only `Content-type` and `Content-Disposition` are applied; any other header is ignored, because a static host has no cache to control and no cookies to set. A request made with `_debug` returns the text frame instead, since its body is the webout wrapped in the log. -Two things to keep in mind: +Some things to keep in mind: * the drive holds text, so a file a service writes as bytes is stored as base64 and reads back as bytes - it is a demo drive, not a filesystem for large binaries * with no `Content-type` declared, a byte body is served as `application/octet-stream`