diff --git a/backend/index.js b/backend/index.ts similarity index 100% rename from backend/index.js rename to backend/index.ts diff --git a/backend/package.json b/backend/package.json index 74508bc..a48f31e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,18 +2,19 @@ "name": "backend", "version": "1.0.0", "description": "Backend para scraping e API de vagas do LinkedIn", - "main": "index.js", + "main": "dist/index.js", "type": "module", "scripts": { - "start": "node src/server.js", - "dev": "node src/server.js", - "scraper": "node index.js", - "scraper:watch": "nodemon index.js", - "api": "node src/server.js", + "build": "tsc -p tsconfig.json", + "start": "node dist/src/server.js", + "dev": "tsx src/server.ts", + "scraper": "tsx index.ts", + "scraper:watch": "tsx watch index.ts", + "api": "tsx src/server.ts", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", - "validate": "npm test" + "validate": "npm test && npm run build" }, "keywords": [ "linkedin", @@ -43,9 +44,15 @@ "node": ">=22.0.0" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/node": "^22.15.35", + "@types/pdfkit": "^0.17.3", "@vitest/coverage-v8": "^3.2.4", "nodemon": "^3.1.14", "supertest": "^7.1.4", + "tsx": "^4.20.6", + "typescript": "^5.8.3", "vitest": "^3.2.4" } } \ No newline at end of file diff --git a/backend/src/adapters/adzuna.js b/backend/src/adapters/adzuna.ts similarity index 100% rename from backend/src/adapters/adzuna.js rename to backend/src/adapters/adzuna.ts diff --git a/backend/src/adapters/empregaCampinas.ts b/backend/src/adapters/empregaCampinas.ts new file mode 100644 index 0000000..50a3278 --- /dev/null +++ b/backend/src/adapters/empregaCampinas.ts @@ -0,0 +1,103 @@ +import axios from "axios"; +import * as cheerio from "cheerio"; + +function buildEmpregaCampinasSearchUrl(keyword) { + const url = new URL("https://www.empregacampinas.com.br/"); + url.searchParams.set("s", keyword); + return url.toString(); +} + +function buildEmpregaCampinasRssUrl(keyword) { + const url = new URL("https://www.empregacampinas.com.br/feed/"); + url.searchParams.set("s", keyword); + return url.toString(); +} + +function normalizeText(value) { + return String(value || "").replace(/\s+/g, " ").trim(); +} + +function parseRssItems(xml, keyword) { + const $ = cheerio.load(xml, { xmlMode: true }); + const jobs = []; + + $("item").each((_, node) => { + const item = $(node); + const titulo = normalizeText(item.find("title").first().text()); + const link = normalizeText(item.find("link").first().text()); + const description = normalizeText(item.find("description").first().text()); + + jobs.push({ + source: "Emprega Campinas", + keyword, + titulo, + empresa: "", + local: "Campinas/SP", + link, + descricao: description, + dataPublicacao: normalizeText(item.find("pubDate").first().text()), + }); + }); + + return jobs.filter((job) => job.titulo || job.link); +} + +function parseHtmlItems(html, keyword) { + const $ = cheerio.load(html); + const jobs = []; + + $("article, .post").each((_, node) => { + const item = $(node); + const link = + item.find("h2 a, .entry-title a").first().attr("href") || + item.find("a").first().attr("href") || + ""; + + const titulo = + normalizeText(item.find("h2 a, .entry-title a").first().text()) || + normalizeText(item.find("h2, .entry-title").first().text()); + + jobs.push({ + source: "Emprega Campinas", + keyword, + titulo, + empresa: "", + local: "Campinas/SP", + link: normalizeText(link), + }); + }); + + return jobs.filter((job) => job.titulo || job.link); +} + +export const empregaCampinasAdapter = { + sourceName: "Emprega Campinas", + + async search(keyword, config) { + const timeout = config.pageTimeoutMs || 15000; + const headers = { + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", + "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + }; + + try { + const rssUrl = buildEmpregaCampinasRssUrl(keyword); + const rssResponse = await axios.get(rssUrl, { timeout, headers }); + const fromRss = parseRssItems(rssResponse.data, keyword); + if (fromRss.length > 0) { + return fromRss; + } + } catch { + // fallback para HTML + } + + try { + const htmlUrl = buildEmpregaCampinasSearchUrl(keyword); + const htmlResponse = await axios.get(htmlUrl, { timeout, headers }); + return parseHtmlItems(htmlResponse.data, keyword); + } catch { + return []; + } + }, +}; diff --git a/backend/src/adapters/glassdoor.ts b/backend/src/adapters/glassdoor.ts new file mode 100644 index 0000000..1662a28 --- /dev/null +++ b/backend/src/adapters/glassdoor.ts @@ -0,0 +1,123 @@ +import axios from "axios"; +import * as cheerio from "cheerio"; + +function buildGlassdoorUrl(keyword, location) { + const url = new URL("https://www.glassdoor.com.br/Job/jobs.htm"); + url.searchParams.set("sc.keyword", keyword); + if (location) { + url.searchParams.set("locT", "C"); + url.searchParams.set("locId", location); + } + return url.toString(); +} + +function normalizeText(value) { + return String(value || "").replace(/\s+/g, " ").trim(); +} + +function parseJsonLdJobs(html, keyword) { + const $ = cheerio.load(html); + const jobs = []; + + $("script[type='application/ld+json']").each((_, node) => { + const rawJson = $(node).contents().text(); + + try { + const parsed = JSON.parse(rawJson); + const items = Array.isArray(parsed) ? parsed : [parsed]; + + for (const item of items) { + if (item?.['@type'] !== "JobPosting") { + continue; + } + + jobs.push({ + source: "Glassdoor", + keyword, + titulo: normalizeText(item.title), + empresa: normalizeText(item.hiringOrganization?.name), + local: normalizeText( + item.jobLocation?.address?.addressLocality || + item.jobLocation?.address?.addressRegion, + ), + link: normalizeText(item.url), + dataPublicacao: normalizeText(item.datePosted), + }); + } + } catch { + // ignora JSON inválido + } + }); + + return jobs; +} + +function parseHtmlJobs(html, keyword) { + const $ = cheerio.load(html); + const jobs = []; + + $("li[data-test='jobListing'], article").each((_, node) => { + const item = $(node); + + const title = normalizeText( + item.find("a[data-test='job-link'], a.jobLink, a").first().text(), + ); + + const company = normalizeText( + item.find("[data-test='employer-name'], .EmployerProfile_compactEmployerName__LE242").first().text(), + ); + + const location = normalizeText( + item.find("[data-test='emp-location'], [data-test='location'], .JobCard_location__Ds1fM").first().text(), + ); + + const href = + item.find("a[data-test='job-link'], a.jobLink, a").first().attr("href") || ""; + + const link = href.startsWith("http") + ? href + : href + ? `https://www.glassdoor.com.br${href}` + : ""; + + jobs.push({ + source: "Glassdoor", + keyword, + titulo: title, + empresa: company, + local: location, + link: normalizeText(link), + }); + }); + + return jobs.filter((job) => job.titulo || job.link); +} + +export const glassdoorAdapter = { + sourceName: "Glassdoor", + + async search(keyword, config) { + const timeout = config.pageTimeoutMs || 15000; + const url = buildGlassdoorUrl(keyword, config.searchLocation || ""); + + try { + const response = await axios.get(url, { + timeout, + headers: { + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", + "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + }, + }); + + const fromJsonLd = parseJsonLdJobs(response.data, keyword); + if (fromJsonLd.length > 0) { + return fromJsonLd; + } + + return parseHtmlJobs(response.data, keyword); + } catch { + return []; + } + }, +}; diff --git a/backend/src/adapters/greenhouse.js b/backend/src/adapters/greenhouse.ts similarity index 100% rename from backend/src/adapters/greenhouse.js rename to backend/src/adapters/greenhouse.ts diff --git a/backend/src/adapters/indeed.ts b/backend/src/adapters/indeed.ts new file mode 100644 index 0000000..04d840b --- /dev/null +++ b/backend/src/adapters/indeed.ts @@ -0,0 +1,130 @@ +import axios from "axios"; +import * as cheerio from "cheerio"; + +function buildIndeedSearchUrl(keyword, location) { + const url = new URL("https://www.indeed.com/jobs"); + url.searchParams.set("q", keyword); + if (location) { + url.searchParams.set("l", location); + } + url.searchParams.set("sort", "date"); + return url.toString(); +} + +function normalizeText(value) { + return String(value || "").replace(/\s+/g, " ").trim(); +} + +function parseJsonLdJobs(html, keyword) { + const $ = cheerio.load(html); + const jobs = []; + + $("script[type='application/ld+json']").each((_, node) => { + const rawJson = $(node).contents().text(); + + try { + const parsed = JSON.parse(rawJson); + const items = Array.isArray(parsed) + ? parsed + : parsed?.itemListElement || [parsed]; + + for (const item of items) { + const payload = item?.item || item; + if (payload?.['@type'] !== "JobPosting") { + continue; + } + + const title = normalizeText(payload.title); + const company = normalizeText(payload.hiringOrganization?.name); + const location = normalizeText( + payload.jobLocation?.address?.addressLocality || + payload.jobLocation?.address?.addressRegion, + ); + const link = normalizeText(payload.url); + + jobs.push({ + source: "Indeed", + keyword, + titulo: title, + empresa: company, + local: location, + link, + dataPublicacao: normalizeText(payload.datePosted), + }); + } + } catch { + // ignora blocos JSON inválidos + } + }); + + return jobs; +} + +function parseHtmlJobs(html, keyword) { + const $ = cheerio.load(html); + const jobs = []; + + $("[data-jk], .job_seen_beacon").each((_, node) => { + const card = $(node); + const title = normalizeText( + card.find("h2 a span, .jobTitle span, h2").first().text(), + ); + const company = normalizeText( + card.find("[data-testid='company-name'], .companyName").first().text(), + ); + const location = normalizeText( + card.find("[data-testid='text-location'], .companyLocation").first().text(), + ); + + const href = + card.find("h2 a").first().attr("href") || + card.find("a").first().attr("href") || + ""; + + const link = href.startsWith("http") + ? href + : href + ? `https://www.indeed.com${href}` + : ""; + + jobs.push({ + source: "Indeed", + keyword, + titulo: title, + empresa: company, + local: location, + link: normalizeText(link), + }); + }); + + return jobs.filter((job) => job.titulo || job.link); +} + +export const indeedAdapter = { + sourceName: "Indeed", + + async search(keyword, config) { + const timeout = config.pageTimeoutMs || 15000; + const url = buildIndeedSearchUrl(keyword, config.searchLocation || ""); + + try { + const response = await axios.get(url, { + timeout, + headers: { + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", + "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + }, + }); + + const fromJsonLd = parseJsonLdJobs(response.data, keyword); + if (fromJsonLd.length > 0) { + return fromJsonLd; + } + + return parseHtmlJobs(response.data, keyword); + } catch { + return []; + } + }, +}; diff --git a/backend/src/adapters/lever.js b/backend/src/adapters/lever.ts similarity index 100% rename from backend/src/adapters/lever.js rename to backend/src/adapters/lever.ts diff --git a/backend/src/adapters/linkedin.js b/backend/src/adapters/linkedin.ts similarity index 97% rename from backend/src/adapters/linkedin.js rename to backend/src/adapters/linkedin.ts index 35d0091..d3500ae 100644 --- a/backend/src/adapters/linkedin.js +++ b/backend/src/adapters/linkedin.ts @@ -124,8 +124,9 @@ export const linkedinAdapter = { jobs = await fetchJobsChunk(keyword, config, start); } catch (error) { logWarn( - `LinkedIn: falha HTTP na busca para "${keyword}" (start=${start})`, - error instanceof Error ? error.message : error + `LinkedIn: falha HTTP na busca para "${keyword}" (start=${start}) - ${ + error instanceof Error ? error.message : String(error) + }` ); } diff --git a/backend/src/adapters/theMuse.js b/backend/src/adapters/theMuse.ts similarity index 100% rename from backend/src/adapters/theMuse.js rename to backend/src/adapters/theMuse.ts diff --git a/backend/src/app.js b/backend/src/app.ts similarity index 100% rename from backend/src/app.js rename to backend/src/app.ts diff --git a/backend/src/browser.js b/backend/src/browser.js deleted file mode 100644 index d3b9b8d..0000000 --- a/backend/src/browser.js +++ /dev/null @@ -1,30 +0,0 @@ -import { existsSync } from "node:fs"; -import puppeteer from "puppeteer-core"; - -const CANDIDATE_BROWSERS = [ - process.env.CHROME_PATH, - process.env.PUPPETEER_EXECUTABLE_PATH, - "C:/Program Files/Google/Chrome/Application/chrome.exe", - "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", - "C:/Program Files/Microsoft/Edge/Application/msedge.exe", - "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" -].filter(Boolean); - -export function findBrowserExecutable() { - return CANDIDATE_BROWSERS.find((path) => existsSync(path)); -} - -export async function openBrowser(config) { - const executablePath = findBrowserExecutable(); - - if (!executablePath) { - throw new Error( - "Nenhum Chrome/Edge foi encontrado. Instale o navegador ou defina CHROME_PATH." - ); - } - - return puppeteer.launch({ - headless: config.headless, - executablePath - }); -} diff --git a/backend/src/cache/cache.js b/backend/src/cache/cache.ts similarity index 98% rename from backend/src/cache/cache.js rename to backend/src/cache/cache.ts index 45310b2..030e276 100644 --- a/backend/src/cache/cache.js +++ b/backend/src/cache/cache.ts @@ -1,4 +1,6 @@ export class MemoryCache { + store; + constructor() { this.store = new Map(); } diff --git a/backend/src/config.js b/backend/src/config.ts similarity index 95% rename from backend/src/config.js rename to backend/src/config.ts index 2ed351a..edd54de 100644 --- a/backend/src/config.js +++ b/backend/src/config.ts @@ -1,11 +1,6 @@ const DEFAULT_KEYWORDS = [ "Java", - "JavaScript", - "React", - "Node.js", - "Mulesoft", - "Desenvolvedor Junior", - "Desenvolvedor Pleno", + "Mulesoft" ]; function parseBoolean(value, fallback) { diff --git a/backend/src/exporter.js b/backend/src/exporter.ts similarity index 97% rename from backend/src/exporter.js rename to backend/src/exporter.ts index e60318e..cb02b64 100644 --- a/backend/src/exporter.js +++ b/backend/src/exporter.ts @@ -31,7 +31,7 @@ function cleanJobUrl(url) { export function exportToPDF(rows, outputFile) { ensureParentDir(outputFile); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const doc = new PDFDocument({ margin: 30, size: "A4", @@ -41,7 +41,9 @@ export function exportToPDF(rows, outputFile) { doc.pipe(stream); stream.on("error", reject); - stream.on("finish", resolve); + stream.on("finish", () => { + resolve(); + }); // Título doc diff --git a/backend/src/interface/greenhouse.interface.js b/backend/src/interface/greenhouse.interface.ts similarity index 100% rename from backend/src/interface/greenhouse.interface.js rename to backend/src/interface/greenhouse.interface.ts diff --git a/backend/src/interface/index.js b/backend/src/interface/index.ts similarity index 100% rename from backend/src/interface/index.js rename to backend/src/interface/index.ts diff --git a/backend/src/interface/lever.interface.js b/backend/src/interface/lever.interface.ts similarity index 100% rename from backend/src/interface/lever.interface.js rename to backend/src/interface/lever.interface.ts diff --git a/backend/src/jobsApiApp.js b/backend/src/jobsApiApp.ts similarity index 97% rename from backend/src/jobsApiApp.js rename to backend/src/jobsApiApp.ts index 82ef8e9..826b4bf 100644 --- a/backend/src/jobsApiApp.js +++ b/backend/src/jobsApiApp.ts @@ -8,10 +8,7 @@ import { getConfig } from "./config.js"; import { searchJobsWithCache } from "./pipeline/searchJobsWithCache.js"; import { sources } from "./sources/index.js"; -/** - * @param {{ outputDir?: string }} [options] - */ -export function createJobsApiApp(options = {}) { +export function createJobsApiApp(options: { outputDir?: string } = {}) { const outputDir = options.outputDir ?? path.resolve(process.cwd(), "output"); const app = express(); let activeScraperRun = null; diff --git a/backend/src/logger.js b/backend/src/logger.ts similarity index 100% rename from backend/src/logger.js rename to backend/src/logger.ts diff --git a/backend/src/pipeline/requestDedup.js b/backend/src/pipeline/requestDedup.ts similarity index 66% rename from backend/src/pipeline/requestDedup.js rename to backend/src/pipeline/requestDedup.ts index 4dc0d45..afe7edb 100644 --- a/backend/src/pipeline/requestDedup.js +++ b/backend/src/pipeline/requestDedup.ts @@ -1,6 +1,6 @@ const inflight = new Map(); -export async function withRequestDedup(key, fn) { +export async function withRequestDedup(key: string, fn: { (): Promise; (): Promise; }) { if (inflight.has(key)) { return inflight.get(key); } diff --git a/backend/src/pipeline/scrapeAllSources.js b/backend/src/pipeline/scrapeAllSources.ts similarity index 77% rename from backend/src/pipeline/scrapeAllSources.js rename to backend/src/pipeline/scrapeAllSources.ts index f2705c4..1d82ea8 100644 --- a/backend/src/pipeline/scrapeAllSources.js +++ b/backend/src/pipeline/scrapeAllSources.ts @@ -1,6 +1,32 @@ import { logInfo, logWarn } from "../logger.js"; -function normalizeJob(job, keyword, adapter) { +interface Job { + link?: string; + jobUrl?: string; + source?: string; + titulo?: string; + title?: string; + empresa?: string; + company?: string; + local?: string; + location?: string; + keyword?: string; + palavraChave?: string; + keywords?: string[]; + palavra?: string; + [key: string]: unknown; +} + +interface Adapter { + sourceName: string; + search(keyword: string, config: unknown): Promise; +} + +interface Config { + keywords: string[]; +} + +function normalizeJob(job: Job, keyword: string, adapter: Adapter): Job { return { ...job, source: job.source || adapter.sourceName || "unknown", @@ -10,7 +36,7 @@ function normalizeJob(job, keyword, adapter) { }; } -function mergeKeywords(existing, incoming) { +function mergeKeywords(existing: Job, incoming: Job): Job { const keywords = new Set([ ...(existing.keywords || []), ...(existing.keyword ? [existing.keyword] : []), @@ -32,11 +58,11 @@ function mergeKeywords(existing, incoming) { }; } -function dedupeJobs(jobs) { - const unique = new Map(); +function dedupeJobs(jobs: Job[]): Job[] { + const unique = new Map(); for (const job of jobs) { - const key = + const key: string = job.link || job.jobUrl || `${job.source}-${job.titulo || job.title}-${job.empresa || job.company}-${job.local || job.location}`; @@ -44,7 +70,7 @@ function dedupeJobs(jobs) { if (!key) continue; if (unique.has(key)) { - const existing = unique.get(key); + const existing = unique.get(key)!; unique.set(key, mergeKeywords(existing, job)); continue; } diff --git a/backend/src/pipeline/searchJobsWithCache.js b/backend/src/pipeline/searchJobsWithCache.ts similarity index 77% rename from backend/src/pipeline/searchJobsWithCache.js rename to backend/src/pipeline/searchJobsWithCache.ts index 006332b..029a972 100644 --- a/backend/src/pipeline/searchJobsWithCache.js +++ b/backend/src/pipeline/searchJobsWithCache.ts @@ -3,14 +3,31 @@ import { cache } from "../cache/cache.js"; import { withRequestDedup } from "./requestDedup.js"; import { scrapeAllSources } from "./scrapeAllSources.js"; -function normalizeKeywords(keywords) { +interface SearchConfig { + keywords: string[]; + searchLocation?: string; + searchGeoId?: string; + searchLanguage?: string; + jobTypes?: string; + timeFilter?: string; + remoteOnly?: boolean; +} + +interface CacheResult { + jobs: unknown[]; + total: number; + cachedAt: string; + fromCache: boolean; +} + +function normalizeKeywords(keywords: string[]): string[] { return [...keywords] .map((k) => String(k).trim().toLowerCase()) .filter(Boolean) .sort(); } -function buildCacheKey(config) { +function buildCacheKey(config: SearchConfig) { return [ "jobs", normalizeKeywords(config.keywords).join(","), diff --git a/backend/src/server.js b/backend/src/server.ts similarity index 100% rename from backend/src/server.js rename to backend/src/server.ts diff --git a/backend/src/sources/green_lever_builder.js b/backend/src/sources/green_lever_builder.ts similarity index 100% rename from backend/src/sources/green_lever_builder.js rename to backend/src/sources/green_lever_builder.ts diff --git a/backend/src/sources/index.js b/backend/src/sources/index.ts similarity index 67% rename from backend/src/sources/index.js rename to backend/src/sources/index.ts index 42f3f85..92c3343 100644 --- a/backend/src/sources/index.js +++ b/backend/src/sources/index.ts @@ -1,4 +1,7 @@ import { createAdzunaAdapter } from "../adapters/adzuna.js"; +import { empregaCampinasAdapter } from "../adapters/empregaCampinas.js"; +import { glassdoorAdapter } from "../adapters/glassdoor.js"; +import { indeedAdapter } from "../adapters/indeed.js"; import { linkedinAdapter } from "../adapters/linkedin.js"; import { theMuseAdapter } from "../adapters/theMuse.js"; import { buildAtsSources } from "./green_lever_builder.js"; @@ -14,5 +17,9 @@ export const sources = [ theMuseAdapter, + empregaCampinasAdapter, + glassdoorAdapter, + indeedAdapter, + ...buildAtsSources(), ].filter(Boolean); diff --git a/backend/tests/integration/jobsApi.test.js b/backend/tests/integration/jobsApi.test.ts similarity index 100% rename from backend/tests/integration/jobsApi.test.js rename to backend/tests/integration/jobsApi.test.ts diff --git a/backend/tests/unit/services/adapters/adzuna.test.js b/backend/tests/unit/services/adapters/adzuna.test.ts similarity index 100% rename from backend/tests/unit/services/adapters/adzuna.test.js rename to backend/tests/unit/services/adapters/adzuna.test.ts diff --git a/backend/tests/unit/services/adapters/empregaCampinas.test.ts b/backend/tests/unit/services/adapters/empregaCampinas.test.ts new file mode 100644 index 0000000..7f653e3 --- /dev/null +++ b/backend/tests/unit/services/adapters/empregaCampinas.test.ts @@ -0,0 +1,64 @@ +import axios from "axios"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { empregaCampinasAdapter } from "../../../../src/adapters/empregaCampinas.js"; + +vi.mock("axios"); + +describe("empregaCampinasAdapter", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it("faz parse via RSS", async () => { + axios.get.mockResolvedValueOnce({ + data: ` + + + + Dev React Campinas + https://www.empregacampinas.com.br/vaga-1 + Vaga remota híbrida + Tue, 25 Mar 2026 10:00:00 GMT + + + + `, + }); + + const jobs = await empregaCampinasAdapter.search("React", { + pageTimeoutMs: 1000, + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Emprega Campinas", + keyword: "React", + titulo: "Dev React Campinas", + link: "https://www.empregacampinas.com.br/vaga-1", + }); + }); + + it("faz fallback para HTML quando RSS falha", async () => { + axios.get + .mockRejectedValueOnce(new Error("rss error")) + .mockResolvedValueOnce({ + data: ` + + `, + }); + + const jobs = await empregaCampinasAdapter.search("Node", { + pageTimeoutMs: 1000, + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Emprega Campinas", + keyword: "Node", + titulo: "Dev Node", + link: "https://www.empregacampinas.com.br/vaga-2", + }); + }); +}); diff --git a/backend/tests/unit/services/adapters/glassdoor.test.ts b/backend/tests/unit/services/adapters/glassdoor.test.ts new file mode 100644 index 0000000..a583789 --- /dev/null +++ b/backend/tests/unit/services/adapters/glassdoor.test.ts @@ -0,0 +1,69 @@ +import axios from "axios"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { glassdoorAdapter } from "../../../../src/adapters/glassdoor.js"; + +vi.mock("axios"); + +describe("glassdoorAdapter", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it("faz parse de JobPosting em JSON-LD", async () => { + axios.get.mockResolvedValueOnce({ + data: ` + + `, + }); + + const jobs = await glassdoorAdapter.search("Java", { + pageTimeoutMs: 1000, + searchLocation: "Campinas", + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Glassdoor", + keyword: "Java", + titulo: "Java Developer", + empresa: "Gamma", + local: "Campinas", + link: "https://www.glassdoor.com.br/job1", + }); + }); + + it("faz fallback para parsing HTML", async () => { + axios.get.mockResolvedValueOnce({ + data: ` +
  • + Frontend Engineer +
    Delta
    +
    São Paulo
    +
  • + `, + }); + + const jobs = await glassdoorAdapter.search("Frontend", { + pageTimeoutMs: 1000, + searchLocation: "Brasil", + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Glassdoor", + keyword: "Frontend", + titulo: "Frontend Engineer", + empresa: "Delta", + local: "São Paulo", + link: "https://www.glassdoor.com.br/vaga/123", + }); + }); +}); diff --git a/backend/tests/unit/services/adapters/greenhouse.test.js b/backend/tests/unit/services/adapters/greenhouse.test.ts similarity index 100% rename from backend/tests/unit/services/adapters/greenhouse.test.js rename to backend/tests/unit/services/adapters/greenhouse.test.ts diff --git a/backend/tests/unit/services/adapters/indeed.test.ts b/backend/tests/unit/services/adapters/indeed.test.ts new file mode 100644 index 0000000..4439a7f --- /dev/null +++ b/backend/tests/unit/services/adapters/indeed.test.ts @@ -0,0 +1,69 @@ +import axios from "axios"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { indeedAdapter } from "../../../../src/adapters/indeed.js"; + +vi.mock("axios"); + +describe("indeedAdapter", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it("faz parse de JobPosting em JSON-LD", async () => { + axios.get.mockResolvedValueOnce({ + data: ` + + `, + }); + + const jobs = await indeedAdapter.search("React", { + pageTimeoutMs: 1000, + searchLocation: "Campinas", + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Indeed", + keyword: "React", + titulo: "Senior React Engineer", + empresa: "ACME", + local: "Campinas", + link: "https://www.indeed.com/viewjob?jk=123", + }); + }); + + it("faz fallback para parsing HTML", async () => { + axios.get.mockResolvedValueOnce({ + data: ` +
    +

    Backend Node

    + Beta +
    Remoto
    +
    + `, + }); + + const jobs = await indeedAdapter.search("Node", { + pageTimeoutMs: 1000, + searchLocation: "Brasil", + }); + + expect(jobs).toHaveLength(1); + expect(jobs[0]).toMatchObject({ + source: "Indeed", + keyword: "Node", + titulo: "Backend Node", + empresa: "Beta", + local: "Remoto", + link: "https://www.indeed.com/viewjob?jk=abc", + }); + }); +}); diff --git a/backend/tests/unit/services/adapters/lever.test.js b/backend/tests/unit/services/adapters/lever.test.ts similarity index 100% rename from backend/tests/unit/services/adapters/lever.test.js rename to backend/tests/unit/services/adapters/lever.test.ts diff --git a/backend/tests/unit/services/adapters/linkedin.test.js b/backend/tests/unit/services/adapters/linkedin.test.ts similarity index 100% rename from backend/tests/unit/services/adapters/linkedin.test.js rename to backend/tests/unit/services/adapters/linkedin.test.ts diff --git a/backend/tests/unit/services/adapters/theMuse.test.js b/backend/tests/unit/services/adapters/theMuse.test.ts similarity index 100% rename from backend/tests/unit/services/adapters/theMuse.test.js rename to backend/tests/unit/services/adapters/theMuse.test.ts diff --git a/backend/tests/unit/services/app.test.js b/backend/tests/unit/services/app.test.ts similarity index 100% rename from backend/tests/unit/services/app.test.js rename to backend/tests/unit/services/app.test.ts diff --git a/backend/tests/unit/services/browser.test.js b/backend/tests/unit/services/browser.test.js deleted file mode 100644 index d3e482e..0000000 --- a/backend/tests/unit/services/browser.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mocks = vi.hoisted(() => ({ - existsSync: vi.fn(), - launch: vi.fn(async () => ({ pid: 1 })), -})); - -vi.mock("node:fs", () => ({ - existsSync: mocks.existsSync, -})); - -vi.mock("puppeteer-core", () => ({ - default: { - launch: mocks.launch, - }, -})); - -describe("browser", () => { - beforeEach(() => { - vi.resetModules(); - vi.clearAllMocks(); - delete process.env.CHROME_PATH; - delete process.env.PUPPETEER_EXECUTABLE_PATH; - }); - - it("retorna executavel encontrado", async () => { - process.env.CHROME_PATH = "C:/chrome.exe"; - mocks.existsSync.mockImplementation((path) => path === "C:/chrome.exe"); - - const { findBrowserExecutable } = await import("../../../src/browser.js"); - expect(findBrowserExecutable()).toBe("C:/chrome.exe"); - }); - - it("abre navegador com o executavel encontrado", async () => { - process.env.CHROME_PATH = "C:/chrome.exe"; - mocks.existsSync.mockImplementation((path) => path === "C:/chrome.exe"); - - const { openBrowser } = await import("../../../src/browser.js"); - await openBrowser({ headless: true }); - - expect(mocks.launch).toHaveBeenCalledWith({ - headless: true, - executablePath: "C:/chrome.exe", - }); - }); - - it("lanca erro quando nao encontra navegador", async () => { - mocks.existsSync.mockReturnValue(false); - - const { openBrowser } = await import("../../../src/browser.js"); - await expect(openBrowser({ headless: true })).rejects.toThrow("Nenhum Chrome/Edge foi encontrado"); - }); -}); diff --git a/backend/tests/unit/services/cache/cache.test.js b/backend/tests/unit/services/cache/cache.test.ts similarity index 100% rename from backend/tests/unit/services/cache/cache.test.js rename to backend/tests/unit/services/cache/cache.test.ts diff --git a/backend/tests/unit/services/exporter.test.js b/backend/tests/unit/services/exporter.test.ts similarity index 100% rename from backend/tests/unit/services/exporter.test.js rename to backend/tests/unit/services/exporter.test.ts diff --git a/backend/tests/unit/services/index.test.js b/backend/tests/unit/services/index.test.ts similarity index 100% rename from backend/tests/unit/services/index.test.js rename to backend/tests/unit/services/index.test.ts diff --git a/backend/tests/unit/services/pipeline/requestDedup.test.js b/backend/tests/unit/services/pipeline/requestDedup.test.ts similarity index 100% rename from backend/tests/unit/services/pipeline/requestDedup.test.js rename to backend/tests/unit/services/pipeline/requestDedup.test.ts diff --git a/backend/tests/unit/services/pipeline/scrapeAllSources.test.js b/backend/tests/unit/services/pipeline/scrapeAllSources.test.ts similarity index 100% rename from backend/tests/unit/services/pipeline/scrapeAllSources.test.js rename to backend/tests/unit/services/pipeline/scrapeAllSources.test.ts diff --git a/backend/tests/unit/services/pipeline/searchJobsWithCache.test.js b/backend/tests/unit/services/pipeline/searchJobsWithCache.test.ts similarity index 100% rename from backend/tests/unit/services/pipeline/searchJobsWithCache.test.js rename to backend/tests/unit/services/pipeline/searchJobsWithCache.test.ts diff --git a/backend/tests/unit/services/server.test.js b/backend/tests/unit/services/server.test.ts similarity index 100% rename from backend/tests/unit/services/server.test.js rename to backend/tests/unit/services/server.test.ts diff --git a/backend/tests/unit/utils/config.test.js b/backend/tests/unit/utils/config.test.ts similarity index 100% rename from backend/tests/unit/utils/config.test.js rename to backend/tests/unit/utils/config.test.ts diff --git a/backend/tests/unit/utils/logger.test.js b/backend/tests/unit/utils/logger.test.ts similarity index 100% rename from backend/tests/unit/utils/logger.test.js rename to backend/tests/unit/utils/logger.test.ts diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..df895f7 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": ".", + "outDir": "dist", + "strict": false, + "noImplicitAny": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["index.ts", "src/**/*.ts"], + "exclude": ["dist", "tests", "coverage", "node_modules"] +} diff --git a/electron/main.js b/electron/main.js index df1294a..adc6db8 100644 --- a/electron/main.js +++ b/electron/main.js @@ -40,7 +40,7 @@ function getFrontendDistPath() { } function getBackendServerPath() { - return path.join(getAppRootPath(), 'backend', 'src', 'server.js'); + return path.join(getAppRootPath(), 'backend', 'dist', 'src', 'server.js'); } // userData is the OS user-data directory – writable even under Program Files, diff --git a/package-lock.json b/package-lock.json index 93e58cd..eecac1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@esbuild/win32-x64": "^0.27.4", + "@rollup/rollup-win32-x64-msvc": "^4.60.0", "concurrently": "^9.2.1", "electron": "^33.0.0", "electron-builder": "^25.0.0" @@ -39,15 +41,31 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/node": "^22.15.35", + "@types/pdfkit": "^0.17.3", "@vitest/coverage-v8": "^3.2.4", "nodemon": "^3.1.14", "supertest": "^7.1.4", + "tsx": "^4.20.6", + "typescript": "^5.8.3", "vitest": "^3.2.4" }, "engines": { "node": ">=22.0.0" } }, + "backend/node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "frontend": { "version": "0.0.0", "dependencies": { @@ -1031,6 +1049,22 @@ "node": ">=18" } }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -1838,6 +1872,19 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "os": [ + "win32" + ] + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1965,6 +2012,17 @@ "license": "MIT", "peer": true }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1987,6 +2045,26 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2007,6 +2085,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -2024,6 +2127,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -2056,6 +2166,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.5.tgz", + "integrity": "sha512-T3ZHnvF91HsEco5ClhBCOuBwobZfPcI2jaiSHybkkKYq4KhVIIurod94JVKvDIG0JXT6o3KiERC0X0//m8dyrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -2068,6 +2188,20 @@ "xmlbuilder": ">=11.0.1" } }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "dev": true, @@ -2094,6 +2228,27 @@ "@types/node": "*" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -5748,6 +5903,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.5.0", "dev": true, @@ -8857,6 +9025,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -10126,6 +10304,26 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, diff --git a/package.json b/package.json index 7d999eb..f2f9056 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,12 @@ "test": "npm run test --workspace=backend", "test:coverage": "npm run test:coverage --workspace=frontend && npm run test:coverage --workspace=backend", "build:frontend": "npm run build --workspace=frontend", - "build": "npm run build --workspace=frontend", - "validate": "npm run test --workspace=backend && npm run lint --workspace=frontend && npm run build --workspace=frontend", + "build:backend": "npm run build --workspace=backend", + "build": "npm run build:backend && npm run build:frontend", + "validate": "npm run test --workspace=backend && npm run build:backend && npm run lint --workspace=frontend && npm run build --workspace=frontend", "electron": "electron .", - "electron:dev": "npm run build:frontend && electron .", - "dist": "npm run build:frontend && electron-builder" + "electron:dev": "npm run build && electron .", + "dist": "npm run build && electron-builder" }, "dependencies": { "axios": "^1.13.6", @@ -34,6 +35,8 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@esbuild/win32-x64": "^0.27.4", + "@rollup/rollup-win32-x64-msvc": "^4.60.0", "concurrently": "^9.2.1", "electron": "^33.0.0", "electron-builder": "^25.0.0" @@ -44,7 +47,7 @@ "asar": false, "files": [ "electron/**/*", - "backend/src/**/*", + "backend/dist/**/*", "backend/package.json", "frontend/dist/**/*", "!**/node_modules/.bin", @@ -60,7 +63,9 @@ "target": [ { "target": "nsis", - "arch": ["x64"] + "arch": [ + "x64" + ] } ] },