feat: use ts, update cd, update README (#12)

Signed-off-by: Jef LeCompte <jeffreylec@gmail.com>
This commit is contained in:
Jef LeCompte
2020-09-18 04:27:23 -04:00
committed by GitHub
parent dcbaa6bb2e
commit e9fc0bf5f7
25 changed files with 6632 additions and 228 deletions
+3
View File
@@ -0,0 +1,3 @@
EMAIL_USERNAME="youremail@gmail.com"
EMAIL_PASSWORD="secretpassword"
STORES="bestbuy,bandh,nvidia"
+1 -1
View File
@@ -1 +1 @@
* @jef @andirew @davidlbowman @fuckingrobot @ioncaza @malbert69 * @jef
+2 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: Bug report name: 🐛 Bug report
about: Report a bug for this project about: Report a bug for this project
title: 'Bug: ' title: 'bug: '
labels: 'bug' labels: 'bug'
assignees: jef assignees: jef
+2 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: Feature request name: 🚀 Feature request
about: Suggest a feature for this project about: Suggest a feature for this project
title: 'Enhancement: ' title: 'enhancement: '
labels: 'enhancement' labels: 'enhancement'
assignees: jef assignees: jef
-20
View File
@@ -16,23 +16,3 @@ jobs:
package-name: nvidia-snatcher package-name: nvidia-snatcher
- name: login into github package registry - name: login into github package registry
run: echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin run: echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
- name: build nightly docker image
if: ${{ ! steps.release.outputs.release_created }}
run: |
docker build \
-t "ghcr.io/${GITHUB_REPOSITORY}:${GITHUB_SHA:0:7}" \
-t "ghcr.io/${GITHUB_REPOSITORY}:nightly" .
- name: publish nightly
if: ${{ ! steps.release.outputs.release_created }}
run: docker push "ghcr.io/${GITHUB_REPOSITORY}"
- name: build latest docker image
if: ${{ steps.release.outputs.release_created }}
run: |
docker build \
-t "ghcr.io/${GITHUB_REPOSITORY}:${TAG_NAME}" \
-t "ghcr.io/${GITHUB_REPOSITORY}:latest" .
env:
TAG_NAME: ${{ steps.release.outputs.tag_name }}
- name: publish latest
if: ${{ steps.release.outputs.release_created }}
run: docker push "ghcr.io/${GITHUB_REPOSITORY}"
+4 -6
View File
@@ -6,19 +6,17 @@ on:
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
name: Lint
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@v2.1.1 - uses: actions/setup-node@v2.1.1
with: with:
node-version: ${{ matrix.node-version }} node-version: 14
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node- restore-keys: ${{ runner.os }}-node-
- name: Pull dependencies - name: Pull dependencies
run: npm ci run: |
npm ci
npm run lint
+5 -1
View File
@@ -1 +1,5 @@
node_modules/ .idea/
build/
node_modules/
.env
View File
+71 -29
View File
@@ -1,44 +1,86 @@
# nvidia-snatcher [![cd](https://github.com/jef/nvidia-snatcher/workflows/cd/badge.svg?branch=master)](https://github.com/jef/nvidia-snatcher/actions?query=workflow%3Acd) # nvidia-snatcher [![cd](https://github.com/jef/nvidia-snatcher/workflows/cd/badge.svg?branch=master)](https://github.com/jef/nvidia-snatcher/actions?query=workflow%3Acd)
## Description The purpose of this bot is to get an Nvidia card. It does multiple things to try to do that.
This is going to check Nvidia's website every 5 seconds to see if the 3080 is out of stock in the background. If it comes into stock, then your browser will open and direct you to a cart with the 3080 in it where you can proceed manually. - Currently, `nvidia-snatcher` is not capable of purchasing a card for you
- Scrapes multiple websites for patterns of being stocked
- Opens browser when stock is available
- Send email to you when away from computer
- Must have Gmail
You may get false positives from time to time, so I apologize for that. If you're getting than more often than not, I would change the `const waitForTimeout = 1000;` to a higher number. <details>
<summary>What you may see if you're lucky</summary>
**Update 1:** Now includes Best Buy 3080 FE ```sh
2020-09-18T07:06:28.535Z info :: ✖ [nvidia] nvidia founders edition is still out of stock
## Installation and running 2020-09-18T07:06:31.241Z info :: ✖ [nvidia] nvidia founders edition is still out of stock
2020-09-18T07:06:34.212Z info :: ✖ [bestbuy] nvidia founder edition is still out of stock
Not going to write a full write up here, but I'm going to assume you know Node.js. If you don't then go to Google and look up how to install for your OS. 2020-09-18T07:06:39.878Z info :: ✖ [bandh] gigabyte black is still out of stock
2020-09-18T07:06:43.236Z info :: ✖ [bestbuy] gigabyte black is still out of stock
Here's how to get it running: 2020-09-18T07:06:43.318Z info :: ↗ trying stores again
2020-09-18T07:06:43.318Z info :: 🚀🚀🚀 [nvidia] nvidia founders edition IN STOCK 🚀🚀🚀
- Save this text to a file in a folder on your Desktop. E.g. `nvidia/nvidia.js` 2020-09-18T07:06:43.318Z info :: https://store.nvidia.com/store/nvidia/en_US/buy/productID.5438481700/clearCart.yes/nextPage.QuickBuyCartPage
- Open up your favorite terminal (`cmd`, `iTerm`, `Tilix`)
- Run the below
```
npm i puppeteer opn nodemailer
node nvidia.js
``` ```
If you want to get an email as well: </details>
- If you have two-factor authentication, use https://myaccount.google.com/apppasswords to get your password Google app password > :point_right: You may get false positives from time to time, so I apologize for that. The library currently waits for all calls to be completed before parsing, but sometimes this can unknown behavior. Patience is a virtue :)
- Otherwise, use your regular password
``` | | Best Buy | B&H | Newegg | Nvidia |
npm i puppeteer opn nodemailer |:---:|:---:|:---:|:---:|:---:|
EMAIL_USERNAME="youremail@gmail.com" EMAIL_PASSWORD="secretpassword" node nvidia.js | 3090 | | | | |
``` | 3080 | ✔ | ✔ | | ✔ |
| 3070 | | | | |
## Further customization and hacking > :point_right: () In the process of getting working. Catchpa problems are intermittent. Use if you'd like, but expect problems.
You can potentially add more `links` and change the `timeout` if you'd like. [FAQ](#FAQ) | [Discord](https://discord.gg/3duFzwk) | [Issues](https://github.com/jef/nvidia-snatcher/issues)
- `timeout` and `waitForTimeout` are in milliseconds. ## Installation and prerequisites
- `links` are specific to find the `"out of stock"` verbiage (forced to lowercase).
Linux, macOS, and Windows are all capable operating systems.
You do not need any computer skills, smarts, or anything of that nature. You are very capable as you have made it this far. Some basic understanding how a terminal, git, and or Node.js is a bonus, but that does not limit you to getting `nvidia-snatcher` running!
- Download [Node.js 14](https://nodejs.org/en/)
- Download [git](https://git-scm.com/)
- Clone this project `https://github.com/jef/nvidia-snatcher.git`
- Run `npm install`
- Edit the `.env` file to your liking
- More on this in [customization](#Customization)
- Run `npm run start`
Then watch the magic happen!
### Customization
There is not much to configure (as of now), but there are some options that you can choose to utilize.
First, you're going to need to copy the `.env.example` to `.env`. The current options are:
| Environment variable | Description |
|:---:|:---:|
| `EMAIL_USERNAME` | Gmail address; e.g. `jensen.robbed.us@gmail.com` |
| `EMAIL_PASSWORD` | Gmail password; see below if you have MFA |
| `STORES` | List of stores you want to be scraped; optional, default: `nvidia` |
> :point_right: If you have multi-factor authentication (MFA), you will need to create an [app password](https://myaccount.google.com/apppasswords) and use this instead of your Gmail password.
## FAQ
**Q: What's Node.js and how do I install it?** Visit [their website](https://nodejs.org/en/) and download and install it. Very straight forward. Otherwise, Google more information related to your system needs.
**Q: Will this harm my computer?** No.
**Q: Have you gotten a card yet?** No. :cry:
**Q: Will I get banned from of the stores?** Perhaps, but getting a card is a nice outcome.
**Q: I got a problem and need help!** File an [issue](https://github.com/jef/nvidia-snatcher/issues/new/choose), I'll do my best to get to you. I work a full time job and this is only a hobby of mine.
**Q: I'd love to contribute, how do I do that?** Make a [pull request](https://github.com/jef/nvidia-snatcher/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc)! All contributions are welcome.
**Q: Why do I have to download all this stuff just to get this bot working?** Well, I would rather you didn't either. See #11.
### Acknowledgements ### Acknowledgements
+6187
View File
File diff suppressed because it is too large Load Diff
+17 -4
View File
@@ -2,9 +2,12 @@
"name": "nvidia-snatcher", "name": "nvidia-snatcher",
"version": "1.0.0", "version": "1.0.0",
"description": "🔮 For all your Nvidia needs", "description": "🔮 For all your Nvidia needs",
"main": "src/index.js", "main": "src/index.ts",
"scripts": { "scripts": {
"start": "node src/index.js" "build": "rimraf ./build && tsc",
"lint": "xo",
"lint:fix": "xo --fix",
"start": "npm run build && node build/index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,8 +21,18 @@
}, },
"homepage": "https://github.com/jef/nvidia-snatcher#readme", "homepage": "https://github.com/jef/nvidia-snatcher#readme",
"dependencies": { "dependencies": {
"dotenv": "^8.2.0",
"nodemailer": "^6.4.11", "nodemailer": "^6.4.11",
"opn": "^6.0.0", "open": "^7.2.1",
"puppeteer": "^5.3.0" "puppeteer": "^5.3.0",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/node": "^14.11.1",
"@types/nodemailer": "^6.4.0",
"@types/puppeteer": "^3.0.2",
"rimraf": "^3.0.2",
"typescript": "^4.0.2",
"xo": "^0.33.1"
} }
} }
+29
View File
@@ -0,0 +1,29 @@
import {resolve} from 'path';
import {config} from 'dotenv';
config({path: resolve(__dirname, '../.env')});
const email = {
username: process.env.EMAIL_USERNAME,
password: process.env.EMAIL_PASSWORD
};
const notifications = {
email: email.username && email.password
};
const page = {
height: 1920,
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36',
width: 1080
};
const stores = process.env.STORES ?? 'nvidia';
export const Config = {
email,
notifications,
page,
rateLimitTimeout: 5000,
stores
};
-129
View File
@@ -1,129 +0,0 @@
const mailer = require("./mailer")
const puppeteer = require("puppeteer");
const opn = require("opn");
const timeout = 5000;
const waitForTimeout = 1000;
async function buy() {
const links = [
{
name: "nvidia.com",
url: "https://www.nvidia.com/en-us/geforce/buy/",
oosText: ["out of stock"],
cartUrl: "https://store.nvidia.com/store/nvidia/en_US/buy/productID.5438481700/clearCart.yes/nextPage.QuickBuyCartPage"
}
,{
name: "nvidia.com 2",
url: "https://www.nvidia.com/en-us/shop/geforce/?page=1&limit=9&locale=en-us&search=3080",
oosText: ["out of stock"],
cartUrl: "https://store.nvidia.com/store/nvidia/en_US/buy/productID.5438481700/clearCart.yes/nextPage.QuickBuyCartPage"
}
,{
name: "bestbuy.com",
url: "https://www.bestbuy.com/site/nvidia-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-graphics-card-titanium-and-black/6429440.p?skuId=6429440",
oosText: ["sold out"]
}
,{
name: "newegg.com EVGA BLACK GAMING",
url: "https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3881-kr/p/N82E16814487522",
oosText: ["auto notify","out of stock"]
}
,{
name: "newegg.com MSI VENTUS",
url: "https://www.newegg.com/msi-geforce-rtx-3080-rtx-3080-ventus-3x-10g/p/N82E16814137600",
oosText: ["auto notify","out of stock"]
}
,{
name: "bestbuy.com GIGABYTE BLACK",
url: "https://www.bestbuy.com/site/gigabyte-geforce-rtx-3080-10g-gddr6x-pci-express-4-0-graphics-card-black/6430620.p?acampID=0&cmp=RMX&loc=Hatch&ref=198&skuId=6430620",
oosText: ["sold out"]
}
,{
name: "B&H GIGABYTE BLACK",
url: "https://www.bhphotovideo.com/c/product/1593333-REG/gigabyte_gv_n3080gaming_oc_10gd_geforce_rtx_3080_gaming.html?SID=s1600391647213ytuua52439",
oosText: ["notify when available"]
}
,{
name: "newegg.com EVGA ARGB LED iCX3",
url: "https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3883-kr/p/N82E16814487521",
oosText: ["auto notify","out of stock"]
}
,{
name: "newegg.com EVGA XC3 ULTRA GAMING",
url: "https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3885-kr/p/N82E16814487520",
oosText: ["auto notify","out of stock"]
}
,{
name: "newegg.com ASUS TUF",
url: "https://www.newegg.com/asus-geforce-rtx-3080-tuf-rtx3080-10g-gaming/p/N82E16814126453",
oosText: ["auto notify","out of stock"]
}
]
for (const link of links) {
await goto(link);
}
setTimeout(buy, timeout);
}
async function goto(link) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setUserAgent(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
);
page.setViewport({
width: 1920,
height: 1080,
});
await page.goto(link.url);
await page.waitForTimeout(1000);
const dom = await page.evaluate(() => {
return {
body: document.body.innerText,
};
});
console.log(dom);
if (isOutOfStock(dom.body, link.oosText)) {
console.log(link.name + " is still out of stock... Attempting next link.")
} else {
console.log("*** IN STOCK AT " + link.name.toUpperCase() + ", BUY NOW ***");
await page.screenshot({ path: `nvidia-${Date.now()}.png` });
let clickUrl;
if (link.cartUrl) {
clickUrl = link.cartUrl
} else {
clickUrl = link.url
}
opn(clickUrl);
mailer.send(clickUrl)
}
await browser.close();
}
function isOutOfStock(domText, oosTextArray) {
domText = domText.toLowerCase();
var result = false;
for(var text of oosTextArray) {
result = domText.includes(text.toLowerCase());
};
return result;
}
try{
buy().then();
}
catch(error)
{
buy();
}
+95
View File
@@ -0,0 +1,95 @@
import {Config} from './config';
import {Store, Stores} from './store';
import puppeteer from 'puppeteer';
import open from 'open';
import sendNotification from './notification';
import {Logger} from './logger';
/**
* Starts the bot.
*/
async function main() {
const results = [];
for (const store of Stores) {
Logger.debug(store.links);
results.push(lookup(store));
}
await Promise.all(results);
Logger.info('↗ trying stores again');
setTimeout(main, Config.rateLimitTimeout);
}
/**
* Responsible for looking up information about a each product within
* a `Store`. It's important that we ignore `no-await-in-loop` here
* because we don't want to get rate limited within the same store.
*
* @param store Vendor of graphics cards.
*/
async function lookup(store: Store) {
/* eslint-disable no-await-in-loop */
for (const link of store.links) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setUserAgent(Config.page.userAgent);
await page.setViewport({
height: Config.page.height,
width: Config.page.width
});
await page.goto(link.url, {waitUntil: 'networkidle0'});
const bodyHandle = await page.$('body');
const textContent = await page.evaluate(body => body.textContent, bodyHandle);
Logger.debug(textContent);
const graphicsCard = `${link.brand} ${link.model}`;
if (isOutOfStock(textContent, link.oosLabels)) {
Logger.info(`✖ [${store.name}] ${graphicsCard} is still out of stock`);
} else {
Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`);
Logger.info(link.url);
Logger.debug(' saving screenshot');
await page.screenshot({path: `success-${Date.now()}.png`});
const givenUrl = store.cartUrl ? store.cartUrl : link.url;
await open(givenUrl);
sendNotification(givenUrl);
}
await browser.close();
}
/* eslint-enable no-await-in-loop */
}
/**
* Checks if DOM has any out-of-stock related text.
*
* @param domText Complete DOM of website.
* @param oosLabels Out-of-stock labels.
*/
function isOutOfStock(domText: string, oosLabels: string[]) {
const domTextLowerCase = domText.toLowerCase();
let result = false;
for (const oosLabel of oosLabels) {
result = domTextLowerCase.includes(oosLabel.toLowerCase());
}
return result;
}
/**
* Will continually run until user interferes.
*/
try {
void main();
} catch (error) {
// Ignoring errors; more than likely due to rate limits
Logger.error(error);
void main();
}
+23
View File
@@ -0,0 +1,23 @@
import winston, {format} from 'winston';
const prettyJson = format.printf(info => {
if (typeof info.message === 'object') {
info.message = JSON.stringify(info.message, null, 4);
}
return `${info.level} :: ${info.message}`;
});
export const Logger = winston.createLogger({
level: process.env.LOG_LEVEL ?? 'info',
format: format.combine(
format.colorize(),
format.prettyPrint(),
format.splat(),
format.simple(),
prettyJson
),
transports: [
new winston.transports.Console({})
]
});
-34
View File
@@ -1,34 +0,0 @@
const nodemailer = require("nodemailer");
const emailUsername = process.env.EMAIL_USERNAME;
const emailPassword = process.env.EMAIL_PASSWORD;
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: emailUsername,
pass: emailPassword,
},
});
let mailOptions = {
from: emailUsername,
to: emailUsername,
subject: "NVIDIA - BUY NOW",
text: '',
};
function send(text) {
mailOptions.text = text;
if (emailUsername && emailPassword) {
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
console.log(error);
} else {
console.log("email sent: " + info.response);
}
});
}
}
module.exports = { send }
+33
View File
@@ -0,0 +1,33 @@
import nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer';
import {Config} from '../config';
import {Logger} from '../logger';
const subject = 'NVIDIA - BUY NOW';
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: Config.email.username,
pass: Config.email.password
}
});
const mailOptions: Mail.Options = {
from: Config.email.username,
to: Config.email.username,
subject
};
export default function sendEmail(text: string) {
mailOptions.text = text;
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
Logger.error(error);
} else {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.info(`✔ email sent: ${info.response}`);
}
});
}
+8
View File
@@ -0,0 +1,8 @@
import {Config} from '../config';
import sendEmail from './email';
export default function sendNotification(cartUrl: string) {
if (Config.notifications.email) {
sendEmail(cartUrl);
}
}
+14
View File
@@ -0,0 +1,14 @@
import {Store} from './store';
export const BAndH: Store = {
cartUrl: '',
links: [
{
brand: 'gigabyte',
model: 'black',
url: 'https://www.bhphotovideo.com/c/product/1593333-REG/gigabyte_gv_n3080gaming_oc_10gd_geforce_rtx_3080_gaming.html?SID=s1600391647213ytuua52439',
oosLabels: ['notify when available']
}
],
name: 'bandh'
};
+20
View File
@@ -0,0 +1,20 @@
import {Store} from './store';
export const BestBuy: Store = {
cartUrl: '',
links: [
{
brand: 'nvidia',
model: 'founder edition',
url: 'https://www.bestbuy.com/site/nvidia-geforce-rtx-3080-10gb-gddr6x-pci-express-4-0-graphics-card-titanium-and-black/6429440.p?skuId=6429440',
oosLabels: ['sold out']
},
{
brand: 'gigabyte',
model: 'black',
url: 'https://www.bestbuy.com/site/gigabyte-geforce-rtx-3080-10g-gddr6x-pci-express-4-0-graphics-card-black/6430620.p?acampID=0&cmp=RMX&loc=Hatch&ref=198&skuId=6430620',
oosLabels: ['sold out']
}
],
name: 'bestbuy'
};
+32
View File
@@ -0,0 +1,32 @@
import {BestBuy} from './bestbuy';
import {BAndH} from './bandh';
import {NewEgg} from './newegg';
import {Nvidia} from './nvidia';
import {Config} from '../config';
const list = new Map([
['bestbuy', BestBuy],
['bandh', BAndH],
['newegg', NewEgg],
['nvidia', Nvidia]
]);
if (!Config.stores.toLowerCase().includes('bestbuy')) {
list.delete('bestbuy');
}
if (!Config.stores.toLowerCase().includes('bandh')) {
list.delete('bandh');
}
if (!Config.stores.toLowerCase().includes('newegg')) {
list.delete('newegg');
}
if (!Config.stores.toLowerCase().includes('nvidia')) {
list.delete('nvidia');
}
export const Stores = Array.from(list.values());
export * from './store';
+38
View File
@@ -0,0 +1,38 @@
import {Store} from './store';
export const NewEgg: Store = {
cartUrl: '',
links: [
{
brand: 'asus',
model: 'tuf',
url: 'https://www.newegg.com/asus-geforce-rtx-3080-tuf-rtx3080-10g-gaming/p/N82E16814126453',
oosLabels: ['auto notify', 'out of stock']
},
{
brand: 'evga',
model: 'black gaming',
url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3881-kr/p/N82E16814487522',
oosLabels: ['auto notify', 'out of stock']
},
{
brand: 'evga',
model: 'argb led icx3',
url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3883-kr/p/N82E16814487521',
oosLabels: ['auto notify', 'out of stock']
},
{
brand: 'evga',
model: 'xc3 ultra gaming',
url: 'https://www.newegg.com/evga-geforce-rtx-3080-10g-p5-3885-kr/p/N82E16814487520',
oosLabels: ['auto notify', 'out of stock']
},
{
brand: 'msi',
model: 'ventus',
url: 'https://www.newegg.com/msi-geforce-rtx-3080-rtx-3080-ventus-3x-10g/p/N82E16814137600',
oosLabels: ['auto notify', 'out of stock']
}
],
name: 'newegg'
};
+20
View File
@@ -0,0 +1,20 @@
import {Store} from './store';
export const Nvidia: Store = {
cartUrl: 'https://store.nvidia.com/store/nvidia/en_US/buy/productID.5438481700/clearCart.yes/nextPage.QuickBuyCartPage',
links: [
{
brand: 'nvidia',
model: 'founders edition',
url: 'https://www.nvidia.com/en-us/geforce/buy/',
oosLabels: ['out of stock']
},
{
brand: 'nvidia',
model: 'founders edition',
url: 'https://www.nvidia.com/en-us/shop/geforce/?page=1&limit=9&locale=en-us&search=3080',
oosLabels: ['out of stock']
}
],
name: 'nvidia'
};
+12
View File
@@ -0,0 +1,12 @@
interface Link {
brand: string;
model: string;
url: string;
oosLabels: string[];
}
export interface Store {
cartUrl: string;
links: Link[];
name: string;
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es6"],
"allowJs": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}