@softr/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +73 -0
- package/dist/tanstack.d.ts +18 -0
- package/dist/tanstack.js +148 -0
- package/dist/types-BLLUI6MI.d.ts +75 -0
- package/dist/vite.d.ts +31 -0
- package/dist/vite.js +449 -0
- package/package.json +38 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { a as ListResult, c as SoftrRecord, d as WorkflowAPI, i as ListOptions, l as SoftrSDK, n as DatasourceAPI, o as SoftrMe, r as DatasourcePermissions, s as SoftrPermissions, t as AuthAPI, u as SoftrUser } from "./types-BLLUI6MI.js";
|
|
2
|
+
|
|
3
|
+
//#region src/routes/visibility.d.ts
|
|
4
|
+
type RouteVisibility = {
|
|
5
|
+
userGroup: "ALL_USERS";
|
|
6
|
+
} | {
|
|
7
|
+
userGroup: "NON_LOGGED_IN_USERS";
|
|
8
|
+
} | {
|
|
9
|
+
userGroup: "LOGGED_IN_USERS";
|
|
10
|
+
userGroupIds?: Array<SoftrUserGroupId>;
|
|
11
|
+
};
|
|
12
|
+
/** Shape of the TanStack route context after the root route's beforeLoad. */
|
|
13
|
+
type SoftrContext = {
|
|
14
|
+
user: SoftrUser | null;
|
|
15
|
+
};
|
|
16
|
+
type RefusalReason = "unauthenticated" | "forbidden" | "alreadySignedIn";
|
|
17
|
+
type RefusalBehaviour = string | {
|
|
18
|
+
strategy: "redirect";
|
|
19
|
+
to: string;
|
|
20
|
+
} | {
|
|
21
|
+
strategy: "throw";
|
|
22
|
+
};
|
|
23
|
+
type GuardOverrides = Partial<Record<RefusalReason, RefusalBehaviour>>;
|
|
24
|
+
type CheckResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
reason: "unauthenticated" | "alreadySignedIn";
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
reason: "forbidden";
|
|
32
|
+
requiredGroupIds: Array<SoftrUserGroupId>;
|
|
33
|
+
};
|
|
34
|
+
declare class VisibilityError extends Error {
|
|
35
|
+
readonly reason: RefusalReason;
|
|
36
|
+
readonly requiredGroupIds: Array<SoftrUserGroupId> | undefined;
|
|
37
|
+
constructor(reason: RefusalReason, requiredGroupIds?: Array<SoftrUserGroupId>);
|
|
38
|
+
}
|
|
39
|
+
interface VisibilityHandle {
|
|
40
|
+
/** Structured check — returns { ok: true } or { ok: false, reason }. */
|
|
41
|
+
check(context: SoftrContext): CheckResult;
|
|
42
|
+
/** Returns true if the visitor passes the rule. */
|
|
43
|
+
isAuthorized(context: SoftrContext): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Throws a VisibilityError if not authorized. Acts as a TypeScript assertion,
|
|
46
|
+
* narrowing context.user to non-null for code that follows.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* beforeLoad: ({ context }) => {
|
|
50
|
+
* visibility.assert(context)
|
|
51
|
+
* if (context.user.trialExpired) throw redirect({ to: '/renew' })
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
assert(context: SoftrContext): asserts context is SoftrContext & {
|
|
55
|
+
user: SoftrUser;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Returns a TanStack beforeLoad handler. On refusal, throws a VisibilityError
|
|
59
|
+
* caught by the route's errorComponent.
|
|
60
|
+
*
|
|
61
|
+
* For redirect-on-refusal, use isAuthorized or check directly and throw
|
|
62
|
+
* TanStack's redirect() yourself.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* beforeLoad: visibility.guard()
|
|
66
|
+
* errorComponent: ({ error }) => isVisibilityError(error) ? <NoAccess /> : <AppError />
|
|
67
|
+
*/
|
|
68
|
+
guard(overrides?: GuardOverrides): (params: {
|
|
69
|
+
context: SoftrContext;
|
|
70
|
+
}) => void;
|
|
71
|
+
}
|
|
72
|
+
declare function defineVisibility(rule: RouteVisibility): VisibilityHandle;
|
|
73
|
+
declare function isVisibilityError(error: unknown): error is VisibilityError;
|
|
74
|
+
declare function isUnauthenticatedError(error: unknown): error is VisibilityError & {
|
|
75
|
+
reason: "unauthenticated";
|
|
76
|
+
};
|
|
77
|
+
declare function isForbiddenError(error: unknown): error is VisibilityError & {
|
|
78
|
+
reason: "forbidden";
|
|
79
|
+
};
|
|
80
|
+
declare function isAlreadySignedInError(error: unknown): error is VisibilityError & {
|
|
81
|
+
reason: "alreadySignedIn";
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { type AuthAPI, type CheckResult, type DatasourceAPI, type DatasourcePermissions, type GuardOverrides, type ListOptions, type ListResult, type RefusalBehaviour, type RefusalReason, type RouteVisibility, type SoftrContext, type SoftrMe, type SoftrPermissions, type SoftrRecord, type SoftrSDK, type SoftrUser, VisibilityError, type VisibilityHandle, type WorkflowAPI, defineVisibility, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, isVisibilityError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/routes/visibility.ts
|
|
2
|
+
var VisibilityError = class extends Error {
|
|
3
|
+
reason;
|
|
4
|
+
requiredGroupIds;
|
|
5
|
+
constructor(reason, requiredGroupIds) {
|
|
6
|
+
super(`Access denied: ${reason}`);
|
|
7
|
+
this.name = "VisibilityError";
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.requiredGroupIds = requiredGroupIds;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function runCheck(rule, context) {
|
|
13
|
+
const { user } = context;
|
|
14
|
+
if (rule.userGroup === "ALL_USERS") return { ok: true };
|
|
15
|
+
if (rule.userGroup === "NON_LOGGED_IN_USERS") return user ? {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: "alreadySignedIn"
|
|
18
|
+
} : { ok: true };
|
|
19
|
+
if (!user) return {
|
|
20
|
+
ok: false,
|
|
21
|
+
reason: "unauthenticated"
|
|
22
|
+
};
|
|
23
|
+
const { userGroupIds } = rule;
|
|
24
|
+
if (userGroupIds?.length) {
|
|
25
|
+
if (!userGroupIds.some((id) => user.userGroupIds.includes(id))) return {
|
|
26
|
+
ok: false,
|
|
27
|
+
reason: "forbidden",
|
|
28
|
+
requiredGroupIds: userGroupIds
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
function throwIfDenied(result) {
|
|
34
|
+
if (result.ok) return;
|
|
35
|
+
throw new VisibilityError(result.reason, result.reason === "forbidden" ? result.requiredGroupIds : void 0);
|
|
36
|
+
}
|
|
37
|
+
function defineVisibility(rule) {
|
|
38
|
+
function check(context) {
|
|
39
|
+
return runCheck(rule, context);
|
|
40
|
+
}
|
|
41
|
+
function isAuthorized(context) {
|
|
42
|
+
return runCheck(rule, context).ok;
|
|
43
|
+
}
|
|
44
|
+
function assert(context) {
|
|
45
|
+
throwIfDenied(runCheck(rule, context));
|
|
46
|
+
}
|
|
47
|
+
function guard(_overrides) {
|
|
48
|
+
return ({ context }) => {
|
|
49
|
+
throwIfDenied(runCheck(rule, context));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
check,
|
|
54
|
+
isAuthorized,
|
|
55
|
+
assert,
|
|
56
|
+
guard
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function isVisibilityError(error) {
|
|
60
|
+
return error instanceof VisibilityError;
|
|
61
|
+
}
|
|
62
|
+
function isUnauthenticatedError(error) {
|
|
63
|
+
return error instanceof VisibilityError && error.reason === "unauthenticated";
|
|
64
|
+
}
|
|
65
|
+
function isForbiddenError(error) {
|
|
66
|
+
return error instanceof VisibilityError && error.reason === "forbidden";
|
|
67
|
+
}
|
|
68
|
+
function isAlreadySignedInError(error) {
|
|
69
|
+
return error instanceof VisibilityError && error.reason === "alreadySignedIn";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { VisibilityError, defineVisibility, isAlreadySignedInError, isForbiddenError, isUnauthenticatedError, isVisibilityError };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { l as SoftrSDK } from "./types-BLLUI6MI.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tanstack.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TanStack Start middleware. Injects a per-request Softr SDK instance with
|
|
7
|
+
* cookies forwarded automatically. The base URL is read from VITE_SOFTR_BASE_URL.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { withSDK } from '@softr/sdk/tanstack'
|
|
11
|
+
*
|
|
12
|
+
* export const getProjects = createServerFn()
|
|
13
|
+
* .middleware([withSDK])
|
|
14
|
+
* .handler(({ context }) => context.sdk.datasource('ds_projects').list())
|
|
15
|
+
*/
|
|
16
|
+
declare const withSDK: any;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { type SoftrSDK, withSDK };
|
package/dist/tanstack.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/start";
|
|
2
|
+
|
|
3
|
+
//#region src/core/datasource.ts
|
|
4
|
+
var DatasourceAPIImpl = class {
|
|
5
|
+
id;
|
|
6
|
+
fetch;
|
|
7
|
+
constructor(id, fetch$1) {
|
|
8
|
+
this.id = id;
|
|
9
|
+
this.fetch = fetch$1;
|
|
10
|
+
}
|
|
11
|
+
async list(options) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
if (options?.page != null) params.set("page", String(options.page));
|
|
14
|
+
if (options?.limit != null) params.set("limit", String(options.limit));
|
|
15
|
+
if (options?.filters?.length) params.set("filters", JSON.stringify(options.filters));
|
|
16
|
+
if (options?.sort?.length) params.set("sort", JSON.stringify(options.sort));
|
|
17
|
+
const query = params.toString() ? `?${params}` : "";
|
|
18
|
+
const res = await this.fetch(`/api/datasources/${this.id}/records${query}`);
|
|
19
|
+
if (!res.ok) throw new Error(`Softr datasource list failed: ${res.status}`);
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
|
22
|
+
async get(recordId) {
|
|
23
|
+
const res = await this.fetch(`/api/datasources/${this.id}/records/${recordId}`);
|
|
24
|
+
if (!res.ok) throw new Error(`Softr datasource get failed: ${res.status}`);
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
async create(data) {
|
|
28
|
+
const res = await this.fetch(`/api/datasources/${this.id}/records`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify(data)
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error(`Softr datasource create failed: ${res.status}`);
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
async update(recordId, data) {
|
|
37
|
+
const res = await this.fetch(`/api/datasources/${this.id}/records/${recordId}`, {
|
|
38
|
+
method: "PATCH",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify(data)
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) throw new Error(`Softr datasource update failed: ${res.status}`);
|
|
43
|
+
return res.json();
|
|
44
|
+
}
|
|
45
|
+
async delete(recordId) {
|
|
46
|
+
const res = await this.fetch(`/api/datasources/${this.id}/records/${recordId}`, { method: "DELETE" });
|
|
47
|
+
if (!res.ok) throw new Error(`Softr datasource delete failed: ${res.status}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/core/workflow.ts
|
|
53
|
+
var WorkflowAPIImpl = class {
|
|
54
|
+
id;
|
|
55
|
+
fetch;
|
|
56
|
+
constructor(id, fetch$1) {
|
|
57
|
+
this.id = id;
|
|
58
|
+
this.fetch = fetch$1;
|
|
59
|
+
}
|
|
60
|
+
async trigger(payload) {
|
|
61
|
+
const res = await this.fetch(`/api/workflows/${this.id}/trigger`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
...payload ? {
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify(payload)
|
|
66
|
+
} : {}
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`Softr workflow trigger failed: ${res.status}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/core/sdk.ts
|
|
74
|
+
var SoftrSDKImpl = class {
|
|
75
|
+
baseUrl;
|
|
76
|
+
extraHeaders;
|
|
77
|
+
cachedMe = null;
|
|
78
|
+
constructor(config) {
|
|
79
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
80
|
+
this.extraHeaders = config.headers;
|
|
81
|
+
}
|
|
82
|
+
async me() {
|
|
83
|
+
if (this.cachedMe) return this.cachedMe;
|
|
84
|
+
const res = await this._fetch("/me");
|
|
85
|
+
if (!res.ok) throw new Error(`Softr /me failed: ${res.status}`);
|
|
86
|
+
this.cachedMe = await res.json();
|
|
87
|
+
return this.cachedMe;
|
|
88
|
+
}
|
|
89
|
+
auth = {
|
|
90
|
+
login: async ({ email, password }) => {
|
|
91
|
+
const res = await this._fetch("/auth/login", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
email,
|
|
96
|
+
password
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
|
|
100
|
+
this.cachedMe = null;
|
|
101
|
+
},
|
|
102
|
+
logout: async () => {
|
|
103
|
+
const res = await this._fetch("/auth/logout", { method: "POST" });
|
|
104
|
+
if (!res.ok) throw new Error(`Logout failed: ${res.status}`);
|
|
105
|
+
this.cachedMe = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
datasource(id) {
|
|
109
|
+
return new DatasourceAPIImpl(id, this._fetch.bind(this));
|
|
110
|
+
}
|
|
111
|
+
workflow(id) {
|
|
112
|
+
return new WorkflowAPIImpl(id, this._fetch.bind(this));
|
|
113
|
+
}
|
|
114
|
+
_fetch(path, init) {
|
|
115
|
+
const headers = new Headers(this.extraHeaders);
|
|
116
|
+
if (init?.headers) new Headers(init.headers).forEach((v, k) => headers.set(k, v));
|
|
117
|
+
const isServer = typeof window === "undefined";
|
|
118
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
119
|
+
...init,
|
|
120
|
+
credentials: isServer ? "omit" : "include",
|
|
121
|
+
headers
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function createSoftrSDKFromRequest(request, baseUrl) {
|
|
126
|
+
return new SoftrSDKImpl({
|
|
127
|
+
baseUrl,
|
|
128
|
+
headers: { cookie: request.headers.get("cookie") ?? "" }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/tanstack.ts
|
|
134
|
+
/**
|
|
135
|
+
* TanStack Start middleware. Injects a per-request Softr SDK instance with
|
|
136
|
+
* cookies forwarded automatically. The base URL is read from VITE_SOFTR_BASE_URL.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* import { withSDK } from '@softr/sdk/tanstack'
|
|
140
|
+
*
|
|
141
|
+
* export const getProjects = createServerFn()
|
|
142
|
+
* .middleware([withSDK])
|
|
143
|
+
* .handler(({ context }) => context.sdk.datasource('ds_projects').list())
|
|
144
|
+
*/
|
|
145
|
+
const withSDK = createMiddleware().server(({ next, request }) => next({ context: { sdk: createSoftrSDKFromRequest(request, import.meta.env.VITE_SOFTR_BASE_URL) } }));
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { withSDK };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface SoftrUser {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
name: string;
|
|
6
|
+
userGroupIds: Array<SoftrUserGroupId>;
|
|
7
|
+
}
|
|
8
|
+
interface DatasourcePermissions {
|
|
9
|
+
read: boolean;
|
|
10
|
+
create: boolean;
|
|
11
|
+
update: boolean;
|
|
12
|
+
delete: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface SoftrPermissions {
|
|
15
|
+
datasources: Record<string, DatasourcePermissions>;
|
|
16
|
+
workflows: Record<string, boolean>;
|
|
17
|
+
}
|
|
18
|
+
interface SoftrMe {
|
|
19
|
+
user: SoftrUser | null;
|
|
20
|
+
permissions: SoftrPermissions;
|
|
21
|
+
}
|
|
22
|
+
interface ListOptions {
|
|
23
|
+
page?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
filters?: Array<{
|
|
26
|
+
field: string;
|
|
27
|
+
operator: string;
|
|
28
|
+
value: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
sort?: Array<{
|
|
31
|
+
field: string;
|
|
32
|
+
direction: 'asc' | 'desc';
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
interface ListResult<T = SoftrRecord> {
|
|
36
|
+
records: T[];
|
|
37
|
+
total: number;
|
|
38
|
+
page: number;
|
|
39
|
+
limit: number;
|
|
40
|
+
}
|
|
41
|
+
interface SoftrRecord {
|
|
42
|
+
id: string;
|
|
43
|
+
[field: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
interface DatasourceAPI {
|
|
46
|
+
list(options?: ListOptions): Promise<ListResult>;
|
|
47
|
+
get(recordId: string): Promise<SoftrRecord>;
|
|
48
|
+
create(data: Record<string, unknown>): Promise<SoftrRecord>;
|
|
49
|
+
update(recordId: string, data: Record<string, unknown>): Promise<SoftrRecord>;
|
|
50
|
+
delete(recordId: string): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
interface WorkflowAPI {
|
|
53
|
+
trigger(payload?: Record<string, unknown>): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
interface AuthAPI {
|
|
56
|
+
/** Sign in with email and password. Clears the cached me() result. */
|
|
57
|
+
login(credentials: {
|
|
58
|
+
email: string;
|
|
59
|
+
password: string;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
/** Sign out the current user. Clears the cached me() result. */
|
|
62
|
+
logout(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
interface SoftrSDK {
|
|
65
|
+
/**
|
|
66
|
+
* Fetch the current user and their permissions from /me.
|
|
67
|
+
* Cached for the lifetime of the instance — create a new instance per request on the server.
|
|
68
|
+
*/
|
|
69
|
+
me(): Promise<SoftrMe>;
|
|
70
|
+
auth: AuthAPI;
|
|
71
|
+
datasource(id: string): DatasourceAPI;
|
|
72
|
+
workflow(id: string): WorkflowAPI;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ListResult as a, SoftrRecord as c, WorkflowAPI as d, ListOptions as i, SoftrSDK as l, DatasourceAPI as n, SoftrMe as o, DatasourcePermissions as r, SoftrPermissions as s, AuthAPI as t, SoftrUser as u };
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "@tanstack/router-generator";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region src/routes/discover.d.ts
|
|
5
|
+
|
|
6
|
+
type VisibilityValue = string | Array<VisibilityValue> | {
|
|
7
|
+
[key: string]: VisibilityValue;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/routes/manifest.d.ts
|
|
11
|
+
type ManifestNode = {
|
|
12
|
+
routePath: string;
|
|
13
|
+
filePath: string;
|
|
14
|
+
_fsRouteType?: string;
|
|
15
|
+
inferFullPath: string | null;
|
|
16
|
+
createFileRouteProps: Array<string>;
|
|
17
|
+
visibility: VisibilityValue | null;
|
|
18
|
+
children: Array<ManifestNode>;
|
|
19
|
+
};
|
|
20
|
+
type Manifest = {
|
|
21
|
+
root: ManifestNode;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/vite.d.ts
|
|
25
|
+
declare const MANIFEST_RELATIVE_PATH: string;
|
|
26
|
+
/** Writes `<root>/dist/routes.json` and returns its path. Throws on a rule it cannot read. */
|
|
27
|
+
declare function generateManifest(root: string): Promise<string>;
|
|
28
|
+
/** Plugin order does not matter: this puts nothing into the module graph. */
|
|
29
|
+
declare function softr(): Plugin;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { MANIFEST_RELATIVE_PATH, type Manifest, type ManifestNode, generateManifest, softr };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Generator, getConfig, inferFullPath } from "@tanstack/router-generator";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { parse } from "@babel/parser";
|
|
8
|
+
import babelTraverse from "@babel/traverse";
|
|
9
|
+
|
|
10
|
+
//#region src/routes/mem-fs.ts
|
|
11
|
+
/**
|
|
12
|
+
* Reads through to disk and swallows every write. Driving the generator without this rewrites the
|
|
13
|
+
* app's route sources and strips Start's `Register` block from its routeTree.gen.ts.
|
|
14
|
+
*/
|
|
15
|
+
function createReadOnlyFs() {
|
|
16
|
+
const writes = /* @__PURE__ */ new Map();
|
|
17
|
+
return {
|
|
18
|
+
async stat(filePath) {
|
|
19
|
+
const stat = await statOrNull(path.resolve(filePath));
|
|
20
|
+
if (stat === null) return {
|
|
21
|
+
mtimeMs: SYNTHETIC_MTIME,
|
|
22
|
+
mode: 420,
|
|
23
|
+
uid: 0,
|
|
24
|
+
gid: 0
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
mtimeMs: stat.mtimeMs,
|
|
28
|
+
mode: Number(stat.mode),
|
|
29
|
+
uid: Number(stat.uid),
|
|
30
|
+
gid: Number(stat.gid)
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
async rename(oldPath, newPath) {
|
|
34
|
+
const from = path.resolve(oldPath);
|
|
35
|
+
const content = writes.get(from);
|
|
36
|
+
if (content !== void 0) {
|
|
37
|
+
writes.delete(from);
|
|
38
|
+
writes.set(path.resolve(newPath), content);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async writeFile(filePath, content) {
|
|
42
|
+
writes.set(path.resolve(filePath), content);
|
|
43
|
+
},
|
|
44
|
+
async readFile(filePath) {
|
|
45
|
+
const resolved = path.resolve(filePath);
|
|
46
|
+
const pending = writes.get(resolved);
|
|
47
|
+
const stat = await statOrNull(resolved);
|
|
48
|
+
if (pending !== void 0) return {
|
|
49
|
+
stat: { mtimeMs: stat?.mtimeMs ?? SYNTHETIC_MTIME },
|
|
50
|
+
fileContent: pending
|
|
51
|
+
};
|
|
52
|
+
if (stat === null) return "file-not-existing";
|
|
53
|
+
try {
|
|
54
|
+
const fileContent = await nodeFs.readFile(resolved, "utf8");
|
|
55
|
+
return {
|
|
56
|
+
stat: { mtimeMs: stat.mtimeMs },
|
|
57
|
+
fileContent
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return "file-not-existing";
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async chmod() {},
|
|
64
|
+
async chown() {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const SYNTHETIC_MTIME = 1n;
|
|
68
|
+
async function statOrNull(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return await nodeFs.stat(filePath, { bigint: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/routes/discover.ts
|
|
78
|
+
async function crawlRoutes(opts) {
|
|
79
|
+
const config = getConfig({
|
|
80
|
+
disableLogging: true,
|
|
81
|
+
tmpDir: TMP_DIR,
|
|
82
|
+
generatedRouteTree: generatedRouteTreePath()
|
|
83
|
+
}, opts.root);
|
|
84
|
+
const generator = new Generator({
|
|
85
|
+
config,
|
|
86
|
+
root: opts.root,
|
|
87
|
+
fs: createReadOnlyFs()
|
|
88
|
+
});
|
|
89
|
+
await generator.run();
|
|
90
|
+
const crawlingResult = await generator.getCrawlingResult();
|
|
91
|
+
if (!crawlingResult) throw new Error(`the route crawl produced no result for ${opts.root}`);
|
|
92
|
+
return {
|
|
93
|
+
rootRouteNode: crawlingResult.rootRouteNode,
|
|
94
|
+
routeNodes: crawlingResult.acc.routeNodes,
|
|
95
|
+
routePiecesByPath: crawlingResult.acc.routePiecesByPath,
|
|
96
|
+
routeTree: crawlingResult.acc.routeTree,
|
|
97
|
+
routesDirectory: path.relative(opts.root, path.resolve(opts.root, config.routesDirectory)).split(path.sep).join("/")
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Reads the rule out of the source text. The route file is never executed. */
|
|
101
|
+
function extractVisibility(source, filePath) {
|
|
102
|
+
let program;
|
|
103
|
+
try {
|
|
104
|
+
program = parse(source, {
|
|
105
|
+
sourceType: "module",
|
|
106
|
+
plugins: ["typescript", "jsx"]
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const position = error.loc;
|
|
110
|
+
return fail("unparsable", `could not parse ${filePath}: ${error.message}`, filePath, position?.line ?? 1, position?.column ?? 0);
|
|
111
|
+
}
|
|
112
|
+
const calls = findVisibilityCalls(program);
|
|
113
|
+
if (calls.length > 1) {
|
|
114
|
+
const second = calls[1].call;
|
|
115
|
+
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);
|
|
116
|
+
}
|
|
117
|
+
const declaration = calls[0];
|
|
118
|
+
if (declaration === void 0) return {
|
|
119
|
+
ok: true,
|
|
120
|
+
visibility: null
|
|
121
|
+
};
|
|
122
|
+
const { call, statement } = declaration;
|
|
123
|
+
const argument = call.arguments[0];
|
|
124
|
+
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);
|
|
125
|
+
const read = readLiteral(argument);
|
|
126
|
+
if (!read.ok) return fail("not-literal", `${filePath}: the visibility rule must be a literal — ${read.reason}`, filePath, read.line, read.column);
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
visibility: {
|
|
130
|
+
value: read.value,
|
|
131
|
+
line: statement.loc?.start.line ?? 1,
|
|
132
|
+
column: statement.loc?.start.column ?? 0
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The generator `mkdirSync`s `tmpDir` and the directory of `generatedRouteTree` outside the `fs`
|
|
138
|
+
* shim, so defaulted they create directories in the crawled project. Aimed at `os.tmpdir()` both
|
|
139
|
+
* calls are no-ops, and nothing is ever written through either path. The uuid keeps the generated
|
|
140
|
+
* tree file a miss rather than whatever a shared temp directory happens to hold.
|
|
141
|
+
*/
|
|
142
|
+
const TMP_DIR = os.tmpdir();
|
|
143
|
+
const generatedRouteTreePath = () => path.join(TMP_DIR, `softr-sdk-route-tree-${randomUUID()}.gen.ts`);
|
|
144
|
+
const SDK_PACKAGE_NAME = "@softr/sdk";
|
|
145
|
+
const VISIBILITY_FUNCTION = "defineVisibility";
|
|
146
|
+
function fail(code, message, filePath, line, column) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: {
|
|
150
|
+
code,
|
|
151
|
+
message,
|
|
152
|
+
filePath,
|
|
153
|
+
line,
|
|
154
|
+
column
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const traverse = babelTraverse.default ?? babelTraverse;
|
|
159
|
+
/** Only a call initialising a top-level variable counts; one inside a function or block does not. */
|
|
160
|
+
function findVisibilityCalls(ast) {
|
|
161
|
+
const found = [];
|
|
162
|
+
traverse(ast, { VariableDeclarator(path$1) {
|
|
163
|
+
const list = path$1.parentPath;
|
|
164
|
+
if (!list.isVariableDeclaration()) return;
|
|
165
|
+
const statement = list.parentPath.isExportNamedDeclaration() ? list.parentPath : list;
|
|
166
|
+
if (!statement.parentPath?.isProgram()) return;
|
|
167
|
+
if (path$1.node.init === null || path$1.node.init === void 0) return;
|
|
168
|
+
const call = unwrap(path$1.node.init);
|
|
169
|
+
if (call.type !== "CallExpression" && call.type !== "OptionalCallExpression") return;
|
|
170
|
+
if (call.callee.type !== "Identifier") return;
|
|
171
|
+
if (!isVisibilityImport(path$1, call.callee.name)) return;
|
|
172
|
+
found.push({
|
|
173
|
+
call,
|
|
174
|
+
statement: statement.node
|
|
175
|
+
});
|
|
176
|
+
} });
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
function isVisibilityImport(path$1, name) {
|
|
180
|
+
const binding = path$1.scope.getBinding(name);
|
|
181
|
+
if (binding === void 0 || binding.kind !== "module") return false;
|
|
182
|
+
const specifier = binding.path.node;
|
|
183
|
+
const declaration = binding.path.parent;
|
|
184
|
+
if (specifier.type !== "ImportSpecifier" || declaration.type !== "ImportDeclaration") return false;
|
|
185
|
+
if (specifier.importKind === "type" || declaration.importKind === "type") return false;
|
|
186
|
+
if (declaration.source.value !== SDK_PACKAGE_NAME) return false;
|
|
187
|
+
return (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === VISIBILITY_FUNCTION;
|
|
188
|
+
}
|
|
189
|
+
/** Strips the wrappers that leave the runtime value unchanged: `as const`, `satisfies`, `!`, `()`. */
|
|
190
|
+
function unwrap(node) {
|
|
191
|
+
switch (node.type) {
|
|
192
|
+
case "TSAsExpression":
|
|
193
|
+
case "TSSatisfiesExpression":
|
|
194
|
+
case "TSNonNullExpression":
|
|
195
|
+
case "TSTypeAssertion":
|
|
196
|
+
case "TSInstantiationExpression":
|
|
197
|
+
case "ParenthesizedExpression": return unwrap(node.expression);
|
|
198
|
+
default: return node;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function reject(node, reason) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
reason,
|
|
205
|
+
line: node.loc?.start.line ?? 1,
|
|
206
|
+
column: node.loc?.start.column ?? 0
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function readLiteral(input) {
|
|
210
|
+
const node = unwrap(input);
|
|
211
|
+
switch (node.type) {
|
|
212
|
+
case "StringLiteral": return {
|
|
213
|
+
ok: true,
|
|
214
|
+
value: node.value
|
|
215
|
+
};
|
|
216
|
+
case "TemplateLiteral":
|
|
217
|
+
if (node.expressions.length > 0) return reject(node, "a template literal with an expression is not a static value");
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
value: node.quasis[0]?.value.cooked ?? ""
|
|
221
|
+
};
|
|
222
|
+
case "ArrayExpression": {
|
|
223
|
+
const values = [];
|
|
224
|
+
for (const element of node.elements) {
|
|
225
|
+
if (element === null) return reject(node, "an array hole is not a static value");
|
|
226
|
+
if (element.type === "SpreadElement") return reject(element, "a spread is not a static value");
|
|
227
|
+
const read = readLiteral(element);
|
|
228
|
+
if (!read.ok) return read;
|
|
229
|
+
values.push(read.value);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
value: values
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case "ObjectExpression": {
|
|
237
|
+
const value = {};
|
|
238
|
+
for (const property of node.properties) {
|
|
239
|
+
if (property.type === "SpreadElement") return reject(property, "a spread is not a static value");
|
|
240
|
+
if (property.type === "ObjectMethod") return reject(property, "a method is not a static value");
|
|
241
|
+
if (property.computed) return reject(property, "a computed key is not a static value");
|
|
242
|
+
if (property.shorthand) return reject(property, "a shorthand property is not a static value");
|
|
243
|
+
const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "StringLiteral" ? property.key.value : null;
|
|
244
|
+
if (key === null) return reject(property.key, "this key is not a static value");
|
|
245
|
+
if (key === "__proto__") return reject(property.key, "a __proto__ key is not a static value");
|
|
246
|
+
const read = readLiteral(property.value);
|
|
247
|
+
if (!read.ok) return read;
|
|
248
|
+
value[key] = read.value;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
value
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
case "NumericLiteral":
|
|
256
|
+
case "BooleanLiteral":
|
|
257
|
+
case "NullLiteral": return reject(node, "a visibility rule is built from strings, arrays and objects");
|
|
258
|
+
case "Identifier": return reject(node, `an identifier (${node.name}) is not a static value`);
|
|
259
|
+
case "CallExpression":
|
|
260
|
+
case "NewExpression": return reject(node, "a call is not a static value");
|
|
261
|
+
default: return reject(node, `${node.type} is not a static value`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/routes/manifest.ts
|
|
267
|
+
var ManifestError = class extends Error {
|
|
268
|
+
detail;
|
|
269
|
+
constructor(detail) {
|
|
270
|
+
super(detail.message);
|
|
271
|
+
this.name = "ManifestError";
|
|
272
|
+
this.detail = detail;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
/** Returns the manifest as the exact text to write. Throws on a declaration it cannot read. */
|
|
276
|
+
async function generateRoutesManifest(root) {
|
|
277
|
+
const crawl = await crawlRoutes({ root });
|
|
278
|
+
const routeFiles = new Set([crawl.rootRouteNode.filePath]);
|
|
279
|
+
for (const node of crawl.routeNodes) routeFiles.add(node.filePath);
|
|
280
|
+
for (const pieces of Object.values(crawl.routePiecesByPath)) if (pieces.lazy) routeFiles.add(pieces.lazy.filePath);
|
|
281
|
+
const sources = /* @__PURE__ */ new Map();
|
|
282
|
+
for (const relative of routeFiles) {
|
|
283
|
+
const absolute = path.resolve(root, crawl.routesDirectory, relative);
|
|
284
|
+
const source = await readIfPresent(absolute);
|
|
285
|
+
if (source !== null) sources.set(absolute, source);
|
|
286
|
+
}
|
|
287
|
+
const manifest = buildManifest({
|
|
288
|
+
crawl,
|
|
289
|
+
root,
|
|
290
|
+
sources
|
|
291
|
+
});
|
|
292
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
293
|
+
}
|
|
294
|
+
function buildManifest(opts) {
|
|
295
|
+
const routesDirectory = opts.crawl.routesDirectory;
|
|
296
|
+
const toNode = (node) => {
|
|
297
|
+
const projectRelative = toProjectRelative(opts.root, routesDirectory, node.filePath);
|
|
298
|
+
const routePath = node.routePath;
|
|
299
|
+
if (routePath === void 0) throw new Error(`the route crawl produced a node with no route path: ${projectRelative}`);
|
|
300
|
+
let declared = visibilityOf(opts, node.filePath);
|
|
301
|
+
const lazyPiece = opts.crawl.routePiecesByPath[routePath]?.lazy;
|
|
302
|
+
if (lazyPiece && lazyPiece.filePath !== node.filePath) {
|
|
303
|
+
const lazyDeclared = visibilityOf(opts, lazyPiece.filePath);
|
|
304
|
+
if (lazyDeclared !== null && declared !== null) throw new ManifestError({
|
|
305
|
+
code: "duplicate-declaration",
|
|
306
|
+
message: `${routePath} declares visibility in both ${projectRelative} and ${lazyDeclared.filePath}; a route has exactly one rule`,
|
|
307
|
+
filePath: lazyDeclared.filePath,
|
|
308
|
+
line: lazyDeclared.line,
|
|
309
|
+
column: lazyDeclared.column
|
|
310
|
+
});
|
|
311
|
+
if (lazyDeclared !== null) declared = lazyDeclared;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
routePath,
|
|
315
|
+
filePath: projectRelative,
|
|
316
|
+
...node._fsRouteType ? { _fsRouteType: node._fsRouteType } : {},
|
|
317
|
+
inferFullPath: inferFullPath(node) ?? null,
|
|
318
|
+
createFileRouteProps: [...node.createFileRouteProps ?? []].sort(),
|
|
319
|
+
visibility: declared?.value ?? null,
|
|
320
|
+
children: (node.children ?? []).map(toNode)
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
const rootNode = opts.crawl.rootRouteNode;
|
|
324
|
+
const rootRelative = toProjectRelative(opts.root, routesDirectory, rootNode.filePath);
|
|
325
|
+
const rootDeclared = visibilityOf(opts, rootNode.filePath);
|
|
326
|
+
if (rootDeclared !== null) throw new ManifestError({
|
|
327
|
+
code: "root-declaration",
|
|
328
|
+
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.",
|
|
329
|
+
filePath: rootRelative,
|
|
330
|
+
line: rootDeclared.line,
|
|
331
|
+
column: rootDeclared.column
|
|
332
|
+
});
|
|
333
|
+
return { root: {
|
|
334
|
+
routePath: ROOT_ROUTE_ID,
|
|
335
|
+
filePath: rootRelative,
|
|
336
|
+
inferFullPath: inferFullPath(rootNode) ?? null,
|
|
337
|
+
createFileRouteProps: [...rootNode.createFileRouteProps ?? []].sort(),
|
|
338
|
+
visibility: null,
|
|
339
|
+
children: opts.crawl.routeTree.map(toNode)
|
|
340
|
+
} };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* `rootRouteId` from `@tanstack/router-core`, which is what a consumer matches against — not the
|
|
344
|
+
* generator's `rootPathId`, which is the base name of `__root.tsx` and names a file, not a route.
|
|
345
|
+
*/
|
|
346
|
+
const ROOT_ROUTE_ID = "__root__";
|
|
347
|
+
/** The crawl's paths are relative to the routes directory; the manifest's are project-relative. */
|
|
348
|
+
function toProjectRelative(root, routesDirectory, filePath) {
|
|
349
|
+
const absolute = path.resolve(root, routesDirectory, filePath);
|
|
350
|
+
return path.relative(root, absolute).split(path.sep).join("/");
|
|
351
|
+
}
|
|
352
|
+
const USER_GROUPS = [
|
|
353
|
+
"ALL_USERS",
|
|
354
|
+
"NON_LOGGED_IN_USERS",
|
|
355
|
+
"LOGGED_IN_USERS"
|
|
356
|
+
];
|
|
357
|
+
function checkVisibilityShape(declared) {
|
|
358
|
+
const reject$1 = (message) => {
|
|
359
|
+
throw new ManifestError({
|
|
360
|
+
code: "invalid-visibility",
|
|
361
|
+
message: `${declared.filePath}: ${message}`,
|
|
362
|
+
filePath: declared.filePath,
|
|
363
|
+
line: declared.line,
|
|
364
|
+
column: declared.column
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const visibility = declared.value;
|
|
368
|
+
if (typeof visibility !== "object" || Array.isArray(visibility)) reject$1(`the visibility rule must be an object literal like { userGroup: 'ALL_USERS' }, not ${JSON.stringify(visibility)}`);
|
|
369
|
+
for (const key of Object.keys(visibility)) {
|
|
370
|
+
if (key === "userGroup" || key === "userGroupIds") continue;
|
|
371
|
+
reject$1(`"${key}" is not part of a visibility rule; a rule has userGroup and, under LOGGED_IN_USERS, userGroupIds. Check the spelling.`);
|
|
372
|
+
}
|
|
373
|
+
const userGroup = visibility["userGroup"];
|
|
374
|
+
if (typeof userGroup !== "string" || !USER_GROUPS.includes(userGroup)) reject$1(`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.`);
|
|
375
|
+
if (!("userGroupIds" in visibility)) return;
|
|
376
|
+
if (userGroup !== "LOGGED_IN_USERS") reject$1(`userGroupIds only means anything under LOGGED_IN_USERS, not under ${userGroup}`);
|
|
377
|
+
const ids = visibility["userGroupIds"];
|
|
378
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) reject$1(`userGroupIds must be an array of group id strings, not ${JSON.stringify(ids)}. Drop the key entirely to mean "any logged-in user".`);
|
|
379
|
+
}
|
|
380
|
+
function visibilityOf(opts, filePath) {
|
|
381
|
+
const projectRelative = toProjectRelative(opts.root, opts.crawl.routesDirectory, filePath);
|
|
382
|
+
const source = opts.sources.get(path.resolve(opts.root, opts.crawl.routesDirectory, filePath));
|
|
383
|
+
if (source === void 0) return null;
|
|
384
|
+
const result = extractVisibility(source, projectRelative);
|
|
385
|
+
if (!result.ok) throw new ManifestError(result.error);
|
|
386
|
+
if (result.visibility === null) return null;
|
|
387
|
+
const declared = {
|
|
388
|
+
...result.visibility,
|
|
389
|
+
filePath: projectRelative
|
|
390
|
+
};
|
|
391
|
+
checkVisibilityShape(declared);
|
|
392
|
+
return declared;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Only ENOENT becomes `null`. The caller reads `null` as "nothing declared here", so swallowing any
|
|
396
|
+
* other read error would turn an unreadable declaration into an ungated route.
|
|
397
|
+
*/
|
|
398
|
+
async function readIfPresent(filePath) {
|
|
399
|
+
try {
|
|
400
|
+
return await readFile(filePath, "utf8");
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error.code === "ENOENT") return null;
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/vite.ts
|
|
409
|
+
const MANIFEST_RELATIVE_PATH = path.join("dist", "routes.json");
|
|
410
|
+
async function renderManifest(root) {
|
|
411
|
+
try {
|
|
412
|
+
return await generateRoutesManifest(root);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error instanceof ManifestError) {
|
|
415
|
+
const { filePath, line, column, message } = error.detail;
|
|
416
|
+
throw new Error(`[softr] ${filePath}:${line}:${column} — ${message}`, { cause: error });
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function writeManifest(root, contents) {
|
|
422
|
+
const target = path.resolve(root, MANIFEST_RELATIVE_PATH);
|
|
423
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
424
|
+
await writeFile(target, contents, "utf8");
|
|
425
|
+
return target;
|
|
426
|
+
}
|
|
427
|
+
/** Writes `<root>/dist/routes.json` and returns its path. Throws on a rule it cannot read. */
|
|
428
|
+
async function generateManifest(root) {
|
|
429
|
+
return writeManifest(root, await renderManifest(root));
|
|
430
|
+
}
|
|
431
|
+
/** Plugin order does not matter: this puts nothing into the module graph. */
|
|
432
|
+
function softr() {
|
|
433
|
+
let root = process.cwd();
|
|
434
|
+
let rendered = null;
|
|
435
|
+
return {
|
|
436
|
+
name: "softr",
|
|
437
|
+
apply: "build",
|
|
438
|
+
configResolved(config) {
|
|
439
|
+
root = config.root;
|
|
440
|
+
},
|
|
441
|
+
async closeBundle() {
|
|
442
|
+
rendered ??= renderManifest(root);
|
|
443
|
+
await writeManifest(root, await rendered);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
//#endregion
|
|
449
|
+
export { MANIFEST_RELATIVE_PATH, generateManifest, softr };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@softr/sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
7
|
+
"./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" },
|
|
8
|
+
"./tanstack": { "types": "./dist/tanstack.d.ts", "import": "./dist/tanstack.js" }
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsdown",
|
|
14
|
+
"test": "node --test",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@babel/parser": "^7.28.0",
|
|
19
|
+
"@babel/traverse": "^7.28.0",
|
|
20
|
+
"@babel/types": "^7.28.0",
|
|
21
|
+
"@tanstack/router-generator": "1.167.23"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/babel__traverse": "^7.28.0",
|
|
25
|
+
"@types/node": "^24.3.0",
|
|
26
|
+
"tsdown": "^0.15.0",
|
|
27
|
+
"typescript": "^5.9.2",
|
|
28
|
+
"vite": "^8.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@tanstack/start": ">=1",
|
|
32
|
+
"vite": "^8.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@tanstack/start": { "optional": true },
|
|
36
|
+
"vite": { "optional": true }
|
|
37
|
+
}
|
|
38
|
+
}
|