fratmscript 0.0.0-stage → 0.10.2
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/README.md +118 -2
- package/browser.d.ts +2 -0
- package/browser.js +44 -0
- package/index.d.ts +57 -0
- package/index.js +7 -0
- package/lib/api.js +51 -0
- package/lib/fetch-middleware.js +36 -0
- package/lib/node-hooks.js +32 -0
- package/lib/scripts.js +75 -0
- package/node.d.ts +3 -0
- package/node.js +7 -0
- package/package.json +66 -4
- package/vite.d.ts +27 -0
- package/vite.js +64 -0
- package/wasm/node/fratm_wasm.js +301 -0
- package/wasm/node/fratm_wasm_bg.wasm +0 -0
- package/wasm/node/package.json +1 -0
- package/wasm/web/fratm_wasm.js +389 -0
- package/wasm/web/fratm_wasm_bg.wasm +0 -0
- package/web.d.ts +12 -0
- package/web.js +38 -0
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# fratmscript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [FratmScript](https://github.com/kkratterf/fratmscript) compiler as an npm package: the real Rust compiler built to WebAssembly, a small JS API and a Vite plugin.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i fratmscript
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Compile from Node
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { compile, format } from 'fratmscript';
|
|
13
|
+
|
|
14
|
+
const { code, map, errors } = compile('stamm a dì("Uè!")', { filename: 'main.fratm' });
|
|
15
|
+
// code → 'console.log("Uè!");\n'
|
|
16
|
+
// map → source map v3 object (pass { sourceMap: false } to skip it)
|
|
17
|
+
// errors → [] or [{ message, line, column, suggestion }], messages in Neapolitan
|
|
18
|
+
|
|
19
|
+
format('si (overo) {\nstamm a dì(1)\n}').code; // same output as `fratm fmt`
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`compile` never throws on invalid source: check `errors`.
|
|
23
|
+
|
|
24
|
+
Errors thrown by the compiled code can be shown in Neapolitan too:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { runtimeError } from 'fratmscript';
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
run(code);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(runtimeError(e));
|
|
33
|
+
// 'nonEsiste' nun esiste: nun l'hê maje dichiarato cu 'chist è' o 'tien' (ReferenceError: nonEsiste is not defined)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Vite plugin
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// vite.config.js
|
|
41
|
+
import fratmscript from 'fratmscript/vite';
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
plugins: [fratmscript()],
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then import `.fratm` files from your code, and from each other with `chiamm { … } da "./file.fratm"`:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { saluta } from './saluto.fratm';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Compile errors show up in the Vite overlay with their line and column, and source maps point at the `.fratm` files. The plugin has no dependency on Vite, so it also works as a Rollup plugin.
|
|
55
|
+
|
|
56
|
+
For React Fast Refresh on `.fratm` components, add `@vitejs/plugin-react` after it and let it see the compiled files:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import react from '@vitejs/plugin-react';
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
plugins: [fratmscript(), react({ include: /\.(fratm|[jt]sx?)$/ })],
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### An API on the dev server
|
|
67
|
+
|
|
68
|
+
`api` mounts a server written in FratmScript on the Vite dev server, so one `npm run dev` serves both the app and its API. The module's default export must have a `fetch(request)` method that returns a `Response`, as a [Hono](https://hono.dev) app does. It answers the requests under `/api`, and it is reloaded when you edit it.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
export default {
|
|
72
|
+
plugins: [fratmscript({ api: 'server/api.fratm' })],
|
|
73
|
+
// or fratmscript({ api: { entry: 'server/api.fratm', prefix: '/dati' } })
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```fratm
|
|
78
|
+
// server/api.fratm
|
|
79
|
+
chiamm { Hono } da "hono"
|
|
80
|
+
chist è api = nu bell Hono().basePath("/api")
|
|
81
|
+
api.get("/pizze", (c) => c.json(["Margherita", "Marinara"]))
|
|
82
|
+
mann for predefinit api
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In production run the same module with `node --import fratmscript/node` and a Node adapter such as `@hono/node-server`. `fratm nuovo --completa` sets all of this up.
|
|
86
|
+
|
|
87
|
+
## Run .fratm files in Node
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
node --import fratmscript/node server.fratm
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`fratmscript/node` registers a module hook (Node 20.6 or later) that compiles every `.fratm` file when it is loaded, so `.fratm` modules can import each other, npm packages and `node:` modules. Stack traces point at the `.fratm` lines, and compile errors are printed in Neapolitan with file, line and column.
|
|
94
|
+
|
|
95
|
+
## In a web page, with a script tag
|
|
96
|
+
|
|
97
|
+
No build step: load `browser.js` and write FratmScript in `<script type="text/fratmscript">` tags, inline or with `src`. They run in document order once the page has loaded; compile errors and uncaught runtime errors are printed in Neapolitan in the console, and dev tools show the `.fratm` source.
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<meta charset="utf-8">
|
|
101
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/fratmscript/browser.js"></script>
|
|
102
|
+
|
|
103
|
+
<script type="text/fratmscript">
|
|
104
|
+
chist è nome = "Gennaro"
|
|
105
|
+
stamm a dì(`Uè ${nome}!`)
|
|
106
|
+
</script>
|
|
107
|
+
<script type="text/fratmscript" src="pizzeria.fratm"></script>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`import { ready } from 'fratmscript/browser'` gives a promise that resolves when every script has run. Code that uses `chiamm`/`mann for` runs as a module.
|
|
111
|
+
|
|
112
|
+
## In the browser, from JavaScript
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { init, compile } from 'fratmscript/web';
|
|
116
|
+
|
|
117
|
+
await init(); // fetches the .wasm file next to the module
|
|
118
|
+
compile('tien x = 1').code;
|
|
119
|
+
```
|
package/browser.d.ts
ADDED
package/browser.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Runs <script type="text/fratmscript"> tags in a web page:
|
|
2
|
+
//
|
|
3
|
+
// <script type="module" src="https://cdn.jsdelivr.net/npm/fratmscript/browser.js"></script>
|
|
4
|
+
// <script type="text/fratmscript">
|
|
5
|
+
// stamm a dì("Uè!")
|
|
6
|
+
// </script>
|
|
7
|
+
//
|
|
8
|
+
// Scripts run in document order once the page has loaded, inline or with
|
|
9
|
+
// src="file.fratm". Compile errors and uncaught runtime errors are printed in
|
|
10
|
+
// Neapolitan in the console. `ready` resolves when every script has run.
|
|
11
|
+
import { init, compile, runtimeError } from './web.js';
|
|
12
|
+
import { runScripts } from './lib/scripts.js';
|
|
13
|
+
|
|
14
|
+
function execute(code, original, isModule) {
|
|
15
|
+
const script = document.createElement('script');
|
|
16
|
+
if (isModule) script.type = 'module';
|
|
17
|
+
script.textContent = code;
|
|
18
|
+
// Insert next to the original tag and drop it again: the code has run
|
|
19
|
+
// (classic scripts run synchronously when inserted).
|
|
20
|
+
original.after(script);
|
|
21
|
+
script.remove();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function load(url) {
|
|
25
|
+
const response = await fetch(url);
|
|
26
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
27
|
+
return response.text();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function domReady() {
|
|
31
|
+
if (document.readyState !== 'loading') return Promise.resolve();
|
|
32
|
+
return new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve, { once: true }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener('error', (event) => {
|
|
36
|
+
if (event.error instanceof Error) console.error(`🤌 ${runtimeError(event.error)}`);
|
|
37
|
+
});
|
|
38
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
39
|
+
if (event.reason instanceof Error) console.error(`🤌 ${runtimeError(event.reason)}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const ready = Promise.all([init(), domReady()]).then(() =>
|
|
43
|
+
runScripts({ document, load, compile, execute, report: (message) => console.error(message) }),
|
|
44
|
+
);
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CompileOptions {
|
|
2
|
+
/** Generate a source map (default: true). */
|
|
3
|
+
sourceMap?: boolean;
|
|
4
|
+
/** Name of the source file, used in the source map (default: "input.fratm"). */
|
|
5
|
+
filename?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Library whose JSX runtime compiles JSX, as in `<source>/jsx-runtime` (default: "react").
|
|
8
|
+
* A `// @jsxImportSource preact` comment in the file wins over this option.
|
|
9
|
+
*/
|
|
10
|
+
jsxImportSource?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FratmError {
|
|
14
|
+
/** Error message, in Neapolitan */
|
|
15
|
+
message: string;
|
|
16
|
+
/** 1-based line, when known */
|
|
17
|
+
line: number | null;
|
|
18
|
+
/** 1-based column, when known */
|
|
19
|
+
column: number | null;
|
|
20
|
+
/** A hint on how to fix it, when there is one */
|
|
21
|
+
suggestion: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SourceMapV3 {
|
|
25
|
+
version: number;
|
|
26
|
+
file?: string;
|
|
27
|
+
sources: string[];
|
|
28
|
+
sourcesContent?: (string | null)[];
|
|
29
|
+
names: string[];
|
|
30
|
+
mappings: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompileResult {
|
|
34
|
+
/** Generated JavaScript, or null when there are errors */
|
|
35
|
+
code: string | null;
|
|
36
|
+
map: SourceMapV3 | null;
|
|
37
|
+
errors: FratmError[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FormatResult {
|
|
41
|
+
/** Formatted source, or null when the source does not compile */
|
|
42
|
+
code: string | null;
|
|
43
|
+
errors: FratmError[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compiles FratmScript source to JavaScript. Never throws on invalid source. */
|
|
47
|
+
export function compile(source: string, options?: CompileOptions): CompileResult;
|
|
48
|
+
/** Formats FratmScript source, like `fratm fmt`. */
|
|
49
|
+
export function format(source: string): FormatResult;
|
|
50
|
+
/** Version of the compiler. */
|
|
51
|
+
export function version(): string;
|
|
52
|
+
/**
|
|
53
|
+
* A JavaScript error thrown by compiled code, in Neapolitan, with the
|
|
54
|
+
* original message in brackets. Errors the program throws itself keep
|
|
55
|
+
* their message.
|
|
56
|
+
*/
|
|
57
|
+
export function runtimeError(error: unknown): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// FratmScript compiler for Node.js (and bundlers that run in Node).
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { createApi } from './lib/api.js';
|
|
4
|
+
|
|
5
|
+
const wasm = createRequire(import.meta.url)('./wasm/node/fratm_wasm.js');
|
|
6
|
+
|
|
7
|
+
export const { compile, format, version, runtimeError } = createApi(wasm);
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Turns the raw responses of the WASM bindings into the public API.
|
|
2
|
+
|
|
3
|
+
/** @param {any} response */
|
|
4
|
+
function toErrors(response) {
|
|
5
|
+
if (response.success) return [];
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
message: response.error,
|
|
9
|
+
line: response.line ?? null,
|
|
10
|
+
column: response.column ?? null,
|
|
11
|
+
suggestion: response.suggestion ?? null,
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {any} wasm */
|
|
17
|
+
export function createApi(wasm) {
|
|
18
|
+
function compile(source, options = {}) {
|
|
19
|
+
const { sourceMap = true, filename = 'input.fratm', jsxImportSource } = options;
|
|
20
|
+
const response = wasm.compile(String(source), sourceMap, jsxImportSource ?? undefined);
|
|
21
|
+
if (!response.success) {
|
|
22
|
+
return { code: null, map: null, errors: toErrors(response) };
|
|
23
|
+
}
|
|
24
|
+
let map = null;
|
|
25
|
+
if (response.sourceMap) {
|
|
26
|
+
map = JSON.parse(response.sourceMap);
|
|
27
|
+
map.sources = [filename];
|
|
28
|
+
map.file = filename.replace(/\.fratm$/, '') + '.js';
|
|
29
|
+
}
|
|
30
|
+
return { code: response.code, map, errors: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function format(source) {
|
|
34
|
+
const response = wasm.format(String(source));
|
|
35
|
+
return response.success
|
|
36
|
+
? { code: response.code, errors: [] }
|
|
37
|
+
: { code: null, errors: toErrors(response) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function version() {
|
|
41
|
+
return wasm.version();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runtimeError(error) {
|
|
45
|
+
const name = error && error.name ? String(error.name) : 'Error';
|
|
46
|
+
const message = error && error.message !== undefined ? String(error.message) : String(error);
|
|
47
|
+
return wasm.runtimeError(name, message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { compile, format, version, runtimeError };
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Serves a fetch-style app (`app.fetch(request) => response`, as Hono and
|
|
2
|
+
// other Web-standard servers have) from a Node or Connect middleware.
|
|
3
|
+
|
|
4
|
+
/** The Web `Request` for a Node `IncomingMessage`. */
|
|
5
|
+
export async function toRequest(req) {
|
|
6
|
+
const url = new URL(req.originalUrl ?? req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
9
|
+
if (value === undefined) continue;
|
|
10
|
+
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
|
|
11
|
+
}
|
|
12
|
+
const init = { method: req.method, headers };
|
|
13
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
16
|
+
init.body = Buffer.concat(chunks);
|
|
17
|
+
}
|
|
18
|
+
return new Request(url, init);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Writes a Web `Response` to a Node `ServerResponse`. */
|
|
22
|
+
export async function sendResponse(res, response) {
|
|
23
|
+
res.statusCode = response.status;
|
|
24
|
+
for (const [name, value] of response.headers) {
|
|
25
|
+
if (name !== 'set-cookie') res.setHeader(name, value);
|
|
26
|
+
}
|
|
27
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
28
|
+
if (cookies.length > 0) res.setHeader('set-cookie', cookies);
|
|
29
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether `url` is `prefix` or below it: `/api` matches `/api/pizze`, not `/apiario`. */
|
|
33
|
+
export function underPrefix(url, prefix) {
|
|
34
|
+
const path = (url ?? '/').split(/[?#]/, 1)[0];
|
|
35
|
+
return path === prefix || path.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Module hooks registered by `fratmscript/node`. They run on Node's loader
|
|
2
|
+
// thread and compile every .fratm file before it is evaluated.
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { compile } from '../index.js';
|
|
6
|
+
import { formatError } from './scripts.js';
|
|
7
|
+
|
|
8
|
+
const FRATM = /\.fratm$/;
|
|
9
|
+
|
|
10
|
+
export async function load(url, context, nextLoad) {
|
|
11
|
+
if (!FRATM.test(url.split(/[?#]/, 1)[0])) return nextLoad(url, context);
|
|
12
|
+
|
|
13
|
+
const path = fileURLToPath(url);
|
|
14
|
+
const source = await readFile(path, 'utf8');
|
|
15
|
+
const result = compile(source, { filename: path });
|
|
16
|
+
if (result.errors.length > 0) {
|
|
17
|
+
const [first] = result.errors;
|
|
18
|
+
const error = new SyntaxError(formatError(path, first));
|
|
19
|
+
// Replace the loader's own frames with the .fratm position (a stack with
|
|
20
|
+
// no frame at all would be printed in brackets)
|
|
21
|
+
error.stack = `${error.name}: ${error.message}\n at ${path}:${first.line ?? 1}:${first.column ?? 1}`;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let code = result.code;
|
|
26
|
+
if (result.map) {
|
|
27
|
+
result.map.sources = [url];
|
|
28
|
+
const map = Buffer.from(JSON.stringify(result.map)).toString('base64');
|
|
29
|
+
code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${map}\n`;
|
|
30
|
+
}
|
|
31
|
+
return { format: 'module', source: code, shortCircuit: true };
|
|
32
|
+
}
|
package/lib/scripts.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Finds <script type="text/fratmscript"> tags, compiles them and runs the
|
|
2
|
+
// JavaScript, in document order. Kept free of globals so it can be tested
|
|
3
|
+
// in Node with a fake document (see test/browser.test.js).
|
|
4
|
+
|
|
5
|
+
export const SCRIPT_TYPE = 'text/fratmscript';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} env
|
|
9
|
+
* @param {Document} env.document
|
|
10
|
+
* @param {(url: string) => Promise<string>} env.load fetches a `src=` script
|
|
11
|
+
* @param {(source: string, options: object) => any} env.compile the fratmscript compile()
|
|
12
|
+
* @param {(code: string, script: Element, isModule: boolean) => void} env.execute runs compiled JS
|
|
13
|
+
* @param {(message: string) => void} env.report prints a compile error
|
|
14
|
+
* @returns {Promise<number>} how many scripts ran
|
|
15
|
+
*/
|
|
16
|
+
export async function runScripts({ document, load, compile, execute, report }) {
|
|
17
|
+
const scripts = Array.from(document.querySelectorAll(`script[type="${SCRIPT_TYPE}"]`));
|
|
18
|
+
// Fetch every external script up front, then run them in order
|
|
19
|
+
const sources = scripts.map((script) => {
|
|
20
|
+
const src = script.getAttribute('src');
|
|
21
|
+
return src ? load(new URL(src, document.baseURI).href) : Promise.resolve(script.textContent ?? '');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let ran = 0;
|
|
25
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
26
|
+
const script = scripts[i];
|
|
27
|
+
const name = scriptName(script, i);
|
|
28
|
+
let source;
|
|
29
|
+
try {
|
|
30
|
+
source = await sources[i];
|
|
31
|
+
} catch (error) {
|
|
32
|
+
report(`FratmScript: nun riesco a caricà ${name} (${error && error.message ? error.message : error})`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const result = compile(source, { filename: name });
|
|
36
|
+
if (result.errors.length > 0) {
|
|
37
|
+
for (const error of result.errors) report(formatError(name, error));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let code = result.code;
|
|
41
|
+
if (result.map) {
|
|
42
|
+
code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64(JSON.stringify(result.map))}`;
|
|
43
|
+
}
|
|
44
|
+
code += `\n//# sourceURL=${name}`;
|
|
45
|
+
execute(code, script, isModule(result.code));
|
|
46
|
+
ran++;
|
|
47
|
+
}
|
|
48
|
+
return ran;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** File name shown in errors and dev tools: the `src`, or `fratmscript-N.fratm`. */
|
|
52
|
+
export function scriptName(script, index) {
|
|
53
|
+
const src = script.getAttribute('src');
|
|
54
|
+
if (src) return src.split(/[?#]/)[0].split('/').pop() || src;
|
|
55
|
+
return `fratmscript-${index + 1}.fratm`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatError(name, error) {
|
|
59
|
+
const where = error.line ? `${name}:${error.line}:${error.column ?? 1}` : name;
|
|
60
|
+
// The message already starts with "Riga X, colonna Y: "; the location is in `where`
|
|
61
|
+
const message = String(error.message).replace(/^Riga \d+, colonna \d+: /, '');
|
|
62
|
+
return `FratmScript ${where}: ${message}${error.suggestion ? `\n${error.suggestion}` : ''}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Compiled code that imports or exports has to run as a module. */
|
|
66
|
+
export function isModule(code) {
|
|
67
|
+
return /^(import|export)\b/m.test(code);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function base64(text) {
|
|
71
|
+
const bytes = new TextEncoder().encode(text);
|
|
72
|
+
let binary = '';
|
|
73
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
74
|
+
return btoa(binary);
|
|
75
|
+
}
|
package/node.d.ts
ADDED
package/node.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Lets Node.js run .fratm files: `node --import fratmscript/node main.fratm`.
|
|
2
|
+
// Registers module hooks that compile each .fratm file when it is loaded.
|
|
3
|
+
import { register } from 'node:module';
|
|
4
|
+
|
|
5
|
+
register('./lib/node-hooks.js', import.meta.url);
|
|
6
|
+
// Stack traces point at the .fratm lines through the inline source maps
|
|
7
|
+
process.setSourceMapsEnabled(true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fratmscript",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.2",
|
|
4
|
+
"description": "FratmScript compiler (WebAssembly) and Vite plugin: JavaScript, ma comme si deve 🤌",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/kkratterf/fratmscript.git",
|
|
10
|
+
"directory": "packages/fratmscript"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/kkratterf/fratmscript/tree/main/packages/fratmscript#readme",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"fratmscript",
|
|
15
|
+
"napoletano",
|
|
16
|
+
"compiler",
|
|
17
|
+
"transpiler",
|
|
18
|
+
"wasm",
|
|
19
|
+
"vite-plugin"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./index.d.ts",
|
|
24
|
+
"default": "./index.js"
|
|
25
|
+
},
|
|
26
|
+
"./vite": {
|
|
27
|
+
"types": "./vite.d.ts",
|
|
28
|
+
"default": "./vite.js"
|
|
29
|
+
},
|
|
30
|
+
"./node": {
|
|
31
|
+
"types": "./node.d.ts",
|
|
32
|
+
"default": "./node.js"
|
|
33
|
+
},
|
|
34
|
+
"./web": {
|
|
35
|
+
"types": "./web.d.ts",
|
|
36
|
+
"default": "./web.js"
|
|
37
|
+
},
|
|
38
|
+
"./browser": {
|
|
39
|
+
"types": "./browser.d.ts",
|
|
40
|
+
"default": "./browser.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"index.d.ts",
|
|
46
|
+
"vite.js",
|
|
47
|
+
"vite.d.ts",
|
|
48
|
+
"node.js",
|
|
49
|
+
"node.d.ts",
|
|
50
|
+
"web.js",
|
|
51
|
+
"web.d.ts",
|
|
52
|
+
"browser.js",
|
|
53
|
+
"browser.d.ts",
|
|
54
|
+
"lib/",
|
|
55
|
+
"wasm/node/package.json",
|
|
56
|
+
"wasm/node/fratm_wasm.js",
|
|
57
|
+
"wasm/node/fratm_wasm_bg.wasm",
|
|
58
|
+
"wasm/web/fratm_wasm.js",
|
|
59
|
+
"wasm/web/fratm_wasm_bg.wasm"
|
|
60
|
+
],
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "node scripts/build-wasm.js",
|
|
66
|
+
"test": "node --test test/*.test.js"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/vite.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface FratmPluginOptions {
|
|
2
|
+
/** Emit source maps for compiled files (default: true). */
|
|
3
|
+
sourceMap?: boolean;
|
|
4
|
+
/** Library whose JSX runtime compiles JSX in `.fratm` files (default: "react"). */
|
|
5
|
+
jsxImportSource?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Serve an API from the dev server: a module (e.g. "server/api.fratm") whose
|
|
8
|
+
* default export has `fetch(request)`, such as a Hono app. It answers the
|
|
9
|
+
* requests under `prefix` (default "/api").
|
|
10
|
+
*/
|
|
11
|
+
api?: string | { entry: string; prefix?: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A Vite/Rollup plugin; typed loosely so it does not depend on vite's types. */
|
|
15
|
+
export interface FratmPlugin {
|
|
16
|
+
name: string;
|
|
17
|
+
enforce: 'pre';
|
|
18
|
+
configureServer(server: {
|
|
19
|
+
middlewares: { use(fn: (req: any, res: any, next: (error?: unknown) => void) => void): void };
|
|
20
|
+
ssrLoadModule(url: string): Promise<Record<string, any>>;
|
|
21
|
+
ssrFixStacktrace?(error: Error): void;
|
|
22
|
+
}): void;
|
|
23
|
+
transform(this: unknown, source: string, id: string): { code: string; map: unknown } | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function fratmscript(options?: FratmPluginOptions): FratmPlugin;
|
|
27
|
+
export { fratmscript };
|
package/vite.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Vite (and Rollup) plugin that compiles imported `.fratm` files.
|
|
2
|
+
import { compile } from './index.js';
|
|
3
|
+
import { sendResponse, toRequest, underPrefix } from './lib/fetch-middleware.js';
|
|
4
|
+
|
|
5
|
+
const FRATM = /\.fratm$/;
|
|
6
|
+
// A request for a .fratm file that Vite would serve as it is
|
|
7
|
+
const RAW_FRATM_REQUEST = /^[^?#]*\.fratm(?:$|[?#])/;
|
|
8
|
+
|
|
9
|
+
export default function fratmscript(options = {}) {
|
|
10
|
+
const { sourceMap = true, jsxImportSource } = options;
|
|
11
|
+
// `api: "server/api.fratm"` or `{ entry, prefix }`: a module whose default
|
|
12
|
+
// export has `fetch(request)` (a Hono app) answers the requests under prefix
|
|
13
|
+
const api = typeof options.api === 'string' ? { entry: options.api } : options.api;
|
|
14
|
+
const apiPrefix = api?.prefix ?? '/api';
|
|
15
|
+
// Vite resolves "/server/api.fratm" from the project root; "server/…" would be a package
|
|
16
|
+
const apiEntry = api && (/^[./]/.test(api.entry) ? api.entry : `/${api.entry}`);
|
|
17
|
+
return {
|
|
18
|
+
name: 'fratmscript',
|
|
19
|
+
enforce: 'pre',
|
|
20
|
+
configureServer(server) {
|
|
21
|
+
// The dev server only compiles requests it knows are modules. Imports
|
|
22
|
+
// between modules get `?import` from Vite, but the `<script src=…>`
|
|
23
|
+
// in index.html does not: add it, so the entry is compiled too.
|
|
24
|
+
server.middlewares.use((req, _res, next) => {
|
|
25
|
+
if (req.url && RAW_FRATM_REQUEST.test(req.url) && !/[?&]import(?:[=&#]|$)/.test(req.url)) {
|
|
26
|
+
req.url += req.url.includes('?') ? '&import' : '?import';
|
|
27
|
+
}
|
|
28
|
+
next();
|
|
29
|
+
});
|
|
30
|
+
if (!api) return;
|
|
31
|
+
server.middlewares.use(async (req, res, next) => {
|
|
32
|
+
if (!underPrefix(req.url, apiPrefix)) return next();
|
|
33
|
+
try {
|
|
34
|
+
// Loaded on each request: Vite reloads it when a server file changes
|
|
35
|
+
const module = await server.ssrLoadModule(apiEntry);
|
|
36
|
+
const app = module.default;
|
|
37
|
+
if (typeof app?.fetch !== 'function') {
|
|
38
|
+
throw new Error(`${api.entry} adda mannà for predefinit n'app cu fetch(), tipo nu bell Hono()`);
|
|
39
|
+
}
|
|
40
|
+
await sendResponse(res, await app.fetch(await toRequest(req)));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
server.ssrFixStacktrace?.(error);
|
|
43
|
+
next(error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
transform(source, id) {
|
|
48
|
+
const file = id.split('?', 1)[0];
|
|
49
|
+
if (!FRATM.test(file)) return null;
|
|
50
|
+
const result = compile(source, { sourceMap, filename: file, jsxImportSource });
|
|
51
|
+
if (result.errors.length > 0) {
|
|
52
|
+
const [error] = result.errors;
|
|
53
|
+
this.error({
|
|
54
|
+
message: error.suggestion ? `${error.message}\n${error.suggestion}` : error.message,
|
|
55
|
+
id: file,
|
|
56
|
+
loc: error.line == null ? undefined : { file, line: error.line, column: (error.column ?? 1) - 1 },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { code: result.code, map: result.map };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { fratmscript };
|