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

Add a CLI to create new Parcel apps #10069

Merged
merged 4 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading