Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard, ui): Support TypeScript in the playground #919

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/orange-items-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lagon/dashboard': patch
'@lagon/ui': patch
---

feat: add playground typescript
2 changes: 2 additions & 0 deletions crates/cli/src/utils/deployments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ struct Asset {
struct CreateDeploymentRequest {
function_id: String,
function_size: usize,
platform: String,
assets: Vec<Asset>,
}

Expand Down Expand Up @@ -410,6 +411,7 @@ pub async fn create_deployment(
CreateDeploymentRequest {
function_id: function_config.function_id.clone(),
function_size: index.len(),
platform: "CLI".into(),
assets: assets
.iter()
.map(|(key, value)| Asset {
Expand Down
17 changes: 17 additions & 0 deletions packages/dashboard/lib/api/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export async function createDeployment(
func: {
id: string;
},
platform: 'CLI' | 'Playground',
assets: string[],
triggerer: string,
): Promise<{
Expand All @@ -26,6 +27,7 @@ export async function createDeployment(
isProduction: false,
assets,
functionId: func.id,
platform,
triggerer,
},
select: {
Expand Down Expand Up @@ -397,6 +399,21 @@ async function streamToString(stream: Readable): Promise<string> {
}

export async function getDeploymentCode(deploymentId: string) {
try {
const tsContent = await s3.send(
new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `${deploymentId}.ts`,
}),
);

if (tsContent.Body instanceof Readable) {
return streamToString(tsContent.Body);
}
} catch (e) {
console.warn(`${deploymentId} haven't ts file, e: ${(e as Error).message}`);
}

const content = await s3.send(
new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Expand Down
4 changes: 4 additions & 0 deletions packages/dashboard/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ export type Regions = keyof typeof REGIONS;
export const DEFAULT_FUNCTION = `export function handler(request) {
return new Response("Hello World!")
}`;

export const DEFAULT_TS_FUNCTION = `export function handler(request: Request) {
return new Response("Hello World!")
}`;
124 changes: 124 additions & 0 deletions packages/dashboard/lib/hooks/useEsbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as esbuild from 'esbuild-wasm';
import { Plugin, Loader } from 'esbuild-wasm';
import { useCallback, useEffect, useState } from 'react';

type EsbuildFileSystem = Map<
string,
{
content: string;
}
>;

const PROJECT_ROOT = '/project/';

const RESOLVE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.css', '.json'];

const extname = (path: string): string => {
const m = /(\.[a-zA-Z0-9]+)$/.exec(path);
return m ? m[1] : '';
};

const inferLoader = (p: string): Loader => {
const ext = extname(p);
if (RESOLVE_EXTENSIONS.includes(ext)) {
return ext.slice(1) as Loader;
}
if (ext === '.mjs' || ext === '.cjs') {
return 'js';
}
return 'text';
};

const resolvePlugin = (files: EsbuildFileSystem): Plugin => {
return {
name: 'resolve',
setup(build) {
build.onResolve({ filter: /.*/ }, async args => {
if (args.path.startsWith(PROJECT_ROOT)) {
return {
path: args.path,
};
}
});

build.onLoad({ filter: /.*/ }, args => {
if (args.path.startsWith(PROJECT_ROOT)) {
const name = args.path.replace(PROJECT_ROOT, '');
const file = files.get(name);
if (file) {
return {
contents: file.content,
loader: inferLoader(args.path),
};
}
}
});
},
};
};

export enum ESBuildStatus {
Success,
Fail,
Loading,
}

class EsBuildSingleton {
static isFirst = true;
static getIsFirst = () => {
if (EsBuildSingleton.isFirst) {
EsBuildSingleton.isFirst = false;
return true;
}
return EsBuildSingleton.isFirst;
};
}

export const useEsbuild = () => {
const [esbuildStatus, setEsbuildStatus] = useState(ESBuildStatus.Loading);
const [isEsbuildLoading, setIsEsbuildLoading] = useState(true);

// React.StrictMode will cause useEffect to run twice
const loadEsbuild = useCallback(async () => {
try {
if (EsBuildSingleton.getIsFirst()) {
await esbuild.initialize({
wasmURL: `https://esm.sh/[email protected]/esbuild.wasm`,
});
}

setEsbuildStatus(ESBuildStatus.Success);
} catch (e) {
setEsbuildStatus(ESBuildStatus.Fail);
console.error(e);
} finally {
setIsEsbuildLoading(false);
}
}, [isEsbuildLoading, esbuildStatus]);

// these options should match the ones in crates/cli/src/utils/deployments.rs
const build = useCallback(
(files: EsbuildFileSystem) =>
esbuild.build({
QuiiBz marked this conversation as resolved.
Show resolved Hide resolved
entryPoints: [`${PROJECT_ROOT}index.ts`],
outdir: '/dist',
format: 'esm',
write: false,
bundle: true,
target: 'esnext',
platform: 'browser',
conditions: ['lagon', 'worker'],
define: { 'process.env.NODE_ENV': 'production' },
plugins: [resolvePlugin(files)],
QuiiBz marked this conversation as resolved.
Show resolved Hide resolved
}),
[isEsbuildLoading, esbuildStatus],
);

useEffect(() => {
loadEsbuild();
}, []);

return { isEsbuildLoading, esbuildStatus, build };
};

export default useEsbuild;
9 changes: 9 additions & 0 deletions packages/dashboard/lib/trpc/deploymentsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const deploymentsRouter = (t: T) =>
z.object({
functionId: z.string(),
functionSize: z.number(),
tsSize: z.number(),
platform: z.enum(['CLI', 'Playground']),
assets: z
.object({
name: z.string(),
Expand All @@ -50,6 +52,7 @@ export const deploymentsRouter = (t: T) =>
{
id: input.functionId,
},
input.platform,
input.assets.map(({ name }) => name),
ctx.session.user.email,
);
Expand All @@ -67,6 +70,11 @@ export const deploymentsRouter = (t: T) =>
};

const codeUrl = await getPresignedUrl(`${deployment.id}.js`, input.functionSize);

let tsCodeUrl: string | undefined;
if (input.tsSize > 0) {
tsCodeUrl = await getPresignedUrl(`${deployment.id}.ts`, input.tsSize);
}
const assetsUrls: Record<string, string> = {};

await Promise.all(
Expand All @@ -80,6 +88,7 @@ export const deploymentsRouter = (t: T) =>
deploymentId: deployment.id,
codeUrl,
assetsUrls,
tsCodeUrl,
};
}),
deploymentDeploy: t.procedure
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/lib/trpc/functionsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const functionsRouter = (t: T) =>
isProduction: true,
commit: true,
triggerer: true,
platform: true,
},
orderBy: {
createdAt: 'desc',
Expand Down
2 changes: 2 additions & 0 deletions packages/dashboard/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export default {
'playground.deploy.success': 'Function deployed successfully.',
'playground.deploy.error': 'Failed to deploy Function.',
'playground.reload': 'Reload',
'playground.esbuild.error': `Since your browser doesn't support wasm, you can't use typescript.`,
'playground.esbuild.loading': 'Initializing esbuild',

'function.nav.playground': 'Playground',
'function.nav.overview': 'Overview',
Expand Down
2 changes: 2 additions & 0 deletions packages/dashboard/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export default defineLocale({
'function.nav.logs': 'Logs',
'function.nav.settings': 'Paramètres',
'function.nav.cron': 'Cron',
'playground.esbuild.error': `Étant donné que votre navigateur ne prend pas en charge wasm, vous ne pouvez pas utiliser le typescript.`,
'playground.esbuild.loading': `esbuild est en cours d'initialisation`,

'functions.overview.usage': 'Utilisation & Limites',
'functions.overview.usage.requests': 'Requêtes',
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@trpc/react-query": "^10.18.0",
"@trpc/server": "^10.18.0",
"clickhouse": "^2.6.0",
"esbuild-wasm": "0.17.19",
"cron-parser": "^4.8.1",
"cronstrue": "^2.27.0",
"final-form": "^4.20.7",
Expand Down
14 changes: 9 additions & 5 deletions packages/dashboard/pages/functions/[functionId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ const Function = () => {
) : null}
<Nav defaultValue="overview">
<Nav.List
rightItem={
<Button href={`/playground/${func?.id}`} leftIcon={<PlayIcon className="h-4 w-4" />}>
{t('playground')}
</Button>
}
{...(func?.deployments?.filter(item => item.isProduction)?.[0]?.platform === 'Playground'
? {
rightItem: (
<Button href={`/playground/${func?.id}`} leftIcon={<PlayIcon className="h-4 w-4" />}>
{t('playground')}
</Button>
),
}
: {})}
>
<Nav.Link value="overview">{t('overview')}</Nav.Link>
<Nav.Link value="deployments">{t('deployments')}</Nav.Link>
Expand Down
27 changes: 19 additions & 8 deletions packages/dashboard/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { trpc } from 'lib/trpc';
import { useRouter } from 'next/router';
import { getLocaleProps, useScopedI18n } from 'locales';
import { GetStaticProps } from 'next';
import { DEFAULT_FUNCTION } from 'lib/constants';
import { DEFAULT_FUNCTION, DEFAULT_TS_FUNCTION } from 'lib/constants';

const Home = () => {
const createFunction = trpc.functionCreate.useMutation();
Expand Down Expand Up @@ -35,16 +35,27 @@ const Home = () => {
const deployment = await createDeployment.mutateAsync({
functionId: func.id,
functionSize: new TextEncoder().encode(DEFAULT_FUNCTION).length,
tsSize: new TextEncoder().encode(DEFAULT_TS_FUNCTION).length,
platform: 'Playground',
assets: [],
});

await fetch(deployment.codeUrl, {
method: 'PUT',
headers: {
'Content-Type': 'text/javascript',
},
body: DEFAULT_FUNCTION,
});
await Promise.all([
fetch(deployment.codeUrl, {
method: 'PUT',
headers: {
'Content-Type': 'text/javascript',
},
body: DEFAULT_FUNCTION,
}),
fetch(deployment.tsCodeUrl!, {
method: 'PUT',
headers: {
'Content-Type': 'text/javascript',
},
body: DEFAULT_TS_FUNCTION,
}),
]);

await deployDeployment.mutateAsync({
functionId: func.id,
Expand Down
Loading