@softr/sdk 0.0.23 → 0.0.25
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/assertions.js +1 -7
- package/dist/auth/methods/authenticator.d.ts +80 -0
- package/dist/auth/methods/authenticator.js +175 -0
- package/dist/auth/methods/code.d.ts +40 -0
- package/dist/auth/methods/code.js +74 -0
- package/dist/auth/methods/magicLink.d.ts +24 -0
- package/dist/auth/methods/magicLink.js +53 -0
- package/dist/auth/methods/password.d.ts +19 -5
- package/dist/auth/methods/password.js +36 -15
- package/dist/auth/methods/signup.d.ts +22 -14
- package/dist/auth/methods/signup.js +48 -54
- package/dist/auth/methods/social.d.ts +35 -0
- package/dist/auth/methods/social.js +58 -0
- package/dist/auth/methods/utils.d.ts +1 -1
- package/dist/auth/platform.js +185 -7
- package/dist/auth/server.d.ts +15 -0
- package/dist/auth/server.js +16 -1
- package/dist/auth/session.js +39 -20
- 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 +222 -36
- 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
|
@@ -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.25
|
|
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.25
|
|
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.25
|
|
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 };
|