@softr/sdk 0.0.23 → 0.0.24
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/dist/auth/session.js +7 -21
- package/dist/core/datasource.d.ts +3 -3
- package/dist/core/datasource.js +17 -17
- package/dist/core/integration.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +3 -3
- package/dist/vite.d.ts +2 -9
- package/dist/vite.js +3 -68
- package/package.json +1 -6
- package/skills/softr-sdk/SKILL.md +3 -3
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/data-mutations/SKILL.md +2 -2
- package/skills/softr-sdk/reading-records/SKILL.md +3 -3
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +5 -5
- package/dist/preview-identity/preview-identity.js +0 -121
- package/dist/routes/discover.d.ts +0 -14
- package/dist/routes/discover.js +0 -305
- package/dist/routes/manifest.d.ts +0 -24
- package/dist/routes/manifest.js +0 -168
- package/dist/routes/mem-fs.js +0 -71
package/dist/auth/session.js
CHANGED
|
@@ -46,8 +46,9 @@ function readSessionData(sessionToken) {
|
|
|
46
46
|
permissions: payload.permissions
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
function makeSessionTokenCookie(token
|
|
50
|
-
|
|
49
|
+
function makeSessionTokenCookie(token) {
|
|
50
|
+
const { name, secure } = sessionCookie(SESSION_TOKEN);
|
|
51
|
+
return serializeCookie(name, sealEnvelope(token, SESSION_TOKEN), {
|
|
51
52
|
maxAge: sessionConfig().sessionMaxAge,
|
|
52
53
|
secure
|
|
53
54
|
});
|
|
@@ -71,19 +72,6 @@ function makeSessionDataCookie(session, sessionToken) {
|
|
|
71
72
|
function clearSession() {
|
|
72
73
|
clearCookies([SESSION_TOKEN, SESSION_DATA]);
|
|
73
74
|
}
|
|
74
|
-
/** The clearing pair, for the same out-of-context caller as `makeSessionTokenCookie`. */
|
|
75
|
-
function makeSessionClearingCookies(secure) {
|
|
76
|
-
return serializeClearingCookies([SESSION_TOKEN, SESSION_DATA], secure);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The name `readSessionToken` would look the token up under on a request of this
|
|
80
|
-
* scheme, for a caller that only needs to know whether one is present and cannot
|
|
81
|
-
* decode it. Same `secure` argument as `makeSessionTokenCookie`, so a caller that
|
|
82
|
-
* writes and a caller that probes cannot disagree about the name.
|
|
83
|
-
*/
|
|
84
|
-
function sessionTokenCookieName(secure) {
|
|
85
|
-
return sessionCookieName(SESSION_TOKEN, secure);
|
|
86
|
-
}
|
|
87
75
|
const sessionDataSchema = z.object({
|
|
88
76
|
user: userSchema.nullable(),
|
|
89
77
|
permissions: permissionsSchema,
|
|
@@ -95,13 +83,11 @@ const sessionDataSchema = z.object({
|
|
|
95
83
|
* schemes, and the one left behind would revive if the scheme changed back.
|
|
96
84
|
*/
|
|
97
85
|
function clearCookies(bases) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function serializeClearingCookies(bases, secure) {
|
|
101
|
-
return bases.flatMap((base) => secure ? [sessionCookieName(base, secure), base] : [base]).map((name) => serializeCookie(name, "", {
|
|
86
|
+
const secure = requestIsSecure();
|
|
87
|
+
writeCookies(bases.flatMap((base) => secure ? [sessionCookieName(base, secure), base] : [base]).map((name) => serializeCookie(name, "", {
|
|
102
88
|
maxAge: 0,
|
|
103
89
|
secure
|
|
104
|
-
}));
|
|
90
|
+
})));
|
|
105
91
|
}
|
|
106
92
|
/**
|
|
107
93
|
* Over the plaintext token, never the cookie value: that is resealed with a
|
|
@@ -177,4 +163,4 @@ function openEnvelope(raw, purpose) {
|
|
|
177
163
|
}
|
|
178
164
|
|
|
179
165
|
//#endregion
|
|
180
|
-
export { clearSession,
|
|
166
|
+
export { clearSession, makeSessionDataCookie, makeSessionTokenCookie, readSessionData, readSessionToken };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChartOptions, ChartResult, CreateOptions, GetOptions, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrRecord, UpdateOptions } from "../types.js";
|
|
2
2
|
|
|
3
3
|
//#region src/core/datasource.d.ts
|
|
4
4
|
declare function listRecords<Id extends string, Sel extends Record<string, string> = Record<never, never>>(datasourceId: Id, options?: Omit<ListOptions, 'select'> & {
|
|
@@ -17,7 +17,7 @@ declare function updateRecord<Id extends string, Sel extends Record<string, stri
|
|
|
17
17
|
}): Promise<SoftrRecord<RecordFieldsFor<Id, Sel>>>;
|
|
18
18
|
declare function deleteRecord(datasourceId: string, id: string): Promise<void>;
|
|
19
19
|
declare function getMetric(datasourceId: string, options: MetricOptions): Promise<MetricResult>;
|
|
20
|
-
declare function
|
|
20
|
+
declare function getChart(datasourceId: string, options: ChartOptions): Promise<ChartResult>;
|
|
21
21
|
declare const ds: {
|
|
22
22
|
list: typeof listRecords;
|
|
23
23
|
get: typeof getRecord;
|
|
@@ -25,7 +25,7 @@ declare const ds: {
|
|
|
25
25
|
update: typeof updateRecord;
|
|
26
26
|
delete: typeof deleteRecord;
|
|
27
27
|
metric: typeof getMetric;
|
|
28
|
-
|
|
28
|
+
chart: typeof getChart;
|
|
29
29
|
};
|
|
30
30
|
//#endregion
|
|
31
31
|
export { ds };
|
package/dist/core/datasource.js
CHANGED
|
@@ -120,9 +120,10 @@ async function deleteRecord(datasourceId, id) {
|
|
|
120
120
|
}
|
|
121
121
|
async function getMetric(datasourceId, options) {
|
|
122
122
|
const { select, where, aggregation, field } = options;
|
|
123
|
+
if (aggregation !== "COUNT" && !field) throw new Error(`field is required for aggregation "${aggregation}"`);
|
|
123
124
|
const fieldId = field ? select?.[field] ?? field : void 0;
|
|
124
125
|
const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
|
|
125
|
-
const res = await fetch(`${dsUrl(datasourceId)}/
|
|
126
|
+
const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
|
|
126
127
|
method: "POST",
|
|
127
128
|
headers: {
|
|
128
129
|
"Content-Type": "application/json",
|
|
@@ -131,10 +132,11 @@ async function getMetric(datasourceId, options) {
|
|
|
131
132
|
},
|
|
132
133
|
body: JSON.stringify({
|
|
133
134
|
options: { userLocale: "en-US" },
|
|
134
|
-
|
|
135
|
+
groupBy: [],
|
|
136
|
+
metrics: [{
|
|
135
137
|
field: fieldId,
|
|
136
138
|
aggregation
|
|
137
|
-
},
|
|
139
|
+
}],
|
|
138
140
|
filterCriteria
|
|
139
141
|
})
|
|
140
142
|
});
|
|
@@ -142,26 +144,24 @@ async function getMetric(datasourceId, options) {
|
|
|
142
144
|
const payload = await res.json();
|
|
143
145
|
const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
|
|
144
146
|
let value;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!fieldId) throw new Error(`field is required for aggregation "${aggregation}"`);
|
|
148
|
-
value = payload.data.find((item) => fieldId in item)?.[fieldId] ?? null;
|
|
149
|
-
}
|
|
147
|
+
const responseKey = aggregation === "COUNT" && !fieldId ? COUNT_CNT : fieldId;
|
|
148
|
+
value = payload.data.find((item) => responseKey in item)?.[responseKey] ?? null;
|
|
150
149
|
return {
|
|
151
150
|
value,
|
|
152
151
|
executionTimestamp
|
|
153
152
|
};
|
|
154
153
|
}
|
|
155
|
-
async function
|
|
154
|
+
async function getChart(datasourceId, options) {
|
|
156
155
|
const { select, where, groupBy, metrics, orderBy } = options;
|
|
157
156
|
const filterCriteria = where ? dealiasFilterCriteria(buildFilterCriteria(where), select ?? {}) : {};
|
|
158
|
-
const
|
|
157
|
+
const sortingOptions = orderBy ? dealiasOrderBy(buildOrderBy(orderBy), select ?? {}) : void 0;
|
|
159
158
|
const groupByFields = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((field) => ({ field: select?.[field] ?? field }));
|
|
160
159
|
const yAxisFields = Object.values(metrics).map(({ field, aggregation }) => ({
|
|
161
160
|
field: field ? select?.[field] ?? field : void 0,
|
|
162
161
|
aggregation
|
|
163
162
|
}));
|
|
164
|
-
|
|
163
|
+
if (!yAxisFields.length) throw new Error("chart requires at least one metric");
|
|
164
|
+
const res = await fetch(`${dsUrl(datasourceId)}/chart/data`, {
|
|
165
165
|
method: "POST",
|
|
166
166
|
headers: {
|
|
167
167
|
"Content-Type": "application/json",
|
|
@@ -171,12 +171,12 @@ async function getChartData(datasourceId, options) {
|
|
|
171
171
|
body: JSON.stringify({
|
|
172
172
|
options: { userLocale: "en-US" },
|
|
173
173
|
filterCriteria,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
yAxisFields
|
|
174
|
+
sortingOptions,
|
|
175
|
+
groupBy: groupByFields,
|
|
176
|
+
metrics: yAxisFields
|
|
177
177
|
})
|
|
178
178
|
});
|
|
179
|
-
await assertResponseOk(res, `Softr datasource
|
|
179
|
+
await assertResponseOk(res, `Softr datasource chart failed for "${datasourceId}"`);
|
|
180
180
|
const payload = await res.json();
|
|
181
181
|
const executionTimestamp = /* @__PURE__ */ new Date(payload.executionTimestamp * 1e3);
|
|
182
182
|
const aliases = {};
|
|
@@ -216,11 +216,11 @@ const ds = {
|
|
|
216
216
|
update: updateRecord,
|
|
217
217
|
delete: deleteRecord,
|
|
218
218
|
metric: getMetric,
|
|
219
|
-
|
|
219
|
+
chart: getChart
|
|
220
220
|
};
|
|
221
221
|
function withoutTrailingSlash(url) {
|
|
222
222
|
return url.replace(/\/+$/, "");
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
//#endregion
|
|
226
|
-
export { ds };
|
|
226
|
+
export { ds, dsUrl };
|
package/dist/core/integration.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readSessionToken } from "../auth/session.js";
|
|
2
|
+
import { dsUrl } from "./datasource.js";
|
|
2
3
|
|
|
3
4
|
//#region src/core/integration.ts
|
|
4
|
-
const BASE_URL = () => (import.meta.env.VITE_SOFTR_BASE_URL ?? "").replace(/\/$/, "");
|
|
5
5
|
async function integrationFetch(integrationId, input, init) {
|
|
6
6
|
const req = new Request(input, init);
|
|
7
7
|
if (req.signal.aborted) throw req.signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -16,7 +16,7 @@ async function integrationFetch(integrationId, input, init) {
|
|
|
16
16
|
};
|
|
17
17
|
const method = req.method.toUpperCase();
|
|
18
18
|
if (method !== "GET" && method !== "HEAD" && req.body !== null) payload.body = await req.clone().text();
|
|
19
|
-
return fetch(`${
|
|
19
|
+
return fetch(`${dsUrl(integrationId)}/proxy`, {
|
|
20
20
|
method: "POST",
|
|
21
21
|
headers: {
|
|
22
22
|
"Content-Type": "application/json",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { q } from "./core/query.js";
|
|
2
2
|
import { SoftrUserGroupId, SoftrUserGroups, User } from "./auth/user.js";
|
|
3
|
-
import { AggregationFunction,
|
|
3
|
+
import { AggregationFunction, ChartOptions, ChartResult, CreateOptions, DatasourcePermissions, DatasourceTypeMap, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions } from "./types.js";
|
|
4
4
|
import { Permissions } from "./auth/permissions.js";
|
|
5
5
|
import { SoftrContext } from "./auth/context.js";
|
|
6
6
|
import { AccessBehaviour, AccessCheck, AccessError, AccessReason, RouteVisibility, VisibilityHandle, defineVisibility, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError } from "./routes/visibility.js";
|
|
7
7
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser, usePermissions } from "./auth/isomorphic.js";
|
|
8
8
|
import { useUserTimezone } from "./timezone.js";
|
|
9
9
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
10
|
-
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type
|
|
10
|
+
export { type AccessBehaviour, type AccessCheck, type AccessError, type AccessReason, type AggregationFunction, type ChartOptions, type ChartResult, type CreateOptions, type DatasourcePermissions, type DatasourceTypeMap, DefaultErrorOverlay, type GetOptions, type JsonValue, type ListOptions, type ListResult, type MetricOptions, type MetricResult, type Permissions, type RecordFieldsFor, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrUser, type SoftrUserGroupId, type SoftrUserGroups, type UpdateOptions, type User, type VisibilityHandle, defineVisibility, getSoftrContext, initialSoftrContext, isAccessError, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, q, softrAuthMiddleware, useCurrentUser, usePermissions, useUserTimezone };
|
package/dist/types.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ interface MetricResult {
|
|
|
90
90
|
value: number | null;
|
|
91
91
|
executionTimestamp: Date;
|
|
92
92
|
}
|
|
93
|
-
interface
|
|
93
|
+
interface ChartOptions {
|
|
94
94
|
select?: Record<string, string>;
|
|
95
95
|
where?: AnyConstructable;
|
|
96
96
|
/** Field alias (resolved via select) or raw field ID. */
|
|
@@ -103,7 +103,7 @@ interface ChartDataOptions {
|
|
|
103
103
|
/** Override the timezone for this request. */
|
|
104
104
|
timeZone?: string;
|
|
105
105
|
}
|
|
106
|
-
interface
|
|
106
|
+
interface ChartResult {
|
|
107
107
|
data: Record<string, JsonValue>[];
|
|
108
108
|
executionTimestamp: Date;
|
|
109
109
|
}
|
|
@@ -125,4 +125,4 @@ interface UpdateOptions {
|
|
|
125
125
|
data: Record<string, JsonValue>;
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
|
-
export { AggregationFunction,
|
|
128
|
+
export { AggregationFunction, ChartOptions, ChartResult, CreateOptions, DatasourcePermissions, DatasourceTypeMap, GetOptions, JsonValue, ListOptions, ListResult, MetricOptions, MetricResult, RecordFieldsFor, SoftrMe, SoftrPermissions, SoftrRecord, SoftrUser, UpdateOptions };
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import { Manifest, ManifestNode } from "./routes/manifest.js";
|
|
2
1
|
import { PluginOption } from "vite";
|
|
3
2
|
|
|
4
3
|
//#region src/vite.d.ts
|
|
5
|
-
|
|
6
|
-
/** Writes `<root>/dist/routes.json` and returns its path. Throws on a rule it cannot read. */
|
|
7
|
-
declare function generateManifest(root: string): Promise<string>;
|
|
8
|
-
/**
|
|
9
|
-
* Everything Softr adds to an app's Vite config: the build-time routes manifest, plus the studio
|
|
10
|
-
* edit engine when the pod supplies one.
|
|
11
|
-
*/
|
|
4
|
+
/** Everything Softr adds to an app's Vite config: the studio edit engine, when the pod supplies one. */
|
|
12
5
|
declare function softr(): PluginOption[];
|
|
13
6
|
//#endregion
|
|
14
|
-
export {
|
|
7
|
+
export { softr };
|
package/dist/vite.js
CHANGED
|
@@ -1,75 +1,10 @@
|
|
|
1
|
-
import { ManifestError, generateRoutesManifest } from "./routes/manifest.js";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
1
|
import path from "node:path";
|
|
4
2
|
import { pathToFileURL } from "node:url";
|
|
5
3
|
|
|
6
4
|
//#region src/vite.ts
|
|
7
|
-
|
|
8
|
-
async function renderManifest(root) {
|
|
9
|
-
try {
|
|
10
|
-
return await generateRoutesManifest(root);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
if (error instanceof ManifestError) {
|
|
13
|
-
const { filePath, line, column, message } = error.detail;
|
|
14
|
-
throw new Error(`[softr] ${filePath}:${line}:${column} — ${message}`, { cause: error });
|
|
15
|
-
}
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async function writeManifest(root, contents) {
|
|
20
|
-
const target = path.resolve(root, MANIFEST_RELATIVE_PATH);
|
|
21
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
22
|
-
await writeFile(target, contents, "utf8");
|
|
23
|
-
return target;
|
|
24
|
-
}
|
|
25
|
-
/** Writes `<root>/dist/routes.json` and returns its path. Throws on a rule it cannot read. */
|
|
26
|
-
async function generateManifest(root) {
|
|
27
|
-
return writeManifest(root, await renderManifest(root));
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Everything Softr adds to an app's Vite config: the build-time routes manifest, plus the studio
|
|
31
|
-
* edit engine when the pod supplies one.
|
|
32
|
-
*/
|
|
5
|
+
/** Everything Softr adds to an app's Vite config: the studio edit engine, when the pod supplies one. */
|
|
33
6
|
function softr() {
|
|
34
|
-
return [
|
|
35
|
-
routesManifest(),
|
|
36
|
-
previewIdentity(),
|
|
37
|
-
editEngine()
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Signing the preview in writes the cookie this app's own session code reads back, so it lives here
|
|
42
|
-
* rather than in the edit engine — one implementation of the format, no version to keep in step.
|
|
43
|
-
*
|
|
44
|
-
* `SOFTR_APPLICATION_ID` gates it: the pod sets it, a developer's machine has none.
|
|
45
|
-
*/
|
|
46
|
-
async function previewIdentity() {
|
|
47
|
-
if (!process.env.SOFTR_APPLICATION_ID) return false;
|
|
48
|
-
const { PREVIEW_REDEMPTION_PATH, PREVIEW_SIGNED_IN_PATH, redeemGrant, reportSignedIn } = await import("./preview-identity/preview-identity.js");
|
|
49
|
-
return {
|
|
50
|
-
name: "softr-preview-identity",
|
|
51
|
-
apply: "serve",
|
|
52
|
-
configureServer(server) {
|
|
53
|
-
server.middlewares.use(PREVIEW_REDEMPTION_PATH, redeemGrant);
|
|
54
|
-
server.middlewares.use(PREVIEW_SIGNED_IN_PATH, reportSignedIn);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/** Plugin order does not matter: this puts nothing into the module graph. */
|
|
59
|
-
function routesManifest() {
|
|
60
|
-
let root = process.cwd();
|
|
61
|
-
let rendered = null;
|
|
62
|
-
return {
|
|
63
|
-
name: "softr",
|
|
64
|
-
apply: "build",
|
|
65
|
-
configResolved(config) {
|
|
66
|
-
root = config.root;
|
|
67
|
-
},
|
|
68
|
-
async closeBundle() {
|
|
69
|
-
rendered ??= renderManifest(root);
|
|
70
|
-
await writeManifest(root, await rendered);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
7
|
+
return [editEngine()];
|
|
73
8
|
}
|
|
74
9
|
/**
|
|
75
10
|
* The edit engine is provided by the pod runtime rather than an app dependency: it is never in the
|
|
@@ -91,4 +26,4 @@ async function editEngine() {
|
|
|
91
26
|
}
|
|
92
27
|
|
|
93
28
|
//#endregion
|
|
94
|
-
export {
|
|
29
|
+
export { softr };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softr/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
@@ -16,17 +16,12 @@
|
|
|
16
16
|
"typecheck": "tsc --noEmit"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@babel/parser": "^7.28.0",
|
|
20
|
-
"@babel/traverse": "^7.28.0",
|
|
21
|
-
"@babel/types": "^7.28.0",
|
|
22
|
-
"@tanstack/router-generator": "^1.167.0",
|
|
23
19
|
"zod": "^4.4.3"
|
|
24
20
|
},
|
|
25
21
|
"devDependencies": {
|
|
26
22
|
"@tanstack/intent": "^0.3.6",
|
|
27
23
|
"@tanstack/react-router": "^1.168.0",
|
|
28
24
|
"@tanstack/react-start": "^1.168.0",
|
|
29
|
-
"@types/babel__traverse": "^7.28.0",
|
|
30
25
|
"@types/node": "^24.3.0",
|
|
31
26
|
"tsdown": "^0.15.0",
|
|
32
27
|
"typescript": "^5.9.2",
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.24
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
# Softr SDK core
|
|
@@ -21,7 +21,7 @@ metadata:
|
|
|
21
21
|
> **CRITICAL**: The SDK is split across four entrypoints and the split is enforced. `ds` and `auth` exist only on `@softr/sdk/server` and only run inside a `createServerFn` handler. Importing them anywhere else fails the build.
|
|
22
22
|
> **CRITICAL**: Every route file must declare its audience with `defineVisibility`. There is no default and nothing is inherited from a sibling. A route file without a declaration is a lint error.
|
|
23
23
|
> **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
|
|
24
|
-
> **CRITICAL**: `
|
|
24
|
+
> **CRITICAL**: `workflow.trigger` is exploratory. It exists on the entrypoints below and typechecks, but its endpoint is not final. Do not build a feature on it. Tell the user the capability is not ready and ask how they want to proceed.
|
|
25
25
|
|
|
26
26
|
Each sub-skill stands alone for its task and repeats the boundary above, so most work needs **one** sub-skill and not this file. Load this one when you are unsure which sub-skill applies, or when the wiring below is what you are touching. Add a second sub-skill only when the work genuinely crosses that boundary: a page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
|
|
27
27
|
|
|
@@ -124,4 +124,4 @@ type User = {
|
|
|
124
124
|
|
|
125
125
|
## Version
|
|
126
126
|
|
|
127
|
-
Targets `@softr/sdk` v0.0.
|
|
127
|
+
Targets `@softr/sdk` v0.0.24.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.24
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.24
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
metadata:
|
|
9
9
|
type: sub-skill
|
|
10
10
|
library: softr-sdk
|
|
11
|
-
library_version: 0.0.
|
|
11
|
+
library_version: 0.0.24
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -118,7 +118,7 @@ Stamping a record with who created it, or scoping an update to the person making
|
|
|
118
118
|
|
|
119
119
|
**A datasource id built from a name.** `'ds_projects'`, the table id, or the table's name glued to a uuid are not datasource ids. The id is the opaque UUID assigned when the table was registered on the application; copy it from there. Anything else fails at request time as not found or forbidden.
|
|
120
120
|
|
|
121
|
-
**A datasource id stored in a variable.**
|
|
121
|
+
**A datasource id stored in a variable.** Pass the UUID as a string literal directly at the call site. The lint rule `@softr/ds-no-computed-id` catches this at development time.
|
|
122
122
|
|
|
123
123
|
**A write that sends `fields`.** The option is `data`. Only typecheck catches this, so it survives any run that does not typecheck.
|
|
124
124
|
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.24
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
@@ -29,13 +29,13 @@ import { q } from '@softr/sdk'
|
|
|
29
29
|
|
|
30
30
|
Every `ds` call names its datasource by an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is not the table id, and it is never derived from the table's name — `'ds_posts'` or a name glued to a uuid fails at request time as not found or forbidden.
|
|
31
31
|
|
|
32
|
-
**The datasource id must be a string literal at the call site.** Do not assign it to a variable and pass the variable
|
|
32
|
+
**The datasource id must be a string literal at the call site.** Do not assign it to a variable and pass the variable. Write the UUID directly as the first argument:
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
35
|
// correct
|
|
36
36
|
ds.list('8ffb4814-b028-4496-b679-7face2167ebc', { ... })
|
|
37
37
|
|
|
38
|
-
// wrong
|
|
38
|
+
// wrong
|
|
39
39
|
const POSTS = '8ffb4814-b028-4496-b679-7face2167ebc'
|
|
40
40
|
ds.list(POSTS, { ... })
|
|
41
41
|
```
|
|
@@ -7,19 +7,19 @@ description: >-
|
|
|
7
7
|
per-reason overrides, the redirect and throw strategies, rendering an access
|
|
8
8
|
error with isAccessError, isUnauthenticatedError, isForbiddenError and
|
|
9
9
|
isAlreadySignedInError, guarding a layout, and the lint rule that requires a
|
|
10
|
-
declaration alongside the
|
|
10
|
+
declaration alongside the rules that reject a duplicate, computed or
|
|
11
11
|
root-level one.
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.24
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `defineVisibility`, the error predicates and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
|
|
19
19
|
|
|
20
20
|
# Route visibility
|
|
21
21
|
|
|
22
|
-
Every route file in a Softr app declares who may see it.
|
|
22
|
+
Every route file in a Softr app declares who may see it. Softr reads that declaration out of the source text without executing it and publishes it to Softr Studio, which shows the app owner every route and its audience. That is why the rules below are strict: a rule Softr cannot read is a rule Studio cannot show.
|
|
23
23
|
|
|
24
24
|
## Setup
|
|
25
25
|
|
|
@@ -43,11 +43,11 @@ function Billing() {
|
|
|
43
43
|
|
|
44
44
|
One `defineVisibility` call per file, at module scope, with an inline object literal as its argument.
|
|
45
45
|
|
|
46
|
-
Three rules about that shape, because
|
|
46
|
+
Three rules about that shape, because Softr reads the file rather than running it:
|
|
47
47
|
|
|
48
48
|
- The call must initialise a top-level `const`.
|
|
49
49
|
- The import must be a direct named import from `@softr/sdk`.
|
|
50
|
-
- Never declare on `__root.tsx`. The root cannot carry an audience, so
|
|
50
|
+
- Never declare on `__root.tsx`. The root cannot carry an audience, so a declaration there would gate nothing while looking like it does. Softr rejects it. Put the rule on a layout below the root instead.
|
|
51
51
|
|
|
52
52
|
The four rules:
|
|
53
53
|
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { makeSessionClearingCookies, makeSessionTokenCookie, sessionTokenCookieName } from "../auth/session.js";
|
|
2
|
-
|
|
3
|
-
//#region src/preview-identity/preview-identity.ts
|
|
4
|
-
const PREVIEW_REDEMPTION_PATH = "/__preview/identity";
|
|
5
|
-
const PREVIEW_SIGNED_IN_PATH = "/__preview/signed-in";
|
|
6
|
-
/** The grant in the URL is the only credential: studio-api authorized the builder before issuing it. */
|
|
7
|
-
const redeemGrant = async (req, res) => {
|
|
8
|
-
res.setHeader("Cache-Control", "no-store");
|
|
9
|
-
res.setHeader("Referrer-Policy", "no-referrer");
|
|
10
|
-
if (req.method !== "GET") {
|
|
11
|
-
sendJson(res, 405, { error: `${PREVIEW_REDEMPTION_PATH} only supports GET` });
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const grant = searchParamsOf(req).get("grant");
|
|
15
|
-
if (!grant) {
|
|
16
|
-
sendJson(res, 400, { error: "no grant" });
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const landing = landingPath(searchParamsOf(req).get("next"));
|
|
20
|
-
let session;
|
|
21
|
-
try {
|
|
22
|
-
session = await redeemPreviewGrant(req, grant);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
sendJson(res, statusOf(error), { error: messageOf(error) });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const secure = requestIsHttps(req);
|
|
28
|
-
res.statusCode = 302;
|
|
29
|
-
res.setHeader("Location", landing);
|
|
30
|
-
res.setHeader("Set-Cookie", session.anonymous ? makeSessionClearingCookies(secure) : makeSessionTokenCookie(session.jwt, secure));
|
|
31
|
-
res.end();
|
|
32
|
-
};
|
|
33
|
-
/** Presence only, never decoded: decoding would put an app user's identity on a route Studio reads. */
|
|
34
|
-
const reportSignedIn = (req, res) => {
|
|
35
|
-
res.setHeader("Cache-Control", "no-store");
|
|
36
|
-
if (req.method !== "GET") {
|
|
37
|
-
sendJson(res, 405, { error: `${PREVIEW_SIGNED_IN_PATH} only supports GET` });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
sendJson(res, 200, { signedIn: hasSessionCookie(req.headers.cookie, sessionTokenCookieName(requestIsHttps(req))) });
|
|
41
|
-
};
|
|
42
|
-
var RedemptionError = class extends Error {
|
|
43
|
-
status;
|
|
44
|
-
constructor(status, message) {
|
|
45
|
-
super(message);
|
|
46
|
-
this.status = status;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
function statusOf(error) {
|
|
50
|
-
return error instanceof RedemptionError ? error.status : 503;
|
|
51
|
-
}
|
|
52
|
-
function messageOf(error) {
|
|
53
|
-
return error instanceof Error ? error.message : String(error);
|
|
54
|
-
}
|
|
55
|
-
async function redeemPreviewGrant(req, grant) {
|
|
56
|
-
const applicationId = process.env.SOFTR_APPLICATION_ID;
|
|
57
|
-
const studioApiUrl = process.env.SOFTR_STUDIO_API_URL?.trim() || studioApiUrlFor(req);
|
|
58
|
-
if (!applicationId || !studioApiUrl) throw new RedemptionError(503, "studio-api is unavailable");
|
|
59
|
-
let response;
|
|
60
|
-
try {
|
|
61
|
-
response = await fetch(`${studioApiUrl}/v1/applications/${encodeURIComponent(applicationId)}/vibe-coding/preview-identity/redeem`, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: { "content-type": "application/json" },
|
|
64
|
-
body: JSON.stringify({ grant })
|
|
65
|
-
});
|
|
66
|
-
} catch {
|
|
67
|
-
throw new RedemptionError(503, "studio-api is unreachable");
|
|
68
|
-
}
|
|
69
|
-
if (!response.ok) throw new RedemptionError(response.status, "studio-api refused the preview identity grant");
|
|
70
|
-
const body = await response.json().catch(() => null);
|
|
71
|
-
if (typeof body !== "object" || body === null) throw new RedemptionError(503, "studio-api returned no session");
|
|
72
|
-
const { anonymous, jwt } = body;
|
|
73
|
-
if (anonymous === true) return { anonymous: true };
|
|
74
|
-
if (typeof jwt !== "string" || jwt.length === 0) throw new RedemptionError(503, "studio-api returned no token");
|
|
75
|
-
return {
|
|
76
|
-
anonymous: false,
|
|
77
|
-
jwt
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const STAGING_HOST_PATTERN = /(?:^|\.)staging(\d+)\.softr\.io$/;
|
|
81
|
-
/** The host is the only per-environment signal the app has. Anything but a staging one gives null. */
|
|
82
|
-
function studioApiUrlFor(req) {
|
|
83
|
-
const forwarded = req.headers["x-forwarded-host"];
|
|
84
|
-
const name = ((Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host ?? "").split(",")[0].trim().split(":")[0];
|
|
85
|
-
const digits = STAGING_HOST_PATTERN.exec(name)?.[1];
|
|
86
|
-
if (!digits) return null;
|
|
87
|
-
return `https://studio-api-staging${digits.padStart(2, "0")}.softr.io`;
|
|
88
|
-
}
|
|
89
|
-
function hasSessionCookie(header, name) {
|
|
90
|
-
if (!header) return false;
|
|
91
|
-
return header.split(";").some((pair) => {
|
|
92
|
-
const separator = pair.indexOf("=");
|
|
93
|
-
return separator !== -1 && pair.slice(0, separator).trim() === name;
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Exactly as strict as h3's `getRequestProtocol`, which reads the cookie back. Being more generous
|
|
98
|
-
* writes `__Host-session_token` where the app looks up `session_token`, and the visitor is silently
|
|
99
|
-
* anonymous with no error on either side.
|
|
100
|
-
*/
|
|
101
|
-
function requestIsHttps(req) {
|
|
102
|
-
const forwarded = req.headers["x-forwarded-proto"];
|
|
103
|
-
if (forwarded === "https") return true;
|
|
104
|
-
if (forwarded === "http") return false;
|
|
105
|
-
return Boolean(req.socket?.encrypted);
|
|
106
|
-
}
|
|
107
|
-
function landingPath(next) {
|
|
108
|
-
if (!next || !next.startsWith("/") || next.startsWith("//") || next.includes("\\") || next.includes("://") || /[\u0000-\u001f\u007f]/.test(next)) return "/";
|
|
109
|
-
return next;
|
|
110
|
-
}
|
|
111
|
-
function searchParamsOf(req) {
|
|
112
|
-
return new URL(req.url ?? "", "http://request.invalid").searchParams;
|
|
113
|
-
}
|
|
114
|
-
function sendJson(res, status, body) {
|
|
115
|
-
res.statusCode = status;
|
|
116
|
-
res.setHeader("Content-Type", "application/json");
|
|
117
|
-
res.end(JSON.stringify(body));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
export { PREVIEW_REDEMPTION_PATH, PREVIEW_SIGNED_IN_PATH, redeemGrant, reportSignedIn };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import "@tanstack/router-generator";
|
|
2
|
-
|
|
3
|
-
//#region src/routes/discover.d.ts
|
|
4
|
-
|
|
5
|
-
type DatasourceMethod = "list" | "get" | "create" | "update" | "delete" | "metric" | "chartData";
|
|
6
|
-
type DatasourceAccess = {
|
|
7
|
-
id: string;
|
|
8
|
-
methods: Array<DatasourceMethod>;
|
|
9
|
-
};
|
|
10
|
-
type VisibilityValue = string | Array<VisibilityValue> | {
|
|
11
|
-
[key: string]: VisibilityValue;
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
export { DatasourceAccess, VisibilityValue };
|
package/dist/routes/discover.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { createReadOnlyFs } from "./mem-fs.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Generator, getConfig } from "@tanstack/router-generator";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { parse } from "@babel/parser";
|
|
7
|
-
import babelTraverse from "@babel/traverse";
|
|
8
|
-
|
|
9
|
-
//#region src/routes/discover.ts
|
|
10
|
-
async function crawlRoutes(opts) {
|
|
11
|
-
const config = getConfig({
|
|
12
|
-
disableLogging: true,
|
|
13
|
-
tmpDir: TMP_DIR,
|
|
14
|
-
generatedRouteTree: generatedRouteTreePath()
|
|
15
|
-
}, opts.root);
|
|
16
|
-
const generator = new Generator({
|
|
17
|
-
config,
|
|
18
|
-
root: opts.root,
|
|
19
|
-
fs: createReadOnlyFs()
|
|
20
|
-
});
|
|
21
|
-
await generator.run();
|
|
22
|
-
const crawlingResult = await generator.getCrawlingResult();
|
|
23
|
-
if (!crawlingResult) throw new Error(`the route crawl produced no result for ${opts.root}`);
|
|
24
|
-
return {
|
|
25
|
-
rootRouteNode: crawlingResult.rootRouteNode,
|
|
26
|
-
routeNodes: crawlingResult.acc.routeNodes,
|
|
27
|
-
routePiecesByPath: crawlingResult.acc.routePiecesByPath,
|
|
28
|
-
routeTree: crawlingResult.acc.routeTree,
|
|
29
|
-
routesDirectory: path.relative(opts.root, path.resolve(opts.root, config.routesDirectory)).split(path.sep).join("/")
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/** Reads the rule out of the source text. The route file is never executed. */
|
|
33
|
-
function extractVisibility(source, filePath) {
|
|
34
|
-
let program;
|
|
35
|
-
try {
|
|
36
|
-
program = parse(source, {
|
|
37
|
-
sourceType: "module",
|
|
38
|
-
plugins: ["typescript", "jsx"]
|
|
39
|
-
});
|
|
40
|
-
} catch (error) {
|
|
41
|
-
const position = error.loc;
|
|
42
|
-
return fail("unparsable", `could not parse ${filePath}: ${error.message}`, filePath, position?.line ?? 1, position?.column ?? 0);
|
|
43
|
-
}
|
|
44
|
-
const calls = findVisibilityCalls(program);
|
|
45
|
-
if (calls.length > 1) {
|
|
46
|
-
const second = calls[1].call;
|
|
47
|
-
return fail("duplicate-declaration", `${filePath} declares visibility more than once; a route has exactly one rule`, filePath, second.loc?.start.line ?? 1, second.loc?.start.column ?? 0);
|
|
48
|
-
}
|
|
49
|
-
const declaration = calls[0];
|
|
50
|
-
if (declaration === void 0) return {
|
|
51
|
-
ok: true,
|
|
52
|
-
visibility: null
|
|
53
|
-
};
|
|
54
|
-
const { call, statement } = declaration;
|
|
55
|
-
const argument = call.arguments[0];
|
|
56
|
-
if (argument === void 0) return fail("not-literal", `${filePath} calls ${VISIBILITY_FUNCTION} with no argument`, filePath, call.loc?.start.line ?? 1, call.loc?.start.column ?? 0);
|
|
57
|
-
const read = readLiteral(argument);
|
|
58
|
-
if (!read.ok) return fail("not-literal", `${filePath}: the visibility rule must be a literal — ${read.reason}`, filePath, read.line, read.column);
|
|
59
|
-
return {
|
|
60
|
-
ok: true,
|
|
61
|
-
visibility: {
|
|
62
|
-
value: read.value,
|
|
63
|
-
line: statement.loc?.start.line ?? 1,
|
|
64
|
-
column: statement.loc?.start.column ?? 0
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/** Reads all ds.method() calls from the source text. The route file is never executed. */
|
|
69
|
-
function extractDatasources(source, filePath) {
|
|
70
|
-
let program;
|
|
71
|
-
try {
|
|
72
|
-
program = parse(source, {
|
|
73
|
-
sourceType: "module",
|
|
74
|
-
plugins: ["typescript", "jsx"]
|
|
75
|
-
});
|
|
76
|
-
} catch (error) {
|
|
77
|
-
const position = error.loc;
|
|
78
|
-
return {
|
|
79
|
-
ok: false,
|
|
80
|
-
error: {
|
|
81
|
-
code: "unparsable",
|
|
82
|
-
message: `could not parse ${filePath}: ${error.message}`,
|
|
83
|
-
filePath,
|
|
84
|
-
line: position?.line ?? 1,
|
|
85
|
-
column: position?.column ?? 0
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const raw = findDatasourceCalls(program, filePath);
|
|
90
|
-
if (!raw.ok) return raw;
|
|
91
|
-
const byId = /* @__PURE__ */ new Map();
|
|
92
|
-
for (const { id, method } of raw.calls) {
|
|
93
|
-
if (!byId.has(id)) byId.set(id, /* @__PURE__ */ new Set());
|
|
94
|
-
byId.get(id).add(method);
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
ok: true,
|
|
98
|
-
datasources: Array.from(byId.entries()).map(([id, methods]) => ({
|
|
99
|
-
id,
|
|
100
|
-
methods: [...methods]
|
|
101
|
-
}))
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* The generator `mkdirSync`s `tmpDir` and the directory of `generatedRouteTree` outside the `fs`
|
|
106
|
-
* shim, so defaulted they create directories in the crawled project. Aimed at `os.tmpdir()` both
|
|
107
|
-
* calls are no-ops, and nothing is ever written through either path. The uuid keeps the generated
|
|
108
|
-
* tree file a miss rather than whatever a shared temp directory happens to hold.
|
|
109
|
-
*/
|
|
110
|
-
const TMP_DIR = os.tmpdir();
|
|
111
|
-
const generatedRouteTreePath = () => path.join(TMP_DIR, `softr-sdk-route-tree-${randomUUID()}.gen.ts`);
|
|
112
|
-
const SDK_PACKAGE_NAME = "@softr/sdk";
|
|
113
|
-
const SDK_SERVER_PACKAGE_NAME = "@softr/sdk/server";
|
|
114
|
-
const VISIBILITY_FUNCTION = "defineVisibility";
|
|
115
|
-
const DS_BINDING = "ds";
|
|
116
|
-
const DS_METHODS = new Set([
|
|
117
|
-
"list",
|
|
118
|
-
"get",
|
|
119
|
-
"create",
|
|
120
|
-
"update",
|
|
121
|
-
"delete",
|
|
122
|
-
"metric",
|
|
123
|
-
"chartData"
|
|
124
|
-
]);
|
|
125
|
-
function fail(code, message, filePath, line, column) {
|
|
126
|
-
return {
|
|
127
|
-
ok: false,
|
|
128
|
-
error: {
|
|
129
|
-
code,
|
|
130
|
-
message,
|
|
131
|
-
filePath,
|
|
132
|
-
line,
|
|
133
|
-
column
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function findDatasourceCalls(ast, filePath) {
|
|
138
|
-
const calls = [];
|
|
139
|
-
let error = null;
|
|
140
|
-
traverse(ast, { CallExpression(path$1) {
|
|
141
|
-
const { node } = path$1;
|
|
142
|
-
const { callee } = node;
|
|
143
|
-
if (callee.type !== "MemberExpression") return;
|
|
144
|
-
if (callee.computed) return;
|
|
145
|
-
if (callee.object.type !== "Identifier") return;
|
|
146
|
-
if (callee.property.type !== "Identifier") return;
|
|
147
|
-
const method = callee.property.name;
|
|
148
|
-
if (!DS_METHODS.has(method)) return;
|
|
149
|
-
if (!isDsImport(path$1, callee.object.name)) return;
|
|
150
|
-
const firstArg = node.arguments[0];
|
|
151
|
-
if (firstArg === void 0 || firstArg.type === "SpreadElement") {
|
|
152
|
-
error = {
|
|
153
|
-
code: "not-literal",
|
|
154
|
-
message: `${filePath}: ds.${method}() must be called with a string literal datasource id`,
|
|
155
|
-
filePath,
|
|
156
|
-
line: node.loc?.start.line ?? 1,
|
|
157
|
-
column: node.loc?.start.column ?? 0
|
|
158
|
-
};
|
|
159
|
-
path$1.stop();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (firstArg.type !== "StringLiteral") {
|
|
163
|
-
error = {
|
|
164
|
-
code: "not-literal",
|
|
165
|
-
message: `${filePath}: the datasource id passed to ds.${method}() must be a string literal`,
|
|
166
|
-
filePath,
|
|
167
|
-
line: firstArg.loc?.start.line ?? 1,
|
|
168
|
-
column: firstArg.loc?.start.column ?? 0
|
|
169
|
-
};
|
|
170
|
-
path$1.stop();
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
calls.push({
|
|
174
|
-
id: firstArg.value,
|
|
175
|
-
method
|
|
176
|
-
});
|
|
177
|
-
} });
|
|
178
|
-
if (error !== null) return {
|
|
179
|
-
ok: false,
|
|
180
|
-
error
|
|
181
|
-
};
|
|
182
|
-
return {
|
|
183
|
-
ok: true,
|
|
184
|
-
calls
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function isDsImport(path$1, name) {
|
|
188
|
-
const binding = path$1.scope.getBinding(name);
|
|
189
|
-
if (binding === void 0 || binding.kind !== "module") return false;
|
|
190
|
-
const specifier = binding.path.node;
|
|
191
|
-
const declaration = binding.path.parent;
|
|
192
|
-
if (specifier.type !== "ImportSpecifier" || declaration.type !== "ImportDeclaration") return false;
|
|
193
|
-
if (specifier.importKind === "type" || declaration.importKind === "type") return false;
|
|
194
|
-
if (declaration.source.value !== SDK_SERVER_PACKAGE_NAME) return false;
|
|
195
|
-
return (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === DS_BINDING;
|
|
196
|
-
}
|
|
197
|
-
const traverse = babelTraverse.default ?? babelTraverse;
|
|
198
|
-
/** Only a call initialising a top-level variable counts; one inside a function or block does not. */
|
|
199
|
-
function findVisibilityCalls(ast) {
|
|
200
|
-
const found = [];
|
|
201
|
-
traverse(ast, { VariableDeclarator(path$1) {
|
|
202
|
-
const list = path$1.parentPath;
|
|
203
|
-
if (!list.isVariableDeclaration()) return;
|
|
204
|
-
const statement = list.parentPath.isExportNamedDeclaration() ? list.parentPath : list;
|
|
205
|
-
if (!statement.parentPath?.isProgram()) return;
|
|
206
|
-
if (path$1.node.init === null || path$1.node.init === void 0) return;
|
|
207
|
-
const call = unwrap(path$1.node.init);
|
|
208
|
-
if (call.type !== "CallExpression" && call.type !== "OptionalCallExpression") return;
|
|
209
|
-
if (call.callee.type !== "Identifier") return;
|
|
210
|
-
if (!isVisibilityImport(path$1, call.callee.name)) return;
|
|
211
|
-
found.push({
|
|
212
|
-
call,
|
|
213
|
-
statement: statement.node
|
|
214
|
-
});
|
|
215
|
-
} });
|
|
216
|
-
return found;
|
|
217
|
-
}
|
|
218
|
-
function isVisibilityImport(path$1, name) {
|
|
219
|
-
const binding = path$1.scope.getBinding(name);
|
|
220
|
-
if (binding === void 0 || binding.kind !== "module") return false;
|
|
221
|
-
const specifier = binding.path.node;
|
|
222
|
-
const declaration = binding.path.parent;
|
|
223
|
-
if (specifier.type !== "ImportSpecifier" || declaration.type !== "ImportDeclaration") return false;
|
|
224
|
-
if (specifier.importKind === "type" || declaration.importKind === "type") return false;
|
|
225
|
-
if (declaration.source.value !== SDK_PACKAGE_NAME) return false;
|
|
226
|
-
return (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === VISIBILITY_FUNCTION;
|
|
227
|
-
}
|
|
228
|
-
/** Strips the wrappers that leave the runtime value unchanged: `as const`, `satisfies`, `!`, `()`. */
|
|
229
|
-
function unwrap(node) {
|
|
230
|
-
switch (node.type) {
|
|
231
|
-
case "TSAsExpression":
|
|
232
|
-
case "TSSatisfiesExpression":
|
|
233
|
-
case "TSNonNullExpression":
|
|
234
|
-
case "TSTypeAssertion":
|
|
235
|
-
case "TSInstantiationExpression":
|
|
236
|
-
case "ParenthesizedExpression": return unwrap(node.expression);
|
|
237
|
-
default: return node;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function reject(node, reason) {
|
|
241
|
-
return {
|
|
242
|
-
ok: false,
|
|
243
|
-
reason,
|
|
244
|
-
line: node.loc?.start.line ?? 1,
|
|
245
|
-
column: node.loc?.start.column ?? 0
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
function readLiteral(input) {
|
|
249
|
-
const node = unwrap(input);
|
|
250
|
-
switch (node.type) {
|
|
251
|
-
case "StringLiteral": return {
|
|
252
|
-
ok: true,
|
|
253
|
-
value: node.value
|
|
254
|
-
};
|
|
255
|
-
case "TemplateLiteral":
|
|
256
|
-
if (node.expressions.length > 0) return reject(node, "a template literal with an expression is not a static value");
|
|
257
|
-
return {
|
|
258
|
-
ok: true,
|
|
259
|
-
value: node.quasis[0]?.value.cooked ?? ""
|
|
260
|
-
};
|
|
261
|
-
case "ArrayExpression": {
|
|
262
|
-
const values = [];
|
|
263
|
-
for (const element of node.elements) {
|
|
264
|
-
if (element === null) return reject(node, "an array hole is not a static value");
|
|
265
|
-
if (element.type === "SpreadElement") return reject(element, "a spread is not a static value");
|
|
266
|
-
const read = readLiteral(element);
|
|
267
|
-
if (!read.ok) return read;
|
|
268
|
-
values.push(read.value);
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
ok: true,
|
|
272
|
-
value: values
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
case "ObjectExpression": {
|
|
276
|
-
const value = {};
|
|
277
|
-
for (const property of node.properties) {
|
|
278
|
-
if (property.type === "SpreadElement") return reject(property, "a spread is not a static value");
|
|
279
|
-
if (property.type === "ObjectMethod") return reject(property, "a method is not a static value");
|
|
280
|
-
if (property.computed) return reject(property, "a computed key is not a static value");
|
|
281
|
-
if (property.shorthand) return reject(property, "a shorthand property is not a static value");
|
|
282
|
-
const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "StringLiteral" ? property.key.value : null;
|
|
283
|
-
if (key === null) return reject(property.key, "this key is not a static value");
|
|
284
|
-
if (key === "__proto__") return reject(property.key, "a __proto__ key is not a static value");
|
|
285
|
-
const read = readLiteral(property.value);
|
|
286
|
-
if (!read.ok) return read;
|
|
287
|
-
value[key] = read.value;
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
ok: true,
|
|
291
|
-
value
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
case "NumericLiteral":
|
|
295
|
-
case "BooleanLiteral":
|
|
296
|
-
case "NullLiteral": return reject(node, "a visibility rule is built from strings, arrays and objects");
|
|
297
|
-
case "Identifier": return reject(node, `an identifier (${node.name}) is not a static value`);
|
|
298
|
-
case "CallExpression":
|
|
299
|
-
case "NewExpression": return reject(node, "a call is not a static value");
|
|
300
|
-
default: return reject(node, `${node.type} is not a static value`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
//#endregion
|
|
305
|
-
export { crawlRoutes, extractDatasources, extractVisibility };
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { DatasourceAccess, VisibilityValue } from "./discover.js";
|
|
2
|
-
|
|
3
|
-
//#region src/routes/manifest.d.ts
|
|
4
|
-
type DatasourceEntry = DatasourceAccess & {
|
|
5
|
-
/** Project-relative path to the file where the datasource is accessed. */
|
|
6
|
-
filename: string;
|
|
7
|
-
/** Route path, if the file is a route file. */
|
|
8
|
-
route?: string;
|
|
9
|
-
};
|
|
10
|
-
type ManifestNode = {
|
|
11
|
-
routePath: string;
|
|
12
|
-
filePath: string;
|
|
13
|
-
_fsRouteType?: string;
|
|
14
|
-
inferFullPath: string | null;
|
|
15
|
-
createFileRouteProps: Array<string>;
|
|
16
|
-
visibility: VisibilityValue | null;
|
|
17
|
-
children: Array<ManifestNode>;
|
|
18
|
-
};
|
|
19
|
-
type Manifest = {
|
|
20
|
-
root: ManifestNode;
|
|
21
|
-
datasources: Array<DatasourceEntry>;
|
|
22
|
-
};
|
|
23
|
-
//#endregion
|
|
24
|
-
export { Manifest, ManifestNode };
|
package/dist/routes/manifest.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { crawlRoutes, extractDatasources, extractVisibility } from "./discover.js";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { inferFullPath } from "@tanstack/router-generator";
|
|
5
|
-
|
|
6
|
-
//#region src/routes/manifest.ts
|
|
7
|
-
var ManifestError = class extends Error {
|
|
8
|
-
detail;
|
|
9
|
-
constructor(detail) {
|
|
10
|
-
super(detail.message);
|
|
11
|
-
this.name = "ManifestError";
|
|
12
|
-
this.detail = detail;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
/** Returns the manifest as the exact text to write. Throws on a declaration it cannot read. */
|
|
16
|
-
async function generateRoutesManifest(root) {
|
|
17
|
-
const crawl = await crawlRoutes({ root });
|
|
18
|
-
const routeFiles = new Set([crawl.rootRouteNode.filePath]);
|
|
19
|
-
for (const node of crawl.routeNodes) routeFiles.add(node.filePath);
|
|
20
|
-
for (const pieces of Object.values(crawl.routePiecesByPath)) if (pieces.lazy) routeFiles.add(pieces.lazy.filePath);
|
|
21
|
-
const sources = /* @__PURE__ */ new Map();
|
|
22
|
-
for (const relative of routeFiles) {
|
|
23
|
-
const absolute = path.resolve(root, crawl.routesDirectory, relative);
|
|
24
|
-
const source = await readIfPresent(absolute);
|
|
25
|
-
if (source !== null) sources.set(absolute, source);
|
|
26
|
-
}
|
|
27
|
-
const manifest = buildManifest({
|
|
28
|
-
crawl,
|
|
29
|
-
root,
|
|
30
|
-
sources
|
|
31
|
-
});
|
|
32
|
-
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
33
|
-
}
|
|
34
|
-
function buildManifest(opts) {
|
|
35
|
-
const routesDirectory = opts.crawl.routesDirectory;
|
|
36
|
-
const datasources = [];
|
|
37
|
-
const toNode = (node) => {
|
|
38
|
-
const projectRelative = toProjectRelative(opts.root, routesDirectory, node.filePath);
|
|
39
|
-
const routePath = node.routePath;
|
|
40
|
-
if (routePath === void 0) throw new Error(`the route crawl produced a node with no route path: ${projectRelative}`);
|
|
41
|
-
let declared = visibilityOf(opts, node.filePath);
|
|
42
|
-
datasources.push(...datasourceEntriesOf(opts, node.filePath, routePath));
|
|
43
|
-
const lazyPiece = opts.crawl.routePiecesByPath[routePath]?.lazy;
|
|
44
|
-
if (lazyPiece && lazyPiece.filePath !== node.filePath) {
|
|
45
|
-
const lazyDeclared = visibilityOf(opts, lazyPiece.filePath);
|
|
46
|
-
if (lazyDeclared !== null && declared !== null) throw new ManifestError({
|
|
47
|
-
code: "duplicate-declaration",
|
|
48
|
-
message: `${routePath} declares visibility in both ${projectRelative} and ${lazyDeclared.filePath}; a route has exactly one rule`,
|
|
49
|
-
filePath: lazyDeclared.filePath,
|
|
50
|
-
line: lazyDeclared.line,
|
|
51
|
-
column: lazyDeclared.column
|
|
52
|
-
});
|
|
53
|
-
if (lazyDeclared !== null) declared = lazyDeclared;
|
|
54
|
-
datasources.push(...datasourceEntriesOf(opts, lazyPiece.filePath, routePath));
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
routePath,
|
|
58
|
-
filePath: projectRelative,
|
|
59
|
-
...node._fsRouteType ? { _fsRouteType: node._fsRouteType } : {},
|
|
60
|
-
inferFullPath: inferFullPath(node) ?? null,
|
|
61
|
-
createFileRouteProps: [...node.createFileRouteProps ?? []].sort(),
|
|
62
|
-
visibility: declared?.value ?? null,
|
|
63
|
-
children: (node.children ?? []).map(toNode)
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
const rootNode = opts.crawl.rootRouteNode;
|
|
67
|
-
const rootRelative = toProjectRelative(opts.root, routesDirectory, rootNode.filePath);
|
|
68
|
-
const rootDeclared = visibilityOf(opts, rootNode.filePath);
|
|
69
|
-
if (rootDeclared !== null) throw new ManifestError({
|
|
70
|
-
code: "root-declaration",
|
|
71
|
-
message: "the root route cannot carry a visibility rule: it would gate sign-in with no way back in. Move this declaration to the routes you want gated.",
|
|
72
|
-
filePath: rootRelative,
|
|
73
|
-
line: rootDeclared.line,
|
|
74
|
-
column: rootDeclared.column
|
|
75
|
-
});
|
|
76
|
-
datasources.push(...datasourceEntriesOf(opts, rootNode.filePath, ROOT_ROUTE_ID));
|
|
77
|
-
return {
|
|
78
|
-
root: {
|
|
79
|
-
routePath: ROOT_ROUTE_ID,
|
|
80
|
-
filePath: rootRelative,
|
|
81
|
-
inferFullPath: inferFullPath(rootNode) ?? null,
|
|
82
|
-
createFileRouteProps: [...rootNode.createFileRouteProps ?? []].sort(),
|
|
83
|
-
visibility: null,
|
|
84
|
-
children: opts.crawl.routeTree.map(toNode)
|
|
85
|
-
},
|
|
86
|
-
datasources
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* `rootRouteId` from `@tanstack/router-core`, which is what a consumer matches against — not the
|
|
91
|
-
* generator's `rootPathId`, which is the base name of `__root.tsx` and names a file, not a route.
|
|
92
|
-
*/
|
|
93
|
-
const ROOT_ROUTE_ID = "__root__";
|
|
94
|
-
/** The crawl's paths are relative to the routes directory; the manifest's are project-relative. */
|
|
95
|
-
function toProjectRelative(root, routesDirectory, filePath) {
|
|
96
|
-
const absolute = path.resolve(root, routesDirectory, filePath);
|
|
97
|
-
return path.relative(root, absolute).split(path.sep).join("/");
|
|
98
|
-
}
|
|
99
|
-
const USER_GROUPS = [
|
|
100
|
-
"ALL_USERS",
|
|
101
|
-
"NON_LOGGED_IN_USERS",
|
|
102
|
-
"LOGGED_IN_USERS"
|
|
103
|
-
];
|
|
104
|
-
function checkVisibilityShape(declared) {
|
|
105
|
-
const reject = (message) => {
|
|
106
|
-
throw new ManifestError({
|
|
107
|
-
code: "invalid-visibility",
|
|
108
|
-
message: `${declared.filePath}: ${message}`,
|
|
109
|
-
filePath: declared.filePath,
|
|
110
|
-
line: declared.line,
|
|
111
|
-
column: declared.column
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
const visibility = declared.value;
|
|
115
|
-
if (typeof visibility !== "object" || Array.isArray(visibility)) reject(`the visibility rule must be an object literal like { userGroup: 'ALL_USERS' }, not ${JSON.stringify(visibility)}`);
|
|
116
|
-
for (const key of Object.keys(visibility)) {
|
|
117
|
-
if (key === "userGroup" || key === "userGroupIds") continue;
|
|
118
|
-
reject(`"${key}" is not part of a visibility rule; a rule has userGroup and, under LOGGED_IN_USERS, userGroupIds. Check the spelling.`);
|
|
119
|
-
}
|
|
120
|
-
const userGroup = visibility["userGroup"];
|
|
121
|
-
if (typeof userGroup !== "string" || !USER_GROUPS.includes(userGroup)) reject(`userGroup must be one of ${USER_GROUPS.join(", ")}, not ${JSON.stringify(userGroup)}. A rule the build cannot read is a route with no rule at all.`);
|
|
122
|
-
if (!("userGroupIds" in visibility)) return;
|
|
123
|
-
if (userGroup !== "LOGGED_IN_USERS") reject(`userGroupIds only means anything under LOGGED_IN_USERS, not under ${userGroup}`);
|
|
124
|
-
const ids = visibility["userGroupIds"];
|
|
125
|
-
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) reject(`userGroupIds must be an array of group id strings, not ${JSON.stringify(ids)}. Drop the key entirely to mean "any logged-in user".`);
|
|
126
|
-
}
|
|
127
|
-
function visibilityOf(opts, filePath) {
|
|
128
|
-
const projectRelative = toProjectRelative(opts.root, opts.crawl.routesDirectory, filePath);
|
|
129
|
-
const source = opts.sources.get(path.resolve(opts.root, opts.crawl.routesDirectory, filePath));
|
|
130
|
-
if (source === void 0) return null;
|
|
131
|
-
const result = extractVisibility(source, projectRelative);
|
|
132
|
-
if (!result.ok) throw new ManifestError(result.error);
|
|
133
|
-
if (result.visibility === null) return null;
|
|
134
|
-
const declared = {
|
|
135
|
-
...result.visibility,
|
|
136
|
-
filePath: projectRelative
|
|
137
|
-
};
|
|
138
|
-
checkVisibilityShape(declared);
|
|
139
|
-
return declared;
|
|
140
|
-
}
|
|
141
|
-
function datasourceEntriesOf(opts, filePath, routePath) {
|
|
142
|
-
const projectRelative = toProjectRelative(opts.root, opts.crawl.routesDirectory, filePath);
|
|
143
|
-
const source = opts.sources.get(path.resolve(opts.root, opts.crawl.routesDirectory, filePath));
|
|
144
|
-
if (source === void 0) return [];
|
|
145
|
-
const result = extractDatasources(source, projectRelative);
|
|
146
|
-
if (!result.ok) throw new ManifestError(result.error);
|
|
147
|
-
return result.datasources.map(({ id, methods }) => ({
|
|
148
|
-
id,
|
|
149
|
-
methods,
|
|
150
|
-
filename: projectRelative,
|
|
151
|
-
route: routePath
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Only ENOENT becomes `null`. The caller reads `null` as "nothing declared here", so swallowing any
|
|
156
|
-
* other read error would turn an unreadable declaration into an ungated route.
|
|
157
|
-
*/
|
|
158
|
-
async function readIfPresent(filePath) {
|
|
159
|
-
try {
|
|
160
|
-
return await readFile(filePath, "utf8");
|
|
161
|
-
} catch (error) {
|
|
162
|
-
if (error.code === "ENOENT") return null;
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
//#endregion
|
|
168
|
-
export { ManifestError, generateRoutesManifest };
|
package/dist/routes/mem-fs.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import * as nodeFs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
//#region src/routes/mem-fs.ts
|
|
5
|
-
/**
|
|
6
|
-
* Reads through to disk and swallows every write. Driving the generator without this rewrites the
|
|
7
|
-
* app's route sources and strips Start's `Register` block from its routeTree.gen.ts.
|
|
8
|
-
*/
|
|
9
|
-
function createReadOnlyFs() {
|
|
10
|
-
const writes = /* @__PURE__ */ new Map();
|
|
11
|
-
return {
|
|
12
|
-
async stat(filePath) {
|
|
13
|
-
const stat = await statOrNull(path.resolve(filePath));
|
|
14
|
-
if (stat === null) return {
|
|
15
|
-
mtimeMs: SYNTHETIC_MTIME,
|
|
16
|
-
mode: 420,
|
|
17
|
-
uid: 0,
|
|
18
|
-
gid: 0
|
|
19
|
-
};
|
|
20
|
-
return {
|
|
21
|
-
mtimeMs: stat.mtimeMs,
|
|
22
|
-
mode: Number(stat.mode),
|
|
23
|
-
uid: Number(stat.uid),
|
|
24
|
-
gid: Number(stat.gid)
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
async rename(oldPath, newPath) {
|
|
28
|
-
const from = path.resolve(oldPath);
|
|
29
|
-
const content = writes.get(from);
|
|
30
|
-
if (content !== void 0) {
|
|
31
|
-
writes.delete(from);
|
|
32
|
-
writes.set(path.resolve(newPath), content);
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
async writeFile(filePath, content) {
|
|
36
|
-
writes.set(path.resolve(filePath), content);
|
|
37
|
-
},
|
|
38
|
-
async readFile(filePath) {
|
|
39
|
-
const resolved = path.resolve(filePath);
|
|
40
|
-
const pending = writes.get(resolved);
|
|
41
|
-
const stat = await statOrNull(resolved);
|
|
42
|
-
if (pending !== void 0) return {
|
|
43
|
-
stat: { mtimeMs: stat?.mtimeMs ?? SYNTHETIC_MTIME },
|
|
44
|
-
fileContent: pending
|
|
45
|
-
};
|
|
46
|
-
if (stat === null) return "file-not-existing";
|
|
47
|
-
try {
|
|
48
|
-
const fileContent = await nodeFs.readFile(resolved, "utf8");
|
|
49
|
-
return {
|
|
50
|
-
stat: { mtimeMs: stat.mtimeMs },
|
|
51
|
-
fileContent
|
|
52
|
-
};
|
|
53
|
-
} catch {
|
|
54
|
-
return "file-not-existing";
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
async chmod() {},
|
|
58
|
-
async chown() {}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const SYNTHETIC_MTIME = 1n;
|
|
62
|
-
async function statOrNull(filePath) {
|
|
63
|
-
try {
|
|
64
|
-
return await nodeFs.stat(filePath, { bigint: true });
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
//#endregion
|
|
71
|
-
export { createReadOnlyFs };
|