crank
crank copied to clipboard
create-crank-app
Setting up JSX and a module bundler can be kinda tricky. If you create an npm project starting with create-
, e.g. create-blah
you can type npm init blah
, which is equivalent to npx create-blah
.
https://docs.npmjs.com/cli/init
npm init <initializer>
can be used to set up a new or existing npm package.
initializer
in this case is an npm package namedcreate-<initializer>
, which will be installed bynpx
, and then have its main bin executed – presumably creating or updatingpackage.json
and running any other initialization-related operations.
https://github.com/facebook/create-react-app is the most famous of these, and infamously has a ton of complicated features. I would also call attention to Stencil's initializer https://github.com/ionic-team/create-stencil which is still pretty complicated, but it's not as bad.
I don't know where to put this in a PR (I think probably create-crank-app
should live in a separate repo?) but here's a quicky script I threw together that seems to do something useful.
import { mkdir, writeFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, basename } from 'path';
import { spawn } from 'child_process';
function die(error) {
console.error(error);
process.exit(1);
}
process.on('unhandledRejection', die);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageName = basename(__dirname);
const packageJson = {
name: packageName,
version: "1.0.0",
}
async function writeJson(filename, obj) {
return writeFile(filename, JSON.stringify(obj, null, 2) + "\n", 'utf8');
}
async function spawnPromise(cmd) {
const child = spawn(cmd, {shell: true});
for (const pipe of ["stdout", "stderr"]) {
child[pipe].setEncoding('utf8');
child[pipe].on('data', data => process[pipe].write(data));
}
return new Promise((resolve, reject) => {
child.on('close', code => {
code ? reject() : resolve();
});
});
}
const html = `<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script src="index.js"></script>
</body>
</html>
`;
const js = `/** @jsx createElement */
import {createElement} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";
function Greeting({name = "World"}) {
return (
<div>Hello {name}</div>
);
}
renderer.render(<Greeting />, document.body);
`
const gitignore = `.cache
dist
node_modules
`;
void async function() {
await writeJson('package.json', {
name: packageName,
version: '1.0.0',
scripts: {
start: "parcel index.html --open",
build: "parcel build index.html",
}
});
await spawnPromise('npm install @bikeshaving/crank');
await spawnPromise('npm install --save-dev parcel-bundler');
await Promise.all([
writeJson('.babelrc', {presets:['@babel/preset-react']}),
writeFile('index.html', html, 'utf8'),
writeFile('index.js', js, 'utf8'),
writeFile('.gitignore', gitignore, 'utf8'),
]);
await spawnPromise('npm run build');
console.log("Now you can `npm run start` to view your Crank app in a browser.");
}();
Possible enhancements:
- I think it's typical for initializers to accept a command-line argument for a directory to create? (I'm hazy on this; I can't see how to
npm init
my script without publishing an actual package tonpm
.) - The current script clobbers any
package.json
file in the current directory, which seems unfriendly. (I'm not sure how othernpm
initializers handle this.) -
npm init
typically interactively prompt the user forpackage.json
fields, and offer a--yes
-y
CLI arg to accept all defaults - This script leaves
description
andlicense
blank, which causesnpm install
to print warnings - The log is pretty noisy as the initializer calls
npm install
on a bunch of stuff. Some initializers hide that behind fancy progress bars. (But, on the other hand, that means you can't see the logs.) - This script lets Parcel install Babel 7. Should it install Babel itself? (I kinda think no, but I'm not sure.)
IIRC npm init foo bar is equivalent to node /path/to/create-foo bar? Entry point is the "bin" property in /path/to/create-foo/package.json, forwarding the arguments unchanged. Like, process.argv[2] should contain the dirname, and you fall back to process.cwd() if it's undefined.
What do you win by using pipes and async apis in a one-off script, if you don't modify output or parallelize commands?
What do you win by using pipes and async apis in a one-off script, if you don't modify output or parallelize commands?
I do parallelize some of the commands, though, admittedly, not any of the spawns. (I thought I was going to, and then decided not to.)
I figured out how to test npm init
without pushing a release. I made a create-crank-app
directory locally. Then I could do this:
cd ..
mkdir cca
cd cca
npm init -y
npm install ../create-crank-app
mkdir test
cd test
npm init crank-app
npm init crank-app
, is indeed equivalent to npx create-crank-app
, which found create-crank-app
in the parent directory's node_modules
.
npm init crank-app subtest
did not, by itself, create a subdirectory; it just passed the arg to my script, indicating that we do have to do this by hand.
Nonetheless, I think the next step is for there to be a github repo for create-crank-app
. Then we can turn my list of enhancements into issues, fix some/all of them, deploy to npm
, and then update the documentation to recommend using it.
Preact cli does more than we need to out of the box (especially for the first iteration of this) but it's a great place to look for inspiration:
- https://github.com/preactjs/preact-cli#preact-create
- https://github.com/preactjs/preact-cli/blob/master/packages/cli/lib/commands/create.js
They keep their cli templates here
I’m investigating snowpack and think it could potentially be revolutionary. I’ve been dissatisfied with all the major bundlers and the features/philosophical clarity of snowpack version 2 is something I admire.
https://www.snowpack.dev/#get-started
Snowpack looks really cool, but Snowpack 2 is not in a good place right now. I couldn't get even the most basic Babel/JSX transpilation to work this afternoon. https://github.com/pikapkg/snowpack/issues/294
@dfabulich Yes as I investigate it seems like it uses both rollup and parcel under the hood somehow? 😨
@dfabulich do you enjoy finding build/environment bugs because you seem to be very good at it haha.
every time I go to try a new bundler/build tool in the JS world I always end up going back to webpack for bundling applications (js + css + html + assets). they have a few sane defaults and if you keep the config simple, it's not horrible and will do what you expect.
I'd highly suggest we use webpack for create-crank-app :)
Apparently the Vue.js Vite project supports preact as well. https://twitter.com/youyuxi/status/1257882133177274369
I really do like the idea of no transpilation for development. I can never ever seem to get sourcemaps/errors working 100% and it seems like it could be a lovely development experience.
@ryhinchey If you snoop around https://github.com/bikeshaving/crank/blob/master/website/webpack.tsx?ts=2 you’ll see that I sorta attempted to create a bundler API which is defined entirely with components. The fact that components on the server can be async today means we can do stuff like this, and a lot of bundler configuration complexity stems from trying to get scripts to match the bundler output. If there was a bundler that had a clear and promise-fluent API which was like... you pass in a local file path and it compiles it for you and it spits out information about the bundle, we could actually get a sort of zero-config type of bundling system where you can just use enhanced versions of script and style tags to both kick off rendering and render html on the server.
I'm not sure if the complications described above came from trying to leverage Babel, but I was able to get the basics going with Snowpack 2 and just using the Typescript TSX compiler. Here is the gist with relevant app files and Snowpack configuration. I started with the Snowpack blank template.
The only thing that doesn't work is HMR. The HMR triggers, but module code is not refreshed. Looking at some of the plugins, it looks like each framework as their own way to register modules with Snowpack. I'm new to looking into the HMR internals. But it would seem that some sort of HMR plugin would need to be built specific to Crank. These plugins don't seem overly complex to write, but they are not trivial. I'll keep looking into it and see if how hard it would be to create a Snowpack plugin for managing the HMR process.
Fast React issue which describes the Fast React implementation.