redweb 0.16.2 → 0.16.4
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/CHANGELOG.md +40 -29
- package/README.md +293 -291
- package/contract.d.ts +11 -11
- package/docs/API_EXAMPLES_VERIFICATION.md +22 -22
- package/docs/APPLICATION.md +96 -94
- package/docs/CLI.md +122 -122
- package/docs/CLIENT_DEVELOPMENT.md +9 -9
- package/docs/CONNECTED_CLIENTS_VERIFICATION.md +65 -65
- package/docs/DEVELOPMENT.md +81 -81
- package/docs/GETTING_STARTED.md +78 -78
- package/docs/LIVE_HTML.md +555 -478
- package/docs/MIGRATION.md +28 -28
- package/docs/RELEASE_TRUST.md +88 -88
- package/docs/RUNTIME_DIAGNOSTICS.md +78 -78
- package/docs/SOCKET_CONTRACTS.md +42 -42
- package/docs/SOCKET_PAGES.md +172 -172
- package/docs/SOCKET_PAGE_RELEASE_PREPARATION.md +120 -120
- package/docs/SOCKET_PAGE_VERIFICATION.md +85 -85
- package/docs/generated.json +2290 -2286
- package/docs/guides/chatroom.md +1 -1
- package/docs/guides/jsx-without-react.md +14 -14
- package/docs/reference.json +1333 -1329
- package/docs/releases/0.15.0.json +2217 -2217
- package/docs/releases/0.16.0.json +2217 -2217
- package/docs/releases/0.16.1.json +2286 -2286
- package/docs/releases/0.16.2.json +2286 -2286
- package/docs/releases/0.16.3.json +2286 -0
- package/docs/releases/0.16.4.json +2290 -0
- package/docs/snippets/components.tsx +24 -24
- package/docs/snippets/counter.tsx +16 -16
- package/docs/snippets/room-access.tsx +11 -11
- package/docs/snippets/site.css +2 -2
- package/docs/snippets/site.tsx +22 -22
- package/docs/topics.json +3 -3
- package/index.d.ts +96 -58
- package/index.js +14 -8
- package/package.json +12 -8
- package/recipes/foundation/README.md +7 -7
- package/recipes/foundation/app.test.cjs +15 -15
- package/recipes/foundation/app.tsx +12 -12
- package/recipes/shared/README.md +7 -7
- package/src/Application.js +4 -4
- package/src/access/failure-codes.json +4 -0
- package/src/cli/ProjectInitializer.js +1 -1
- package/src/cli/arguments.js +21 -21
- package/src/cli/run.js +12 -12
- package/src/cli/templates.js +40 -40
- package/src/docs/Documentation.js +29 -29
- package/src/htmx/CodeHighlight.js +98 -0
- package/src/htmx/Html.js +4 -3
- package/src/htmx/Jsx.js +2 -2
- package/src/htmx/LiveHtmlServer.js +5 -1
- package/src/htmx/LivePage.js +5 -1
- package/src/htmx/LiveResource.js +96 -0
- package/src/htmx/PageManager.js +126 -13
- package/src/htmx/PageSocketRoute.js +132 -132
- package/src/htmx/PageTaskLane.js +39 -0
- package/src/htmx/ReactiveRenderer.js +8 -8
- package/src/htmx/SocketAction.js +19 -19
- package/src/htmx/TemplateRenderer.js +1 -1
- package/src/htmx/index.js +4 -2
- package/src/htmx/metadata.js +117 -6
- package/src/ws/BaseHandler.js +6 -6
- package/src/ws/ConnectedClients.js +207 -207
- package/src/ws/HandlerGuard.js +4 -4
- package/src/ws/RoomRegistry.js +4 -4
- package/src/ws/RouteRuntime.js +11 -11
- package/src/ws/SocketAction.js +16 -16
- package/src/ws/SocketContract.js +3 -3
- package/src/ws/SocketRoute.js +7 -7
- package/styles/code-highlight.css +16 -0
package/recipes/shared/README.md
CHANGED
|
@@ -40,10 +40,10 @@ and application-specific rate limits. These starters are demonstrations, not a h
|
|
|
40
40
|
Never commit secrets; `.env` is ignored but is not loaded automatically.
|
|
41
41
|
|
|
42
42
|
`npx --no-install redweb doctor --json` reports configuration problems without changing your files.
|
|
43
|
-
## Dependency security
|
|
44
|
-
|
|
45
|
-
This starter includes an application-root npm override for Express 4's `qs`
|
|
46
|
-
dependency, selecting patched `qs@6.16.0`. Keep the override when merging this
|
|
47
|
-
starter into an existing application, refresh its lockfile and run `npm audit`.
|
|
48
|
-
Overrides in Redweb's own package do not apply to installed consumers. Recheck
|
|
49
|
-
upstream Express/body-parser releases before removing this temporary mitigation.
|
|
43
|
+
## Dependency security
|
|
44
|
+
|
|
45
|
+
This starter includes an application-root npm override for Express 4's `qs`
|
|
46
|
+
dependency, selecting patched `qs@6.16.0`. Keep the override when merging this
|
|
47
|
+
starter into an existing application, refresh its lockfile and run `npm audit`.
|
|
48
|
+
Overrides in Redweb's own package do not apply to installed consumers. Recheck
|
|
49
|
+
upstream Express/body-parser releases before removing this temporary mitigation.
|
package/src/Application.js
CHANGED
|
@@ -37,7 +37,7 @@ function bounded(operation, milliseconds, label, signal) {
|
|
|
37
37
|
class Application {
|
|
38
38
|
constructor(options = {}) {
|
|
39
39
|
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Application options must be an object.');
|
|
40
|
-
const { pages = [], sockets = [], services = [], port = 8181, bind = '0.0.0.0',
|
|
40
|
+
const { pages = [], sockets = [], services = [], providers = {}, port = 8181, bind = '0.0.0.0',
|
|
41
41
|
startupTimeoutMs = 5000, shutdownTimeoutMs = 5000, signals = true, ...rest } = options;
|
|
42
42
|
if ('static' in rest) throw new TypeError('Use exportStatic() for static output; defineApp() owns a live HTTP listener.');
|
|
43
43
|
for (const name of ['listen', 'routes', 'socketRoutes', 'closeServerOnShutdown']) {
|
|
@@ -49,7 +49,7 @@ class Application {
|
|
|
49
49
|
}
|
|
50
50
|
if (typeof signals !== 'boolean') throw new TypeError('`signals` must be a boolean.');
|
|
51
51
|
this.options = { ...rest, pages: classes(pages, 'pages'), sockets: classes(sockets, 'sockets'),
|
|
52
|
-
services: classes(services, 'services'), port, bind, startupTimeoutMs, shutdownTimeoutMs, signals };
|
|
52
|
+
services: classes(services, 'services'), providers, port, bind, startupTimeoutMs, shutdownTimeoutMs, signals };
|
|
53
53
|
if (this.options.services.some(Type => Type === SocketService || Type.prototype instanceof SocketService)) {
|
|
54
54
|
throw new TypeError('SocketService belongs to a socket route, not application services.');
|
|
55
55
|
}
|
|
@@ -98,14 +98,14 @@ class Application {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
async _start() {
|
|
101
|
-
const { pages, sockets, services, startupTimeoutMs, shutdownTimeoutMs, signals, httpServices = [], ...options } = this.options;
|
|
101
|
+
const { pages, sockets, services, providers, startupTimeoutMs, shutdownTimeoutMs, signals, httpServices = [], ...options } = this.options;
|
|
102
102
|
if (signals) {
|
|
103
103
|
process.on('SIGINT', this._onSignal);
|
|
104
104
|
process.on('SIGTERM', this._onSignal);
|
|
105
105
|
}
|
|
106
106
|
const httpOptions = { ...options, services: httpServices, listen: false, shutdownTimeoutMs };
|
|
107
107
|
if (pages.length) {
|
|
108
|
-
this._live = new LiveHtmlServer({ ...httpOptions, pages, socketRoutes: sockets });
|
|
108
|
+
this._live = new LiveHtmlServer({ ...httpOptions, pages, providers, socketRoutes: sockets });
|
|
109
109
|
this.http = this._live.http;
|
|
110
110
|
this.sockets = this._live.sockets;
|
|
111
111
|
this._owner = this._live._ownedServer;
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"ACCESS_CAPACITY": { "status": 503, "message": "Authorization capacity reached. The operation was not run." },
|
|
20
20
|
"PAGE_FAILED": { "status": 500, "message": "Page request failed." },
|
|
21
21
|
"PAGE_CAPACITY": { "status": 503, "message": "Page session capacity reached." },
|
|
22
|
+
"UPLOAD_TOO_LARGE": { "status": 413, "message": "The uploaded file exceeds this action's size limit." },
|
|
23
|
+
"UPLOAD_TYPE_REJECTED": { "status": 415, "message": "This action does not accept that file type." },
|
|
24
|
+
"UPLOAD_TIMEOUT": { "status": 408, "message": "The uploaded file took too long to arrive." },
|
|
25
|
+
"UPLOAD_FAILED": { "status": 500, "message": "File upload failed." },
|
|
22
26
|
"ACTION_INVALID_INPUT": { "status": 400, "message": "Action input is invalid. Check the form values and try again." },
|
|
23
27
|
"ACTION_VALIDATION_TIMEOUT": { "status": 503, "message": "Action input validation timed out. The action was not run." },
|
|
24
28
|
"ACTION_CANCELLED": { "status": 503, "message": "The connection closed before input validation completed. The action was not run." }
|
|
@@ -11,7 +11,7 @@ class ProjectInitializer {
|
|
|
11
11
|
|
|
12
12
|
initialize(target, options = {}) {
|
|
13
13
|
const root = path.resolve(target);
|
|
14
|
-
const templateFiles = projectFiles(this.version, options.template ?? null, undefined, options);
|
|
14
|
+
const templateFiles = projectFiles(this.version, options.template ?? null, undefined, options);
|
|
15
15
|
const files = options.existing ? templateFiles.filter(file => file.path === 'tsconfig.json') : templateFiles;
|
|
16
16
|
return new FilePlan(root, files).write({ dryRun: options.dryRun });
|
|
17
17
|
}
|
package/src/cli/arguments.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { CAPABILITIES, TEMPLATES } = require('./templates');
|
|
3
|
+
const { CAPABILITIES, TEMPLATES } = require('./templates');
|
|
4
4
|
const { KINDS } = require('./ProjectAddition');
|
|
5
5
|
|
|
6
6
|
const USAGE = [
|
|
7
|
-
`Usage: redweb init [directory] [--with ${CAPABILITIES.join(',')}] [--template ${TEMPLATES.join('|')}] [--bare] [--existing] [--dry-run] [--json]`,
|
|
7
|
+
`Usage: redweb init [directory] [--with ${CAPABILITIES.join(',')}] [--template ${TEMPLATES.join('|')}] [--bare] [--existing] [--dry-run] [--json]`,
|
|
8
8
|
' redweb doctor [directory] [--port number] [--json]',
|
|
9
9
|
` redweb add <${KINDS.join('|')}> <name> [directory] [--config file] [--source-dir dir] [--test-dir dir] [--dry-run] [--json]`,
|
|
10
10
|
' redweb --help | --version',
|
|
11
11
|
'',
|
|
12
|
-
'--existing creates only a missing tsconfig.json; no starter or package changes.',
|
|
13
|
-
'--with adds neutral capability dependencies without generating example-domain code.',
|
|
14
|
-
'--bare omits generated tests; templates remain explicit examples.',
|
|
12
|
+
'--existing creates only a missing tsconfig.json; no starter or package changes.',
|
|
13
|
+
'--with adds neutral capability dependencies without generating example-domain code.',
|
|
14
|
+
'--bare omits generated tests; templates remain explicit examples.',
|
|
15
15
|
'--dry-run reports planned files without writing anything.',
|
|
16
16
|
'doctor inspects configuration without executing application code or repairing files.',
|
|
17
17
|
].join('\n') + '\n';
|
|
@@ -20,7 +20,7 @@ function parseArguments(args) {
|
|
|
20
20
|
const [command = '--help', ...rest] = args;
|
|
21
21
|
if (['--help', '-h', '--version'].includes(command) && !rest.length) return { command };
|
|
22
22
|
if (!['init', 'doctor', 'add'].includes(command)) throw new Error('Unknown command. Run redweb --help.');
|
|
23
|
-
const result = { command, target: '.', existing: false, dryRun: false, json: false, port: null };
|
|
23
|
+
const result = { command, target: '.', existing: false, dryRun: false, json: false, port: null };
|
|
24
24
|
if (command === 'add') {
|
|
25
25
|
result.kind = rest.shift();
|
|
26
26
|
result.name = rest.shift();
|
|
@@ -39,35 +39,35 @@ function parseArguments(args) {
|
|
|
39
39
|
if (seen.has(value)) throw new Error(`Duplicate option: ${value}`);
|
|
40
40
|
seen.add(value);
|
|
41
41
|
if (value === '--json') result.json = true;
|
|
42
|
-
else if (value === '--existing' && command === 'init') result.existing = true;
|
|
43
|
-
else if (value === '--bare' && command === 'init') result.bare = true;
|
|
42
|
+
else if (value === '--existing' && command === 'init') result.existing = true;
|
|
43
|
+
else if (value === '--bare' && command === 'init') result.bare = true;
|
|
44
44
|
else if (value === '--dry-run' && ['init', 'add'].includes(command)) result.dryRun = true;
|
|
45
45
|
else if (command === 'add' && ['--config', '--source-dir', '--test-dir'].includes(value)) {
|
|
46
46
|
const argument = rest[++i];
|
|
47
47
|
if (!argument || argument.startsWith('-')) throw new Error(`${value} requires a path.`);
|
|
48
48
|
result[{ '--config': 'configFile', '--source-dir': 'sourceDir', '--test-dir': 'testDir' }[value]] = argument;
|
|
49
49
|
}
|
|
50
|
-
else if (value === '--template' && command === 'init') {
|
|
50
|
+
else if (value === '--template' && command === 'init') {
|
|
51
51
|
const template = rest[++i];
|
|
52
52
|
if (!TEMPLATES.includes(template)) throw new Error(`--template must be one of: ${TEMPLATES.join(', ')}.`);
|
|
53
|
-
result.template = template;
|
|
54
|
-
}
|
|
55
|
-
else if (value === '--with' && command === 'init') {
|
|
56
|
-
const raw = rest[++i];
|
|
57
|
-
if (!raw || raw.startsWith('-')) throw new Error('--with requires a comma-separated capability list.');
|
|
58
|
-
const capabilities = raw.split(',');
|
|
59
|
-
if (capabilities.some(capability => !CAPABILITIES.includes(capability)) || new Set(capabilities).size !== capabilities.length) {
|
|
60
|
-
throw new Error(`--with must contain unique capabilities from: ${CAPABILITIES.join(', ')}.`);
|
|
61
|
-
}
|
|
62
|
-
result.with = capabilities;
|
|
63
|
-
}
|
|
53
|
+
result.template = template;
|
|
54
|
+
}
|
|
55
|
+
else if (value === '--with' && command === 'init') {
|
|
56
|
+
const raw = rest[++i];
|
|
57
|
+
if (!raw || raw.startsWith('-')) throw new Error('--with requires a comma-separated capability list.');
|
|
58
|
+
const capabilities = raw.split(',');
|
|
59
|
+
if (capabilities.some(capability => !CAPABILITIES.includes(capability)) || new Set(capabilities).size !== capabilities.length) {
|
|
60
|
+
throw new Error(`--with must contain unique capabilities from: ${CAPABILITIES.join(', ')}.`);
|
|
61
|
+
}
|
|
62
|
+
result.with = capabilities;
|
|
63
|
+
}
|
|
64
64
|
else if (value === '--port' && command === 'doctor') {
|
|
65
65
|
const port = rest[++i];
|
|
66
66
|
if (!/^\d+$/.test(port) || Number(port) > 65535) throw new Error('--port must be an integer from 0 through 65535.');
|
|
67
67
|
result.port = Number(port);
|
|
68
68
|
} else throw new Error(`Unknown option for ${command}: ${value}`);
|
|
69
69
|
}
|
|
70
|
-
if (result.existing && (result.template || result.with || result.bare)) throw new Error('--existing cannot be combined with --template, --with, or --bare.');
|
|
70
|
+
if (result.existing && (result.template || result.with || result.bare)) throw new Error('--existing cannot be combined with --template, --with, or --bare.');
|
|
71
71
|
return result;
|
|
72
72
|
}
|
|
73
73
|
|
package/src/cli/run.js
CHANGED
|
@@ -36,18 +36,18 @@ async function run(args, cwd, version) {
|
|
|
36
36
|
return { exitCode: 0, stdout: `${output}\n`, stderr: '' };
|
|
37
37
|
}
|
|
38
38
|
const result = new ProjectInitializer(version).initialize(root, options);
|
|
39
|
-
const report = {
|
|
40
|
-
schemaVersion: 1,
|
|
41
|
-
operation: 'init',
|
|
42
|
-
dryRun: options.dryRun,
|
|
43
|
-
foundation: options.template ?? 'default',
|
|
44
|
-
capabilities: options.with ?? [],
|
|
45
|
-
tests: !options.bare,
|
|
46
|
-
...result,
|
|
47
|
-
};
|
|
48
|
-
const output = options.json ? JSON.stringify(report) : [
|
|
49
|
-
`${options.dryRun ? 'Planned initialization' : 'Initialization complete'} in ${result.root}`,
|
|
50
|
-
`Foundation: ${options.template ? `example template "${options.template}"` : 'neutral default'}; capabilities: ${options.with?.join(', ') || 'base'}; tests: ${options.bare ? 'omitted' : 'included'}.`,
|
|
39
|
+
const report = {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
operation: 'init',
|
|
42
|
+
dryRun: options.dryRun,
|
|
43
|
+
foundation: options.template ?? 'default',
|
|
44
|
+
capabilities: options.with ?? [],
|
|
45
|
+
tests: !options.bare,
|
|
46
|
+
...result,
|
|
47
|
+
};
|
|
48
|
+
const output = options.json ? JSON.stringify(report) : [
|
|
49
|
+
`${options.dryRun ? 'Planned initialization' : 'Initialization complete'} in ${result.root}`,
|
|
50
|
+
`Foundation: ${options.template ? `example template "${options.template}"` : 'neutral default'}; capabilities: ${options.with?.join(', ') || 'base'}; tests: ${options.bare ? 'omitted' : 'included'}.`,
|
|
51
51
|
`Created: ${result.created.join(', ')}`,
|
|
52
52
|
`Kept existing: ${result.skipped.join(', ')}`,
|
|
53
53
|
`Planned: ${result.planned.join(', ')}`,
|
package/src/cli/templates.js
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
-
const json = value => `${JSON.stringify(value, null, 2)}\n`;
|
|
7
|
-
const TEMPLATES = Object.freeze(['realtime', 'chat', 'site', 'socket', 'dashboard', 'http-ws']);
|
|
8
|
-
const CAPABILITIES = Object.freeze(['auth', 'multiplayer']);
|
|
9
|
-
|
|
10
|
-
function projectFiles(version, template = null, root = path.resolve(__dirname, '../..'), options = {}) {
|
|
11
|
-
if (template !== null && !TEMPLATES.includes(template)) throw new Error('Unknown starter template.');
|
|
12
|
-
if (options.with !== undefined && !Array.isArray(options.with)) throw new TypeError('Initializer capabilities must be an array.');
|
|
13
|
-
const capabilities = new Set(options.with || []);
|
|
14
|
-
if ([...capabilities].some(capability => !CAPABILITIES.includes(capability))) throw new Error('Unknown initializer capability.');
|
|
15
|
-
const selected = template ?? 'foundation';
|
|
16
|
-
const authenticated = template === 'dashboard' || capabilities.has('auth');
|
|
17
|
-
const multiplayer = capabilities.has('multiplayer');
|
|
18
|
-
const { devDependencies, dependencies, overrides } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
6
|
+
const json = value => `${JSON.stringify(value, null, 2)}\n`;
|
|
7
|
+
const TEMPLATES = Object.freeze(['realtime', 'chat', 'site', 'socket', 'dashboard', 'http-ws']);
|
|
8
|
+
const CAPABILITIES = Object.freeze(['auth', 'multiplayer']);
|
|
9
|
+
|
|
10
|
+
function projectFiles(version, template = null, root = path.resolve(__dirname, '../..'), options = {}) {
|
|
11
|
+
if (template !== null && !TEMPLATES.includes(template)) throw new Error('Unknown starter template.');
|
|
12
|
+
if (options.with !== undefined && !Array.isArray(options.with)) throw new TypeError('Initializer capabilities must be an array.');
|
|
13
|
+
const capabilities = new Set(options.with || []);
|
|
14
|
+
if ([...capabilities].some(capability => !CAPABILITIES.includes(capability))) throw new Error('Unknown initializer capability.');
|
|
15
|
+
const selected = template ?? 'foundation';
|
|
16
|
+
const authenticated = template === 'dashboard' || capabilities.has('auth');
|
|
17
|
+
const multiplayer = capabilities.has('multiplayer');
|
|
18
|
+
const { devDependencies, dependencies, overrides } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
19
19
|
const read = relative => fs.readFileSync(path.join(root, 'recipes', relative), 'utf8');
|
|
20
20
|
const manifest = {
|
|
21
21
|
name: 'redweb-app', private: true, version: '0.0.0',
|
|
@@ -23,22 +23,22 @@ function projectFiles(version, template = null, root = path.resolve(__dirname, '
|
|
|
23
23
|
build: 'tsc && node scripts/copy-assets.cjs',
|
|
24
24
|
start: 'node dist/app.js',
|
|
25
25
|
dev: 'nodemon',
|
|
26
|
-
...(!options.bare ? {
|
|
27
|
-
test: 'npm run build && node --test test/app.test.cjs test/lifecycle.test.cjs',
|
|
28
|
-
'test:coverage': 'npm run build && c8 --all --src=dist --include=dist/** --reporter=text --reporter=json node --test test/app.test.cjs test/lifecycle.test.cjs',
|
|
29
|
-
} : {}),
|
|
26
|
+
...(!options.bare ? {
|
|
27
|
+
test: 'npm run build && node --test test/app.test.cjs test/lifecycle.test.cjs',
|
|
28
|
+
'test:coverage': 'npm run build && c8 --all --src=dist --include=dist/** --reporter=text --reporter=json node --test test/app.test.cjs test/lifecycle.test.cjs',
|
|
29
|
+
} : {}),
|
|
30
30
|
},
|
|
31
31
|
dependencies: {
|
|
32
32
|
redweb: `^${version}`,
|
|
33
|
-
...(['chat', 'socket', 'dashboard'].includes(template) || authenticated || multiplayer ? { zod: devDependencies.zod } : {}),
|
|
34
|
-
...(authenticated ? { express: dependencies.express } : {}),
|
|
35
|
-
...(multiplayer ? { 'redweb-client': dependencies['redweb-client'] } : {}),
|
|
33
|
+
...(['chat', 'socket', 'dashboard'].includes(template) || authenticated || multiplayer ? { zod: devDependencies.zod } : {}),
|
|
34
|
+
...(authenticated ? { express: dependencies.express } : {}),
|
|
35
|
+
...(multiplayer ? { 'redweb-client': dependencies['redweb-client'] } : {}),
|
|
36
36
|
},
|
|
37
|
-
overrides,
|
|
38
|
-
devDependencies: {
|
|
39
|
-
typescript: devDependencies.typescript, nodemon: devDependencies.nodemon, ws: dependencies.ws,
|
|
40
|
-
...(!options.bare ? { c8: devDependencies.c8 } : {}),
|
|
41
|
-
...(authenticated ? {
|
|
37
|
+
overrides,
|
|
38
|
+
devDependencies: {
|
|
39
|
+
typescript: devDependencies.typescript, nodemon: devDependencies.nodemon, ws: dependencies.ws,
|
|
40
|
+
...(!options.bare ? { c8: devDependencies.c8 } : {}),
|
|
41
|
+
...(authenticated ? {
|
|
42
42
|
'@types/node': devDependencies['redweb-dashboard-types'].replace('npm:@types/node@', ''),
|
|
43
43
|
'@types/express': dependencies['@types/express'],
|
|
44
44
|
} : {}),
|
|
@@ -51,10 +51,10 @@ function projectFiles(version, template = null, root = path.resolve(__dirname, '
|
|
|
51
51
|
delay: 200,
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
|
-
if (authenticated) manifest.engines = { node: '>=22.13.0' };
|
|
55
|
-
if (template === 'dashboard') {
|
|
56
|
-
manifest.scripts['add-user'] = 'npm run build && node dist/admin.js';
|
|
57
|
-
if (!options.bare) manifest.scripts['test:coverage'] += ' test/rate-window.test.cjs';
|
|
54
|
+
if (authenticated) manifest.engines = { node: '>=22.13.0' };
|
|
55
|
+
if (template === 'dashboard') {
|
|
56
|
+
manifest.scripts['add-user'] = 'npm run build && node dist/admin.js';
|
|
57
|
+
if (!options.bare) manifest.scripts['test:coverage'] += ' test/rate-window.test.cjs';
|
|
58
58
|
}
|
|
59
59
|
const files = [
|
|
60
60
|
{ path: 'package.json', content: json(manifest) },
|
|
@@ -63,15 +63,15 @@ function projectFiles(version, template = null, root = path.resolve(__dirname, '
|
|
|
63
63
|
compilerOptions: { rootDir: 'src', outDir: 'dist', sourceMap: true },
|
|
64
64
|
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
65
65
|
}) },
|
|
66
|
-
{ path: 'src/app.tsx', content: read(`${selected}/app.tsx`) },
|
|
67
|
-
{ path: 'src/app.css', content: read(`${selected === 'dashboard' ? selected : 'shared'}/app.css`) },
|
|
68
|
-
{ path: 'scripts/copy-assets.cjs', content: read('shared/copy-assets.cjs') },
|
|
69
|
-
...(!options.bare ? [
|
|
70
|
-
{ path: 'test/network.cjs', content: read('shared/network.cjs') },
|
|
71
|
-
{ path: 'test/app.test.cjs', content: read(`${selected}/app.test.cjs`) },
|
|
72
|
-
{ path: 'test/lifecycle.test.cjs', content: read('shared/lifecycle.test.cjs') },
|
|
73
|
-
] : []),
|
|
74
|
-
{ path: 'README.md', content: `${read('shared/README.md')}\n${read(`${selected}/README.md`)}` },
|
|
66
|
+
{ path: 'src/app.tsx', content: read(`${selected}/app.tsx`) },
|
|
67
|
+
{ path: 'src/app.css', content: read(`${selected === 'dashboard' ? selected : 'shared'}/app.css`) },
|
|
68
|
+
{ path: 'scripts/copy-assets.cjs', content: read('shared/copy-assets.cjs') },
|
|
69
|
+
...(!options.bare ? [
|
|
70
|
+
{ path: 'test/network.cjs', content: read('shared/network.cjs') },
|
|
71
|
+
{ path: 'test/app.test.cjs', content: read(`${selected}/app.test.cjs`) },
|
|
72
|
+
{ path: 'test/lifecycle.test.cjs', content: read('shared/lifecycle.test.cjs') },
|
|
73
|
+
] : []),
|
|
74
|
+
{ path: 'README.md', content: `${read('shared/README.md')}\n${read(`${selected}/README.md`)}` },
|
|
75
75
|
{ path: '.gitignore', content: 'node_modules/\ndist/\ncoverage/\n.env\ndata/\n*.sqlite\n*.sqlite-wal\n*.sqlite-shm\n' },
|
|
76
76
|
];
|
|
77
77
|
if (template === 'chat') {
|
|
@@ -88,7 +88,7 @@ function projectFiles(version, template = null, root = path.resolve(__dirname, '
|
|
|
88
88
|
}
|
|
89
89
|
if (template === 'dashboard') {
|
|
90
90
|
files.push({ path: '.npmrc', content: 'engine-strict=true\n' });
|
|
91
|
-
if (!options.bare) files.push({ path: 'test/rate-window.test.cjs', content: read('dashboard/rate-window.test.cjs') });
|
|
91
|
+
if (!options.bare) files.push({ path: 'test/rate-window.test.cjs', content: read('dashboard/rate-window.test.cjs') });
|
|
92
92
|
for (const name of ['store.ts', 'auth.ts', 'cards.tsx', 'admin.ts']) {
|
|
93
93
|
files.push({ path: `src/${name}`, content: read(`dashboard/${name}`) });
|
|
94
94
|
}
|
|
@@ -96,4 +96,4 @@ function projectFiles(version, template = null, root = path.resolve(__dirname, '
|
|
|
96
96
|
return Object.freeze(files.map(Object.freeze));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
module.exports = { projectFiles, CAPABILITIES, TEMPLATES };
|
|
99
|
+
module.exports = { projectFiles, CAPABILITIES, TEMPLATES };
|
|
@@ -77,8 +77,8 @@ class Documentation {
|
|
|
77
77
|
: `> Documentation for Redweb ${this.channel}. Install that exact version when following these examples.`;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
setup(template) {
|
|
81
|
-
if (!TEMPLATES.includes(template)) throw new Error('Unknown starter template.');
|
|
80
|
+
setup(template) {
|
|
81
|
+
if (!TEMPLATES.includes(template)) throw new Error('Unknown starter template.');
|
|
82
82
|
const acceptance = `${template === 'dashboard' ? 'npm run add-user -- alice\n' : ''}npm test\nnpm run dev`;
|
|
83
83
|
return this.channel === 'unreleased'
|
|
84
84
|
? [
|
|
@@ -86,18 +86,18 @@ class Documentation {
|
|
|
86
86
|
fence(`npx --yes --package TARBALL redweb init my-${template} --template ${template}\ncd my-${template}\nnpm install --save-exact TARBALL\n${acceptance}`, 'sh'),
|
|
87
87
|
'This prerelease Redweb artifact is development-only until its release checks finish. For released applications, use an available versioned release guide.',
|
|
88
88
|
].join('\n\n')
|
|
89
|
-
: fence(`npx --yes redweb@${this.channel} init my-${template} --template ${template}\ncd my-${template}\nnpm install --save-exact redweb@${this.channel}\n${acceptance}`, 'sh');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
foundationSetup() {
|
|
93
|
-
return this.channel === 'unreleased'
|
|
94
|
-
? [
|
|
95
|
-
'Replace `TARBALL` with the absolute path to the matching Redweb tarball produced by `npm pack` (quoted if it contains spaces):',
|
|
96
|
-
fence('npx --yes --package TARBALL redweb init my-app\ncd my-app\nnpm install --save-exact TARBALL\nnpm test\nnpm run dev', 'sh'),
|
|
97
|
-
'This prerelease Redweb artifact is development-only until its release checks finish.',
|
|
98
|
-
].join('\n\n')
|
|
99
|
-
: fence(`npx --yes redweb@${this.channel} init my-app\ncd my-app\nnpm install --save-exact redweb@${this.channel}\nnpm test\nnpm run dev`, 'sh');
|
|
100
|
-
}
|
|
89
|
+
: fence(`npx --yes redweb@${this.channel} init my-${template} --template ${template}\ncd my-${template}\nnpm install --save-exact redweb@${this.channel}\n${acceptance}`, 'sh');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
foundationSetup() {
|
|
93
|
+
return this.channel === 'unreleased'
|
|
94
|
+
? [
|
|
95
|
+
'Replace `TARBALL` with the absolute path to the matching Redweb tarball produced by `npm pack` (quoted if it contains spaces):',
|
|
96
|
+
fence('npx --yes --package TARBALL redweb init my-app\ncd my-app\nnpm install --save-exact TARBALL\nnpm test\nnpm run dev', 'sh'),
|
|
97
|
+
'This prerelease Redweb artifact is development-only until its release checks finish.',
|
|
98
|
+
].join('\n\n')
|
|
99
|
+
: fence(`npx --yes redweb@${this.channel} init my-app\ncd my-app\nnpm install --save-exact redweb@${this.channel}\nnpm test\nnpm run dev`, 'sh');
|
|
100
|
+
}
|
|
101
101
|
|
|
102
102
|
recipe(template) {
|
|
103
103
|
const files = projectFiles(this.manifest.version, template, this.root).map(file => ({ ...file, content: normalize(file.content) }));
|
|
@@ -114,28 +114,28 @@ class Documentation {
|
|
|
114
114
|
return { id: `recipes/${template}`, title: `${template[0].toUpperCase()}${template.slice(1)} starter`, summary: explanation.split('\n').find(line => line && !line.startsWith('#')), source, markdown, files };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
recipeCode(entry) {
|
|
117
|
+
recipeCode(entry) {
|
|
118
118
|
const files = projectFiles(this.manifest.version, entry.template, this.root);
|
|
119
119
|
const file = files.find(file => file.path === entry.file);
|
|
120
120
|
if (!file) throw new Error(`Unknown documentation recipe file: ${entry.template}/${entry.file}`);
|
|
121
|
-
return normalize(file.content);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
code(entry, field) {
|
|
125
|
-
return entry.recipe ? this.recipeCode(entry.recipe) : entry.codeSource ? this.read(entry.codeSource) : entry[field];
|
|
126
|
-
}
|
|
121
|
+
return normalize(file.content);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
code(entry, field) {
|
|
125
|
+
return entry.recipe ? this.recipeCode(entry.recipe) : entry.codeSource ? this.read(entry.codeSource) : entry[field];
|
|
126
|
+
}
|
|
127
127
|
|
|
128
128
|
topic(topic) {
|
|
129
|
-
let markdown = `${this.notice()}\n\n${this.links(this.read(topic.source), topic.source)}`;
|
|
130
|
-
markdown = markdown.replace(/<!-- source: ([\w./-]+) -->/g,
|
|
131
|
-
(_match, file) => fence(this.read(file), language(file)));
|
|
129
|
+
let markdown = `${this.notice()}\n\n${this.links(this.read(topic.source), topic.source)}`;
|
|
130
|
+
markdown = markdown.replace(/<!-- source: ([\w./-]+) -->/g,
|
|
131
|
+
(_match, file) => fence(this.read(file), language(file)));
|
|
132
132
|
if (topic.recipe) {
|
|
133
133
|
const { template, file } = topic.recipe;
|
|
134
134
|
markdown += [
|
|
135
135
|
'\n## Build and run the complete application', this.setup(template),
|
|
136
|
-
`The [complete ${template} recipe](${this.basePath}/recipes/${template}.md) contains every generated file, its real acceptance tests, and deployment instructions.`,
|
|
137
|
-
...(topic.codeSource ? [] : [`The source below is one of those files, not a standalone program; initialize the whole project before modifying it.`,
|
|
138
|
-
`## Source walkthrough: ${file}`, fence(this.recipeCode(topic.recipe), language(file))]),
|
|
136
|
+
`The [complete ${template} recipe](${this.basePath}/recipes/${template}.md) contains every generated file, its real acceptance tests, and deployment instructions.`,
|
|
137
|
+
...(topic.codeSource ? [] : [`The source below is one of those files, not a standalone program; initialize the whole project before modifying it.`,
|
|
138
|
+
`## Source walkthrough: ${file}`, fence(this.recipeCode(topic.recipe), language(file))]),
|
|
139
139
|
].join('\n\n') + '\n';
|
|
140
140
|
}
|
|
141
141
|
return { ...topic, markdown };
|
|
@@ -145,8 +145,8 @@ class Documentation {
|
|
|
145
145
|
const pages = this.topics.map(topic => this.topic(topic));
|
|
146
146
|
pages.push(...TEMPLATES.map(template => this.recipe(template)));
|
|
147
147
|
const reference = this.reference;
|
|
148
|
-
const api = reference.api.map(section => ({ ...section, usage: this.code(section, 'usage') }));
|
|
149
|
-
const examples = reference.examples.map(example => ({ ...example, code: this.code(example, 'code') }));
|
|
148
|
+
const api = reference.api.map(section => ({ ...section, usage: this.code(section, 'usage') }));
|
|
149
|
+
const examples = reference.examples.map(example => ({ ...example, code: this.code(example, 'code') }));
|
|
150
150
|
for (const example of examples) {
|
|
151
151
|
pages.push({ id: `examples/${example.id}`, title: example.title, summary: example.summary, source: 'docs/reference.json', markdown: [
|
|
152
152
|
`# ${example.title}`, this.notice(), example.summary,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const KEYWORDS = new Set([
|
|
2
|
+
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
3
|
+
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
|
|
4
|
+
'from', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new',
|
|
5
|
+
'of', 'return', 'static', 'super', 'switch', 'throw', 'try', 'typeof',
|
|
6
|
+
'var', 'while', 'yield',
|
|
7
|
+
]);
|
|
8
|
+
const LITERALS = new Set(['false', 'null', 'true', 'undefined']);
|
|
9
|
+
const LANGUAGES = new Set(['js', 'javascript', 'ts', 'typescript', 'tsx']);
|
|
10
|
+
const identifierStart = character => /[A-Za-z_$]/.test(character);
|
|
11
|
+
const identifierPart = character => /[A-Za-z0-9_$]/.test(character);
|
|
12
|
+
|
|
13
|
+
function actionAttributeBefore(source, brace) {
|
|
14
|
+
let index = brace - 1;
|
|
15
|
+
while (index >= 0 && /\s/.test(source[index])) index -= 1;
|
|
16
|
+
if (source[index] !== '=') return false;
|
|
17
|
+
index -= 1;
|
|
18
|
+
while (index >= 0 && /\s/.test(source[index])) index -= 1;
|
|
19
|
+
const end = index + 1;
|
|
20
|
+
while (index >= 0 && /[\w-]/.test(source[index])) index -= 1;
|
|
21
|
+
return /^rw-[\w-]+$/.test(source.slice(index + 1, end));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function quotedEnd(source, start) {
|
|
25
|
+
const quote = source[start];
|
|
26
|
+
let index = start + 1;
|
|
27
|
+
while (index < source.length) {
|
|
28
|
+
if (source[index] === '\\') index += 2;
|
|
29
|
+
else if (source[index++] === quote) break;
|
|
30
|
+
}
|
|
31
|
+
return index;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tokens(source) {
|
|
35
|
+
const result = [];
|
|
36
|
+
let index = 0;
|
|
37
|
+
let actionDepth = 0;
|
|
38
|
+
const push = (kind, end) => {
|
|
39
|
+
result.push({ kind, value: source.slice(index, end) });
|
|
40
|
+
index = end;
|
|
41
|
+
};
|
|
42
|
+
while (index < source.length) {
|
|
43
|
+
if (source.startsWith('//', index)) {
|
|
44
|
+
const newline = source.indexOf('\n', index);
|
|
45
|
+
push('comment', newline < 0 ? source.length : newline);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (source.startsWith('/*', index)) {
|
|
49
|
+
const close = source.indexOf('*/', index + 2);
|
|
50
|
+
push('comment', close < 0 ? source.length : close + 2);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const character = source[index];
|
|
54
|
+
if (character === '{') {
|
|
55
|
+
if (actionDepth) actionDepth += 1;
|
|
56
|
+
else if (actionAttributeBefore(source, index)) actionDepth = 1;
|
|
57
|
+
push('plain', index + 1);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (character === '}') {
|
|
61
|
+
if (actionDepth) actionDepth -= 1;
|
|
62
|
+
push('plain', index + 1);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
66
|
+
push('string', quotedEnd(source, index));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (/\d/.test(character)) {
|
|
70
|
+
const match = /^(?:0[xob][\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i.exec(source.slice(index));
|
|
71
|
+
push('number', index + match[0].length);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (identifierStart(character)) {
|
|
75
|
+
let end = index + 1;
|
|
76
|
+
while (end < source.length && identifierPart(source[end])) end += 1;
|
|
77
|
+
const value = source.slice(index, end);
|
|
78
|
+
push(KEYWORDS.has(value) ? 'keyword' : LITERALS.has(value) ? 'literal' : actionDepth ? 'reference' : 'plain', end);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let end = index + 1;
|
|
82
|
+
while (end < source.length && !source.startsWith('//', end) && !source.startsWith('/*', end)
|
|
83
|
+
&& !['{', '}', "'", '"', '`'].includes(source[end]) && !/\d/.test(source[end])
|
|
84
|
+
&& !identifierStart(source[end])) end += 1;
|
|
85
|
+
push('plain', end);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function highlightCode(source, language) {
|
|
91
|
+
const { html } = require('./Html');
|
|
92
|
+
if (!LANGUAGES.has(language)) return html`${source}`;
|
|
93
|
+
return html`${tokens(source).map(token => token.kind === 'plain'
|
|
94
|
+
? html`${token.value}`
|
|
95
|
+
: html`<span class="${`token-${token.kind}`}">${token.value}</span>`)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { highlightCode };
|
package/src/htmx/Html.js
CHANGED
|
@@ -123,12 +123,13 @@ function codeBlock(code, options = {}) {
|
|
|
123
123
|
throw new TypeError('codeBlock() language must be a safe name of at most 32 characters.');
|
|
124
124
|
}
|
|
125
125
|
if (typeof label !== 'string') throw new TypeError('codeBlock() label must be a string.');
|
|
126
|
-
if (highlight !== undefined && typeof highlight !== 'function') throw new TypeError('codeBlock() highlight must be a function.');
|
|
126
|
+
if (highlight !== undefined && highlight !== false && typeof highlight !== 'function') throw new TypeError('codeBlock() highlight must be a function or false.');
|
|
127
127
|
const caption = label ? html`<figcaption>${label}</figcaption>` : html``;
|
|
128
128
|
let content = isHtml(code) ? code : String(code ?? '');
|
|
129
|
-
|
|
129
|
+
const highlighter = highlight === false ? undefined : highlight ?? (isHtml(code) ? undefined : require('./CodeHighlight').highlightCode);
|
|
130
|
+
if (highlighter) {
|
|
130
131
|
if (isHtml(code)) throw new TypeError('codeBlock() cannot highlight an HtmlFragment.');
|
|
131
|
-
content = synchronous(
|
|
132
|
+
content = synchronous(highlighter(content, language), 'codeBlock() highlight must render synchronously.');
|
|
132
133
|
if (!isHtml(content)) throw new TypeError('codeBlock() highlight must return an HtmlFragment.');
|
|
133
134
|
}
|
|
134
135
|
return html`<figure class="redweb-code">${caption}<pre><code class="${attribute(`language-${language}`)}">${content}</code></pre></figure>`;
|
package/src/htmx/Jsx.js
CHANGED
|
@@ -83,13 +83,13 @@ function renderComponent(Component, properties) {
|
|
|
83
83
|
|
|
84
84
|
function createElement(type, properties, key) {
|
|
85
85
|
const reactive = ReactiveRenderer.jsx();
|
|
86
|
-
const props = properties == null ? {} : properties;
|
|
86
|
+
const props = properties == null ? {} : properties;
|
|
87
87
|
if (!props || typeof props !== 'object' || Array.isArray(props)) {
|
|
88
88
|
throw new TypeError('JSX properties must be an object.');
|
|
89
89
|
}
|
|
90
90
|
let result;
|
|
91
91
|
if (type === Fragment) result = trustedHtml(renderChild(props.children));
|
|
92
|
-
else if (typeof type === 'string') result = renderIntrinsic(type, require('./SocketAction').attributes(props));
|
|
92
|
+
else if (typeof type === 'string') result = renderIntrinsic(type, require('./SocketAction').attributes(props));
|
|
93
93
|
else if (typeof type === 'function') result = renderComponent(type, props);
|
|
94
94
|
else throw new TypeError('JSX element types must be intrinsic names or function components.');
|
|
95
95
|
const elementKey = key ?? props.key;
|
|
@@ -28,10 +28,12 @@ class LiveHtmlServer {
|
|
|
28
28
|
maxSessions,
|
|
29
29
|
maxConcurrentRenders,
|
|
30
30
|
shutdownTimeoutMs = 1000,
|
|
31
|
+
uploadTimeoutMs,
|
|
31
32
|
heartbeat,
|
|
32
33
|
authenticate,
|
|
33
34
|
authenticationTimeoutMs,
|
|
34
35
|
origins,
|
|
36
|
+
providers,
|
|
35
37
|
development,
|
|
36
38
|
server: suppliedApp,
|
|
37
39
|
...httpOptions
|
|
@@ -54,10 +56,12 @@ class LiveHtmlServer {
|
|
|
54
56
|
maxSessions,
|
|
55
57
|
maxConcurrentRenders,
|
|
56
58
|
shutdownTimeoutMs,
|
|
59
|
+
uploadTimeoutMs,
|
|
57
60
|
heartbeat,
|
|
58
61
|
authenticate,
|
|
59
62
|
authenticationTimeoutMs,
|
|
60
63
|
origins,
|
|
64
|
+
providers,
|
|
61
65
|
logger: httpOptions.logger,
|
|
62
66
|
});
|
|
63
67
|
if (this._inspection) this.manager.Renderer = this._inspection.Renderer;
|
|
@@ -71,7 +75,7 @@ class LiveHtmlServer {
|
|
|
71
75
|
if (this.manager.hasLivePages || socketRoutes.length) {
|
|
72
76
|
this.sockets = new SocketServer({
|
|
73
77
|
server: this.http.server,
|
|
74
|
-
routes: [...(this.manager.hasLivePages ? [this.manager.route()] : []), ...require('./PageSocketRoute').bindRoutes(this.manager, socketRoutes)],
|
|
78
|
+
routes: [...(this.manager.hasLivePages ? [this.manager.route()] : []), ...require('./PageSocketRoute').bindRoutes(this.manager, socketRoutes)],
|
|
75
79
|
listen,
|
|
76
80
|
port: this.http.port,
|
|
77
81
|
bind: this.http.bind,
|
package/src/htmx/LivePage.js
CHANGED
|
@@ -5,7 +5,8 @@ const ReactiveRenderer = require('./ReactiveRenderer');
|
|
|
5
5
|
const dataProperty = require('../dataProperty');
|
|
6
6
|
const { ActionInputError } = require('./ActionDefinition');
|
|
7
7
|
const { isHtml, markHtml, renderValue } = require('./Html');
|
|
8
|
-
const { forEachState, getActionImplementation, getActionDefinition, getStateConfig, isComponentClass } = require('./metadata');
|
|
8
|
+
const { forEachState, getActionImplementation, getActionDefinition, getResourceMetadata, getStateConfig, isComponentClass } = require('./metadata');
|
|
9
|
+
const { LiveResource } = require('./LiveResource');
|
|
9
10
|
|
|
10
11
|
const RUNTIME = new WeakMap();
|
|
11
12
|
const COMPONENT_RENDER_CONTEXT = new AsyncLocalStorage();
|
|
@@ -159,6 +160,7 @@ class LivePage {
|
|
|
159
160
|
});
|
|
160
161
|
});
|
|
161
162
|
internal.stateActive = true;
|
|
163
|
+
getResourceMetadata(this.constructor).forEach((config, name) => config.resource.bind(this, name, config.select));
|
|
162
164
|
Object.keys(this).forEach(name => {
|
|
163
165
|
const value = this[name];
|
|
164
166
|
if (isComponentClass(value?.constructor) && getStateConfig(this.constructor, name)) {
|
|
@@ -226,6 +228,7 @@ class LivePage {
|
|
|
226
228
|
|
|
227
229
|
_stateChanged(name, value) {
|
|
228
230
|
if (!getStateConfig(this.constructor, name)) return false;
|
|
231
|
+
LiveResource.refresh(this);
|
|
229
232
|
const payload = LivePage.statePayload(this, name, value, true);
|
|
230
233
|
runtime(this).connections.forEach(socket => {
|
|
231
234
|
const session = socket.__redwebPageSession;
|
|
@@ -261,6 +264,7 @@ class LivePage {
|
|
|
261
264
|
const internal = runtime(this);
|
|
262
265
|
if (internal.disposePromise) return internal.disposePromise;
|
|
263
266
|
internal.disposed = true;
|
|
267
|
+
LiveResource.release(this);
|
|
264
268
|
internal.connections.clear();
|
|
265
269
|
internal.disposePromise = Promise.resolve().then(async () => {
|
|
266
270
|
const tasks = [...internal.children.values()].map(component => LivePage.dispose(component));
|