Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat / tanstack router #12

Merged
merged 14 commits into from
Jan 30, 2024
Merged
6 changes: 2 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ It should:
- hotfix : urgent bug fix
- feat : new or updated feature
- docs : documentation updates
- BREAKING : if commit is a breaking change
- refactor : code refactoring (no functional change)
- style : UX and display updates
- test : tests and CI updates
- chore : updates on build, tools, configuration ...
- test : tests updates
- chore : miscellaneous housekeeping updates
- Merge branch : when merging branch
- Merge pull request : when merging PR

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Template react app with batteries included 🔋
- [Vitest (testing for Vite)](https://vitest.dev/)
- [Typescript](https://www.typescriptlang.org)
- [Redux](https://redux.js.org)
- [React-Router](https://reactrouter.com)
- [TanStack Router](https://tanstack.com/router/latest)
- [Linaria (SASS-in-JS)](https://github.com/callstack/linaria)
- Custom (hackable) build script
- Custom utils and helper functions
Expand Down
20 changes: 19 additions & 1 deletion config/linaria-rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,29 @@ export default function wywInJS({

let { cssText, dependencies } = result;

if (!cssText) return;
// Heads up, there are three cases:
// 1. cssText is undefined, it means that file was not transformed
// 2. cssText is empty, it means that file was transformed, but it does not contain any styles
// 3. cssText is not empty, it means that file was transformed and it contains styles

if (typeof cssText === "undefined") {
return;
}

if (cssText === "") {
/* eslint-disable-next-line consistent-return */
return {
code: result.code,
map: result.sourceMap
};
}

dependencies ??= [];

const slug = slugify(cssText);

// @IMPORTANT: We *need* to use `.scss` extension here.
// This tiny change is the only difference between this file and using `https://github.com/Anber/wyw-in-js/blob/main/packages/vite/src/index.ts`.
const cssFilename = path
.normalize(`${id.replace(/\.[jt]sx?$/, "")}_${slug}.scss`)
.replace(/\\/g, path.posix.sep);
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
"@linaria/core": "6.0.0",
"@linaria/react": "6.0.0",
"@reduxjs/toolkit": "^2.1.0",
"@tanstack/react-router": "^1.15.5",
"@tanstack/router-devtools": "^1.15.5",
"@tanstack/router-vite-plugin": "^1.15.4",
"dayjs": "^1.11.10",
"plausible-tracker": "^0.3.8",
"react": "^18.2.0",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.21.3",
"redux-logger": "^3.0.6",
"sass": "^1.70.0"
},
Expand Down
29 changes: 16 additions & 13 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,35 @@ import { screen } from "@testing-library/react";
import { getStyle, render, select } from "tests";
import { expect, test } from "vitest";

import { Home } from "view/screens";

import App from "./App";
import { NotFoundRoute } from "./view/routes/__root";
import { IndexRoute } from "./view/routes/index";

test("renders app", () => {
const r = render(<App />);
test("renders app", async () => {
const r = await render(<App />);

const linkElement = screen.getByText(/Template react app with batteries included/i);
const linkElement = r.getByText(/Template react app with batteries included/i);
expect(linkElement).toBeInTheDocument();
});

test("renders 404 page", () => {
render(<App />, "/wow");
test("renders home", async () => {
const r = await render(<IndexRoute />);

const linkElement = screen.getByText(/Page not found/i);
const linkElement = screen.getByText(/Template react app with batteries included/i);
expect(linkElement).toBeInTheDocument();
});

test("renders Home page", () => {
const r = render(<Home />);

// Test @linaria styles are working.
// Worth doing for a few components to test the `theme` object imports properly.
const style = getStyle(select(r, "h1"));
const style = getStyle(select(r.container, "h1"));
expect(style.color).toBe("#bee3f8");
expect(style.textShadow).toBe(
"0.25px 0.25px 0 #4299e1, 0.5px 0.5px 0 #4299e1, 0.75px 0.75px 0 #4299e1, 1px 1px 0 #4299e1, 1.25px 1.25px 0 #4299e1, 1.5px 1.5px 0 #4299e1, 1.75px 1.75px 0 #4299e1, 2px 2px 0 #4299e1, 2.25px 2.25px 0 #4299e1, 2.5px 2.5px 0 #4299e1, 2.75px 2.75px 0 #4299e1, 3px 3px 0 #4299e1, 3.25px 3.25px 0 #4299e1, 3.5px 3.5px 0 #4299e1, 3.75px 3.75px 0 #4299e1, 4px 4px 0 #4299e1, 4.25px 4.25px 0 #4299e1, 4.5px 4.5px 0 #4299e1, 4.75px 4.75px 0 #4299e1, 5px 5px 0 #4299e1, 5.25px 5.25px 0 #4299e1, 5.5px 5.5px 0 #4299e1, 5.75px 5.75px 0 #4299e1, 6px 6px 0 #4299e1"
);
});

test("renders 404 page", async () => {
await render(<NotFoundRoute />);

const linkElement = screen.getByText(/Page not found/i);
expect(linkElement).toBeInTheDocument();
});
35 changes: 25 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { Routes, Route } from "react-router-dom";
import { ErrorComponent, RouterProvider, createRouter } from "@tanstack/react-router";
import { Provider as Redux } from "react-redux";

import { Home, NotFound } from "view/screens";
import store from "state/index";

import { HaloProvider } from "view/components";

import { routeTree } from "./routeTree.gen";

const router = createRouter({
routeTree,
defaultPreload: "intent",
defaultPendingComponent: () => null,
defaultErrorComponent: ({ error }) => <ErrorComponent error={error} />
});

declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}

function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />

{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<Redux store={store}>
<HaloProvider>
<RouterProvider router={router} defaultPreload="intent" context={{}} />
</HaloProvider>
</Redux>
);
}

Expand Down
13 changes: 1 addition & 12 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider as Redux } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import store from "state";

import { plausibleBootstrap } from "lib/analytics";
import "lib/styles/global/index.scss";

import { HaloProvider } from "view/components";

import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
import App from "./App";

Expand All @@ -19,13 +14,7 @@ const root = createRoot(rootElement as HTMLElement);

root.render(
<StrictMode>
<Redux store={store}>
<Router>
<HaloProvider>
<App />
</HaloProvider>
</Router>
</Redux>
<App />
</StrictMode>
);

Expand Down
14 changes: 8 additions & 6 deletions src/lib/global/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setGlobalValue } from "./utils";
import { parseEnv, setGlobalValue } from "./utils";

/**
* Environment variables.
Expand All @@ -11,15 +11,17 @@ export const env = Object.freeze({
appVersion: import.meta.env.VITE_VERSION,
gitBranch: import.meta.env.VITE_GIT_BRANCH,
gitCommitHash: import.meta.env.VITE_GIT_COMMIT,
mode: import.meta.env.MODE,
isDevelopment: import.meta.env.MODE === "development",
isProduction: import.meta.env.MODE === "production",
isTesting: import.meta.env.MODE === "test" || import.meta.env.MODE === "testing",
showDevTools: parseEnv(import.meta.env.VITE_SHOW_DEVTOOLS) || true,
plausible: {
enable: import.meta.env.VITE_PLAUSIBLE_ENABLE === "true",
enable: parseEnv(import.meta.env.VITE_PLAUSIBLE_ENABLE),
domain: import.meta.env.VITE_PLAUSIBLE_DOMAIN,
apiHost: import.meta.env.VITE_PLAUSIBLE_API_HOST
},
mode: import.meta.env.MODE,
isDevelopment: import.meta.env.MODE === "development",
isProduction: import.meta.env.MODE === "production",
isTesting: import.meta.env.MODE === "test" || import.meta.env.MODE === "testing",
isStaging: import.meta.env.MODE === "stage" || import.meta.env.MODE === "staging",
// Features
timerIncrement: import.meta.env.VITE_FEATURE_INCREMENT,
someOtherFeature: false
Expand Down
13 changes: 10 additions & 3 deletions src/lib/global/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ import { versionString } from "./version";

export const globalInit = () => {
// Inject global functions.
injectLog();
injectEnv();
injectLog();
injectFeature();
if (!env.isProduction) injectDevTools();

if (!env.isTesting) console.log(`%c${versionString()}`, "font-size: 1.1em;");
// Log app name+version. Hide for tests to reduce clutter in console.
if (!env.isTesting) {
console.log(`%c${versionString()}`, "font-size: 1.1em;padding: 1rem 0;");
}

if (env.isDevelopment) {
injectDevTools();
console.log("env", env);
}
};

globalInit();
Expand Down
2 changes: 1 addition & 1 deletion src/lib/global/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const debugn = (namespace: string, logLevel: any, ...args: any[]) => {
};

export const injectLog = () => {
setGlobalValue("logStore", new LogStore());
$global.logStore = new LogStore();
setGlobalValue("log", log);
setGlobalValue("logn", logn);
setGlobalValue("debug", debug);
Expand Down
18 changes: 18 additions & 0 deletions src/lib/global/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,21 @@ export const setGlobalValue = (key: string, value: any) => {
};

export const $global = getGlobal();

/**
* Parse string environment variable into a primitive.
*
* @exmaple `parseEnv("VITE_FEATURE_ENABLED") => true`
*/
export const parseEnv = (value: any, isJson = false) => {
if (value === "true") return true;
if (value === "false") return false;
if (value === "undefined") return undefined;
if (value === "null") return null;
if (isJson) {
try {
return JSON.parse(value);
} catch (e) {}
}
return value;
};
24 changes: 12 additions & 12 deletions src/lib/styles/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getStyle, render, select, selectTestId } from "tests";
import { describe, expect, test } from "vitest";

import StylesMock from "tests/StylesMock";
import { getStyle, render, select } from "tests";

/**
* Test to see if the styles are being compiled and injected correctly.
Expand All @@ -12,8 +12,8 @@ import { getStyle, render, select } from "tests";
*/

describe("@linaria with theme injection", () => {
test("renders colors", () => {
const { container } = render(<StylesMock />);
test("renders colors", async () => {
const { container } = await render(<StylesMock />);

const styleTitle = getStyle(select(container, "h1"));
expect(styleTitle.color).toBe("#38a169");
Expand All @@ -22,29 +22,29 @@ describe("@linaria with theme injection", () => {
expect(styleSubTitle.color).toBe("#dd6b20");
});

test("renders mixins", () => {
const { container } = render(<StylesMock />);
test("renders mixins", async () => {
const { container } = await render(<StylesMock />);

const styleContainer = getStyle(select(container, "div"));
const styleContainer = getStyle(selectTestId(container, "StylesMock"));
expect(styleContainer.maxWidth).toBe("567px");
expect(styleContainer.marginLeft).toBe("auto");
expect(styleContainer.marginRight).toBe("auto");
expect(styleContainer.transition).toBe("all, 80ms, ease");
});

test("renders shadows", () => {
const { container } = render(<StylesMock />);
test("renders shadows", async () => {
const { container } = await render(<StylesMock />);

const styleContainer = getStyle(select(container, "div"));
const styleContainer = getStyle(selectTestId(container, "StylesMock"));
expect(styleContainer.boxShadow).toBe(
"0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)"
);
});

test("renders variables", () => {
const { container } = render(<StylesMock />);
test("renders variables", async () => {
const { container } = await render(<StylesMock />);

const styleContainer = getStyle(select(container, "div"));
const styleContainer = getStyle(selectTestId(container, "StylesMock"));
expect(styleContainer.width).toBe("5678px");
});
});
58 changes: 58 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// This file is auto-generated by TanStack Router

import { createFileRoute } from '@tanstack/react-router'

// Import Routes

import { Route as rootRoute } from './view/routes/__root'
import { Route as IndexImport } from './view/routes/index'

// Create Virtual Routes

const UserLazyImport = createFileRoute('/user')()
const UserUserIdLazyImport = createFileRoute('/user/$userId')()

// Create/Update Routes

const UserLazyRoute = UserLazyImport.update({
path: '/user',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./view/routes/user.lazy').then((d) => d.Route))

const IndexRoute = IndexImport.update({
path: '/',
getParentRoute: () => rootRoute,
} as any)

const UserUserIdLazyRoute = UserUserIdLazyImport.update({
path: '/$userId',
getParentRoute: () => UserLazyRoute,
} as any).lazy(() =>
import('./view/routes/user.$userId.lazy').then((d) => d.Route),
)

// Populate the FileRoutesByPath interface

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/user': {
preLoaderRoute: typeof UserLazyImport
parentRoute: typeof rootRoute
}
'/user/$userId': {
preLoaderRoute: typeof UserUserIdLazyImport
parentRoute: typeof UserLazyImport
}
}
}

// Create and export the route tree

export const routeTree = rootRoute.addChildren([
IndexRoute,
UserLazyRoute.addChildren([UserUserIdLazyRoute]),
])
Loading
Loading