@locale-labs/miniapp-builder 0.1.9 → 0.2.0-dev.54.7fe6f45
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/configs/eslint.config.mjs +22 -0
- package/configs/prettier.json +7 -0
- package/configs/tailwind.config.cjs +11 -0
- package/configs/tsconfig.base.json +18 -0
- package/dist/index.js +10512 -10386
- package/package.json +12 -6
- package/templates/functions/_shared/client.ts +42 -0
- package/templates/functions/_shared/cors.ts +5 -0
- package/templates/functions/_shared/responses.ts +45 -0
- package/templates/functions/deploy_miniapp/deno.json +5 -0
- package/templates/functions/deploy_miniapp/deploy_miniapp.ts +169 -0
- package/templates/functions/deploy_miniapp/index.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@locale-labs/miniapp-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.54.7fe6f45",
|
|
4
4
|
"description": "Build tool for Localé Mini-Apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -11,28 +11,34 @@
|
|
|
11
11
|
"locale-miniapp-builder": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"dist"
|
|
14
|
+
"dist",
|
|
15
|
+
"configs",
|
|
16
|
+
"templates"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=cjs --banner:js=\"#!/usr/bin/env node\" --external:esbuild",
|
|
19
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=cjs --banner:js=\"#!/usr/bin/env node\" --external:esbuild --external:tailwindcss",
|
|
18
20
|
"prepublishOnly": "bun run build",
|
|
19
21
|
"security-check": "bunx @socketsecurity/cli ci"
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|
|
24
|
+
"@eslint/js": "^9.0.0",
|
|
22
25
|
"@locale-labs/miniapp-sdk": "0.2.0-dev.2.095bf24",
|
|
23
|
-
"@types/html-minifier-terser": "7.0.2",
|
|
24
26
|
"autoprefixer": "10.4.23",
|
|
27
|
+
"esbuild": "0.27.2",
|
|
28
|
+
"eslint-config-prettier": "^9.1.0",
|
|
29
|
+
"globals": "^15.0.0",
|
|
25
30
|
"html-minifier-terser": "7.2.0",
|
|
26
31
|
"postcss": "8.5.6",
|
|
27
32
|
"posthtml": "0.16.7",
|
|
28
33
|
"posthtml-include": "2.0.1",
|
|
29
|
-
"tailwindcss": "4.
|
|
34
|
+
"tailwindcss": "^3.4.3",
|
|
35
|
+
"typescript-eslint": "^8.0.0"
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
32
38
|
"@semantic-release/changelog": "6.0.3",
|
|
39
|
+
"@types/html-minifier-terser": "7.0.2",
|
|
33
40
|
"@semantic-release/git": "10.0.1",
|
|
34
41
|
"@types/node": "25.0.10",
|
|
35
|
-
"esbuild": "0.27.2",
|
|
36
42
|
"semantic-release": "25.0.2",
|
|
37
43
|
"typescript": "^5.0.0"
|
|
38
44
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Follow this setup guide to integrate the Deno language server with your editor:
|
|
2
|
+
// https://deno.land/manual/getting_started/setup_your_environment
|
|
3
|
+
|
|
4
|
+
// Setup type definitions for built-in Supabase Runtime APIs
|
|
5
|
+
// deno-lint-ignore no-unversioned-import no-import-prefix
|
|
6
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
7
|
+
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
8
|
+
|
|
9
|
+
import { response500_internal_server_error } from "./responses.ts";
|
|
10
|
+
|
|
11
|
+
export const createSupabaseAdminClient = ():
|
|
12
|
+
| {
|
|
13
|
+
error: null;
|
|
14
|
+
client: SupabaseClient;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
error: Response;
|
|
18
|
+
client: null;
|
|
19
|
+
} => {
|
|
20
|
+
try {
|
|
21
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
|
22
|
+
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
|
23
|
+
|
|
24
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
25
|
+
return {
|
|
26
|
+
error: response500_internal_server_error(
|
|
27
|
+
new Error("Missing Supabase environment variables")
|
|
28
|
+
),
|
|
29
|
+
client: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
error: null,
|
|
35
|
+
client: createClient(supabaseUrl, serviceRoleKey, {
|
|
36
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { error: response500_internal_server_error(error), client: null };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Nota: aca solo se usaba el TYPE `PostgrestError` (union `PostgrestError | unknown`),
|
|
2
|
+
// union que colapsa a `unknown` (sin efecto). La dependencia @supabase/supabase-js
|
|
3
|
+
// SI es real y sigue en uso: client.ts la importa (createClient) y se resuelve via
|
|
4
|
+
// el import map de deno.json (jsr:@supabase/supabase-js@2).
|
|
5
|
+
import { corsHeaders } from "./cors.ts";
|
|
6
|
+
|
|
7
|
+
const headers = { ...corsHeaders, "Content-Type": "application/json" };
|
|
8
|
+
|
|
9
|
+
const errorMessageOrUnknownError = (error: unknown) =>
|
|
10
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
11
|
+
|
|
12
|
+
export const response200 = (data: unknown) =>
|
|
13
|
+
new Response(JSON.stringify(data), {
|
|
14
|
+
headers,
|
|
15
|
+
status: 200,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const response400_bad_request = (error: unknown) =>
|
|
19
|
+
new Response(JSON.stringify({ error: errorMessageOrUnknownError(error) }), {
|
|
20
|
+
headers,
|
|
21
|
+
status: 400,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const response401_unauthorized = () =>
|
|
25
|
+
new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
26
|
+
headers,
|
|
27
|
+
status: 401,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const response405_method_not_allowed = () =>
|
|
31
|
+
new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
32
|
+
headers,
|
|
33
|
+
status: 405,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const response500_internal_server_error = (error: unknown) =>
|
|
37
|
+
new Response(JSON.stringify({ error: errorMessageOrUnknownError(error) }), {
|
|
38
|
+
headers,
|
|
39
|
+
status: 500,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export const response_cors_preflight = () =>
|
|
45
|
+
new Response("ok", { headers: corsHeaders });
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// TODO: next step -> extract all of this code to outside miniapps (easy to maintain and error prone code)
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
response400_bad_request,
|
|
5
|
+
response500_internal_server_error,
|
|
6
|
+
response401_unauthorized,
|
|
7
|
+
} from "../_shared/responses.ts";
|
|
8
|
+
import { createSupabaseAdminClient } from "../_shared/client.ts";
|
|
9
|
+
|
|
10
|
+
interface DeployMiniappParams {
|
|
11
|
+
version: string;
|
|
12
|
+
git_sha?: string | null;
|
|
13
|
+
html_content: string;
|
|
14
|
+
env?: "dev" | "prod";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const deployMiniapp = async ({
|
|
18
|
+
req,
|
|
19
|
+
}: {
|
|
20
|
+
req: Request;
|
|
21
|
+
}): Promise<
|
|
22
|
+
| {
|
|
23
|
+
error: null;
|
|
24
|
+
data: { success: boolean; message: string; url: string };
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
error: Response;
|
|
28
|
+
data: null;
|
|
29
|
+
}
|
|
30
|
+
> => {
|
|
31
|
+
// ── Get request body ─────────────────────────────────────────────────────
|
|
32
|
+
let body: DeployMiniappParams;
|
|
33
|
+
try {
|
|
34
|
+
body = await req.json();
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
return {
|
|
37
|
+
data: null,
|
|
38
|
+
error: response400_bad_request(new Error("Invalid JSON body")),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { version, git_sha, html_content, env = "prod" } = body;
|
|
43
|
+
const registeredVersion = git_sha ? `${version}+${git_sha}` : version;
|
|
44
|
+
|
|
45
|
+
if (!version || !html_content) {
|
|
46
|
+
return {
|
|
47
|
+
data: null,
|
|
48
|
+
error: response400_bad_request(
|
|
49
|
+
new Error("Missing version or html_content")
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Validate API Key (Deploy Token) ──────────────────────────────────────
|
|
55
|
+
const apiKey = req.headers.get("x-miniapp-api-key");
|
|
56
|
+
const validApiKey = Deno.env.get("MINIAPP_API_KEY");
|
|
57
|
+
|
|
58
|
+
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
|
59
|
+
return {
|
|
60
|
+
data: null,
|
|
61
|
+
error: response401_unauthorized(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Get Supabase admin client (MiniApp) ──────────────────────────────────
|
|
66
|
+
const { error: adminError, client: supabaseAdmin } =
|
|
67
|
+
createSupabaseAdminClient();
|
|
68
|
+
if (adminError) {
|
|
69
|
+
return { data: null, error: adminError };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Upload to Storage ────────────────────────────────────────────────────
|
|
73
|
+
const storagePath = `builds/${registeredVersion}/index.html`;
|
|
74
|
+
const { error: uploadError } = await supabaseAdmin.storage
|
|
75
|
+
.from("miniapp-builds")
|
|
76
|
+
.upload(storagePath, html_content, {
|
|
77
|
+
contentType: "text/html",
|
|
78
|
+
upsert: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (uploadError) {
|
|
82
|
+
return {
|
|
83
|
+
data: null,
|
|
84
|
+
error: response500_internal_server_error(uploadError),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
data: { publicUrl },
|
|
90
|
+
} = supabaseAdmin.storage.from("miniapp-builds").getPublicUrl(storagePath);
|
|
91
|
+
|
|
92
|
+
// ── Register with both Cores ────────────────────────────────────────────
|
|
93
|
+
const coreDevUrl = Deno.env.get("CORE_DEV_SUPABASE_URL");
|
|
94
|
+
const coreDevAnonKey = Deno.env.get("CORE_DEV_SUPABASE_ANON_PUBLIC");
|
|
95
|
+
const coreProdUrl = Deno.env.get("CORE_PROD_SUPABASE_URL");
|
|
96
|
+
const coreProdAnonKey = Deno.env.get("CORE_PROD_SUPABASE_ANON_PUBLIC");
|
|
97
|
+
const coreDeploySecret = Deno.env.get("MINIAPP_DEPLOY_SECRET");
|
|
98
|
+
const slug = Deno.env.get("MINIAPP_SLUG");
|
|
99
|
+
const name = Deno.env.get("MINIAPP_NAME") ?? slug ?? "Mini-app";
|
|
100
|
+
|
|
101
|
+
const cores = [
|
|
102
|
+
{ name: "core-dev", url: coreDevUrl, anonKey: coreDevAnonKey },
|
|
103
|
+
{ name: "core-prod", url: coreProdUrl, anonKey: coreProdAnonKey },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const registerFailures: string[] = [];
|
|
107
|
+
|
|
108
|
+
for (const core of cores) {
|
|
109
|
+
if (!core.url || !core.anonKey) {
|
|
110
|
+
registerFailures.push(`${core.name}: missing URL or anon key`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(
|
|
116
|
+
`${core.url}/functions/v1/register_miniapp_version`,
|
|
117
|
+
{
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
Authorization: `Bearer ${core.anonKey}`,
|
|
122
|
+
"x-deploy-secret": coreDeploySecret || "",
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
slug,
|
|
126
|
+
version: registeredVersion,
|
|
127
|
+
url: publicUrl,
|
|
128
|
+
name,
|
|
129
|
+
env,
|
|
130
|
+
}),
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
registerFailures.push(`${core.name}: HTTP ${response.status} ${text}`);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
registerFailures.push(
|
|
140
|
+
`${core.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fail the deploy if registration with any core failed. The HTML is already
|
|
146
|
+
// in storage, but a green deploy that didn't update the core DB is a silent
|
|
147
|
+
// failure — surface it so the CI job fails.
|
|
148
|
+
if (registerFailures.length > 0) {
|
|
149
|
+
return {
|
|
150
|
+
data: null,
|
|
151
|
+
error: response500_internal_server_error(
|
|
152
|
+
new Error(
|
|
153
|
+
`Uploaded to storage but failed to register version with core(s): ${registerFailures.join(
|
|
154
|
+
"; "
|
|
155
|
+
)}`
|
|
156
|
+
)
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
data: {
|
|
163
|
+
success: true,
|
|
164
|
+
message: "Miniapp deployed successfully",
|
|
165
|
+
url: publicUrl,
|
|
166
|
+
},
|
|
167
|
+
error: null,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response_cors_preflight,
|
|
3
|
+
response200,
|
|
4
|
+
response405_method_not_allowed,
|
|
5
|
+
response500_internal_server_error,
|
|
6
|
+
} from "../_shared/responses.ts";
|
|
7
|
+
|
|
8
|
+
import { deployMiniapp } from "./deploy_miniapp.ts";
|
|
9
|
+
|
|
10
|
+
// Setup type definitions for built-in Supabase Runtime APIs
|
|
11
|
+
// deno-lint-ignore no-unversioned-import no-import-prefix
|
|
12
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
13
|
+
|
|
14
|
+
Deno.serve(async (req) => {
|
|
15
|
+
if (req.method === "OPTIONS") {
|
|
16
|
+
return response_cors_preflight();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (req.method !== "POST") {
|
|
20
|
+
return response405_method_not_allowed();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { data, error } = await deployMiniapp({ req });
|
|
25
|
+
if (error) return error;
|
|
26
|
+
|
|
27
|
+
return response200(data);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return response500_internal_server_error(error);
|
|
30
|
+
}
|
|
31
|
+
});
|