$ cnpm install @jsenv/core
Holistic likable builder of JavaScript projects.
@jsenv/core
was first created to be able to write tests that could be executed in different browsers AND Node.js. In the end it became a tool covering the core needs of a JavaScript project:
Jsenv integrates naturally with standard html, css and js. It can be configured to work with TypeScript and React.
@jsenv/core
provides a test runner: A function executing test files to know if some are failing. This function is called executeTestPlan
. Check steps below to get an idea of its usage.
In order to show code unrelated to a specific codebase the example below is testing
Math.max
. In reality you wouldn't testMath.max
.
Math.max.test.js
const actual = Math.max(2, 4)
const expected = 4
if (actual !== expected) {
throw new Error(`Math.max(2, 4) should return ${expected}, got ${actual}`)
}
Math.max.test.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<link rel="icon" href="data:," />
</head>
<body>
<script type="module" src="./Math.max.test.js"></script>
</body>
</html>
execute-test-plan.js
import { executeTestPlan, launchChromiumTab, launchFirefoxTab, launchNode } from "@jsenv/core"
executeTestPlan({
projectDirectoryUrl: new URL("./", import.meta.url),
testPlan: {
"**/*.test.html": {
chromium: {
launch: launchChromiumTab,
},
firefox: {
launch: launchFirefoxTab,
},
},
"**/*.test.js": {
node: {
launchNode,
},
},
},
})
Code above translates into the following sentence: "Execute all files in my project that ends with
test.html
on Chrome and Firefox AND execute all files that ends withtest.js
on Node.js"
Read more on testing documentation
@jsenv/core
provides a server capable to turn any html file into an entry point. This power can be used to create a storybook, debug a file in isolation and more. This server is called exploring server
. This server is designed for development: it provides livereloading out of the box and does not bundle files.
The following example shows how it can be used to execute a single test file. As mentioned previously it can execute any html file, not only test files.
import { startExploring } from "@jsenv/core"
startExploring({
projectDirectoryUrl: new URL("./", import.meta.url),
explorableConfig: {
source: {
"**/*.html": true,
},
},
compileServerPort: 3456,
})
When you open https://localhost:3456
in a browser, a page called jsenv exploring index is shown. It displays a list of all your html files. You can click a file to execute it inside the browser. In our previous example we created Math.max.test.html
so it is displayed in that list.
Maybe you noticed the black toolbar at the bottom of the page? We'll see that further in the documentation.
Math.max.test.html
</summary>
Clicking Math.max.test.html
load an empty blank page because code inside this html file display nothing and does not throw.
To get a better idea of how this would integrate in everyday workflow, let's go a bit further and see what happens if we make test file throw. After that we'll revert the changes.
Math.max.test.html
fail</summary>
- const expected = 4
+ const expected = 3
The browser page is reloaded and page displays the failure. Jsenv also uses Notification API to display a system notification.
Math.max.test.html
</summary>
- const expected = 3
+ const expected = 4
Browser livereloads again and error is gone together with a system notification.
Read more exploring documentation
Building can be described as: generating files optimized for production thanks to minification, concatenation and long term caching.
Jsenv only needs to know your main html file and where to write the builded files. You can create a script and execute it with node.
Following the simplified steps below turns index.html
into a production optimized dist/main.html
.
index.html
</summary>
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" />
<script type="importmap" src="./import-map.importmap"></script>
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<script type="module" src="./main.js"></script>
</body>
</html>
To keep example concise, the content of the following files is not shown:
favicon.ico
import-map.importmap
main.css
main.js
build-project.js
</summary>
import { buildProject } from "@jsenv/core"
await buildProject({
projectDirectoryUrl: new URL("./", import.meta.url),
buildDirectoryRelativeUrl: "dist",
enryPointMap: {
"./index.html": "./main.html",
},
minify: false,
})
build-project.js
</summary>
node ./build-project.js
dist/index.html
</summary>
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8" />
<link rel="icon" href="assets/favicon-5340s4789a.ico" />
<script type="importmap" src="import-map-b237a334.importmap"></script>
<link rel="stylesheet" type="text/css" href="assets/main-3b329ff0.css" />
</head>
<body>
<script type="module" src="./main-f7379e10.js"></script>
</body>
</html>
To keep example concise, the content of the following files is not shown:
dist/assets/favicon-5340s4789a.ico
dist/import-map-b237a334.importmap
dist/assets/main-3b329ff0.css
dist/main-f7379e10.js
Read more building documentation
Jsenv focuses on one thing: developer experience. Everything was carefully crafted to get explicit and coherent apis. This section list most important features provided by jsenv. Click them to get more details.
Jsenv is dispensable by default. As long as your code is using only standards, you could remove jsenv from your project and still be able to run your code. You can double click your html file to open it inside your browser -> it works. Or if this is a Node.js file execute it directly using the node
command.
Being dispensable by default highlights jsenv philosophy: no new concept to learn. It also means you can switch to an other tool easily as no part of your code is specific to jsenv.
Jsenv also don't like blackboxes. @jsenv/core
functions always choose expliciteness over magic. It makes things much simpler to understand and follow both for jsenv and for you.
One example of expliciteness over magic: You control and tell jsenv where is your project directory. Jsenv don't try to guess or assume where it is.
Context switching happens when you are in context A and switch to context B. The more context A differ from context B, the harder it is to switch.
Some example where context switching occurs:
With jsenv context switching almost vanishes because:
Less context switching saves lot of energy making a project codebase faster to write and easier to maintain.
This proposal allows control over what URLs get fetched by JavaScript import statements and import() expressions. This allows "bare import specifiers", such as import moment from "moment", to work.
— Domenic Denicola in WICG/import-maps
Jsenv supports import maps out of the box. The following html can be used with jsenv:
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8" />
<script type="importmap" src="./project.importmap"></script>
</head>
<body>
<script type="module">
import moment from "moment"
console.log(moment)
</script>
</body>
</html>
Top-Level await has moved to stage 3, so the answer to your question How can I use async/await at the top level? is to just add await the call to main()
Jsenv supports top level await out of the box. Top level await allow jsenv to know when a file code is done executing. This is used to kill a file that is too long to execute and know when to collect code coverage.
The lazy-loading capabilities enabled by dynamic import() can be quite powerful when applied correctly. For demonstration purposes, Addy modified an example Hacker News PWA that statically imported all its dependencies, including comments, on first load. The updated version uses dynamic import() to lazily load the comments, avoiding the load, parse, and compile cost until the user really needs them.
— Mathias Bynens on Dynamic import()
Dynamic import are supported by jsenv. When building project using buildProject
, dynamic import are turned into separate chunks.
It's a proposal to add the ability for ES modules to figure out what their file name or full path is. This behaves similarly to __dirname in Node which prints out the file path to the current module. According to caniuse, most browsers already support it (including the latest Chromium Edge)
— Jake Deichert on A Super Hacky Alternative to import.meta.url
Jsenv supports import.meta.url
.
A common pattern to reference an asset is to use an import statement. This import would actually return an url to the asset.
import imageUrl from "./img.png"
As it's not standard, it doesn't work in the browser without transformation. However, import.meta.url
does work in the browser.
const imageUrl = new URL("./img.png", import.meta.url)
You can use both patterns to reference an asset but prefer the one relying on import.meta.url
.
A common pattern to write code specific to dev environment consists into using process.env.NODE_ENV
and rely on dead code elimination provided by tree shaking.
if (process.env.NODE_ENV !== "production") {
console.log("log visible only in dev")
}
Is transformed, when building for production, into a false
constant and eliminated by tree shaking.
if (false) {
console.log("log visible only in dev")
}
But process.env.NODE_ENV
is specific to Node.js. It would not work in a browser without transformation. Using import.meta.dev
it's possible to write something browser can understand.
if (import.meta.dev) {
console.log("log visible only in dev")
}
The list above is non exaustive, there is more like long term caching, livereload without configuration, service workers, ...
npm install --save-dev @jsenv/core
@jsenv/core
is tested on Mac, Windows, Linux on Node.js 14.5.0. Other operating systems and Node.js versions are not tested.
Jsenv can execute standard JavaScript and be configured to run non-standard JavaScript.
Standard corresponds to JavaScript Modules, destructuring, optional chaining and so on.
Non-standard corresponds to CommonJS modules, JSX or TypeScript.
Keep in mind one of your dependency may use non-standard JavaScript. For instance react uses CommonJS modules.
We recommend to regroup configuration in a jsenv.config.js
file at the root of your working directory.
To get a better idea see jsenv.config.js. The file can be imported and passed using the spread operator. This technic helps to see jsenv custom configuration quickly and share it between files.
— See script/test/test.js
That being said it's only a recommendation. There is nothing enforcing or checking the presence of jsenv.config.js
.
CommonJS module format rely on module.exports
and require
. It was invented by Node.js and is not standard JavaScript. If your code or one of your dependency uses it, it requires some configuration.
jsenv.config.js
to use code written in CommonJS</summary>
import { jsenvBabelPluginMap, convertCommonJsWithRollup } from "@jsenv/core"
export const convertMap = {
"./node_modules/whatever/index.js": convertCommonJsWithRollup,
}
jsenv.config.js
above makes jsenv compatible with a package named whatever
that would be written in CommonJS.
React is written in CommonJS and comes with JSX. If you use them it requires some configuration.
jsenv.config.js
for react and jsx</summary>
import { createRequire } from "module"
import { jsenvBabelPluginMap, convertCommonJsWithRollup } from "@jsenv/core"
const require = createRequire(import.meta.url)
const transformReactJSX = require("@babel/plugin-transform-react-jsx")
export const babelPluginMap = {
...jsenvBabelPluginMap,
"transform-react-jsx": [
transformReactJSX,
{ pragma: "React.createElement", pragmaFrag: "React.Fragment" },
],
}
export const convertMap = {
"./node_modules/react/index.js": convertCommonJsWithRollup,
"./node_modules/react-dom/index.js": (options) => {
return convertCommonJsWithRollup({ ...options, external: ["react"] })
},
}
See also
TypeScript needs some configuration if you use it.
jsenv.config.js
for TypeScript</summary>
import { createRequire } from "module"
import { jsenvBabelPluginMap } from "@jsenv/core"
const require = createRequire(import.meta.url)
const transformTypeScript = require("@babel/plugin-transform-typescript")
export const babelPluginMap = {
...jsenvBabelPluginMap,
"transform-typescript": [transformTypeScript, { allowNamespaces: true }],
}
See also
An overview of the main dependencies used by @jsenv/core
.
Dependency | How it is used by jsenv |
---|---|
systemjs | "Polyfill" js modules, import maps and more |
playwright | Launch Chromium, Firefox and WebKit |
istanbul | Collect and generate code coverage |
rollup | Tree shaking when building |
babel | Parse and transform js |
parse5 | Parse and transform html |
postCSS | Parse and transform css |
The name jsenv
stands for JavaScript environments. This is because the original purpose of jsenv
was to bring closer two JavaScript runtimes: web browsers and Node.js. This aspect of jsenv
is not highlighted in the documentation but it exists.
Maybe jsenv
should be written JSEnv
? It's too boring to hold shift
on keyboard while typing JSE
, then release shift
, then type nv
, so it's a no.
The logo is composed by the name at the center and two circles orbiting around it. One of the circle is web browsers, the other is Node.js. It represents the two JavaScript environments supported by jsenv.
Warning: This is a joke
Copyright 2014 - 2017 © taobao.org |