feat: implement core product scraping infrastructure with Playwright extractors and data formatters
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
output/
|
||||
dist/
|
||||
build/
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
Generated
+1129
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "scraper",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"scrape": "npx tsx src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"playwright": "^1.50.1",
|
||||
"playwright-extra": "^4.3.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.21",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Page } from "playwright";
|
||||
import type { RawProduct, Product } from "../types.js";
|
||||
import { extractors } from "./extractors.js";
|
||||
|
||||
export const extractProduct = async (page: Page): Promise<RawProduct> => {
|
||||
const raw: Partial<RawProduct> = {};
|
||||
|
||||
const keys = Object.keys(extractors) as Array<keyof Product>;
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
raw[key] = await extractors[key](page);
|
||||
} catch (error) {
|
||||
console.error(`Error extracting field "${key}":`, error);
|
||||
raw[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return raw as RawProduct;
|
||||
};
|
||||
@@ -0,0 +1,309 @@
|
||||
import type { Page } from "playwright";
|
||||
import type { ExtractorMap, SpecEntry, JsonObject, JsonValue } from "../types.js";
|
||||
|
||||
async function getJsonLd(page: Page): Promise<JsonObject[]> {
|
||||
return page.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
|
||||
return scripts.map(s => {
|
||||
try {
|
||||
const parsed = JSON.parse(s.textContent || "{}");
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function findJsonLdType(page: Page, typeStr: string): Promise<JsonObject | null> {
|
||||
const jsonLds = await getJsonLd(page);
|
||||
for (const item of jsonLds) {
|
||||
const typeObj = item["@type"];
|
||||
if (typeObj === typeStr || (Array.isArray(typeObj) && typeObj.includes(typeStr))) {
|
||||
return item;
|
||||
}
|
||||
const graphObj = item["@graph"];
|
||||
if (graphObj && Array.isArray(graphObj)) {
|
||||
const found = graphObj.find((g: JsonValue) => g && typeof g === 'object' && !Array.isArray(g) && g["@type"] === typeStr);
|
||||
if (found) return found as JsonObject;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const extractors: ExtractorMap = {
|
||||
url: async (page) => {
|
||||
return page.url();
|
||||
},
|
||||
|
||||
item_id: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.sku) return String(productData.sku);
|
||||
if (productData?.productID) return String(productData.productID);
|
||||
|
||||
// Try finding data attributes or hidden inputs
|
||||
return page.evaluate(() => {
|
||||
const skuEl = document.querySelector('[data-product-sku], [data-sku], input[name="product_id"]');
|
||||
if (skuEl) {
|
||||
return skuEl.getAttribute('data-product-sku') || skuEl.getAttribute('data-sku') || (skuEl as HTMLInputElement).value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
title: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.name) return String(productData.name);
|
||||
|
||||
return page.evaluate(() => {
|
||||
const imgAlt = document.querySelector('.product-detail-thumb-bto, .main-image img');
|
||||
if (imgAlt && imgAlt.getAttribute('alt')) return imgAlt.getAttribute('alt')?.trim();
|
||||
|
||||
const titleStr = document.title;
|
||||
if (titleStr) {
|
||||
const dashPart = titleStr.split(' - ')[0] || titleStr;
|
||||
const pipePart = dashPart.split(' | ')[0] || dashPart;
|
||||
return pipePart.trim();
|
||||
}
|
||||
|
||||
const h1s = Array.from(document.querySelectorAll('h1'));
|
||||
const validH1 = h1s.find(el => {
|
||||
const text = el.textContent?.toLowerCase() || '';
|
||||
return !text.includes('cookie') && !text.includes('choice') && !text.includes('consent');
|
||||
});
|
||||
if (validH1) return validH1.textContent?.trim();
|
||||
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
brand: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.brand && typeof productData.brand === 'object' && !Array.isArray(productData.brand)) {
|
||||
const brandName = (productData.brand as JsonObject).name;
|
||||
if (brandName) return String(brandName);
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const brandEl = document.querySelector('[itemprop="brand"]');
|
||||
if (brandEl) return brandEl.textContent?.trim() || brandEl.getAttribute('content');
|
||||
|
||||
const siteName = document.querySelector('meta[property="og:site_name"]');
|
||||
if (siteName) {
|
||||
const content = siteName.getAttribute('content');
|
||||
if (content) return (content.split('-')[0] || content).trim();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
product_category: async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const breadcrumbs = Array.from(document.querySelectorAll('.breadcrumb li, .breadcrumbs li, [aria-label="breadcrumb"] li, [itemtype="http://schema.org/BreadcrumbList"] li, .path-item'));
|
||||
if (breadcrumbs.length > 0) {
|
||||
const texts = breadcrumbs.map(b => b.textContent?.replace(/\s+/g, ' ').trim()).filter(Boolean);
|
||||
const filtered = texts.filter(t => t && t.toLowerCase() !== 'home');
|
||||
if (filtered.length > 0) return filtered.join(' > ');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
category_tree: async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const items = Array.from(document.querySelectorAll('.breadcrumb a, .breadcrumbs a, [aria-label="breadcrumb"] a, [itemprop="itemListElement"] a'));
|
||||
if (items.length > 0) {
|
||||
const tree = items.map(el => {
|
||||
const name = el.textContent?.trim() || '';
|
||||
const url = (el as HTMLAnchorElement).href || null;
|
||||
return { name, url };
|
||||
}).filter(item => item.name && item.name.toLowerCase() !== 'home');
|
||||
return tree;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
description: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.description) return String(productData.description);
|
||||
|
||||
return page.evaluate(() => {
|
||||
const descEl = document.querySelector('.product-description, #description, [itemprop="description"]');
|
||||
if (descEl) return descEl.textContent?.trim();
|
||||
const metaDesc = document.querySelector('meta[name="description"], meta[property="og:description"]');
|
||||
if (metaDesc) return metaDesc.getAttribute('content');
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
price: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.offers) {
|
||||
if (Array.isArray(productData.offers)) {
|
||||
const offer = productData.offers[0] as JsonObject;
|
||||
if (offer?.price) return String(offer.price);
|
||||
} else if (typeof productData.offers === 'object') {
|
||||
const offer = productData.offers as JsonObject;
|
||||
if (offer.price) return String(offer.price);
|
||||
}
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const priceEl = document.querySelector('.prices-new, .price-new, [itemprop="price"], .product-price, .current-price, .regular-price');
|
||||
if (priceEl) return priceEl.textContent?.trim();
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
sale_price: async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const hasOldPrice = document.querySelector('.prices-old, .price-old, .price-was');
|
||||
if (hasOldPrice) {
|
||||
const saleEl = document.querySelector('.prices-new, .price-new');
|
||||
if (saleEl) return saleEl.textContent?.trim();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
availability: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
let offerUrl: unknown = null;
|
||||
|
||||
if (productData?.offers) {
|
||||
if (Array.isArray(productData.offers)) {
|
||||
const offer = productData.offers[0] as JsonObject;
|
||||
offerUrl = offer?.availability;
|
||||
} else if (typeof productData.offers === 'object') {
|
||||
const offer = productData.offers as JsonObject;
|
||||
offerUrl = offer.availability;
|
||||
}
|
||||
}
|
||||
|
||||
if (offerUrl) {
|
||||
return String(offerUrl).split('/').pop() || String(offerUrl);
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const btn = document.querySelector('#button-cart, .add-to-cart, [data-action="add-to-cart"], button.btn-cart');
|
||||
if (btn) return btn.textContent?.trim() || "Add to Cart";
|
||||
|
||||
const stockEl = document.querySelector('.stock, .availability, [itemprop="availability"]');
|
||||
if (stockEl) return stockEl.textContent?.trim() || stockEl.getAttribute('href')?.split('/').pop();
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
image_url: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.image) {
|
||||
if (typeof productData.image === 'string') return productData.image;
|
||||
if (Array.isArray(productData.image)) return String(productData.image[0]);
|
||||
if (typeof productData.image === 'object') {
|
||||
const url = (productData.image as JsonObject).url;
|
||||
if (url) return String(url);
|
||||
}
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const img = document.querySelector('.product-detail-thumb-bto, .product-image img, .main-image img, [property="og:image"], img[itemprop="image"]');
|
||||
if (img) {
|
||||
if (img.tagName.toLowerCase() === 'meta') return img.getAttribute('content');
|
||||
return img.getAttribute('popup_img') || (img as HTMLImageElement).src || img.getAttribute('data-src') || img.getAttribute('src');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
additional_image_urls: async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const imgs = Array.from(document.querySelectorAll('.product-detail-thumb-bto, .gallery img, .thumbnails img, .product-thumbnails img'));
|
||||
return imgs.map(img => {
|
||||
return img.getAttribute('popup_img') || (img as HTMLImageElement).src || img.getAttribute('data-src') || img.getAttribute('src') || '';
|
||||
}).filter(Boolean);
|
||||
});
|
||||
},
|
||||
|
||||
specs: async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const specs: SpecEntry[] = [];
|
||||
const rows = Array.from(document.querySelectorAll('.spec-table tr, .specifications tr, table tr, .tech-specs li'));
|
||||
|
||||
for (const row of rows) {
|
||||
const th = row.querySelector('th');
|
||||
const td = row.querySelector('td');
|
||||
if (th && td) {
|
||||
specs.push({
|
||||
name: th.textContent?.trim() || '',
|
||||
value: td.textContent?.trim() || null
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = row.querySelector('.label, .spec-name, dt');
|
||||
const value = row.querySelector('.value, .spec-value, dd');
|
||||
if (label && value) {
|
||||
specs.push({
|
||||
name: label.textContent?.trim() || '',
|
||||
value: value.textContent?.trim() || null
|
||||
});
|
||||
}
|
||||
}
|
||||
return specs.filter(s => s.name);
|
||||
});
|
||||
},
|
||||
|
||||
star_rating: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.aggregateRating && typeof productData.aggregateRating === 'object' && !Array.isArray(productData.aggregateRating)) {
|
||||
const val = (productData.aggregateRating as JsonObject).ratingValue;
|
||||
if (val) return String(val);
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const ratingEl = document.querySelector('.rating-value, [itemprop="ratingValue"], .stars');
|
||||
if (ratingEl) return ratingEl.textContent?.trim() || ratingEl.getAttribute('content');
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
review_count: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.aggregateRating && typeof productData.aggregateRating === 'object' && !Array.isArray(productData.aggregateRating)) {
|
||||
const val = (productData.aggregateRating as JsonObject).reviewCount;
|
||||
if (val) return String(val);
|
||||
}
|
||||
|
||||
return page.evaluate(() => {
|
||||
const countEl = document.querySelector('.review-count, [itemprop="reviewCount"]');
|
||||
if (countEl) return countEl.textContent?.trim() || countEl.getAttribute('content');
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
gtin: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.gtin13) return String(productData.gtin13);
|
||||
if (productData?.gtin) return String(productData.gtin);
|
||||
|
||||
return page.evaluate(() => {
|
||||
const el = document.querySelector('[itemprop="gtin13"], [itemprop="gtin"]');
|
||||
return el ? (el.textContent?.trim() || el.getAttribute('content')) : null;
|
||||
});
|
||||
},
|
||||
|
||||
mpn: async (page) => {
|
||||
const productData = await findJsonLdType(page, "Product");
|
||||
if (productData?.mpn) return String(productData.mpn);
|
||||
|
||||
return page.evaluate(() => {
|
||||
const el = document.querySelector('[itemprop="mpn"]');
|
||||
return el ? (el.textContent?.trim() || el.getAttribute('content')) : null;
|
||||
});
|
||||
},
|
||||
|
||||
scraped_at: async () => {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { RawProduct, Product } from "../types.js";
|
||||
import { formatters } from "./formatters.js";
|
||||
|
||||
export const formatProduct = (raw: RawProduct): Product => {
|
||||
const product: Partial<Product> = {};
|
||||
|
||||
const keys = Object.keys(formatters) as Array<keyof Product>;
|
||||
|
||||
for (const key of keys) {
|
||||
(product as Record<string, unknown>)[key] = formatters[key](raw[key]);
|
||||
}
|
||||
|
||||
return product as Product;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { FormatterMap, Availability, CategoryEntry, SpecEntry } from "../types.js";
|
||||
|
||||
const availabilityMap: Record<string, Availability> = {
|
||||
"add to cart": "in_stock",
|
||||
"in stock": "in_stock",
|
||||
"instock": "in_stock",
|
||||
"out of stock": "out_of_stock",
|
||||
"outofstock": "out_of_stock",
|
||||
"sold out": "out_of_stock",
|
||||
"pre-order": "pre_order",
|
||||
"pre order": "pre_order",
|
||||
"preorder": "pre_order"
|
||||
};
|
||||
|
||||
const parseNumber = (val: unknown): number | null => {
|
||||
if (typeof val === "number") return val;
|
||||
if (typeof val !== "string") return null;
|
||||
const cleaned = val.replace(/[^\d.-]/g, "");
|
||||
const parsed = parseFloat(cleaned);
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
const parseString = (val: unknown): string | null => {
|
||||
if (typeof val === "string") {
|
||||
const trimmed = val.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const formatters: FormatterMap = {
|
||||
url: (val) => parseString(val) || "",
|
||||
|
||||
item_id: parseString,
|
||||
|
||||
title: parseString,
|
||||
|
||||
brand: parseString,
|
||||
|
||||
product_category: parseString,
|
||||
|
||||
category_tree: (val) => {
|
||||
if (!Array.isArray(val)) return [];
|
||||
return val as CategoryEntry[];
|
||||
},
|
||||
|
||||
description: parseString,
|
||||
|
||||
price: parseNumber,
|
||||
|
||||
sale_price: parseNumber,
|
||||
|
||||
availability: (val) => {
|
||||
const str = parseString(val);
|
||||
if (!str) return null;
|
||||
const lower = str.toLowerCase();
|
||||
return availabilityMap[lower] ?? null;
|
||||
},
|
||||
|
||||
image_url: parseString,
|
||||
|
||||
additional_image_urls: (val) => {
|
||||
if (!Array.isArray(val)) return [];
|
||||
// Deduplicate array
|
||||
const urls = val.map(parseString).filter(Boolean) as string[];
|
||||
return Array.from(new Set(urls));
|
||||
},
|
||||
|
||||
specs: (val) => {
|
||||
if (!Array.isArray(val)) return [];
|
||||
return val as SpecEntry[];
|
||||
},
|
||||
|
||||
star_rating: parseNumber,
|
||||
|
||||
review_count: parseNumber,
|
||||
|
||||
gtin: parseString,
|
||||
|
||||
mpn: parseString,
|
||||
|
||||
scraped_at: (val) => parseString(val) || new Date().toISOString()
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { loadPage } from "./request/page-loader.js";
|
||||
import { extractProduct } from "./extract/extract-product.js";
|
||||
import { formatProduct } from "./format/format-product.js";
|
||||
|
||||
const DEFAULT_URL = "https://us-store.msi.com/Motherboards/Intel-Platform-Motherboard/INTEL-Z890/MAG-Z890-TOMAHAWK-WIFI";
|
||||
const TARGET_URL = process.argv[2] || DEFAULT_URL;
|
||||
const OUTPUT_FILE = "output/product.json";
|
||||
|
||||
async function main() {
|
||||
console.log(`Starting scraper for URL: ${TARGET_URL}`);
|
||||
|
||||
let browser;
|
||||
try {
|
||||
const loaderResult = await loadPage(TARGET_URL);
|
||||
browser = loaderResult.browser;
|
||||
const page = loaderResult.page;
|
||||
|
||||
console.log("Page loaded. Extracting data...");
|
||||
const rawData = await extractProduct(page);
|
||||
|
||||
console.log("Data extracted. Formatting...");
|
||||
const product = formatProduct(rawData);
|
||||
|
||||
console.log("Formatting complete. Saving output...");
|
||||
mkdirSync(dirname(OUTPUT_FILE), { recursive: true });
|
||||
writeFileSync(OUTPUT_FILE, JSON.stringify(product, null, 2), "utf8");
|
||||
|
||||
const html = await page.content();
|
||||
writeFileSync("output/page.html", html, "utf8");
|
||||
|
||||
console.log(`Success! Data saved to ${OUTPUT_FILE}`);
|
||||
} catch (error) {
|
||||
console.error("Scraping failed:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
console.log("Browser closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { chromium } from "playwright-extra";
|
||||
import stealth from "puppeteer-extra-plugin-stealth";
|
||||
import type { Browser, Page } from "playwright";
|
||||
|
||||
chromium.use(stealth());
|
||||
|
||||
export const loadPage = async (url: string): Promise<{ page: Page; browser: Browser }> => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
try {
|
||||
await page.waitForSelector("h1", { timeout: 10000 });
|
||||
} catch (error) {
|
||||
console.warn("Warning: Could not find <h1> element within timeout.");
|
||||
}
|
||||
|
||||
return { page, browser };
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Page } from "playwright";
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
export interface JsonObject {
|
||||
[key: string]: JsonValue;
|
||||
}
|
||||
export interface JsonArray extends Array<JsonValue> {}
|
||||
|
||||
export type Availability = "in_stock" | "out_of_stock" | "pre_order";
|
||||
|
||||
export type CategoryEntry = {
|
||||
name: string;
|
||||
url: string | null;
|
||||
};
|
||||
|
||||
export type SpecEntry = {
|
||||
name: string;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export type Product = {
|
||||
url: string;
|
||||
item_id: string | null;
|
||||
title: string | null;
|
||||
brand: string | null;
|
||||
product_category: string | null;
|
||||
category_tree: CategoryEntry[];
|
||||
description: string | null;
|
||||
price: number | null;
|
||||
sale_price: number | null;
|
||||
availability: Availability | null;
|
||||
image_url: string | null;
|
||||
additional_image_urls: string[];
|
||||
specs: SpecEntry[];
|
||||
star_rating: number | null;
|
||||
review_count: number | null;
|
||||
gtin: string | null;
|
||||
mpn: string | null;
|
||||
scraped_at: string;
|
||||
};
|
||||
|
||||
export type RawProduct = {
|
||||
[K in keyof Product]: unknown;
|
||||
};
|
||||
|
||||
export type Extractor = (page: Page) => Promise<unknown>;
|
||||
export type Formatter = (raw: unknown) => unknown;
|
||||
|
||||
export type ExtractorMap = Record<keyof Product, Extractor>;
|
||||
export type FormatterMap = Record<keyof Product, Formatter>;
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": ["node"],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user