@ollie-shop/cli 1.2.2 → 1.3.3
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/.env.example +9 -5
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +51 -0
- package/CONTEXT.md +3 -3
- package/README.md +6 -10
- package/dist/index.js +280 -51
- package/package.json +1 -1
- package/src/cli.tsx +8 -1
- package/src/commands/function-cmd.ts +42 -10
- package/src/commands/help.tsx +5 -1
- package/src/commands/start.tsx +8 -18
- package/src/commands/store-cmd.ts +14 -4
- package/src/commands/whoami.ts +70 -9
- package/src/core/function.ts +1 -1
- package/src/core/schema.ts +8 -3
- package/src/core/store.ts +50 -3
- package/src/utils/auth.ts +4 -0
- package/src/utils/esbuild.ts +99 -15
- package/src/utils/parse-args.ts +2 -0
- package/src/utils/supabase.ts +66 -11
- package/tsup.config.ts +7 -0
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import { HelpCommand } from "./commands/help.js";
|
|
|
3
3
|
import { LoginCommand } from "./commands/login.js";
|
|
4
4
|
import { StartCommand } from "./commands/start.js";
|
|
5
5
|
|
|
6
|
+
// Inlined by tsup at build time from package.json (see tsup.config.ts).
|
|
7
|
+
declare const __OLLIE_CLI_VERSION__: string;
|
|
8
|
+
|
|
6
9
|
interface AppProps {
|
|
7
10
|
command: string;
|
|
8
11
|
args: string[];
|
|
@@ -28,9 +31,13 @@ export function App({ command, args }: AppProps) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function VersionCommand() {
|
|
34
|
+
const version =
|
|
35
|
+
typeof __OLLIE_CLI_VERSION__ === "string" && __OLLIE_CLI_VERSION__
|
|
36
|
+
? __OLLIE_CLI_VERSION__
|
|
37
|
+
: "unknown";
|
|
31
38
|
return (
|
|
32
39
|
<Box>
|
|
33
|
-
<Text>
|
|
40
|
+
<Text>ollieshop v{version}</Text>
|
|
34
41
|
</Box>
|
|
35
42
|
);
|
|
36
43
|
}
|
|
@@ -17,6 +17,43 @@ import {
|
|
|
17
17
|
|
|
18
18
|
const ON_ERROR_VALUES = ["throw", "skip"] as const;
|
|
19
19
|
|
|
20
|
+
function parseTriggerFromFlags(
|
|
21
|
+
urlFlag: string | undefined,
|
|
22
|
+
expressionFlag: string | undefined,
|
|
23
|
+
): { url: string; expression: string } | undefined {
|
|
24
|
+
if (urlFlag === undefined && expressionFlag === undefined) return undefined;
|
|
25
|
+
if (urlFlag === undefined || expressionFlag === undefined) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Both --trigger-url and --trigger-expression are required when configuring a trigger.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
url: validateTriggerUrl(urlFlag, "trigger-url"),
|
|
32
|
+
expression: validateRequired(expressionFlag, "trigger-expression"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseTriggerFromData(
|
|
37
|
+
raw: unknown,
|
|
38
|
+
): { url: string; expression: string } | undefined {
|
|
39
|
+
if (raw === undefined || raw === null) return undefined;
|
|
40
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Invalid trigger: must be an object with "url" and "expression" string properties.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const obj = raw as Record<string, unknown>;
|
|
46
|
+
if (typeof obj.url !== "string" || typeof obj.expression !== "string") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Invalid trigger: both "url" and "expression" must be non-empty strings.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
url: validateTriggerUrl(obj.url, "trigger.url"),
|
|
53
|
+
expression: validateRequired(obj.expression, "trigger.expression"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export async function functionCommand(parsed: ParsedArgs): Promise<void> {
|
|
21
58
|
const sub = parsed.subcommand;
|
|
22
59
|
if (sub === "create") return functionCreateCommand(parsed);
|
|
@@ -35,7 +72,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
35
72
|
let input: {
|
|
36
73
|
versionId: string;
|
|
37
74
|
name: string;
|
|
38
|
-
trigger?: string;
|
|
75
|
+
trigger?: { url: string; expression: string };
|
|
39
76
|
active?: boolean;
|
|
40
77
|
onError?: "throw" | "skip";
|
|
41
78
|
priority?: number;
|
|
@@ -46,10 +83,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
46
83
|
input = {
|
|
47
84
|
versionId: validateUuid(raw.versionId, "versionId"),
|
|
48
85
|
name: validateRequired(raw.name, "name"),
|
|
49
|
-
trigger:
|
|
50
|
-
raw.trigger !== undefined && raw.trigger !== null
|
|
51
|
-
? validateTriggerUrl(String(raw.trigger), "trigger")
|
|
52
|
-
: undefined,
|
|
86
|
+
trigger: parseTriggerFromData(raw.trigger),
|
|
53
87
|
active: raw.active === true,
|
|
54
88
|
onError:
|
|
55
89
|
raw.onError !== undefined && raw.onError !== null
|
|
@@ -61,7 +95,8 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
61
95
|
: 0,
|
|
62
96
|
};
|
|
63
97
|
} else {
|
|
64
|
-
const
|
|
98
|
+
const triggerUrlFlag = getFlag(parsed.flags, "trigger-url");
|
|
99
|
+
const triggerExpressionFlag = getFlag(parsed.flags, "trigger-expression");
|
|
65
100
|
const onErrorFlag = getFlag(parsed.flags, "on-error");
|
|
66
101
|
const priorityFlag = getFlag(parsed.flags, "priority");
|
|
67
102
|
input = {
|
|
@@ -70,10 +105,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
70
105
|
"version-id",
|
|
71
106
|
),
|
|
72
107
|
name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
|
|
73
|
-
trigger:
|
|
74
|
-
triggerFlag !== undefined
|
|
75
|
-
? validateTriggerUrl(triggerFlag, "trigger")
|
|
76
|
-
: undefined,
|
|
108
|
+
trigger: parseTriggerFromFlags(triggerUrlFlag, triggerExpressionFlag),
|
|
77
109
|
active: getBoolFlag(parsed.flags, "active"),
|
|
78
110
|
onError:
|
|
79
111
|
onErrorFlag !== undefined
|
package/src/commands/help.tsx
CHANGED
|
@@ -41,7 +41,7 @@ export function HelpCommand() {
|
|
|
41
41
|
<Box width={24}>
|
|
42
42
|
<Text color="green">whoami</Text>
|
|
43
43
|
</Box>
|
|
44
|
-
<Text>Show current user and
|
|
44
|
+
<Text>Show current user (and linked store/org from ollie.json)</Text>
|
|
45
45
|
</Box>
|
|
46
46
|
<Box>
|
|
47
47
|
<Box width={24}>
|
|
@@ -153,6 +153,10 @@ export function HelpCommand() {
|
|
|
153
153
|
$ ollieshop store create --name "My Store" --platform vtex
|
|
154
154
|
--platform-store-id mystore
|
|
155
155
|
</Text>
|
|
156
|
+
<Text dimColor>
|
|
157
|
+
$ ollieshop store create --org UUID --name "My Store" --platform vtex
|
|
158
|
+
--platform-store-id mystore
|
|
159
|
+
</Text>
|
|
156
160
|
<Text dimColor>
|
|
157
161
|
$ ollieshop store create --data{" "}
|
|
158
162
|
{
|
package/src/commands/start.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServeOnRequestArgs } from "esbuild";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import open from "open";
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
6
6
|
import {
|
|
7
7
|
type ComponentInfo,
|
|
8
|
-
createBuildContext,
|
|
9
8
|
discoverComponents,
|
|
10
9
|
startDevServer,
|
|
11
10
|
} from "../utils/esbuild.js";
|
|
@@ -55,7 +54,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
55
54
|
const [buildCount, setBuildCount] = useState(0);
|
|
56
55
|
const [lastBuildTime, setLastBuildTime] = useState<Date | null>(null);
|
|
57
56
|
const logIdRef = useRef(0);
|
|
58
|
-
const
|
|
57
|
+
const rebuildRef = useRef<(() => Promise<void>) | null>(null);
|
|
59
58
|
const stopRef = useRef<(() => Promise<void>) | null>(null);
|
|
60
59
|
|
|
61
60
|
// Parse args
|
|
@@ -121,28 +120,19 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
121
120
|
setComponents(found);
|
|
122
121
|
setState({ status: "building" });
|
|
123
122
|
|
|
124
|
-
//
|
|
125
|
-
const
|
|
123
|
+
// Start dev server
|
|
124
|
+
const server = await startDevServer({
|
|
125
|
+
port: PORT,
|
|
126
126
|
stage,
|
|
127
|
+
onRequest: handleRequest,
|
|
127
128
|
onBuildEnd: (updatedComponents) => {
|
|
128
129
|
setComponents(updatedComponents);
|
|
129
130
|
setBuildCount((c) => c + 1);
|
|
130
131
|
setLastBuildTime(new Date());
|
|
131
132
|
},
|
|
132
133
|
});
|
|
133
|
-
ctxRef.current = ctx;
|
|
134
|
-
|
|
135
|
-
// Do initial build (manifest is written by the plugin)
|
|
136
|
-
await ctx.rebuild();
|
|
137
|
-
|
|
138
|
-
if (!mounted) return;
|
|
139
|
-
|
|
140
|
-
// Start dev server
|
|
141
|
-
const server = await startDevServer(ctx, {
|
|
142
|
-
port: PORT,
|
|
143
|
-
onRequest: handleRequest,
|
|
144
|
-
});
|
|
145
134
|
|
|
135
|
+
rebuildRef.current = server.rebuild;
|
|
146
136
|
stopRef.current = server.stop;
|
|
147
137
|
|
|
148
138
|
if (!mounted) {
|
|
@@ -190,7 +180,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
190
180
|
|
|
191
181
|
// Manual rebuild with 'r' (manifest is updated by the plugin)
|
|
192
182
|
if (input === "r" && state.status === "running") {
|
|
193
|
-
|
|
183
|
+
rebuildRef.current?.();
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
// Open Studio in browser with 'o'
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
} from "../utils/output.js";
|
|
8
8
|
import { type ParsedArgs, getFlag } from "../utils/parse-args.js";
|
|
9
9
|
import { getAuthenticatedClient } from "../utils/supabase.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
validateEnum,
|
|
12
|
+
validateRequired,
|
|
13
|
+
validateUuid,
|
|
14
|
+
} from "../utils/validate.js";
|
|
11
15
|
|
|
12
16
|
export async function storeCommand(parsed: ParsedArgs): Promise<void> {
|
|
13
17
|
const sub = parsed.subcommand;
|
|
@@ -63,13 +67,16 @@ async function storeCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
const orgFlag = getFlag(parsed.flags, "org");
|
|
71
|
+
const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
|
|
72
|
+
|
|
66
73
|
if (parsed.global.dryRun) {
|
|
67
|
-
outputDryRun("store.create", input, format);
|
|
74
|
+
outputDryRun("store.create", { ...input, orgId }, format);
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
const client = await getAuthenticatedClient();
|
|
72
|
-
const result = await createStore(client, input);
|
|
79
|
+
const result = await createStore(client, input, orgId);
|
|
73
80
|
|
|
74
81
|
outputResult(result, format, parsed.global.fields);
|
|
75
82
|
if (result.error) process.exit(1);
|
|
@@ -88,8 +95,11 @@ async function storeListCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
88
95
|
const format = detectOutputFormat(parsed.global.output);
|
|
89
96
|
|
|
90
97
|
try {
|
|
98
|
+
const orgFlag = getFlag(parsed.flags, "org");
|
|
99
|
+
const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
|
|
100
|
+
|
|
91
101
|
const client = await getAuthenticatedClient();
|
|
92
|
-
const result = await listStores(client);
|
|
102
|
+
const result = await listStores(client, orgId);
|
|
93
103
|
|
|
94
104
|
outputResult(result, format, parsed.global.fields);
|
|
95
105
|
if (result.error) process.exit(1);
|
package/src/commands/whoami.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { getStoreById } from "../core/store.js";
|
|
1
2
|
import { getCurrentUser } from "../utils/auth.js";
|
|
3
|
+
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
2
4
|
import { detectOutputFormat, outputResult } from "../utils/output.js";
|
|
3
5
|
import type { ParsedArgs } from "../utils/parse-args.js";
|
|
6
|
+
import { getAuthenticatedClient } from "../utils/supabase.js";
|
|
4
7
|
|
|
5
8
|
export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
|
|
6
9
|
const format = detectOutputFormat(parsed.global.output);
|
|
@@ -9,20 +12,78 @@ export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
9
12
|
if (!user) {
|
|
10
13
|
outputResult(
|
|
11
14
|
{
|
|
12
|
-
error: {
|
|
15
|
+
error: {
|
|
16
|
+
message: "Not logged in or session expired. Run `ollieshop login`.",
|
|
17
|
+
},
|
|
13
18
|
},
|
|
14
19
|
format,
|
|
15
20
|
);
|
|
16
21
|
process.exit(1);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
try {
|
|
25
|
+
const stage = resolveStage(parsed.global.stage);
|
|
26
|
+
const config = await loadConfig({ stage });
|
|
27
|
+
|
|
28
|
+
// No project config: just report the authenticated user.
|
|
29
|
+
if (!config?.storeId) {
|
|
30
|
+
outputResult(
|
|
31
|
+
{
|
|
32
|
+
data: {
|
|
33
|
+
email: user.email,
|
|
34
|
+
hint: "No ollie.json found. Run `ollieshop init` to link a store.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
format,
|
|
38
|
+
parsed.global.fields,
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Config present: resolve the store/org it points at, gated by access.
|
|
44
|
+
const client = await getAuthenticatedClient();
|
|
45
|
+
const result = await getStoreById(client, config.storeId);
|
|
46
|
+
|
|
47
|
+
if (result.error || !result.data) {
|
|
48
|
+
outputResult(
|
|
49
|
+
{
|
|
50
|
+
data: {
|
|
51
|
+
email: user.email,
|
|
52
|
+
storeId: config.storeId,
|
|
53
|
+
store: null,
|
|
54
|
+
note:
|
|
55
|
+
result.error?.message ??
|
|
56
|
+
"Store not found. Run `ollieshop login` or check the storeId in ollie.json.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
format,
|
|
60
|
+
parsed.global.fields,
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const store = result.data;
|
|
66
|
+
outputResult(
|
|
67
|
+
{
|
|
68
|
+
data: {
|
|
69
|
+
email: user.email,
|
|
70
|
+
orgId: store.organizationId,
|
|
71
|
+
org: store.organizationName,
|
|
72
|
+
storeId: store.id,
|
|
73
|
+
store: store.name,
|
|
74
|
+
...(config.versionId ? { versionId: config.versionId } : {}),
|
|
75
|
+
},
|
|
23
76
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
77
|
+
format,
|
|
78
|
+
parsed.global.fields,
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
outputResult(
|
|
82
|
+
{
|
|
83
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
84
|
+
},
|
|
85
|
+
format,
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
28
89
|
}
|
package/src/core/function.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { functionCreateSchema } from "./schema.js";
|
|
|
4
4
|
export interface CreateFunctionInput {
|
|
5
5
|
versionId: string;
|
|
6
6
|
name: string;
|
|
7
|
-
trigger?: string;
|
|
7
|
+
trigger?: { url: string; expression: string };
|
|
8
8
|
active?: boolean;
|
|
9
9
|
onError?: "throw" | "skip";
|
|
10
10
|
priority?: number;
|
package/src/core/schema.ts
CHANGED
|
@@ -65,11 +65,16 @@ export const functionCreateSchema = z.object({
|
|
|
65
65
|
versionId: z.string().uuid().describe("Parent version UUID"),
|
|
66
66
|
name: z.string().min(1).describe("Function name"),
|
|
67
67
|
trigger: z
|
|
68
|
-
.
|
|
69
|
-
|
|
68
|
+
.object({
|
|
69
|
+
url: z.string().min(1).describe("Trigger URL — absolute http(s) URL"),
|
|
70
|
+
expression: z
|
|
71
|
+
.string()
|
|
72
|
+
.min(1)
|
|
73
|
+
.describe("JSONata-like match expression (e.g. 'method in [\"GET\"]')"),
|
|
74
|
+
})
|
|
70
75
|
.optional()
|
|
71
76
|
.describe(
|
|
72
|
-
"Function trigger
|
|
77
|
+
"Function trigger — both url and expression are required together",
|
|
73
78
|
),
|
|
74
79
|
active: z
|
|
75
80
|
.boolean()
|
package/src/core/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { getOrganizationId } from "../utils/supabase.js";
|
|
2
|
+
import { getOrganizationId, unwrapToOne } from "../utils/supabase.js";
|
|
3
3
|
import { storeCreateSchema } from "./schema.js";
|
|
4
4
|
|
|
5
5
|
export interface CreateStoreInput {
|
|
@@ -24,6 +24,7 @@ export interface StoreRecord {
|
|
|
24
24
|
export async function createStore(
|
|
25
25
|
client: SupabaseClient,
|
|
26
26
|
input: CreateStoreInput,
|
|
27
|
+
orgId?: string,
|
|
27
28
|
): Promise<{ data?: { id: string }; error?: { message: string } }> {
|
|
28
29
|
// Validate input against schema
|
|
29
30
|
const parsed = storeCreateSchema.safeParse(input);
|
|
@@ -33,7 +34,7 @@ export async function createStore(
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const organizationId = await getOrganizationId(client);
|
|
37
|
+
const organizationId = await getOrganizationId(client, orgId);
|
|
37
38
|
|
|
38
39
|
const { data, error } = await client
|
|
39
40
|
.from("stores")
|
|
@@ -55,10 +56,56 @@ export async function createStore(
|
|
|
55
56
|
return { data: { id: data.id } };
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
export interface StoreWithOrg {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
organizationId: string;
|
|
63
|
+
organizationName: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolves a store + org by id; RLS returns no row when the user lacks access.
|
|
67
|
+
export async function getStoreById(
|
|
68
|
+
client: SupabaseClient,
|
|
69
|
+
storeId: string,
|
|
70
|
+
): Promise<{ data?: StoreWithOrg; error?: { message: string } }> {
|
|
71
|
+
const { data, error } = await client
|
|
72
|
+
.from("stores")
|
|
73
|
+
.select("id, name, organization_id, organizations(id, name)")
|
|
74
|
+
.eq("id", storeId)
|
|
75
|
+
.maybeSingle();
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
return { error: { message: error.message } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!data) {
|
|
82
|
+
return {
|
|
83
|
+
error: {
|
|
84
|
+
message: `Store ${storeId} not found or you don't have access to it.`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const org = unwrapToOne<{ name?: string }>(
|
|
90
|
+
(data as { organizations?: unknown }).organizations,
|
|
91
|
+
);
|
|
92
|
+
const orgName = org?.name ?? null;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
data: {
|
|
96
|
+
id: data.id as string,
|
|
97
|
+
name: data.name as string,
|
|
98
|
+
organizationId: data.organization_id as string,
|
|
99
|
+
organizationName: orgName,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
export async function listStores(
|
|
59
105
|
client: SupabaseClient,
|
|
106
|
+
orgId?: string,
|
|
60
107
|
): Promise<{ data?: StoreRecord[]; error?: { message: string } }> {
|
|
61
|
-
const organizationId = await getOrganizationId(client);
|
|
108
|
+
const organizationId = await getOrganizationId(client, orgId);
|
|
62
109
|
|
|
63
110
|
const { data, error } = await client
|
|
64
111
|
.from("stores")
|
package/src/utils/auth.ts
CHANGED
|
@@ -231,6 +231,10 @@ export async function getCurrentUser(): Promise<{ email: string } | null> {
|
|
|
231
231
|
|
|
232
232
|
try {
|
|
233
233
|
const decoded = jwtDecode<JwtPayload>(credentials.accessToken);
|
|
234
|
+
// Reject expired sessions (local `exp` check; misses server-side revocation).
|
|
235
|
+
if (decoded.exp && decoded.exp * 1000 <= Date.now()) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
234
238
|
return decoded.email ? { email: decoded.email } : null;
|
|
235
239
|
} catch {
|
|
236
240
|
return null;
|
package/src/utils/esbuild.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import http from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
@@ -221,32 +222,95 @@ export interface ServeResult {
|
|
|
221
222
|
/**
|
|
222
223
|
* Starts the esbuild serve + watch mode with a proxy server.
|
|
223
224
|
* The proxy handles /bundle/:componentName requests and forwards others to esbuild.
|
|
224
|
-
*
|
|
225
|
+
* Owns the esbuild context lifecycle and recreates it when a component folder is
|
|
226
|
+
* added or removed. Returns the server info, a manual rebuild, and a way to stop it.
|
|
225
227
|
*/
|
|
226
228
|
export async function startDevServer(
|
|
227
|
-
ctx: esbuild.BuildContext,
|
|
228
229
|
options: {
|
|
229
230
|
port?: number;
|
|
230
231
|
host?: string;
|
|
231
232
|
cwd?: string;
|
|
233
|
+
stage?: string;
|
|
232
234
|
onRequest?: (args: esbuild.ServeOnRequestArgs) => void;
|
|
233
|
-
|
|
235
|
+
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
234
236
|
} = {},
|
|
235
|
-
): Promise<
|
|
236
|
-
|
|
237
|
+
): Promise<
|
|
238
|
+
ServeResult & { rebuild: () => Promise<void>; stop: () => Promise<void> }
|
|
239
|
+
> {
|
|
240
|
+
const {
|
|
241
|
+
port = 4000,
|
|
242
|
+
host = "localhost",
|
|
243
|
+
cwd = process.cwd(),
|
|
244
|
+
stage,
|
|
245
|
+
onRequest,
|
|
246
|
+
onBuildEnd,
|
|
247
|
+
} = options;
|
|
237
248
|
|
|
238
249
|
const servedir = path.join(cwd, "node_modules/.ollie", "build");
|
|
250
|
+
const componentsDir = path.join(cwd, "components");
|
|
251
|
+
const internalPort = port + 1;
|
|
239
252
|
|
|
240
|
-
|
|
241
|
-
|
|
253
|
+
let ctx: esbuild.BuildContext | null = null;
|
|
254
|
+
let entryNames = new Set<string>();
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
servedir,
|
|
249
|
-
|
|
256
|
+
async function buildAndServe(components: ComponentInfo[]): Promise<void> {
|
|
257
|
+
entryNames = new Set(components.map((c) => c.name));
|
|
258
|
+
ctx = await createBuildContext(components, { cwd, stage, onBuildEnd });
|
|
259
|
+
await ctx.rebuild();
|
|
260
|
+
await ctx.watch();
|
|
261
|
+
await ctx.serve({ port: internalPort, host, servedir, onRequest });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function recreate(): Promise<void> {
|
|
265
|
+
const components = await discoverComponents({ cwd, stage });
|
|
266
|
+
const oldCtx = ctx;
|
|
267
|
+
ctx = null;
|
|
268
|
+
if (oldCtx) await oldCtx.dispose();
|
|
269
|
+
await buildAndServe(components);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await buildAndServe(await discoverComponents({ cwd, stage }));
|
|
273
|
+
|
|
274
|
+
async function currentComponentNames(): Promise<Set<string>> {
|
|
275
|
+
try {
|
|
276
|
+
const entries = await glob("*/index.tsx", { cwd: componentsDir });
|
|
277
|
+
return new Set(entries.map((e) => path.dirname(e)));
|
|
278
|
+
} catch {
|
|
279
|
+
return new Set();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Recreate the context when the set of component folders changes
|
|
284
|
+
let watchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
285
|
+
let recreating = false;
|
|
286
|
+
let pending = false;
|
|
287
|
+
|
|
288
|
+
async function maybeRecreate(): Promise<void> {
|
|
289
|
+
if (recreating) {
|
|
290
|
+
pending = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
recreating = true;
|
|
294
|
+
try {
|
|
295
|
+
do {
|
|
296
|
+
pending = false;
|
|
297
|
+
const names = await currentComponentNames();
|
|
298
|
+
const changed =
|
|
299
|
+
names.size !== entryNames.size ||
|
|
300
|
+
[...names].some((n) => !entryNames.has(n));
|
|
301
|
+
if (!changed) break;
|
|
302
|
+
await recreate();
|
|
303
|
+
} while (pending);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error("[DevServer] Failed to reload components:", err);
|
|
306
|
+
} finally {
|
|
307
|
+
recreating = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const componentsWatcher = watch(componentsDir, { recursive: true }, () => {
|
|
312
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
313
|
+
watchTimer = setTimeout(maybeRecreate, 150);
|
|
250
314
|
});
|
|
251
315
|
|
|
252
316
|
// Create proxy server that handles /bundle/* and forwards to esbuild
|
|
@@ -356,6 +420,21 @@ export async function startDevServer(
|
|
|
356
420
|
// File doesn't exist, start fresh
|
|
357
421
|
}
|
|
358
422
|
|
|
423
|
+
if (
|
|
424
|
+
typeof updates.id === "string" &&
|
|
425
|
+
typeof existingMeta.id === "string" &&
|
|
426
|
+
existingMeta.id !== updates.id
|
|
427
|
+
) {
|
|
428
|
+
res.statusCode = 409;
|
|
429
|
+
res.setHeader("Content-Type", "application/json");
|
|
430
|
+
res.end(
|
|
431
|
+
JSON.stringify({
|
|
432
|
+
error: `id mismatch: ${componentName} is linked to ${existingMeta.id}`,
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
359
438
|
// Merge updates
|
|
360
439
|
const newMeta = { ...existingMeta, ...updates };
|
|
361
440
|
|
|
@@ -435,9 +514,14 @@ export async function startDevServer(
|
|
|
435
514
|
return {
|
|
436
515
|
host,
|
|
437
516
|
port,
|
|
517
|
+
rebuild: async () => {
|
|
518
|
+
await ctx?.rebuild();
|
|
519
|
+
},
|
|
438
520
|
stop: async () => {
|
|
521
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
522
|
+
componentsWatcher.close();
|
|
439
523
|
proxyServer.close();
|
|
440
|
-
await ctx
|
|
524
|
+
await ctx?.dispose();
|
|
441
525
|
},
|
|
442
526
|
};
|
|
443
527
|
}
|
package/src/utils/parse-args.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface GlobalFlags {
|
|
|
3
3
|
dryRun: boolean;
|
|
4
4
|
fields?: string[];
|
|
5
5
|
data?: string;
|
|
6
|
+
stage?: string;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export interface ParsedArgs {
|
|
@@ -63,6 +64,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
63
64
|
.map((f) => f.trim())
|
|
64
65
|
: undefined,
|
|
65
66
|
data: (flags.data as string) || (flags.d as string) || undefined,
|
|
67
|
+
stage: (flags.stage as string) || (flags.s as string) || undefined,
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
return { command, subcommand, flags, global, positional };
|