-
Notifications
You must be signed in to change notification settings - Fork 131
feat: Add Apify Scrape Single URL and Run Actor actions #136
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
Open
drobnikj
wants to merge
21
commits into
vercel-labs:main
Choose a base branch
from
apify:feat/apify
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+852
−8
Open
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
62803a4
feat: add Apify integration for running Actors
yfe404 1cb415d
fix: lint and formatting issues
yfe404 6f5074d
fix: sort CSS classes and format onChange handler
yfe404 b598228
fix: add apify to IntegrationType in api-client and integrations-manager
yfe404 5aaeb7b
feat: polish apify actions
drobnikj 93aee74
fix: adding apify-client package
drobnikj ed8b30c
fix: update action
drobnikj 9e1ae7f
Merge main into feat/apify - resolved all conflicts
drobnikj 61207bc
fix: after merge fixes
drobnikj 850a060
fix: sync codegen
drobnikj 9e1f48a
fix: fix lint
drobnikj dacaa24
fix: remove spaces to reduce diff
drobnikj dc0ea28
fix: remove debugging logs
drobnikj 9ec9d35
fix: clean up code, add JSON error parsing
drobnikj fce3519
fix: review comments fix
drobnikj 3885666
fix: update readme
drobnikj d391d21
fix: update apifyRunActorStep to accept object same as string
drobnikj 84b56de
Merge main into feat/apify - resolved conflicts
drobnikj 9966acc
Merge branch 'refs/heads/main' into feat/apify
drobnikj eb90102
fix: resolving merge conflicts
drobnikj 83a1f46
fix: fix readme imports
drobnikj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useRef, useState } from "react"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { TemplateBadgeTextarea } from "./template-badge-textarea"; | ||
|
|
||
| export interface TemplateBadgeJsonProps { | ||
| value?: string; | ||
| onChange?: (value: string) => void; | ||
| placeholder?: string; | ||
| disabled?: boolean; | ||
| className?: string; | ||
| id?: string; | ||
| rows?: number; | ||
| } | ||
|
|
||
| /** | ||
| * A textarea component that validates JSON input in real-time | ||
| * Wraps TemplateBadgeTextarea and adds JSON validation and formatting | ||
| */ | ||
| export function TemplateBadgeJson({ | ||
| value = "", | ||
| onChange, | ||
| placeholder, | ||
| disabled, | ||
| className, | ||
| id, | ||
| rows = 3, | ||
| }: TemplateBadgeJsonProps) { | ||
| const [jsonError, setJsonError] = useState<string | null>(null); | ||
| const [isFocused, setIsFocused] = useState(false); | ||
| const formatTimeoutRef = useRef<NodeJS.Timeout | null>(null); | ||
| const lastFormattedValueRef = useRef<string>(""); | ||
|
|
||
| // Validate JSON on value change | ||
| useEffect(() => { | ||
| if (!value || typeof value !== "string") { | ||
| setJsonError(null); | ||
| return; | ||
| } | ||
|
|
||
| // If empty or only whitespace, no error | ||
| if (!value.trim()) { | ||
| setJsonError(null); | ||
| return; | ||
| } | ||
|
|
||
| // Ensure that parsable values (not object) throws | ||
| if (!/^\s*\{[\s\S]*\}\s*$/.test(value)) { | ||
| setJsonError("Value must be a JSON object"); | ||
| return; | ||
| } | ||
|
|
||
| // Parse JSON directly - template variables will be treated as normal strings | ||
| try { | ||
| JSON.parse(value); | ||
| setJsonError(null); | ||
| } catch (error) { | ||
| setJsonError( | ||
| error instanceof Error ? error.message : "Invalid JSON format" | ||
| ); | ||
| } | ||
| }, [value]); | ||
|
|
||
| // Format JSON when it becomes valid (debounced to avoid formatting while typing) | ||
| useEffect(() => { | ||
| // Clear any pending format timeout | ||
| if (formatTimeoutRef.current) { | ||
| clearTimeout(formatTimeoutRef.current); | ||
| } | ||
|
|
||
| // Don't format if there's an error, field is focused, or value is empty | ||
| if (jsonError || isFocused || !value || typeof value !== "string") { | ||
| return; | ||
| } | ||
|
|
||
| if (!value.trim()) { | ||
| return; | ||
| } | ||
|
|
||
| // Debounce formatting - wait 500ms after user stops typing | ||
| formatTimeoutRef.current = setTimeout(() => { | ||
| try { | ||
| // Parse JSON directly - template variables are treated as normal strings | ||
| const parsed = JSON.parse(value); | ||
| const formatted = JSON.stringify(parsed, null, 2); | ||
|
|
||
| // Only format if different from current value and we haven't already formatted this value | ||
| if (formatted !== value && formatted !== lastFormattedValueRef.current) { | ||
| lastFormattedValueRef.current = formatted; | ||
| onChange?.(formatted); | ||
| } | ||
| } catch { | ||
| // If parsing fails, don't format | ||
| } | ||
| }, 500); | ||
|
|
||
| return () => { | ||
| if (formatTimeoutRef.current) { | ||
| clearTimeout(formatTimeoutRef.current); | ||
| } | ||
| }; | ||
| }, [value, isFocused, jsonError, onChange]); | ||
|
|
||
| // Track focus state by listening to focus/blur events on the wrapper | ||
| const handleWrapperFocus = () => { | ||
| setIsFocused(true); | ||
| }; | ||
|
|
||
| const handleWrapperBlur = () => { | ||
| setIsFocused(false); | ||
| // Format immediately on blur if JSON is valid | ||
| if (!jsonError && value && typeof value === "string" && value.trim()) { | ||
| try { | ||
| // Parse JSON directly - template variables are treated as normal strings | ||
| const parsed = JSON.parse(value); | ||
| const formatted = JSON.stringify(parsed, null, 2); | ||
|
|
||
| if (formatted !== value) { | ||
| onChange?.(formatted); | ||
| } | ||
| } catch { | ||
| // If parsing fails, don't format | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className="space-y-1" | ||
| onBlur={handleWrapperBlur} | ||
| onFocus={handleWrapperFocus} | ||
| > | ||
| <TemplateBadgeTextarea | ||
| className={cn( | ||
| jsonError && "border-destructive focus-within:ring-destructive", | ||
| className | ||
| )} | ||
| disabled={disabled} | ||
| id={id} | ||
| onChange={onChange} | ||
| placeholder={placeholder} | ||
| rows={rows} | ||
| value={value} | ||
| /> | ||
| {jsonError && ( | ||
| <p className="ml-1 text-destructive text-xs">{jsonError}</p> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * Code generation template for Run Actor action | ||
| * This template is used when exporting workflows to standalone Next.js projects | ||
| * It uses environment variables instead of integrationId | ||
| */ | ||
| export const runActorCodegenTemplate = `import { ApifyClient } from "apify-client"; | ||
|
|
||
| export async function apifyRunActorStep(input: { | ||
| actorId: string; | ||
| actorInput?: string | Record<string, unknown> | null; | ||
| }) { | ||
| "use step"; | ||
|
|
||
| const apiKey = process.env.APIFY_API_TOKEN; | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error("Apify API Token is not configured. Set APIFY_API_TOKEN environment variable."); | ||
| } | ||
|
|
||
| let parsedActorInput: Record<string, unknown> = {}; | ||
| if (input.actorInput) { | ||
| // If it's already an object, use it directly | ||
| if (typeof input.actorInput === "object" && !Array.isArray(input.actorInput)) { | ||
| parsedActorInput = input.actorInput; | ||
| } else if (typeof input.actorInput === "string") { | ||
| // If it's a string, parse it | ||
| try { | ||
| parsedActorInput = JSON.parse(input.actorInput); | ||
| } catch (err) { | ||
| throw new Error(\`Cannot parse Actor input: \${err instanceof Error ? err.message : String(err)}\`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| const client = new ApifyClient({ token: apiKey }); | ||
| const actorClient = client.actor(input.actorId); | ||
|
|
||
| // Run synchronously and wait for completion | ||
| const runData = await actorClient.call(parsedActorInput); | ||
|
|
||
| // Get dataset items | ||
| let datasetItems: unknown[] = []; | ||
| if (runData.defaultDatasetId) { | ||
| const dataset = await client | ||
| .dataset(runData.defaultDatasetId) | ||
| .listItems(); | ||
| datasetItems = dataset.items; | ||
| } | ||
|
|
||
| return { | ||
| runId: runData.id || "unknown", | ||
| status: runData.status || "SUCCEEDED", | ||
| datasetId: runData.defaultDatasetId, | ||
| data: datasetItems, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(\`Failed to run Actor: \${error instanceof Error ? error.message : String(error)}\`); | ||
| } | ||
| }`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
| * Code generation template for Scrape Single URL action | ||
| * This template is used when exporting workflows to standalone Next.js projects | ||
| * It uses environment variables instead of integrationId | ||
| */ | ||
| export const scrapeSingleUrlCodegenTemplate = `import { ApifyClient } from "apify-client"; | ||
|
|
||
| export async function scrapeSingleUrlStep(input: { | ||
| url: string; | ||
| crawlerType?: string; | ||
| }) { | ||
| "use step"; | ||
|
|
||
| const apiKey = process.env.APIFY_API_TOKEN; | ||
|
|
||
| if (!apiKey) { | ||
| throw new Error("Apify API Token is not configured. Set APIFY_API_TOKEN environment variable."); | ||
| } | ||
|
|
||
| if (!input.url) { | ||
| throw new Error("URL is required."); | ||
| } | ||
|
|
||
| try { | ||
| const client = new ApifyClient({ token: apiKey }); | ||
| const actorClient = client.actor("apify/website-content-crawler"); | ||
| const crawlerType = input.crawlerType || "playwright:adaptive"; | ||
|
|
||
| // Prepare actor input | ||
| const actorInput = { | ||
| startUrls: [{ url: input.url }], | ||
| crawlerType, | ||
| maxCrawlDepth: 0, | ||
| maxCrawlPages: 1, | ||
| maxResults: 1, | ||
| proxyConfiguration: { | ||
| useApifyProxy: true, | ||
| }, | ||
| removeCookieWarnings: true, | ||
| saveMarkdown: true, | ||
| }; | ||
|
|
||
| // Run synchronously and wait for completion | ||
| const runData = await actorClient.call(actorInput); | ||
|
|
||
| // Get dataset items | ||
| let markdown: string | undefined; | ||
| if (runData.defaultDatasetId) { | ||
| const datasetItems = await client | ||
| .dataset(runData.defaultDatasetId) | ||
| .listItems(); | ||
|
|
||
| // Extract markdown from the first item | ||
| if (datasetItems.items && datasetItems.items.length > 0) { | ||
| const firstItem = datasetItems.items[0] as Record<string, unknown>; | ||
| markdown = firstItem.markdown as string; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| runId: runData.id || "unknown", | ||
| status: runData.status || "SUCCEEDED", | ||
| markdown, | ||
| }; | ||
| } catch (error) { | ||
| throw new Error(\`Failed to scrape URL: \${error instanceof Error ? error.message : String(error)}\`); | ||
| } | ||
| }`; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README shows an incorrect import path that doesn't exist, which would cause a runtime import error when users follow the documentation.
View Details
📝 Patch Details
Analysis
Missing barrel export for Apify step functions in README documentation
What fails: README.md (lines 276-278) shows an import statement from
@/lib/steps/apify, but this barrel export file does not exist. TypeScript compilation fails with errorTS2307: Cannot find module '@/lib/steps/apify'.How to reproduce:
pnpm type-checkbefore the fix - fails witherror TS2307: Cannot find module '@/lib/steps/apify'What happened: Users following the README documentation would be unable to import these functions from the documented path. The functions exist in
plugins/apify/steps/run-actor/step.tsandplugins/apify/steps/scrape-single-url/step.tsbut are not re-exported from any barrel file that matches the import path shown in the documentation.Expected: The README example should work without TypeScript errors. The fix creates a barrel export file at
lib/steps/apify.tsthat re-exports both functions, allowing the documented import path to work correctly.