Skip to content

Commit 8424c4d

Browse files
committed
More fixes
1 parent 599f418 commit 8424c4d

4 files changed

Lines changed: 86 additions & 10 deletions

File tree

apps/backend/src/lib/workflows.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ import { Freestyle } from "./freestyle";
1515
import { Tenancy } from "./tenancies";
1616
import { upstash } from "./upstash";
1717

18-
const externalPackages = {
19-
'@stackframe/stack': 'latest',
20-
};
18+
const externalPackages: Record<string, string> = {};
2119

2220
type WorkflowRegisteredTriggerType = "sign-up";
2321

@@ -56,7 +54,9 @@ export async function compileWorkflowSource(source: string): Promise<Result<stri
5654
const bundleResult = await bundleJavaScript({
5755
"/source.tsx": source,
5856
"/entry.js": `
59-
import { StackServerApp } from '@stackframe/stack';
57+
import { StackServerApp } from 'https://esm.sh/@stackframe/js@2.8.36?target=es2021&standalone';
58+
59+
globalThis.navigator.onLine = true;
6060
6161
export default async () => {
6262
globalThis.stackApp = new StackServerApp({
@@ -125,6 +125,7 @@ export async function compileWorkflowSource(source: string): Promise<Result<stri
125125
}, {
126126
format: 'esm',
127127
keepAsImports: Object.keys(externalPackages),
128+
allowHttpImports: true,
128129
});
129130
if (bundleResult.status === "error") {
130131
return Result.error(bundleResult.error);
@@ -145,7 +146,7 @@ async function compileWorkflow(tenancy: Tenancy, workflowId: string): Promise<Re
145146
return Result.error({ compileError: `Failed to compile workflow: ${compiledCodeResult.error}` });
146147
}
147148

148-
console.log(`Compiled workflow source for ${workflowId}, running compilation trigger...`, { compiledCodeResult });
149+
console.log(`Compiled workflow source for ${workflowId}, running compilation trigger...`, { compiledCodeLength: compiledCodeResult.data.length });
149150

150151
const compileTriggerResult = await triggerWorkflowRaw(tenancy, compiledCodeResult.data, {
151152
type: "compile",
@@ -154,7 +155,7 @@ async function compileWorkflow(tenancy: Tenancy, workflowId: string): Promise<Re
154155
return Result.error({ compileError: `Failed to initialize workflow: ${compileTriggerResult.error}` });
155156
}
156157

157-
console.log(`Compilation trigger result:`, { compileTriggerResult });
158+
console.log(`Compilation trigger completed!`);
158159

159160
const compileTriggerOutputResult = compileTriggerResult.data;
160161
if (typeof compileTriggerOutputResult !== "object" || !compileTriggerOutputResult || !("triggerOutput" in compileTriggerOutputResult)) {
@@ -167,7 +168,7 @@ async function compileWorkflow(tenancy: Tenancy, workflowId: string): Promise<Re
167168
return Result.error({ compileError: `Failed to parse compile trigger output, should be array of strings` });
168169
}
169170

170-
console.log(`Workflow ${workflowId} compiled successfully, returning result...`, { registeredTriggers, compiledCodeResult });
171+
console.log(`Workflow ${workflowId} compiled successfully, returning result...`, { registeredTriggers });
171172

172173
return Result.ok({
173174
compiledCode: compiledCodeResult.data,
@@ -403,16 +404,24 @@ async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string
403404

404405
try {
405406
const freestyle = new Freestyle();
407+
const apiUrl = new URL("/", getEnvVariable("NEXT_PUBLIC_STACK_API_URL").replace("http://localhost", "http://host.docker.internal"));
406408
const freestyleRes = await freestyle.executeScript(compiledWorkflowCode, {
407409
envVars: {
408410
STACK_WORKFLOW_TRIGGER_DATA: JSON.stringify(trigger),
409411
NEXT_PUBLIC_STACK_PROJECT_ID: tenancy.project.id,
410-
NEXT_PUBLIC_STACK_API_URL: getEnvVariable("NEXT_PUBLIC_STACK_API_URL").replace("http://localhost", "http://host.docker.internal"), // the replace is a hardcoded hack for the Freestyle mock server
412+
NEXT_PUBLIC_STACK_API_URL: apiUrl.toString(),
411413
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: "<placeholder publishable client key; the actual auth happens with the workflow token>",
412414
STACK_SECRET_SERVER_KEY: "<placeholder secret server key; the actual auth happens with the workflow token>",
413415
STACK_WORKFLOW_TOKEN_SECRET: workflowToken,
414416
},
415417
nodeModules: Object.fromEntries(Object.entries(externalPackages).map(([packageName, version]) => [packageName, version])),
418+
networkPermissions: [
419+
{
420+
action: "allow",
421+
behavior: "exact",
422+
query: apiUrl.host,
423+
},
424+
],
416425
});
417426
return Result.map(freestyleRes, (data) => data.result);
418427
} finally {

apps/e2e/tests/backend/workflows.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ test("disabled workflows do not trigger", async ({ expect }) => {
236236
},
237237
]
238238
`);
239+
}, {
240+
timeout: 90_000,
239241
});
240242

241243
test("compile/runtime errors in one workflow don't block others", async ({ expect }) => {

packages/stack-shared/src/interface/client-interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ export class StackClientInterface {
125125

126126
// try to diagnose the error for the user
127127
if (retriedResult.status === "error") {
128-
if (globalVar.navigator && !globalVar.navigator.onLine) {
129-
throw new Error("You are offline. Please check your internet connection and try again. (window.navigator.onLine is falsy)", { cause: retriedResult.error });
128+
if (globalVar.navigator && globalVar.navigator.onLine === false) {
129+
throw new Error("You are offline. Please check your internet connection and try again. (window.navigator.onLine is false)", { cause: retriedResult.error });
130130
}
131131
throw await this._createNetworkError(retriedResult.error, session, requestType);
132132
}

packages/stack-shared/src/utils/esbuild.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ export async function bundleJavaScript(sourceFiles: Record<string, string> & { '
3535
externalPackages?: Record<string, string>,
3636
keepAsImports?: string[],
3737
sourcemap?: false | 'inline',
38+
allowHttpImports?: boolean,
3839
} = {}): Promise<Result<string, string>> {
3940
await initializeEsbuild();
4041

4142
const sourceFilesMap = new Map(Object.entries(sourceFiles));
4243
const externalPackagesMap = new Map(Object.entries(options.externalPackages ?? {}));
4344
const keepAsImports = options.keepAsImports ?? [];
45+
46+
const httpImportCache = new Map<string, { contents: string; loader: esbuild.Loader; resolveDir: string }>();
4447

4548
const extToLoader: Map<string, esbuild.Loader> = new Map([
4649
['tsx', 'tsx'],
@@ -63,6 +66,68 @@ export async function bundleJavaScript(sourceFiles: Record<string, string> & { '
6366
sourcemap: options.sourcemap ?? 'inline',
6467
external: keepAsImports,
6568
plugins: [
69+
...options.allowHttpImports ? [{
70+
name: "esm-sh-only",
71+
setup(build: esbuild.PluginBuild) {
72+
// Handle absolute URLs and relative imports from esm.sh modules.
73+
build.onResolve({ filter: /.*/ }, (args) => {
74+
// Only touch absolute http(s) specifiers or children of our own namespace
75+
const isHttp = args.path.startsWith("http://") || args.path.startsWith("https://");
76+
const fromEsmNs = args.namespace === "esm-sh";
77+
78+
if (!isHttp && !fromEsmNs) return null; // Let other plugins handle bare/relative/local
79+
80+
// Resolve relative URLs inside esm.sh-fetched modules
81+
const url = new URL(args.path, fromEsmNs ? args.importer : undefined);
82+
83+
if (url.protocol !== "https:" || url.host !== "esm.sh") {
84+
throw new Error(`Blocked non-esm.sh URL import: ${url.href}`);
85+
}
86+
87+
return { path: url.href, namespace: "esm-sh" };
88+
});
89+
90+
build.onLoad({ filter: /.*/, namespace: "esm-sh" }, async (args) => {
91+
if (httpImportCache.has(args.path)) return httpImportCache.get(args.path)!;
92+
93+
const res = await fetch(args.path, { redirect: "follow" });
94+
if (!res.ok) throw new Error(`Fetch ${res.status} ${res.statusText} for ${args.path}`);
95+
const finalUrl = new URL(res.url);
96+
// Defensive: follow shouldn’t leave esm.sh, but re-check.
97+
if (finalUrl.host !== "esm.sh") {
98+
throw new Error(`Redirect escaped esm.sh: ${finalUrl.href}`);
99+
}
100+
101+
const ct = (res.headers.get("content-type") || "").toLowerCase();
102+
let loader: esbuild.Loader =
103+
ct.includes("css") ? "css" :
104+
ct.includes("json") ? "json" :
105+
ct.includes("typescript") ? "ts" :
106+
ct.includes("jsx") ? "jsx" :
107+
ct.includes("tsx") ? "tsx" :
108+
"js";
109+
110+
// Fallback by extension (esm.sh sometimes omits CT)
111+
const p = finalUrl.pathname;
112+
if (p.endsWith(".css")) loader = "css";
113+
else if (p.endsWith(".json")) loader = "json";
114+
else if (p.endsWith(".ts")) loader = "ts";
115+
else if (p.endsWith(".tsx")) loader = "tsx";
116+
else if (p.endsWith(".jsx")) loader = "jsx";
117+
118+
const contents = await res.text();
119+
const result = {
120+
contents,
121+
loader,
122+
// Ensures relative imports inside that module resolve against the file’s URL
123+
resolveDir: new URL(".", finalUrl.href).toString(),
124+
watchFiles: [finalUrl.href],
125+
};
126+
httpImportCache.set(args.path, result);
127+
return result;
128+
});
129+
},
130+
} as esbuild.Plugin] : [],
66131
{
67132
name: 'replace-packages-with-globals',
68133
setup(build) {

0 commit comments

Comments
 (0)