@tiaforge-ui/sinter 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +158 -2
- package/dist/cache.d.ts +5 -0
- package/dist/cache.js +95 -0
- package/dist/check/index.d.ts +85 -0
- package/dist/check/index.js +156 -0
- package/dist/check/openapi-types.d.ts +6 -0
- package/dist/check/openapi-types.js +84 -0
- package/dist/check/shadow.d.ts +49 -0
- package/dist/check/shadow.js +279 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/compiler/check-unit.d.ts +109 -0
- package/dist/compiler/check-unit.js +3 -0
- package/dist/compiler/compile.d.ts +66 -0
- package/dist/compiler/compile.js +1316 -0
- package/dist/compiler/diagnostic-docs.d.ts +10 -0
- package/dist/compiler/diagnostic-docs.js +142 -0
- package/dist/compiler/diagnostics.d.ts +57 -0
- package/dist/compiler/diagnostics.js +77 -0
- package/dist/compiler/expr.d.ts +97 -0
- package/dist/compiler/expr.js +853 -0
- package/dist/compiler/index.d.ts +22 -0
- package/dist/compiler/index.js +160 -0
- package/dist/compiler/openapi.d.ts +40 -0
- package/dist/compiler/openapi.js +177 -0
- package/dist/compiler/vocabulary.d.ts +12 -0
- package/dist/compiler/vocabulary.js +39 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.js +92 -0
- package/dist/each.d.ts +3 -0
- package/dist/each.js +121 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/ir.d.ts +152 -0
- package/dist/ir.js +121 -0
- package/dist/lsp/quickfix.d.ts +15 -0
- package/dist/lsp/quickfix.js +58 -0
- package/dist/lsp/rpc.d.ts +31 -0
- package/dist/lsp/rpc.js +120 -0
- package/dist/lsp/server.d.ts +36 -0
- package/dist/lsp/server.js +281 -0
- package/dist/lsp/spec-location.d.ts +9 -0
- package/dist/lsp/spec-location.js +42 -0
- package/dist/lsp/typescript.d.ts +77 -0
- package/dist/lsp/typescript.js +242 -0
- package/dist/lsp/workspace.d.ts +56 -0
- package/dist/lsp/workspace.js +228 -0
- package/dist/optimistic.d.ts +3 -0
- package/dist/optimistic.js +15 -0
- package/dist/paginate.d.ts +3 -0
- package/dist/paginate.js +198 -0
- package/dist/persist.d.ts +3 -0
- package/dist/persist.js +55 -0
- package/dist/project.d.ts +42 -0
- package/dist/project.js +103 -0
- package/dist/runtime/app.d.ts +152 -0
- package/dist/runtime/app.js +1256 -0
- package/dist/runtime/config.d.ts +53 -0
- package/dist/runtime/config.js +118 -0
- package/dist/runtime/env.d.ts +18 -0
- package/dist/runtime/env.js +23 -0
- package/dist/runtime/helpers.d.ts +9 -0
- package/dist/runtime/helpers.js +55 -0
- package/dist/runtime/request.d.ts +23 -0
- package/dist/runtime/request.js +93 -0
- package/dist/runtime/security.d.ts +12 -0
- package/dist/runtime/security.js +34 -0
- package/dist/runtime/store.d.ts +98 -0
- package/dist/runtime/store.js +255 -0
- package/dist/sse.d.ts +7 -0
- package/dist/sse.js +209 -0
- package/dist/testing/index.d.ts +73 -0
- package/dist/testing/index.js +359 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +96 -0
- package/dist/ws.d.ts +18 -0
- package/dist/ws.js +57 -0
- package/docs/diagnostics.md +283 -0
- package/html-custom-data.json +250 -0
- package/llms.txt +632 -0
- package/package.json +93 -4
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CheckUnit } from '../compiler/check-unit.js';
|
|
2
|
+
import { type Diagnostic } from '../compiler/index.js';
|
|
3
|
+
import { type Project, type ProjectOptions } from '../project.js';
|
|
4
|
+
export interface ProjectState {
|
|
5
|
+
root: string;
|
|
6
|
+
options: ProjectOptions;
|
|
7
|
+
project: Project;
|
|
8
|
+
/** HTML entries from the Vite config */
|
|
9
|
+
entries: Set<string>;
|
|
10
|
+
/** every page compiled so far (entries, plus open HTML files no page includes) */
|
|
11
|
+
pages: Set<string>;
|
|
12
|
+
/** per page: the files it includes */
|
|
13
|
+
includes: Map<string, string[]>;
|
|
14
|
+
/** per page: its last compile's diagnostics */
|
|
15
|
+
diagnostics: Map<string, Diagnostic[]>;
|
|
16
|
+
/** per page: its last compile's check unit */
|
|
17
|
+
units: Map<string, CheckUnit>;
|
|
18
|
+
/** TypeScript's diagnostics, per page (or per check run); they lag behind compiles */
|
|
19
|
+
types: Map<string, Diagnostic[]>;
|
|
20
|
+
/** files whose change reloads the project: Vite config, helpers, types, specs */
|
|
21
|
+
inputs: Set<string>;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkspaceHooks {
|
|
24
|
+
/** the diagnostics of `file` changed (an empty list clears them) */
|
|
25
|
+
publish(file: string, diagnostics: Diagnostic[]): void;
|
|
26
|
+
/** pages were compiled; their check units are in `state.units` */
|
|
27
|
+
compiled?(state: ProjectState, pages: string[]): void;
|
|
28
|
+
}
|
|
29
|
+
export declare class Workspace {
|
|
30
|
+
private hooks;
|
|
31
|
+
/** open buffers by absolute path */
|
|
32
|
+
readonly docs: Map<string, string>;
|
|
33
|
+
readonly projects: Map<string, Promise<ProjectState>>;
|
|
34
|
+
/** files that currently have diagnostics published */
|
|
35
|
+
private published;
|
|
36
|
+
constructor(hooks: WorkspaceHooks);
|
|
37
|
+
/** The project a file belongs to: the nearest folder with a Vite config, else with a package.json. */
|
|
38
|
+
rootOf(file: string): string;
|
|
39
|
+
projectFor(file: string): Promise<ProjectState>;
|
|
40
|
+
private load;
|
|
41
|
+
open(file: string, text: string): Promise<void>;
|
|
42
|
+
change(file: string, text: string): Promise<void>;
|
|
43
|
+
close(file: string): Promise<void>;
|
|
44
|
+
/** A file changed on disk (saved, or changed by another program). */
|
|
45
|
+
fileChanged(file: string): Promise<void>;
|
|
46
|
+
/** a non-HTML file: reload projects that read it */
|
|
47
|
+
private otherChanged;
|
|
48
|
+
private htmlChanged;
|
|
49
|
+
/** the text of a file: the open buffer, else the disk */
|
|
50
|
+
read(file: string): string;
|
|
51
|
+
private compilePages;
|
|
52
|
+
/** Sets TypeScript's diagnostics under `key` (a page) and publishes. Ignored for a replaced project state. */
|
|
53
|
+
setTypes(state: ProjectState, key: string, list: Diagnostic[]): Promise<void>;
|
|
54
|
+
/** Publishes every file's diagnostics, merged across the pages that include it. */
|
|
55
|
+
private publishAll;
|
|
56
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// What the language server knows: the projects under the editor's folders, their
|
|
2
|
+
// pages and partials, open buffers, and the diagnostics of each file. Independent of
|
|
3
|
+
// LSP itself, so tests drive it directly.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { compile } from '../compiler/index.js';
|
|
7
|
+
import { project, viteProject } from '../project.js';
|
|
8
|
+
const VITE_CONFIGS = ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs', 'vite.config.cts', 'vite.config.cjs'];
|
|
9
|
+
export class Workspace {
|
|
10
|
+
hooks;
|
|
11
|
+
/** open buffers by absolute path */
|
|
12
|
+
docs = new Map();
|
|
13
|
+
projects = new Map();
|
|
14
|
+
/** files that currently have diagnostics published */
|
|
15
|
+
published = new Set();
|
|
16
|
+
constructor(hooks) {
|
|
17
|
+
this.hooks = hooks;
|
|
18
|
+
}
|
|
19
|
+
/** The project a file belongs to: the nearest folder with a Vite config, else with a package.json. */
|
|
20
|
+
rootOf(file) {
|
|
21
|
+
let pkg = null;
|
|
22
|
+
for (let dir = dirname(file);; dir = dirname(dir)) {
|
|
23
|
+
if (VITE_CONFIGS.some((c) => existsSync(join(dir, c))))
|
|
24
|
+
return dir;
|
|
25
|
+
if (!pkg && existsSync(join(dir, 'package.json')))
|
|
26
|
+
pkg = dir;
|
|
27
|
+
if (dirname(dir) === dir)
|
|
28
|
+
return pkg ?? dirname(file);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
projectFor(file) {
|
|
32
|
+
const root = this.rootOf(file);
|
|
33
|
+
let p = this.projects.get(root);
|
|
34
|
+
if (!p) {
|
|
35
|
+
p = this.load(root);
|
|
36
|
+
this.projects.set(root, p);
|
|
37
|
+
}
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
async load(root) {
|
|
41
|
+
let pages = [];
|
|
42
|
+
let options = {};
|
|
43
|
+
try {
|
|
44
|
+
({ pages, options } = await viteProject(root));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
process.stderr.write(`sinter lsp: reading the Vite config in ${root} failed: ${e.message}\n`);
|
|
48
|
+
}
|
|
49
|
+
const p = project(root, options);
|
|
50
|
+
const inputs = new Set(VITE_CONFIGS.map((c) => join(root, c)));
|
|
51
|
+
for (const f of [p.helpersFile, p.actionsFile, p.types, ...p.openapi.map((s) => s.file)])
|
|
52
|
+
if (f)
|
|
53
|
+
inputs.add(f);
|
|
54
|
+
for (const spec of options.openapi == null ? [] : [].concat(options.openapi))
|
|
55
|
+
inputs.add(resolve(root, spec));
|
|
56
|
+
for (const ext of ['.js', '.ts']) {
|
|
57
|
+
inputs.add(join(root, 'src/sinter.helpers' + ext));
|
|
58
|
+
inputs.add(join(root, 'src/sinter.actions' + ext));
|
|
59
|
+
inputs.add(join(root, 'src/sinter.types' + ext));
|
|
60
|
+
}
|
|
61
|
+
inputs.add(join(root, 'src/sinter.types.d.ts'));
|
|
62
|
+
for (const m of Object.values(p.helpers.namespaces))
|
|
63
|
+
if (m.module.startsWith('/'))
|
|
64
|
+
inputs.add(join(root, m.module));
|
|
65
|
+
const state = {
|
|
66
|
+
root, options, project: p, entries: new Set(pages), pages: new Set(pages),
|
|
67
|
+
includes: new Map(), diagnostics: new Map(), units: new Map(), types: new Map(), inputs,
|
|
68
|
+
};
|
|
69
|
+
for (const [file] of this.docs)
|
|
70
|
+
if (file.endsWith('.html') && this.rootOf(file) === root)
|
|
71
|
+
state.pages.add(file);
|
|
72
|
+
this.compilePages(state, [...state.pages]);
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
async open(file, text) {
|
|
76
|
+
this.docs.set(file, text);
|
|
77
|
+
if (file.endsWith('.html'))
|
|
78
|
+
await this.htmlChanged(file);
|
|
79
|
+
else
|
|
80
|
+
await this.otherChanged(file);
|
|
81
|
+
}
|
|
82
|
+
async change(file, text) {
|
|
83
|
+
this.docs.set(file, text);
|
|
84
|
+
if (file.endsWith('.html'))
|
|
85
|
+
await this.htmlChanged(file);
|
|
86
|
+
}
|
|
87
|
+
async close(file) {
|
|
88
|
+
this.docs.delete(file);
|
|
89
|
+
if (!file.endsWith('.html'))
|
|
90
|
+
return;
|
|
91
|
+
const state = await this.projectFor(file);
|
|
92
|
+
// a page that is neither an entry nor open is forgotten; its diagnostics go with it
|
|
93
|
+
if (!state.entries.has(file) && state.pages.has(file)) {
|
|
94
|
+
state.pages.delete(file);
|
|
95
|
+
state.includes.delete(file);
|
|
96
|
+
state.diagnostics.delete(file);
|
|
97
|
+
state.units.delete(file);
|
|
98
|
+
state.types.delete(file);
|
|
99
|
+
this.publishAll(state);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await this.htmlChanged(file);
|
|
103
|
+
}
|
|
104
|
+
/** A file changed on disk (saved, or changed by another program). */
|
|
105
|
+
async fileChanged(file) {
|
|
106
|
+
if (file.endsWith('.html')) {
|
|
107
|
+
if (!this.docs.has(file))
|
|
108
|
+
await this.htmlChanged(file);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await this.otherChanged(file);
|
|
112
|
+
}
|
|
113
|
+
/** a non-HTML file: reload projects that read it */
|
|
114
|
+
async otherChanged(file) {
|
|
115
|
+
for (const [root, p] of this.projects) {
|
|
116
|
+
const state = await p;
|
|
117
|
+
if (!state.inputs.has(file))
|
|
118
|
+
continue;
|
|
119
|
+
const next = this.load(root);
|
|
120
|
+
this.projects.set(root, next);
|
|
121
|
+
const fresh = await next;
|
|
122
|
+
// pages the old state knew but the new one doesn't lose their diagnostics
|
|
123
|
+
for (const page of state.pages)
|
|
124
|
+
if (!fresh.pages.has(page))
|
|
125
|
+
state.diagnostics.delete(page);
|
|
126
|
+
this.publishAll(fresh);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async htmlChanged(file) {
|
|
130
|
+
const state = await this.projectFor(file);
|
|
131
|
+
const includers = [...state.includes].filter(([, files]) => files.includes(file)).map(([page]) => page);
|
|
132
|
+
const pages = new Set(includers);
|
|
133
|
+
if (state.pages.has(file) || !includers.length) {
|
|
134
|
+
if (this.docs.has(file) || existsSync(file)) {
|
|
135
|
+
state.pages.add(file);
|
|
136
|
+
pages.add(file);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.compilePages(state, [...pages]);
|
|
140
|
+
}
|
|
141
|
+
/** the text of a file: the open buffer, else the disk */
|
|
142
|
+
read(file) {
|
|
143
|
+
return this.docs.get(file) ?? readFileSync(file, 'utf8');
|
|
144
|
+
}
|
|
145
|
+
compilePages(state, pages) {
|
|
146
|
+
const p = state.project;
|
|
147
|
+
const readFile = (path) => {
|
|
148
|
+
const disk = p.readFile(path); // refuses files outside the root
|
|
149
|
+
return this.docs.get(resolve(path)) ?? disk;
|
|
150
|
+
};
|
|
151
|
+
for (const page of pages) {
|
|
152
|
+
let html;
|
|
153
|
+
try {
|
|
154
|
+
html = this.read(page);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// a page that is now included by another one is a partial, not a page
|
|
160
|
+
if (!state.entries.has(page) && [...state.includes].some(([other, files]) => other !== page && files.includes(page))) {
|
|
161
|
+
state.pages.delete(page);
|
|
162
|
+
state.diagnostics.delete(page);
|
|
163
|
+
state.includes.delete(page);
|
|
164
|
+
state.units.delete(page);
|
|
165
|
+
state.types.delete(page);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const out = compile(html, {
|
|
170
|
+
file: page, root: state.root, dev: false, helpers: p.helpers, actions: p.actions, config: p.config, readFile, check: true, openapi: p.openapi,
|
|
171
|
+
});
|
|
172
|
+
state.includes.set(page, out.files.map((f) => resolve(f)).filter((f) => f !== page));
|
|
173
|
+
state.diagnostics.set(page, out.diagnostics);
|
|
174
|
+
if (out.check)
|
|
175
|
+
state.units.set(page, out.check);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
state.diagnostics.set(page, [{ severity: 'error', code: 'SN401', message: `sinter could not compile this page: ${e.message}`, file: page, line: 1, col: 1 }]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
this.publishAll(state);
|
|
182
|
+
this.hooks.compiled?.(state, pages.filter((x) => state.pages.has(x)));
|
|
183
|
+
}
|
|
184
|
+
/** Sets TypeScript's diagnostics under `key` (a page) and publishes. Ignored for a replaced project state. */
|
|
185
|
+
async setTypes(state, key, list) {
|
|
186
|
+
if ((await this.projects.get(state.root)) !== state)
|
|
187
|
+
return;
|
|
188
|
+
state.types.set(key, list);
|
|
189
|
+
this.publishAll(state);
|
|
190
|
+
}
|
|
191
|
+
/** Publishes every file's diagnostics, merged across the pages that include it. */
|
|
192
|
+
publishAll(state) {
|
|
193
|
+
const byFile = new Map();
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const add = (d) => {
|
|
196
|
+
const key = `${d.file}\0${d.line}\0${d.col}\0${d.code}\0${d.message}`;
|
|
197
|
+
if (seen.has(key))
|
|
198
|
+
return;
|
|
199
|
+
seen.add(key);
|
|
200
|
+
const list = byFile.get(d.file) ?? [];
|
|
201
|
+
list.push(d);
|
|
202
|
+
byFile.set(d.file, list);
|
|
203
|
+
};
|
|
204
|
+
for (const d of state.project.diagnostics)
|
|
205
|
+
add(d);
|
|
206
|
+
for (const page of state.pages)
|
|
207
|
+
for (const d of state.diagnostics.get(page) ?? [])
|
|
208
|
+
add(d);
|
|
209
|
+
for (const list of state.types.values())
|
|
210
|
+
for (const d of list)
|
|
211
|
+
add(d);
|
|
212
|
+
// files of this project that had diagnostics and now have none
|
|
213
|
+
for (const file of this.published) {
|
|
214
|
+
if (byFile.has(file) || this.rootOf(file) !== state.root)
|
|
215
|
+
continue;
|
|
216
|
+
this.published.delete(file);
|
|
217
|
+
this.hooks.publish(file, []);
|
|
218
|
+
}
|
|
219
|
+
for (const [file, list] of byFile) {
|
|
220
|
+
this.published.add(file);
|
|
221
|
+
this.hooks.publish(file, list);
|
|
222
|
+
}
|
|
223
|
+
// open files without problems get an empty list, so editors show them as checked
|
|
224
|
+
for (const file of this.docs.keys())
|
|
225
|
+
if (!byFile.has(file) && this.rootOf(file) === state.root)
|
|
226
|
+
this.hooks.publish(file, []);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const optimistic = {
|
|
2
|
+
name: 'optimistic',
|
|
3
|
+
install(app) {
|
|
4
|
+
app.ext.optimistic = {
|
|
5
|
+
begin(fn, scope) {
|
|
6
|
+
return app.record(() => {
|
|
7
|
+
const s = Object.create(scope);
|
|
8
|
+
s.$f = app.storeFns(null);
|
|
9
|
+
app.fns[fn](s);
|
|
10
|
+
});
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default optimistic;
|
package/dist/paginate.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
class PagerImpl {
|
|
2
|
+
loader;
|
|
3
|
+
move;
|
|
4
|
+
cfg;
|
|
5
|
+
constructor(loader) {
|
|
6
|
+
this.loader = loader;
|
|
7
|
+
this.cfg = loader.def.paginate;
|
|
8
|
+
this.move = this.first();
|
|
9
|
+
const m = loader.res.meta;
|
|
10
|
+
if (this.cfg.mode === 'offset' || this.cfg.offsetParam)
|
|
11
|
+
m.page = 1;
|
|
12
|
+
Object.assign(m, { pages: null, from: null, to: null, hasPrev: false, hasNext: false });
|
|
13
|
+
}
|
|
14
|
+
base(dir, size = this.cfg.size) {
|
|
15
|
+
const p = { [this.cfg.sizeParam]: String(size) };
|
|
16
|
+
if (dir)
|
|
17
|
+
p[this.cfg.directionParam] = this.cfg.directionValues?.[dir] ?? dir;
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
first() {
|
|
21
|
+
if (this.cfg.mode === 'offset')
|
|
22
|
+
return { dir: 'page', page: 1, params: { [this.cfg.pageParam]: '1', ...this.base(null) }, from: null, to: null };
|
|
23
|
+
return { dir: 'first', page: 1, params: this.base('first'), from: null, to: null };
|
|
24
|
+
}
|
|
25
|
+
reset() {
|
|
26
|
+
this.move = this.first();
|
|
27
|
+
}
|
|
28
|
+
current() {
|
|
29
|
+
if (this.cfg.mode === 'append' && this.move.dir !== 'first') {
|
|
30
|
+
// refetch reloads everything loaded so far in one request
|
|
31
|
+
const n = Math.max(this.cfg.size, this.loader.res.all().length);
|
|
32
|
+
return { dir: 'all', page: 1, params: this.base('first', n), from: null, to: null };
|
|
33
|
+
}
|
|
34
|
+
return this.move;
|
|
35
|
+
}
|
|
36
|
+
params(move) {
|
|
37
|
+
return move.params;
|
|
38
|
+
}
|
|
39
|
+
appends(move) {
|
|
40
|
+
return this.cfg.mode === 'append' && move.dir === 'next';
|
|
41
|
+
}
|
|
42
|
+
go(target) {
|
|
43
|
+
const meta = this.loader.res.meta;
|
|
44
|
+
const items = this.loader.res.all();
|
|
45
|
+
const cursor = (item) => {
|
|
46
|
+
if (this.cfg.cursor == null || item === undefined)
|
|
47
|
+
return {};
|
|
48
|
+
const scope = Object.create(this.loader.app.page);
|
|
49
|
+
scope.$p = item;
|
|
50
|
+
const c = this.loader.app.fns[this.cfg.cursor](scope);
|
|
51
|
+
const out = {};
|
|
52
|
+
// like a URL template's query parameters: null and undefined fields are left out
|
|
53
|
+
for (const [k, v] of Object.entries(c ?? {}))
|
|
54
|
+
if (v != null)
|
|
55
|
+
out[k] = String(v);
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
let move;
|
|
59
|
+
const from = meta.from ?? null, to = meta.to ?? null;
|
|
60
|
+
if (this.cfg.mode === 'offset') {
|
|
61
|
+
const page = meta.page ?? 1;
|
|
62
|
+
let n;
|
|
63
|
+
if (typeof target === 'number')
|
|
64
|
+
n = target;
|
|
65
|
+
else if (target === 'first')
|
|
66
|
+
n = 1;
|
|
67
|
+
else if (target === 'prev')
|
|
68
|
+
n = page - 1;
|
|
69
|
+
else if (target === 'next')
|
|
70
|
+
n = page + 1;
|
|
71
|
+
else if (target === 'last') {
|
|
72
|
+
if (meta.pages == null) {
|
|
73
|
+
this.loader.app.warn(`page('${this.loader.def.name}', 'last') needs a known total`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
n = meta.pages;
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
return;
|
|
80
|
+
n = Math.max(1, Math.floor(n));
|
|
81
|
+
move = { dir: 'page', page: n, params: { [this.cfg.pageParam]: String(n), ...this.base(null) }, from, to };
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (typeof target === 'number') {
|
|
85
|
+
if (!this.cfg.offsetParam) {
|
|
86
|
+
this.loader.app.warn('keyset pagination cannot jump to a page number; add offset-param or use paginate="offset"');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// the server skips (n - 1) pages; prev/next continue by cursor from there
|
|
90
|
+
let n = Math.max(1, Math.floor(target));
|
|
91
|
+
if (meta.pages != null)
|
|
92
|
+
n = Math.min(n, meta.pages);
|
|
93
|
+
move = { dir: 'page', page: n, params: { [this.cfg.offsetParam]: String((n - 1) * this.cfg.size), ...this.base('first') }, from, to };
|
|
94
|
+
void this.loader.load(move);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.cfg.mode === 'append' && target !== 'next')
|
|
98
|
+
return;
|
|
99
|
+
const dir = target;
|
|
100
|
+
const c = dir === 'next' ? cursor(items[items.length - 1]) : dir === 'prev' ? cursor(items[0]) : {};
|
|
101
|
+
move = { dir, page: 1, params: { ...c, ...this.base(dir) }, from, to };
|
|
102
|
+
}
|
|
103
|
+
void this.loader.load(move);
|
|
104
|
+
}
|
|
105
|
+
received(items, total, mv) {
|
|
106
|
+
const move = mv;
|
|
107
|
+
if (move.dir !== 'all')
|
|
108
|
+
this.move = move;
|
|
109
|
+
const meta = this.loader.res.meta;
|
|
110
|
+
const n = items.length;
|
|
111
|
+
const size = this.cfg.size;
|
|
112
|
+
const full = n >= size;
|
|
113
|
+
const pages = total != null ? Math.max(1, Math.ceil(total / size)) : null;
|
|
114
|
+
let from = null, to = null, hasPrev = false, hasNext = false;
|
|
115
|
+
switch (move.dir) {
|
|
116
|
+
case 'page':
|
|
117
|
+
from = n ? (move.page - 1) * size + 1 : 0;
|
|
118
|
+
to = n ? from + n - 1 : 0;
|
|
119
|
+
hasPrev = move.page > 1;
|
|
120
|
+
hasNext = pages != null ? move.page < pages : full;
|
|
121
|
+
meta.page = move.page;
|
|
122
|
+
break;
|
|
123
|
+
case 'first':
|
|
124
|
+
from = n ? 1 : 0;
|
|
125
|
+
to = n;
|
|
126
|
+
hasPrev = false;
|
|
127
|
+
hasNext = full;
|
|
128
|
+
break;
|
|
129
|
+
case 'all':
|
|
130
|
+
from = n ? 1 : 0;
|
|
131
|
+
to = n;
|
|
132
|
+
hasPrev = false;
|
|
133
|
+
hasNext = meta.hasNext ?? full;
|
|
134
|
+
break;
|
|
135
|
+
case 'next':
|
|
136
|
+
if (this.cfg.mode === 'append') {
|
|
137
|
+
const all = this.loader.res.all().length;
|
|
138
|
+
from = all ? 1 : 0;
|
|
139
|
+
to = all;
|
|
140
|
+
hasPrev = false;
|
|
141
|
+
hasNext = full;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
from = move.to != null ? move.to + 1 : null;
|
|
145
|
+
to = from != null ? from + n - 1 : null;
|
|
146
|
+
hasPrev = true;
|
|
147
|
+
hasNext = full;
|
|
148
|
+
break;
|
|
149
|
+
case 'prev':
|
|
150
|
+
hasNext = true;
|
|
151
|
+
if (!full) {
|
|
152
|
+
from = n ? 1 : 0;
|
|
153
|
+
to = n;
|
|
154
|
+
hasPrev = false;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
to = move.from != null ? move.from - 1 : null;
|
|
158
|
+
from = to != null ? to - n + 1 : null;
|
|
159
|
+
hasPrev = from == null || from > 1;
|
|
160
|
+
break;
|
|
161
|
+
case 'last':
|
|
162
|
+
hasNext = false;
|
|
163
|
+
if (total != null) {
|
|
164
|
+
to = total;
|
|
165
|
+
from = total - n + 1;
|
|
166
|
+
hasPrev = from > 1;
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
hasPrev = full;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
// keyset with jumps: the page that holds the last item shown (after `last`, the last page)
|
|
173
|
+
if (this.cfg.offsetParam && move.dir !== 'page')
|
|
174
|
+
meta.page = to != null ? Math.max(1, Math.ceil(to / size)) : null;
|
|
175
|
+
Object.assign(meta, { pages, from, to, hasPrev, hasNext });
|
|
176
|
+
this.loader.res.onDirty('meta');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const paginate = {
|
|
180
|
+
name: 'paginate',
|
|
181
|
+
install(app) {
|
|
182
|
+
const pagers = new Map();
|
|
183
|
+
app.ext.paginate = {
|
|
184
|
+
pager(loader) {
|
|
185
|
+
const p = new PagerImpl(loader);
|
|
186
|
+
pagers.set(loader.def.name, p);
|
|
187
|
+
return p;
|
|
188
|
+
},
|
|
189
|
+
page(name, target) {
|
|
190
|
+
const p = pagers.get(name);
|
|
191
|
+
if (!p)
|
|
192
|
+
throw new Error(`sinter: resource \`${name}\` is not paginated`);
|
|
193
|
+
p.go(target);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
export default paginate;
|
package/dist/persist.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const PREFIX = 'sn:';
|
|
2
|
+
const persist = {
|
|
3
|
+
name: 'persist',
|
|
4
|
+
install(app) {
|
|
5
|
+
const read = (key) => {
|
|
6
|
+
try {
|
|
7
|
+
const v = app.env.storage()?.getItem(PREFIX + key);
|
|
8
|
+
return v == null ? undefined : JSON.parse(v);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const write = (key, value) => {
|
|
15
|
+
try {
|
|
16
|
+
app.env.storage()?.setItem(PREFIX + key, JSON.stringify(value));
|
|
17
|
+
}
|
|
18
|
+
catch { /* ignored */ }
|
|
19
|
+
};
|
|
20
|
+
app.ext.persist = {
|
|
21
|
+
model(rb, set) {
|
|
22
|
+
const key = rb.b.p;
|
|
23
|
+
const v = read(key);
|
|
24
|
+
if (v !== undefined)
|
|
25
|
+
set(v);
|
|
26
|
+
const paths = (rb.b.w ?? []).map((w) => w.split('.').map((s) => (s[0] === '{' ? '*' : s)));
|
|
27
|
+
app.afterFlush.push((dirty) => {
|
|
28
|
+
if (!dirty.some((d) => paths.some((p) => overlaps(p, d))))
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
write(key, app.fns[rb.b.f](rb.s));
|
|
32
|
+
}
|
|
33
|
+
catch { /* unreadable: skip */ }
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
state(key, obj) {
|
|
37
|
+
const v = read(key);
|
|
38
|
+
if (!v || typeof v !== 'object')
|
|
39
|
+
return;
|
|
40
|
+
for (const k of Object.keys(obj))
|
|
41
|
+
if (Object.hasOwn(v, k))
|
|
42
|
+
obj[k] = v[k];
|
|
43
|
+
},
|
|
44
|
+
saveState: write,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
function overlaps(a, b) {
|
|
49
|
+
const n = Math.min(a.length, b.length);
|
|
50
|
+
for (let i = 0; i < n; i++)
|
|
51
|
+
if (a[i] !== b[i] && a[i] !== '*' && b[i] !== '*')
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
export default persist;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Diagnostic, type HelperManifest } from './compiler/index.js';
|
|
2
|
+
import type { OpenApiSpec } from './compiler/openapi.js';
|
|
3
|
+
export interface ProjectOptions {
|
|
4
|
+
/** helpers module, relative to the root (default `src/sinter.helpers.js`, or `.ts`) */
|
|
5
|
+
helpers?: string;
|
|
6
|
+
/** actions module for `data-sn-call` and `<sn-resource call>` (default `src/sinter.actions.js`, or `.ts`) */
|
|
7
|
+
actions?: string;
|
|
8
|
+
/** app configuration module imported before mount (default `src/sinter.config.js`, or `.ts`) */
|
|
9
|
+
config?: string;
|
|
10
|
+
/** module whose exported types `type="…"` on `<sn-resource>` may name (default `src/sinter.types.ts`, `.d.ts` or `.js`) */
|
|
11
|
+
types?: string;
|
|
12
|
+
/** OpenAPI specs (JSON or YAML, inside the root) that resources without `type` are matched against */
|
|
13
|
+
openapi?: string | string[];
|
|
14
|
+
}
|
|
15
|
+
export interface Project {
|
|
16
|
+
root: string;
|
|
17
|
+
helpers: HelperManifest;
|
|
18
|
+
/** absolute path of the helpers module, or null */
|
|
19
|
+
helpersFile: string | null;
|
|
20
|
+
/** the actions module's functions and namespaces (same shape as helpers) */
|
|
21
|
+
actions: HelperManifest;
|
|
22
|
+
/** absolute path of the actions module, or null */
|
|
23
|
+
actionsFile: string | null;
|
|
24
|
+
/** root-relative URL of the config module (`/src/sinter.config.js`), or null */
|
|
25
|
+
config: string | null;
|
|
26
|
+
/** absolute path of the types module, or null */
|
|
27
|
+
types: string | null;
|
|
28
|
+
/** reads a file only if it resolves inside the root, symlinks included */
|
|
29
|
+
readFile: (path: string) => string;
|
|
30
|
+
openapi: OpenApiSpec[];
|
|
31
|
+
/** problems loading the specs (SN307) */
|
|
32
|
+
diagnostics: Diagnostic[];
|
|
33
|
+
}
|
|
34
|
+
export declare function project(root: string, options?: ProjectOptions): Project;
|
|
35
|
+
/**
|
|
36
|
+
* The pages (HTML entries) and sinter plugin options from the root's Vite config. Without
|
|
37
|
+
* Vite or entries, `index.html` if it exists.
|
|
38
|
+
*/
|
|
39
|
+
export declare function viteProject(root: string): Promise<{
|
|
40
|
+
pages: string[];
|
|
41
|
+
options: ProjectOptions;
|
|
42
|
+
}>;
|