Skip to content

Commit

Permalink
Add a CLI to create new Parcel apps (#10069)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Jan 12, 2025
1 parent ee909d4 commit f86f5f2
Show file tree
Hide file tree
Showing 46 changed files with 1,035 additions and 37 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ packages/*/*/test/mochareporters.json
packages/core/integration-tests/test/input/**
packages/core/utils/test/input/**
packages/utils/create-react-app/templates
packages/utils/create-parcel-app/templates
packages/examples

# Generated by the build
Expand Down
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const paths = {
packageJson: [
'packages/core/parcel/package.json',
'packages/utils/create-react-app/package.json',
'packages/utils/create-parcel/package.json',
'packages/dev/query/package.json',
'packages/dev/bundle-stats-cli/package.json',
],
Expand Down
3 changes: 2 additions & 1 deletion packages/core/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"@babel/preset-env": "^7.22.14",
"@babel/preset-typescript": "^7.22.11",
"@mdx-js/react": "^1.5.3",
"@types/react": "^17",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.0",
"chalk": "^4.1.2",
"command-exists": "^1.2.6",
Expand Down
1 change: 0 additions & 1 deletion packages/packagers/react-static/src/ReactStaticPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ async function loadBundleUncached(
];
});
} else if (entryBundle) {
// console.log('here', entryBundle)
queue.add(async () => {
let {assets: subAssets} = await loadBundle(
entryBundle,
Expand Down
30 changes: 23 additions & 7 deletions packages/reporters/dev-server/src/NodeRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {md, errorToDiagnostic} from '@parcel/diagnostic';
import nullthrows from 'nullthrows';
import {Worker} from 'worker_threads';
import path from 'path';
import {type Deferred, makeDeferredWithPromise} from '@parcel/utils';
import type {HMRMessage} from './HMRServer';

export type NodeRunnerOptions = {|
Expand All @@ -15,7 +16,8 @@ export type NodeRunnerOptions = {|
export class NodeRunner {
worker: Worker | null = null;
bundleGraph: BundleGraph<PackagedBundle> | null = null;
pending: boolean = true;
pending: Promise<void> | null = null;
deferred: Deferred<void> | null = null;
logger: PluginLogger;
hmr: boolean;

Expand All @@ -25,17 +27,25 @@ export class NodeRunner {
}

buildStart() {
this.pending = true;
let {deferred, promise} = makeDeferredWithPromise();
this.pending = promise;
this.deferred = deferred;
}

buildSuccess(bundleGraph: BundleGraph<PackagedBundle>) {
async buildSuccess(bundleGraph: BundleGraph<PackagedBundle>) {
this.bundleGraph = bundleGraph;
this.pending = false;

let deferred = this.deferred;
this.pending = null;
this.deferred = null;

if (this.worker == null) {
this.startWorker();
await this.startWorker();
} else if (!this.hmr) {
this.restartWorker();
await this.restartWorker();
}

deferred?.resolve();
}

startWorker(): Promise<void> {
Expand Down Expand Up @@ -88,7 +98,11 @@ export class NodeRunner {
this.worker = worker;

return new Promise(resolve => {
worker.once('online', () => resolve());
if (this.hmr) {
worker.once('message', () => resolve());
} else {
worker.once('online', () => resolve());
}
});
} else {
return Promise.resolve();
Expand All @@ -107,6 +121,8 @@ export class NodeRunner {
// If the build is still pending, wait until it completes to restart.
if (!this.pending) {
await this.startWorker();
} else {
await this.pending;
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/reporters/dev-server/src/ServerReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ export default (new Reporter({
// If running in node, wait for the server to update before emitting the update
// on the client. This ensures that when the client reloads the server is ready.
if (nodeRunner) {
await nodeRunner.emitUpdate(update);
// Don't await here because that blocks the build from continuing
// and we may need to wait for the buildSuccess event.
let hmr = hmrServer;
nodeRunner.emitUpdate(update).then(() => hmr.broadcast(update));
} else {
hmrServer.broadcast(update);
}
hmrServer.broadcast(update);
}
}
break;
Expand Down
3 changes: 3 additions & 0 deletions packages/runtimes/hmr/src/loaders/hmr-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ if (!parent || !parent.isParcelRequire) {
parentPort.postMessage('restart');
}
});

// After the bundle has finished running, notify the dev server that the HMR update is complete.
queueMicrotask(() => parentPort.postMessage('ready'));
}
} catch {
if (typeof WebSocket !== 'undefined') {
Expand Down
10 changes: 4 additions & 6 deletions packages/transformers/js/src/JSTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,12 +404,10 @@ export default (new Transformer({
let supportsModuleWorkers =
asset.env.shouldScopeHoist && asset.env.supports('worker-module', true);
let isJSX = Boolean(config?.isJSX);
if (asset.isSource) {
if (asset.type === 'ts') {
isJSX = false;
} else if (!isJSX) {
isJSX = Boolean(JSX_EXTENSIONS[asset.type]);
}
if (asset.type === 'ts') {
isJSX = false;
} else if (!isJSX) {
isJSX = Boolean(JSX_EXTENSIONS[asset.type]);
}

let type = 'js';
Expand Down
23 changes: 23 additions & 0 deletions packages/utils/create-parcel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "create-parcel",
"version": "2.13.3",
"bin": {
"create-parcel": "lib/create-parcel.js"
},
"main": "src/create-parcel.js",
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git",
"directory": "packages/utils/create-parcel"
},
"source": "src/create-parcel.js",
"files": [
"templates",
"lib"
],
"license": "MIT",
"publishConfig": {
"access": "public"
},
"dependencies": {}
}
190 changes: 190 additions & 0 deletions packages/utils/create-parcel/src/create-parcel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env node

// @flow
/* eslint-disable no-console */

// $FlowFixMe
import fs from 'fs/promises';
import {readdirSync} from 'fs';
import path from 'path';
import {spawn as _spawn} from 'child_process';
// $FlowFixMe
import {parseArgs, styleText} from 'util';

const supportsEmoji = isUnicodeSupported();

// Fallback symbols for Windows from https://en.wikipedia.org/wiki/Code_page_437
const success: string = supportsEmoji ? '✨' : '√';
const error: string = supportsEmoji ? '🚨' : '×';

const {positionals} = parseArgs({
allowPositionals: true,
options: {},
});

let template = positionals[0];
if (!template) {
let packageManager = getCurrentPackageManager()?.name;
console.error(
`Usage: ${packageManager ?? 'npm'} create <template> [directory]\n`,
);
printAvailableTemplates();
console.log('');
process.exit(1);
}

let name = positionals[1];
if (!name) {
name = '.';
}

install(template, name).then(
() => {
process.exit(0);
},
err => {
console.error(err);
process.exit(1);
},
);

async function install(template: string, name: string) {
let templateDir = path.join(__dirname, '..', 'templates', template);
try {
await fs.stat(templateDir);
} catch {
console.error(
style(['red', 'bold'], `${error} Unknown template ${template}.\n`),
);
printAvailableTemplates();
console.log('');
process.exit(1);
return;
}

if (name === '.') {
if ((await fs.readdir(name)).length !== 0) {
console.error(style(['red', 'bold'], `${error} Directory is not empty.`));
process.exit(1);
return;
}
} else {
try {
await fs.stat(name);
console.error(style(['red', 'bold'], `${error} ${name} already exists.`));
process.exit(1);
return;
} catch {
// ignore
}
await fs.mkdir(name, {recursive: true});
}

await spawn('git', ['init'], {
stdio: 'inherit',
cwd: name,
});

await fs.cp(templateDir, name, {
recursive: true,
});

let packageManager = getCurrentPackageManager()?.name;
switch (packageManager) {
case 'yarn':
await spawn('yarn', [], {cwd: name, stdio: 'inherit'});
break;
case 'pnpm':
await spawn('pnpm', ['install'], {cwd: name, stdio: 'inherit'});
break;
case 'npm':
default:
await spawn(
'npm',
['install', '--legacy-peer-deps', '--no-audit', '--no-fund'],
{cwd: name, stdio: 'inherit'},
);
break;
}

await spawn('git', ['add', '-A'], {cwd: name});
await spawn(
'git',
['commit', '--quiet', '-a', '-m', 'Initial commit from create-parcel'],
{
stdio: 'inherit',
cwd: name,
},
);

console.log('');
console.log(style(['green', 'bold'], `${success} Your new app is ready!\n`));
console.log('To get started, run the following commands:');
console.log('');
if (name !== '.') {
console.log(` cd ${name}`);
}
console.log(` ${packageManager ?? 'npm'} start`);
console.log('');
}

function spawn(cmd, args, opts) {
return new Promise((resolve, reject) => {
let p = _spawn(cmd, args, opts);
p.on('close', (code, signal) => {
if (code || signal) {
reject(new Error(`${cmd} failed with exit code ${code}`));
} else {
resolve();
}
});
});
}

function getCurrentPackageManager(
userAgent: ?string = process.env.npm_config_user_agent,
): ?{|name: string, version: string|} {
if (!userAgent) {
return undefined;
}

const pmSpec = userAgent.split(' ')[0];
const separatorPos = pmSpec.lastIndexOf('/');
const name = pmSpec.substring(0, separatorPos);
return {
name: name,
version: pmSpec.substring(separatorPos + 1),
};
}

function printAvailableTemplates() {
console.error('Available templates:\n');
for (let dir of readdirSync(path.join(__dirname, '..', 'templates'))) {
console.error(` • ${dir}`);
}
}

// From https://github.com/sindresorhus/is-unicode-supported/blob/8f123916d5c25a87c4f966dcc248b7ca5df2b4ca/index.js
// This package is ESM-only so it has to be vendored
function isUnicodeSupported() {
if (process.platform !== 'win32') {
return process.env.TERM !== 'linux'; // Linux console (kernel)
}

return (
Boolean(process.env.CI) ||
Boolean(process.env.WT_SESSION) || // Windows Terminal
process.env.ConEmuTask === '{cmd::Cmder}' || // ConEmu and cmder
process.env.TERM_PROGRAM === 'vscode' ||
process.env.TERM === 'xterm-256color' ||
process.env.TERM === 'alacritty'
);
}

function style(format, text) {
if (styleText) {
return styleText(format, text);
} else {
return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.parcel-cache/
dist/
node_modules/
19 changes: 19 additions & 0 deletions packages/utils/create-parcel/templates/react-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "parcel-react-client-starter",
"private": true,
"version": "0.0.0",
"source": "src/index.html",
"scripts": {
"start": "parcel",
"build": "parcel build"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"parcel": "^2.13.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
html {
color-scheme: light dark;
font-family: system-ui;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
Loading

0 comments on commit f86f5f2

Please sign in to comment.