Compare commits

...

10 commits

Author SHA1 Message Date
Brent Schroeter
7ebe53f0a4 Update dependencies. 2022-12-29 16:44:45 -08:00
Brent Schroeter
07fde82912 Disable CI/CD for GitLab Pages. 2022-04-04 09:49:49 -07:00
Brent Schroeter
314a89d185 Consolidate .gitignores. 2022-04-04 09:49:07 -07:00
Brent Schroeter
a9d9c1a3af Use Cloudflare workers to host + sync with Spelling Bee. 2022-04-04 09:47:52 -07:00
Brent Schroeter
57c05a265a Fix npm run build. 2022-03-26 15:16:01 -07:00
Brent Schroeter
18c6ab739e Test CI and hosting. 2022-03-26 15:15:03 -07:00
Brent Schroeter
b49ff9f0f4 Update README.md. 2021-01-05 13:52:21 -05:00
Brent Schroeter
b73ff68d17 Fix pangram scoring. 2021-01-05 13:46:16 -05:00
Brent Schroeter
5cb4eeae72 Fix icon sizes. 2021-01-05 13:11:42 -05:00
Brent Schroeter
9ff8e0ba71 Update README.md. 2021-01-05 13:08:45 -05:00
30 changed files with 13711 additions and 15869 deletions

View file

@ -1 +0,0 @@
module.exports = {};

View file

@ -1,5 +0,0 @@
{
"projects": {
"default": "built-to-spell"
}
}

6
.gitignore vendored
View file

@ -1,5 +1,8 @@
dist dist
public
.cache .cache
.parcel-cache
worker
# Logs # Logs
logs logs
@ -67,3 +70,6 @@ node_modules/
# dotenv environment variables file # dotenv environment variables file
.env .env
# Workers
transpiled

3
.parcelrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "@parcel/config-default",
}

View file

@ -2,7 +2,8 @@
## What It Is ## What It Is
A browser-based game inspired by the New York Times Spelling Bee. A browser-based game inspired by the New York Times Spelling Bee. Also, a
weekend project to expand my CSS3 skill set (and my vocabulary).
## What It Isn't ## What It Isn't
@ -12,4 +13,6 @@ your friends when you beat their scores; they might be playing by different
rules. (If you're playing Built to Spell, of course, you're still automatically rules. (If you're playing Built to Spell, of course, you're still automatically
the coolest kid on the block regardless.) the coolest kid on the block regardless.)
## [Play It](https://bts.brentschroeter.com/) ## Play It
[https://bts.brentschroeter.com/](https://bts.brentschroeter.com/)

View file

@ -1,16 +0,0 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

23320
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,10 @@
"name": "built-to-spell", "name": "built-to-spell",
"version": "0.0.1", "version": "0.0.1",
"description": "NYT Spelling Bee simulator", "description": "NYT Spelling Bee simulator",
"main": "index.js",
"scripts": { "scripts": {
"lint": "eslint src", "lint": "eslint src",
"deploy": "rm -r .cache ; parcel build -d dist src/index.html && firebase deploy --only=hosting", "build": "parcel build --no-cache --dist-dir=public src/index.html",
"start": "rm -r .cache ; parcel -d dist src/index.html" "start": "rm -r .cache ; parcel serve --dist-dir public src/index.html"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -19,20 +18,28 @@
}, },
"homepage": "https://gitlab.com/brentschroeter/built-to-spell#readme", "homepage": "https://gitlab.com/brentschroeter/built-to-spell#readme",
"dependencies": { "dependencies": {
"react": "^17.0.1", "react": "^18.2.0",
"react-dom": "^17.0.1" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.20.7",
"@types/node": "^14.14.20", "@parcel/packager-raw-url": "^2.8.2",
"@types/react": "^17.0.0", "@parcel/transformer-typescript-tsc": "^2.8.2",
"@types/react-dom": "^17.0.0", "@parcel/transformer-webmanifest": "^2.8.2",
"@typescript-eslint/eslint-plugin": "^4.11.1", "@types/node": "^18.11.18",
"eslint": "^7.17.0", "@types/react": "^18.0.26",
"eslint-config-airbnb": "^18.2.1", "@types/react-dom": "^17.0.14",
"eslint-plugin-jsx-a11y": "^6.4.1", "@typescript-eslint/eslint-plugin": "^5.47.1",
"eslint-plugin-react": "^7.22.0", "@typescript-eslint/parser": "^5.47.1",
"parcel-bundler": "^1.12.4", "eslint": "^8.30.0",
"typescript": "^4.1.3" "eslint_d": "^11.1.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.24.1",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"parcel": "^2.8.2",
"process": "^0.11.10",
"typescript": "^4.6.3"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -4,6 +4,7 @@ import React, {
} from 'react'; } from 'react';
import Honeycomb from './honeycomb'; import Honeycomb from './honeycomb';
import { isPangram } from './scoring';
import { StoreContext } from './store'; import { StoreContext } from './store';
const GameView: FunctionComponent = () => { const GameView: FunctionComponent = () => {
@ -46,7 +47,7 @@ const GameView: FunctionComponent = () => {
)} )}
{words.map((word) => ( {words.map((word) => (
<div <div
className="m-1" className={`m-1 ${isPangram(letters, word) ? 'text-yellow-400' : ''}`}
key={word} key={word}
> >
{word} {word}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 MiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -13,6 +13,6 @@
</head> </head>
<body class="bg-gray-50"> <body class="bg-gray-50">
<div id="app-root" class="w-screen h-screen"></div> <div id="app-root" class="w-screen h-screen"></div>
<script type="text/javascript" src="./index.tsx"></script> <script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View file

@ -2,12 +2,15 @@ import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import App from './app'; import App from './app';
import { NytProvider } from './nyt';
import { StoreProvider } from './store'; import { StoreProvider } from './store';
render( render(
( (
<StoreProvider> <StoreProvider>
<App /> <NytProvider>
<App />
</NytProvider>
</StoreProvider> </StoreProvider>
), ),
document.getElementById('app-root'), document.getElementById('app-root'),

View file

@ -1,6 +1,6 @@
{ {
"icons": [ "icons": [
{"src": "/icon-192.png", "type": "image/png", "sizes": "192x192"}, {"src": "./icon-192.png", "type": "image/png", "sizes": "192x192"},
{"src": "/icon-512.png", "type": "image/png", "sizes": "512x512"} {"src": "./icon-512.png", "type": "image/png", "sizes": "512x512"}
] ]
} }

37
src/nyt.tsx Normal file
View file

@ -0,0 +1,37 @@
import React, {
createContext,
FC,
useEffect,
useState,
} from 'react';
interface GameData {
today: {
validLetters: string[];
};
}
export interface NytData {
letters: string[];
}
export const Context = createContext<NytData>({ letters: [] });
export const NytProvider: FC = ({ children }) => {
const [nytData, setNytData] = useState({ letters: [] } as NytData);
useEffect(() => {
fetch('/api/game-data/latest')
.then((res) => res.json() as Promise<GameData>)
.then((body) => {
setNytData({ letters: body.today.validLetters.map((x) => x.toLocaleUpperCase('en')) });
})
.catch(console.error);
});
return (
<Context.Provider value={nytData}>
{children}
</Context.Provider>
);
};

20
src/scoring.ts Normal file
View file

@ -0,0 +1,20 @@
import twl06 = require('./twl06.json');
export const isPangram = (letters: string[], word: string): boolean => {
const chars = word.split('');
return letters.every((letter) => chars.includes(letter));
};
export const scoreWord = (letters: string[], words: string[], word: string): number => {
if (word.length < 4 || words.includes(word) || !word.split('').includes(letters[0])
|| !twl06.includes(word.toLowerCase())) {
return 0;
}
if (word.length === 4) {
return 1;
}
if (isPangram(letters, word)) {
return word.length + 7;
}
return word.length;
};

View file

@ -5,6 +5,7 @@ import React, {
useEffect, useEffect,
} from 'react'; } from 'react';
import { Context as NytContext } from './nyt';
import { StoreContext } from './store'; import { StoreContext } from './store';
const range7 = [0, 1, 2, 3, 4, 5, 6]; const range7 = [0, 1, 2, 3, 4, 5, 6];
@ -12,8 +13,12 @@ const range7 = [0, 1, 2, 3, 4, 5, 6];
const isValidLetter = (ch: string): boolean => ch.length === 1 && ch >= 'A' && ch <= 'Z'; const isValidLetter = (ch: string): boolean => ch.length === 1 && ch >= 'A' && ch <= 'Z';
const SetupView: FunctionComponent = () => { const SetupView: FunctionComponent = () => {
const { dispatch, state } = useContext(StoreContext); const {
const { letters } = state; dispatch,
state: { letters },
} = useContext(StoreContext);
const { letters: nytLetters } = useContext(NytContext);
useEffect(() => { useEffect(() => {
const firstInput = document.getElementById('setup-view-letter-selector-0'); const firstInput = document.getElementById('setup-view-letter-selector-0');
@ -82,20 +87,30 @@ const SetupView: FunctionComponent = () => {
/> />
))} ))}
</div> </div>
<div className="flex justify-center"> <button
<button className={`
className={` w-full my-6 px-4 py-2 rounded font-bold text-sm
my-6 px-4 py-2 rounded font-bold text-sm bg-yellow-400 hover:bg-yellow-500 transition-all
bg-yellow-400 hover:bg-yellow-500 transition-all ${nytLetters.length === 7 ? 'bg-yellow-400 cursor-pointer' : 'bg-gray-400 cursor-default'}
${letters.length < 7 ? 'opacity-0 cursor-default' : ''} `}
`} disabled={nytLetters.length !== 7}
disabled={letters.length !== 7} onClick={() => dispatch({ type: 'SET_LETTERS', payload: nytLetters })}
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })} type="button"
type="button" >
> {nytLetters.length === 7 ? 'Populate from NYT' : 'Loading from NYT...'}
Play Built to Spell </button>
</button> <button
</div> className={`
w-full my-6 px-4 py-2 rounded font-bold text-sm
bg-yellow-400 hover:bg-yellow-500 transition-all
${letters.length < 7 ? 'opacity-0 cursor-default' : 'cursor-pointer'}
`}
disabled={letters.length !== 7}
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })}
type="button"
>
Play Built to Spell
</button>
</div> </div>
); );
}; };

View file

@ -7,7 +7,7 @@ import React, {
useReducer, useReducer,
} from 'react'; } from 'react';
import twl06 = require('./twl06.json'); import { scoreWord } from './scoring';
export interface State { export interface State {
currentWord: string; currentWord: string;
@ -69,22 +69,13 @@ const shuffle = (letters: string[]): string[] => {
return newLetters; return newLetters;
}; };
const canSubmitWord = (state: State): boolean => {
const { currentWord, words } = state;
return currentWord.length > 3 && !words.includes(currentWord)
&& currentWord.split('').includes(state.letters[0])
&& twl06.includes(currentWord.toLowerCase());
};
const scoreWord = (word: string): number => (word.length === 4 ? 1 : word.length);
const enterWordReducer: Reducer<State, EnterWordAction> = (state) => { const enterWordReducer: Reducer<State, EnterWordAction> = (state) => {
const valid = canSubmitWord(state); const score = scoreWord(state.letters, state.words, state.currentWord);
return { return {
...state, ...state,
currentWord: '', currentWord: '',
score: state.score + (valid ? scoreWord(state.currentWord) : 0), score: state.score + score,
words: valid ? [...state.words, state.currentWord] : state.words, words: score > 0 ? [...state.words, state.currentWord] : state.words,
}; };
}; };

View file

@ -33,7 +33,7 @@
"isolatedModules": true "isolatedModules": true
}, },
"include": [ "include": [
"src/**/*", "src/",
".eslintrc.js", ".eslintrc.js",
], ],
"exclude": [ "exclude": [

0
workers-site/.cargo-ok Normal file
View file

170
workers-site/.eslintrc.js Normal file
View file

@ -0,0 +1,170 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 12,
project: ['./tsconfig.json'],
sourceType: 'module',
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'airbnb',
],
plugins: [
'@typescript-eslint',
'import',
'jsx-a11y',
'react',
'react-hooks',
],
rules: {
'@typescript-eslint/brace-style': ['error'],
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/keyword-spacing': ['error'],
'@typescript-eslint/member-delimiter-style': ['error'],
'@typescript-eslint/method-signature-style': ['warn', 'method'],
'@typescript-eslint/naming-convention': [
'warn',
{
selector: 'default',
format: ['camelCase', 'PascalCase'],
leadingUnderscore: 'forbid',
trailingUnderscore: 'forbid',
filter: {
// skip names requiring quotes, e.g. HTTP headers
regex: '[- ]',
match: false,
},
},
{
selector: ['enumMember'],
format: ['UPPER_CASE'],
},
{
selector: ['property', 'parameter', 'parameterProperty'],
format: ['camelCase', 'PascalCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'forbid',
filter: {
// skip names requiring quotes, e.g. HTTP headers
regex: '[- ]',
match: false,
},
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
],
'@typescript-eslint/no-duplicate-imports': ['error'],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-misused-promises': ['error'],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': ['error'],
'@typescript-eslint/no-unnecessary-condition': ['error', { allowConstantLoopConditions: true }],
'@typescript-eslint/no-unnecessary-boolean-literal-compare': ['error'],
'@typescript-eslint/no-unused-expressions': ['error'],
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/no-use-before-define': ['error'],
'@typescript-eslint/prefer-nullish-coalescing': ['warn'],
'@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true }],
'@typescript-eslint/semi': ['error'],
'@typescript-eslint/space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'@typescript-eslint/switch-exhaustiveness-check': ['error'],
'@typescript-eslint/type-annotation-spacing': ['warn', {
before: false,
after: true,
overrides: {
arrow: { before: true },
},
}],
'brace-style': 'off',
camelcase: 'off',
'class-methods-use-this': 'off',
'comma-dangle': 'off',
'keyword-spacing': 'off',
'no-duplicate-imports': 'off',
'no-shadow': 'off',
'no-unused-expressions': 'off',
'no-unused-vars': 'off',
'no-use-before-define': 'off',
quotes: 'off',
semi: 'off',
'space-before-function-paren': 'off',
'implicit-arrow-linebreak': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
indent: ['error', 2, {
CallExpression: { arguments: 'first' },
FunctionDeclaration: { parameters: 'first' },
FunctionExpression: { parameters: 'first' },
SwitchCase: 1,
VariableDeclarator: 'first',
offsetTernaryExpressions: true,
}],
'jsx-a11y/label-has-associated-control': ['warn', {
assert: 'either',
}],
'max-classes-per-file': 'off',
'max-len': ['warn', { code: 100, tabWidth: 2 }],
'no-console': 'off',
'no-underscore-dangle': 'off',
'prefer-arrow-callback': ['error'],
'react/function-component-definition': ['error', {
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
}],
'react/jsx-curly-newline': ['error', { multiline: 'forbid', singleline: 'forbid' }],
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/jsx-first-prop-new-line': ['error', 'multiline'],
'react/jsx-fragments': 'off',
'react/jsx-indent': ['error', 2, { checkAttributes: true, indentLogicalExpressions: true }],
'react/jsx-indent-props': ['error', 2],
'react/jsx-key': ['error', { checkFragmentShorthand: true }],
'react/jsx-max-props-per-line': ['error', { maximum: 3, when: 'always' }],
'react/jsx-no-target-blank': ['error'],
'react/jsx-wrap-multilines': ['error', {
declaration: 'parens-new-line',
assignment: 'parens-new-line',
return: 'parens-new-line',
arrow: 'parens-new-line',
condition: 'parens-new-line',
logical: 'parens-new-line',
prop: 'parens-new-line',
}],
'react/prop-types': 'off',
'react/no-deprecated': ['error'],
'react/no-multi-comp': ['error'],
'react/no-unused-prop-types': ['error'],
'react/prefer-stateless-function': ['error'],
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': ['error'],
'react-hooks/exhaustive-deps': ['warn'],
},
settings: {
'import/resolver': {
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
},
},
};

View file

@ -0,0 +1,7 @@
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "/test/.*\\.test\\.ts$",
"collectCoverageFrom": ["src/**/*.{ts,js}"]
}

5662
workers-site/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

36
workers-site/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "worker",
"version": "1.0.0",
"description": "",
"main": "dist/worker.js",
"author": "",
"license": "MIT",
"scripts": {
"build": "webpack",
"format": "prettier --write '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
"lint": "eslint --max-warnings=0 src && prettier --check '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
"test": "jest --config jestconfig.json --verbose"
},
"dependencies": {
"@cloudflare/kv-asset-handler": "~0.1.2"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.0.0",
"@types/jest": "^26.0.23",
"@types/service-worker-mock": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"eslint": "^7.21.0",
"eslint-config-prettier": "^8.1.0",
"eslint-config-typescript": "^3.0.0",
"eslint_d": "^11.1.1",
"jest": "^27.0.1",
"prettier": "^2.3.0",
"service-worker-mock": "^2.0.5",
"ts-jest": "^27.0.1",
"ts-loader": "^9.2.8",
"typescript": "^4.6.3",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2"
}
}

5
workers-site/src/bindings.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
export {};
declare global {
const NYT_GAME_DATA: KVNamespace;
}

102
workers-site/src/index.ts Normal file
View file

@ -0,0 +1,102 @@
import {
getAssetFromKV,
Options as GetAssetFromKVOptions,
} from '@cloudflare/kv-asset-handler';
/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = false;
addEventListener('fetch', (event) => {
try {
event.respondWith(handleFetch(event));
} catch (e) {
if (e instanceof Error && DEBUG) {
return event.respondWith(new Response(e.message || e.toString(), { status: 500 }));
}
event.respondWith(new Response('Internal Error', { status: 500 }));
}
});
addEventListener('scheduled', (event) => {
event.waitUntil(handleCron());
});
interface DailyGameData {
printDate: string;
centerLetter: string;
outerLetters: string[];
answers: string[];
}
interface GameData {
today: DailyGameData;
}
async function handleFetch(event: FetchEvent) {
const url = new URL(event.request.url);
const options: Partial<GetAssetFromKVOptions> = {};
try {
if (DEBUG) {
options.cacheControl = { bypassCache: true };
}
let response = new Response('', {});
if (url.pathname.toLocaleLowerCase('en') === '/api/game-data/latest') {
const gameData = await NYT_GAME_DATA.get('latest');
response = new Response(gameData || '', { status: gameData ? 200 : 500 });
response.headers.set('Content-Type', 'application/json');
} else {
const page = await getAssetFromKV(event, options);
response = new Response(page.body, page);
}
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'unsafe-url');
response.headers.set('Feature-Policy', 'none');
return response;
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (e instanceof Error && DEBUG) {
return new Response(e.message || e.toString(), { status: 500 });
}
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: (req) => new Request(`${new URL(req.url).origin}/404.html`, req),
});
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 });
} catch (e) { /* no-op */ }
return new Response('Not found.', { status: 404 });
}
}
async function handleCron() {
console.log('Starting cron trigger...');
const resp = await fetch('https://www.nytimes.com/puzzles/spelling-bee');
console.log('Fetched from NYT.');
let text = await resp.text();
const startStr = '<script type="text/javascript">window.gameData = {';
const startIdx = text.indexOf(startStr);
if (startIdx === -1) {
throw new Error('Unable to locate gameData.');
}
text = text.slice(startIdx + startStr.length - 1);
const endIdx = text.indexOf('</script>');
if (endIdx === -1) {
throw new Error('Unable to locate end of gameData.');
}
text = text.slice(0, endIdx);
console.log('Found gameData.');
const gameData = JSON.parse(text) as GameData;
console.log('Parsed gameData.');
await Promise.all([
NYT_GAME_DATA.put('latest', text),
NYT_GAME_DATA.put(gameData.today.printDate, text),
]);
console.log('Saved gameData.');
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"outDir": "./dist",
"module": "commonjs",
"target": "esnext",
"lib": ["esnext"],
"alwaysStrict": true,
"strict": true,
"preserveConstEnums": true,
"moduleResolution": "node",
"sourceMap": true,
"esModuleInterop": true,
"types": [
"@cloudflare/workers-types",
"@types/jest",
"@types/service-worker-mock"
]
},
"include": ["src"],
"exclude": ["node_modules", "dist", "test"]
}

View file

@ -0,0 +1,22 @@
const path = require('path');
module.exports = {
entry: path.join(__dirname, 'src/index.ts'),
output: {
filename: 'worker.js',
path: path.join(__dirname, 'dist'),
},
devtool: 'cheap-module-source-map',
mode: 'development',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
},
],
},
}

View file

@ -0,0 +1,21 @@
name = "built-to-spell"
main = "./dist/worker.js"
route = 'bts.brentsch.com/*'
account_id = '7c81d50e4fc6ec51eecd802aacd6304b'
usage_model = 'bundled'
compatibility_flags = []
workers_dev = false
compatibility_date = "2022-03-27"
[triggers]
crons = ["0 * * * *"]
[build]
command = "npm --prefix=../ run build && npm run build"
[[kv_namespaces]]
binding = "NYT_GAME_DATA"
id = "ad58b382cc884b889f42ed94cf4dbb6b"
[site]
bucket = "../public"