React Project Files and Package Scripts
Open a React project for the first time and there are quite a few files sitting there. package.json, package-lock.json, vite.config.js, index.html, some config file for linting, then src, public... and if you're new to all this, it's very easy to assume they're all somehow part of React.
They aren't.
Actually different programs read different files from that folder. Browser reads index.html. npm reads package.json and package-lock.json. Vite reads vite.config.js. Oxlint has its own config. Git reads .gitignore. Then your JavaScript modules import other JavaScript modules.
I think a good question to ask whenever you're confused about some project file is: who actually reads this file?
Because if you edit a file meant for npm and expect Vite to somehow react to that setting, you're already debugging the wrong thing.
Start with the project directory
A Vite React project today might look something around this:
reactbook-first-screen/
public/
src/
.gitignore
.oxlintrc.json
index.html
package-lock.json
package.json
vite.config.jsDon't worry too much if your project has one or two extra files. Templates change, tools change, and obviously projects start adding their own stuff after some time.
The root directory is the folder containing package.json. This is usually the directory you're standing inside when running npm commands.
npm run devIf you're one directory above it and run the same command, npm may complain that it cannot find package.json.
Nothing mysterious happened there. npm looked in the current project directory for the package file it needs, and there wasn't one.
Inside the root you'll usually have src, which is where application source code goes, and public, which is for files that Vite should serve directly with their filename kept as-is.
So your React components, JavaScript modules, imported CSS, imported images etc usually live under src. Something such as robots.txt, on the other hand, makes more sense in public because you want /robots.txt to stay /robots.txt.
Follow what the browser loads first
For a normal Vite app, browser starts with index.html.
You might see something like this:
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>That div is the DOM container React will later render into. The script below it tells the browser to load /src/main.jsx as a JavaScript module.
Now open main.jsx and you'll probably find imports around these:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";These imports may look almost the same, but they're not being resolved in the same way.
react and react-dom/client are package imports. Vite finds those from your installed packages. ./App.jsx means "find App.jsx relative to this file". Then ./index.css is a CSS import which Vite knows how to process as part of the app.
And this is how your client application starts getting connected together.
main.jsx imports App.jsx. Maybe App.jsx imports ChapterCard.jsx. ChapterCard.jsx imports some CSS file and an image. Now all of those files are reachable from the entry module.
But suppose you create this:
src/NewApp.jsxand never import it anywhere.
Does Vite see that file sitting there? Sure, it's on disk.
Does it automatically become part of your running app just because it's inside src? Nope.
There needs to be an import path leading to it from something your app already loads. So if main.jsx still imports ./App.jsx, you can edit NewApp.jsx for the next half hour and your browser will happily show absolutely no difference.
I've definitely seen this one happen.
What package.json actually does
Now let's open package.json.
A smaller one might look something like this:
{
"name": "reactbook-first-screen",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "oxlint",
"preview": "vite preview"
}
}This file tells npm and other tooling quite a bit about the project.
name gives the package a name.
private: true tells npm not to publish this package accidentally. And just to clear up one confusion I've seen before, it has nothing to do with making your frontend code private. If JavaScript gets shipped to somebody's browser, they can receive that JavaScript. private here is about npm publishing.
Then we have:
"type": "module"This tells Node.js to treat .js files in this package as ES modules by default. That's why Node-side files such as vite.config.js can use import and export.
Your browser-side modules are a separate thing. Remember, index.html already loaded main.jsx using:
<script type="module">and Vite also processes those source files for development and builds.
Then there's the scripts object.
This part doesn't install anything. It's just naming commands that npm can run for this project.
What npm run dev is really doing
Suppose your package.json contains:
"dev": "vite"When you type:
npm run devnpm looks up the dev script and runs the command stored there, which in this case is:
viteBut where did that vite command come from? You probably never installed Vite globally.
That's because when npm runs a script, locally installed package executables are made available to that script. So the Vite installed for this specific project can be started just by writing vite there.
Which is good, because maybe project A uses one Vite version and project B uses another. Each project can run whatever version it has installed instead of depending on some random global version on your machine.
You can create your own script names too.
{
"scripts": {
"check": "npm run lint && npm run build"
}
}Now:
npm run checkruns the lint script first. If that command exits successfully, the shell runs the build command after it because of &&.
No special React feature happening there. npm starts the script and normal command-line behavior handles the &&.
Also, if you ever forget what scripts a project has, just run:
npm runnpm will print them for you. Much better than trying npm run serve, npm run start, npm run dev-server, and whatever else you remember from some completely different repo.
Dependencies and devDependencies
You'll also see packages split into dependencies and devDependencies.
For example:
{
"dependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.3",
"oxlint": "^1.71.0",
"vite": "^8.1.1"
}
}React and React DOM are packages the application itself uses. Your source code literally imports from them.
Vite, the React Vite plugin, and Oxlint are tools used while developing, checking, or building the project, so they sit under devDependencies.
But don't read too much into those two sections when asking what code ends up in the browser.
Putting something in dependencies does not automatically put it into your browser bundle, and putting something in devDependencies does not automatically keep it out.
Your imports and build process decide what gets included in the browser output.
A production build machine may install dev dependencies because it needs Vite to produce the final files. After the build is finished, you deploy the produced assets, not the Vite development server itself.
For an application project, the split mostly tells you which packages are used by the app and which ones are project tooling.
What does that ^ in a version mean?
You'll see versions written like this:
"react": "^19.2.7"That doesn't necessarily mean npm is only allowed to install exactly 19.2.7.
For a version this high, the caret allows updates from 19.2.7 up to, but not including, 20.0.0.
So a later compatible React 19 release can satisfy that range.
Then how do two developers avoid randomly ending up with different dependency versions?
That's one of the jobs of the lockfile, which we'll get to in a second.
And if you want to know what's actually installed right now, you can ask npm:
npm ls react react-dom viteThat's better than looking at some tutorial, seeing a version number there, and assuming your project has the same one.
Also don't manually change:
"react": "^19.2.7"to some newer version and assume React has now magically updated on your machine.
You've only changed some JSON text at that point. Run npm so it can resolve the requested version, install packages, and update the lockfile properly.
package.json and package-lock.json are doing different jobs
This pair confuses people quite a lot in the beginning.
package.json says what your project directly asks for. You might say React should satisfy ^19.2.7, Vite should satisfy another range, and so on.
package-lock.json records what npm actually resolved for that installation graph, including all the packages your direct dependencies pulled in as well.
Because remember, installing React isn't necessarily the only thing npm has to record. One package can depend on other packages, and those can depend on even more packages.
For an application, commit the lockfile.
Then another developer or your CI machine can install using:
npm cinpm ci follows the lockfile and expects it to agree with package.json. It also removes the existing node_modules before installing, which makes it useful when you want a clean install from the recorded package versions.
During normal development, you'll more often use:
npm installespecially when adding, removing, or changing dependencies. npm resolves those changes and updates package-lock.json when needed.
The lockfile doesn't mean your application will behave exactly the same on every operating system, every Node version, every browser, forever. But it does stop package resolution from changing every time somebody installs the project.
node_modules is just the installed packages
Then we have everyone's favourite small folder containing only three or four files:
node_modules/Yes, obviously I'm lying.
It can get huge because your direct dependencies bring their own dependencies with them.
The main thing to know is that node_modules can be recreated from the package files. Don't go editing files inside some installed package and expect that to become a permanent project fix.
Maybe your edit works locally today. Then you run a clean install tomorrow and it's gone.
Your teammate also never got that change.
CI never got that change.
So if your installation gets into some weird state, the files you really care about preserving are:
package.json
package-lock.jsonThose describe what should be installed. node_modules is the installed result.
One warning though: don't delete package-lock.json every time npm behaves strangely.
Deleting node_modules asks npm to reinstall the recorded packages.
Deleting the lockfile tells npm it can resolve allowed versions again.
Those are not the same operation.
What vite.config.js is for
A fresh React project may have a Vite config around this:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});This file is read by Vite, and Vite loads it in Node.js.
The React plugin adds the React-specific handling Vite needs during development and build work, including React Fast Refresh support.
defineConfig() itself doesn't start the development server. It mainly gives you a nicer config API and editor support.
The server starts because you ran something such as:
npm run devwhich eventually ran:
viteThen Vite finds its config and loads it.
I would keep vite.config.js pretty boring until you actually need something in there. It's very tempting to keep copying random config from Stack Overflow, GitHub issues, old projects, blog posts etc, then six months later nobody knows why half of it exists.
If you don't need a setting, don't add it.
Oxlint checks code without running your app
You may also have .oxlintrc.json in the project.
That's read by Oxlint, not React and not the browser.
When you run:
npm run lintnpm runs whatever command your lint script contains. If that's oxlint, Oxlint parses your source files and checks them using the configured rules.
It doesn't need to open Chrome and click around your app to find an unused variable.
Depending on your configured rules, it can also catch various React and Hooks mistakes just by reading the source.
But a successful lint run only tells you the configured lint rules passed.
It doesn't prove your app behaves correctly. It doesn't prove the UI is accessible. It doesn't prove your API calls work. It doesn't prove your production build even succeeds.
Those are different checks.
You can have:
npm run lintpassing while:
npm run buildfails.
Or both can pass while the page still has a bug when you actually use it.
Also don't assume every Vite React project uses Oxlint. Older projects and many existing codebases use ESLint. Open package.json, read the lint script, then you know what this specific project is using.
Again: who reads the file?
That question keeps coming back.
public files and imported files
Now let's come back to public, because this one also feels a little weird at first.
Suppose you have:
public/react-mark.svgYou can refer to it from your app using:
<img src="/react-mark.svg" alt="" />The file is served from the site root, and its name stays react-mark.svg.
Now suppose the image lives inside your source instead:
src/assets/react.svgYou can import it:
import reactMark from "./assets/react.svg";
function Logo() {
return <img src={reactMark} alt="" />;
}Now that asset is part of Vite's module processing. Vite handles the final production URL for you and includes the asset during the build if it is actually used.
So when should something go in public?
Usually when you need a file available at a fixed URL and don't want to import it through application source. Files such as robots.txt are an obvious example.
For images and other assets being used directly from components, importing them from src is generally nicer because Vite knows about that dependency and can manage it during the build.
So who reads what?
After seeing all these files, the folder should start looking a little less random.
Browser initially receives index.html. From there, the module script leads into src/main.jsx.
Vite handles that source module graph during development and build time, including imports such as App.jsx, CSS, and imported assets.
React runs the components reached through that application.
npm reads package.json to understand package information, dependency requests, and scripts. It uses package-lock.json to work with the dependency versions npm previously resolved.
Vite reads vite.config.js.
Oxlint reads its lint configuration and your source files.
Git reads .gitignore to know which files shouldn't normally be tracked.
And public contains files Vite serves or copies without making you import them through the normal source module graph.
Once you know this, config bugs become a bit less irritating because at least you know which program you're arguing with.
One last check
If I gave you a Vite React project now, you should be able to start from:
index.htmlfind:
<script type="module" src="/src/main.jsx"></script>open main.jsx, then follow its imports into App.jsx and whatever components come after that.
You should also be able to explain these three without mixing them up.
package.json says what packages and commands the project asks for.
package-lock.json records the dependency versions npm resolved.
node_modules contains the packages that are currently installed on your machine.
And if you ever forget what some random config file is doing there, don't immediately start changing it.
First ask who reads it.
Usually that gets you much closer to the answer.