From 93b04f8583b3b78a1f33195248bb7e3ebc4cd3bc Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 6 Jan 2024 15:03:44 -0800 Subject: [PATCH] chore(esm): convert crwa to esm and bundle (#9786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up to https://github.com/redwoodjs/redwood/pull/9783. This PR converts the `create-redwood-app` package to ESM and bundles all its dependencies. I started with `create-redwood-app` because the requirements for making it ESM were relatively trivial compared to the other packages since it just needs to be run by `yarn create`, and yarn create just runs a bin. For `create-redwood-app` I'm just using esbuild. Why not use tsup? 1. We're just distributing a bin. We're not distributing a dual-module package with types 2. tsup hasn't been committed to in over a month and a half, whereas esbuild releases often and is committed to more-or-less daily. That doesn't automatically disqualify it (OSS is hard), but makes me wary. I'll consider it for packages that distribute more than a bin More on bundling. Bundling this package has benefits, namely decreasing the install time—yarn doesn't have to fetch its dependencies, there are none. But I'm mostly doing it as an exercise because we need to do it more. For background on the shims (`jsBanner`) see https://github.com/evanw/esbuild/issues/1921. It's nothing bespoke and it's what tsup[^tsup] and Vite[^vite] would've done anyway. Other notes: - Updates the package's `README.md`; this could be updated more but I didn't want to spend too much time on it - Adds e2e tests for the node version check Two new e2e tests make sure we're checking node version correctly. I can't use nvm since 1. it's not easily scriptable (it's a shell built-in or something) and 2. it doesn't seem like we all use it, so I just added these tests to CI and use the GitHub action to change the node version - Fixes a bug I introduced in https://github.com/redwoodjs/redwood/pull/9728 The node version check would throw if it didn't pass because `engines.yarn` was removed. This wasn't released - Converted files to just `.js` since Node recognized them as ESM from `type` in `package.json` - Reordered `yarn create-redwood-app`'s options in help; I tried to put the ones that were more likely to be used first - Removed the header from `--help` and `--version` [^tsup]: https://github.com/egoist/tsup/blob/8c26e63c92711d60c05aedd3cdc358530ba266c5/assets/esm_shims.js [^vite]: https://github.com/vitejs/vite/blob/8de7bd2b68db27b83d9484cc8d4e26436615168e/packages/vite/rollup.config.ts#L288-L295 --- .github/workflows/ci.yml | 26 ++- packages/create-redwood-app/README.md | 72 ++++--- packages/create-redwood-app/build.mjs | 23 --- packages/create-redwood-app/jest.config.js | 5 +- packages/create-redwood-app/package.json | 23 ++- packages/create-redwood-app/scripts/build.js | 34 ++++ ...tUpTestProject.mjs => setUpTestProject.js} | 0 packages/create-redwood-app/scripts/tsToJS.js | 113 +++++++++++ .../create-redwood-app/scripts/tsToJS.mjs | 126 ------------- .../src/create-redwood-app.js | 175 +++++++----------- packages/create-redwood-app/src/telemetry.js | 2 +- .../tests/{e2e.test.mjs => e2e.test.js} | 35 ++-- .../create-redwood-app/tests/e2e_prompts.sh | 4 +- .../tests/e2e_prompts_git.sh | 2 +- .../create-redwood-app/tests/e2e_prompts_m.sh | 4 +- .../tests/e2e_prompts_node_greater.sh | 48 +++++ .../tests/e2e_prompts_node_less.sh | 30 +++ .../tests/e2e_prompts_overwrite.sh | 6 +- .../tests/e2e_prompts_ts.sh | 2 +- .../{templates.test.mjs => templates.test.js} | 4 +- 20 files changed, 385 insertions(+), 349 deletions(-) delete mode 100644 packages/create-redwood-app/build.mjs create mode 100644 packages/create-redwood-app/scripts/build.js rename packages/create-redwood-app/scripts/{setUpTestProject.mjs => setUpTestProject.js} (100%) create mode 100644 packages/create-redwood-app/scripts/tsToJS.js delete mode 100644 packages/create-redwood-app/scripts/tsToJS.mjs rename packages/create-redwood-app/tests/{e2e.test.mjs => e2e.test.js} (74%) create mode 100755 packages/create-redwood-app/tests/e2e_prompts_node_greater.sh create mode 100755 packages/create-redwood-app/tests/e2e_prompts_node_less.sh rename packages/create-redwood-app/tests/{templates.test.mjs => templates.test.js} (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 336f8f802a1f..923978293a20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -750,11 +750,13 @@ jobs: env: PROJECT_PATH: ${{ env.PROJECT_PATH }} - - name: Prompt tests + - name: Install expect run: | sudo apt-get update sudo apt-get install expect + - name: Prompt tests + run: | ./tests/e2e_prompts.sh ./tests/e2e_prompts_git.sh ./tests/e2e_prompts_m.sh @@ -764,6 +766,28 @@ jobs: env: PROJECT_PATH: ${{ env.PROJECT_PATH }} + - name: ⬢ Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Prompt tests + run: ./tests/e2e_prompts_node_less.sh + working-directory: ./packages/create-redwood-app + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + + - name: ⬢ Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + + - name: Prompt tests + run: ./tests/e2e_prompts_node_greater.sh + working-directory: ./packages/create-redwood-app + env: + PROJECT_PATH: ${{ env.PROJECT_PATH }} + crwa-skip: needs: detect-changes if: needs.detect-changes.outputs.onlydocs == 'true' diff --git a/packages/create-redwood-app/README.md b/packages/create-redwood-app/README.md index ea65f31b707d..3719968f56f5 100644 --- a/packages/create-redwood-app/README.md +++ b/packages/create-redwood-app/README.md @@ -18,39 +18,35 @@

Ship today with architecture for tomorrow.

-Redwood is an opinionated, edge-ready framework for modern multi-client applications, built on React, GraphQL, and Prisma with full TypeScript support and ready to go with zero config. +Redwood is an opinionated framework for modern multi-client applications, built on React, GraphQL, and Prisma with full TypeScript support and ready to go with zero config. Want great developer experience and easy scaling? How about an integrated front- and back-end test suite, boilerplate code generators, component design, logging, API security + auth, and serverless or traditional deploy support? Redwood is here! Redwood works with the components and development workflow you love but with simple conventions and helpers to make your experience even better. -
-

Quick Start

-Redwood requires Node.js >=14.x <=16.x and Yarn v1.15 (or newer). -```console -yarn create redwood-app redwood-project -cd redwood-project +Redwood requires Node.js =20.x. + +```bash +yarn create redwood-app my-redwood-app +cd my-redwood-app +yarn install yarn redwood dev ```

Resources

-- The [Redwood Tutorial](https://redwoodjs.com/docs/tutorial): The best way to learn Redwood +- The [Redwood Tutorial](https://redwoodjs.com/docs/tutorial): the best way to learn Redwood - The [Redwood CLI](https://redwoodjs.com/docs/cli-commands): code generators, DB helpers, setup commands, and more - [Documentation](https://redwoodjs.com/docs) and [How To's](https://redwoodjs.com/how-to/custom-function) - Join the Community [Forums](https://community.redwoodjs.com) and [Chat](https://discord.gg/redwoodjs) -
- -

Contributing to create-redwood-app

+

Contributing to create-redwood-app

-_Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contributing). And don't hesitate to ask for help on the forums and chat_ +_Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contributing). And don't hesitate to ask for help on the forums and chat_. **Table of Contents** - [Description](#description) -- [Package Leads](#package-leads) -- [Roadmap](#roadmap) - [Local Development](#local-development) - [Installation Script](#installation-script) - [Template Codebase](#template-codebase) @@ -60,55 +56,45 @@ _Contributors are Welcome! Get started [here](https://redwoodjs.com/docs/contrib ## Description This package creates and installs a Redwood project, which is the entry point for anyone using Redwood. It has two parts: -- The installation script `create-redwood-app.js` -- Project template code in the `template/` directory - -> _For information about contributing to the Redwood Framework in general, [please start here](https://redwoodjs.com/docs/contributing)._ - -## Package Leads -- [@peterp](https://github.com/peterp) -- [@thedavidprice](https://github.com/thedavidprice) - -## Roadmap - -v1 Priorities: -- convert `template/` codebase to TypeScript -- add option to install as either TypeScript or JavaScript project (defaults to TypeScript) -- add package tests, which may be accomplished by including in Cypress E2E CI +- The installation script [`src/create-redwood-app.js`](./src/create-redwood-app.js) +- Project template code in the [`templates/`](./templates/) directory ## Local Development ### Installation Script -The installation script is built with [Yargs](https://github.com/yargs/yargs) + +The installation script is built with [Yargs](https://github.com/yargs/yargs). ### Template Codebase -The project codebase in `template/` uses [Yarn Workspace v1](https://classic.yarnpkg.com/en/docs/workspaces/) for a monorepo project containing the API and Web Sides. Redwood packages are included in `template/package.json`, `template/web/package.json`, and `template/api/package.json`, respectively. -### How to run create-redwood-app from your local repo and create a project +The project codebase in [`templates/`](./templates/) uses [Yarn Workspaces](https://yarnpkg.com/features/workspaces) for a monorepo project containing the API and Web Sides. Redwood packages are included in `templates/ts/package.json`, `templates/ts/web/package.json`, and `templates/ts/api/package.json`, respectively. + +### How to run `create-redwood-app` from your local repo and create a project + First, run the following commands in the root of the monorepo: + ```bash yarn install yarn build ``` -Then, we need to navigate to the create redwood app package and build the script: +Then, navigate to the create redwood app package: + ```bash cd packages/create-redwood-app -yarn build ``` -_Note:_ You can also use `yarn build:watch` instead of `yarn build` to watch for changes and rebuild automatically. - -This will generate the `create-redwood-app.js` file inside the `dist` directory. +Run `yarn node` on the built file (`dist/create-redwood-app.js`) and pass in the path to the new project: -To use the script, run `node` on that file (dist/create-redwood-app.js) and pass in the path to the new project: ```bash -node dist/create-redwood-app.js /path/to/new/redwood-app +yarn node ./dist/create-redwood-app.js /path/to/new/redwood-app ``` -> Note: the new project will install with the most recent major Redwood package version by default +> [!NOTE] +> the new project will install with the most recent major Redwood package version by default. ### How to run other published versions for debugging + By default yarn create will pick the latest stable version to run, but you can specify a different version via yarn too! To try the canary version, run: @@ -121,12 +107,16 @@ Note that this will still create a project with the latest stable version, but r You can specify any tag or version instead of `@canary` ### Develop using the new project + There are three options for developing with the installed project: **1. Upgrade the project to use the latest canary release** + ```bash cd /path/to/new/redwood-app yarn rw upgrade -t canary ``` + **2. Use the workflow and tools for local package development** -- [Local Development Instructions](https://github.com/redwoodjs/redwood/blob/main/CONTRIBUTING.md#local-development) + +- [Local Development Instructions](https://github.com/redwoodjs/redwood/blob/main/CONTRIBUTING.md) diff --git a/packages/create-redwood-app/build.mjs b/packages/create-redwood-app/build.mjs deleted file mode 100644 index 3ed59c69cd9b..000000000000 --- a/packages/create-redwood-app/build.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'node:fs' - -import * as esbuild from 'esbuild' - -const result = await esbuild.build({ - entryPoints: ['src/create-redwood-app.js'], - outfile: 'dist/create-redwood-app.js', - - bundle: true, - minify: true, - - platform: 'node', - target: ['node20'], - packages: 'external', - - logLevel: 'info', - - // For visualizing the bundle. - // See https://esbuild.github.io/api/#metafile and https://esbuild.github.io/analyze/. - metafile: true, -}) - -fs.writeFileSync('meta.json', JSON.stringify(result.metafile)) diff --git a/packages/create-redwood-app/jest.config.js b/packages/create-redwood-app/jest.config.js index 2c638b714e5b..99951968fbc0 100644 --- a/packages/create-redwood-app/jest.config.js +++ b/packages/create-redwood-app/jest.config.js @@ -1,8 +1,5 @@ /** @type {import('jest').Config} */ -const config = { - testMatch: ['/tests/*.test.mjs'], +export default { testPathIgnorePatterns: ['/node_modules/', '/templates/'], transform: {}, } - -module.exports = config diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index e90e7e89148e..1ea46667c57b 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -7,34 +7,41 @@ "directory": "packages/create-redwood-app" }, "license": "MIT", + "type": "module", "bin": "./dist/create-redwood-app.js", "files": [ "dist", "templates" ], "scripts": { - "build": "yarn node ./build.mjs", + "build": "node ./scripts/build.js", "build:pack": "yarn pack -o create-redwood-app.tgz", "build:watch": "nodemon --watch src --ignore dist,template --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "set-up-test-project": "node ./scripts/setUpTestProject.mjs", + "set-up-test-project": "node ./scripts/setUpTestProject.js", "test": "node --experimental-vm-modules $(yarn bin jest) templates", "test:e2e": "node --experimental-vm-modules $(yarn bin jest) e2e", - "ts-to-js": "yarn node ./scripts/tsToJS.mjs" + "ts-to-js": "yarn node ./scripts/tsToJS.js" }, - "dependencies": { + "devDependencies": { + "@babel/core": "^7.22.20", + "@babel/plugin-transform-typescript": "^7.22.15", "@opentelemetry/api": "1.7.0", "@opentelemetry/exporter-trace-otlp-http": "0.45.1", "@opentelemetry/resources": "1.18.1", "@opentelemetry/sdk-trace-node": "1.18.1", "@opentelemetry/semantic-conventions": "1.18.1", "@redwoodjs/tui": "6.0.7", + "@types/babel__core": "7.20.4", "chalk": "4.1.2", "check-node-version": "4.2.1", "ci-info": "4.0.0", "envinfo": "7.11.0", + "esbuild": "0.19.9", "execa": "5.1.1", "fs-extra": "11.2.0", + "jest": "29.7.0", + "klaw-sync": "6.0.0", "semver": "7.5.4", "systeminformation": "5.21.20", "terminal-link": "2.1.1", @@ -42,13 +49,5 @@ "uuid": "9.0.1", "yargs": "17.7.2" }, - "devDependencies": { - "@babel/core": "^7.22.20", - "@babel/plugin-transform-typescript": "^7.22.15", - "@types/babel__core": "7.20.4", - "esbuild": "0.19.9", - "jest": "29.7.0", - "klaw-sync": "6.0.0" - }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/packages/create-redwood-app/scripts/build.js b/packages/create-redwood-app/scripts/build.js new file mode 100644 index 000000000000..6694591a9998 --- /dev/null +++ b/packages/create-redwood-app/scripts/build.js @@ -0,0 +1,34 @@ +/* eslint-env node */ + +import * as esbuild from 'esbuild' +import fs from 'fs-extra' + +const jsBanner = `\ +#!/usr/bin/env node + +const require = (await import("node:module")).createRequire(import.meta.url); +const __filename = (await import("node:url")).fileURLToPath(import.meta.url); +const __dirname = (await import("node:path")).dirname(__filename); +` + +const result = await esbuild.build({ + entryPoints: ['src/create-redwood-app.js'], + outdir: 'dist', + + platform: 'node', + target: ['node20'], + format: 'esm', + bundle: true, + banner: { + js: jsBanner, + }, + + minify: true, + + logLevel: 'info', + metafile: true, +}) + +await fs.writeJSON(new URL('./meta.json', import.meta.url), result.metafile, { + spaces: 2, +}) diff --git a/packages/create-redwood-app/scripts/setUpTestProject.mjs b/packages/create-redwood-app/scripts/setUpTestProject.js similarity index 100% rename from packages/create-redwood-app/scripts/setUpTestProject.mjs rename to packages/create-redwood-app/scripts/setUpTestProject.js diff --git a/packages/create-redwood-app/scripts/tsToJS.js b/packages/create-redwood-app/scripts/tsToJS.js new file mode 100644 index 000000000000..7e5c881330d9 --- /dev/null +++ b/packages/create-redwood-app/scripts/tsToJS.js @@ -0,0 +1,113 @@ +/* eslint-env node */ + +import { fileURLToPath } from 'node:url' + +import { transformFileSync } from '@babel/core' +import { format } from 'prettier' +import { fs, glob, path } from 'zx' + +const TS_TEMPLATE_PATH = fileURLToPath( + new URL('../templates/ts', import.meta.url) +) + +// Remove `node_modules`, `.yarn/install-state.gz`. +console.log('Removing `node_modules` in the TS template') +const tsTemplateNodeModulesPath = path.join(TS_TEMPLATE_PATH, 'node_modules') +await fs.rm(tsTemplateNodeModulesPath, { recursive: true, force: true }) + +console.log("Removing yarn's `install-state.gz` in the TS template") +const tsTemplateYarnInstallStatePath = path.join( + TS_TEMPLATE_PATH, + '.yarn', + 'install-state.gz' +) +await fs.rm(tsTemplateYarnInstallStatePath, { force: true }) + +// Clean and copy the TS template to the JS template. +const JS_TEMPLATE_PATH = fileURLToPath( + new URL('../templates/js', import.meta.url) +) + +console.log('Removing the JS template') +await fs.rm(JS_TEMPLATE_PATH, { recursive: true, force: true }) +console.log('Copying the TS template to the JS template') +await fs.copy(TS_TEMPLATE_PATH, JS_TEMPLATE_PATH) + +// Find files and transform. +const filePaths = await glob(['{api,web,scripts}/**/*.{ts,tsx}'], { + cwd: JS_TEMPLATE_PATH, + absolute: true, +}) + +console.group('Transforming files in the JS template') + +const { default: prettierConfig } = await import( + new URL('../templates/ts/prettier.config.js', import.meta.url) +) + +for (const filePath of filePaths) { + console.log(`• ${filePath}`) + + const result = transformFileSync(filePath, { + cwd: TS_TEMPLATE_PATH, + configFile: false, + plugins: [ + [ + '@babel/plugin-transform-typescript', + { + isTSX: true, + allExtensions: true, + }, + ], + ], + retainLines: true, + }) + + if (!result) { + throw new Error(`Error: Couldn't transform ${filePath}`) + } + + const formattedCode = format(result.code, { + ...prettierConfig, + parser: 'babel', + }) + + await fs.writeFile( + filePath.replace('.tsx', '.jsx').replace('.ts', '.js'), + formattedCode, + 'utf-8' + ) + + await fs.rm(filePath) +} + +console.groupEnd() + +console.group( + 'Transforming `tsconfig.json`s in the JS template to `jsconfig.json`s' +) + +const tsConfigFilePaths = await glob(['{api,web,scripts}/**/tsconfig.json'], { + cwd: JS_TEMPLATE_PATH, + absolute: true, +}) + +for (const tsConfigFilePath of tsConfigFilePaths) { + console.log(`• ${tsConfigFilePath}`) + + const jsConfigFilePath = path.join( + path.dirname(tsConfigFilePath), + 'jsconfig.json' + ) + + await fs.rename(tsConfigFilePath, jsConfigFilePath) + + const jsConfig = await fs.readJSON(jsConfigFilePath) + + // This property has no meaning in JS projects. + delete jsConfig.compilerOptions.allowJs + + await fs.writeJSON(jsConfigFilePath, jsConfig, { spaces: 2 }) +} + +console.groupEnd() diff --git a/packages/create-redwood-app/scripts/tsToJS.mjs b/packages/create-redwood-app/scripts/tsToJS.mjs deleted file mode 100644 index 990386cc8600..000000000000 --- a/packages/create-redwood-app/scripts/tsToJS.mjs +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env node -/* eslint-env node */ - -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import { transformFileSync } from '@babel/core' -import fg from 'fast-glob' -import fs from 'fs-extra' -import { format } from 'prettier' - -const [TS_TEMPLATE_FILEPATH, JS_TEMPLATE_FILEPATH] = [ - new URL('../templates/ts', import.meta.url), - new URL('../templates/js', import.meta.url), -].map(fileURLToPath) - -const { default: prettierConfig } = await import( - new URL('../templates/ts/prettier.config.js', import.meta.url) -) - -// Handle node_modules, .yarn/install-state.gz. -const tsTemplateNodeModulesPath = path.join( - TS_TEMPLATE_FILEPATH, - 'node_modules' -) - -if (fs.existsSync(tsTemplateNodeModulesPath)) { - console.log('Removing node modules in TS template') - fs.rmSync(tsTemplateNodeModulesPath, { recursive: true }) -} - -const tsTemplateYarnInstallState = path.join( - TS_TEMPLATE_FILEPATH, - '.yarn', - 'install-state.gz' -) - -if (fs.existsSync(tsTemplateYarnInstallState)) { - console.log('Removing .yarn/install-state.gz in TS template') - fs.rmSync(tsTemplateYarnInstallState, { recursive: true }) -} - -// Clean and copy the TS template to the JS template. -console.log('Cleaning JS template') -fs.rmSync(JS_TEMPLATE_FILEPATH, { recursive: true }) - -console.log('Copying TS template to JS template') -fs.copySync(TS_TEMPLATE_FILEPATH, JS_TEMPLATE_FILEPATH) - -// Find files and transform. -const apiWebFilePaths = fg.sync('{api,web}/**/*.{ts,tsx}', { - cwd: JS_TEMPLATE_FILEPATH, - absolute: true, -}) - -const scriptFilePaths = fg.sync('scripts/**/*.ts', { - cwd: JS_TEMPLATE_FILEPATH, - absolute: true, -}) - -console.group('Transforming TS files in JS template to JS') - -for (const filePath of [...apiWebFilePaths, ...scriptFilePaths]) { - console.log('Transforming', filePath) - - const result = transformFileSync(filePath, { - cwd: TS_TEMPLATE_FILEPATH, - configFile: false, - plugins: [ - [ - '@babel/plugin-transform-typescript', - { - isTSX: true, - allExtensions: true, - }, - ], - ], - retainLines: true, - }) - - if (!result) { - throw new Error(`Babel transform for ${filePath} failed`) - } - - const formattedCode = format(result.code, { - ...prettierConfig, - parser: 'babel', - }) - - fs.writeFileSync( - filePath.replace('.tsx', '.jsx').replace('.ts', '.js'), - formattedCode, - 'utf-8' - ) - - fs.rmSync(filePath) -} - -console.groupEnd() - -console.group('Transforming tsconfig files to jsconfig') - -const tsConfigFilePaths = fg.sync('{api,web,scripts}/**/tsconfig.json', { - cwd: JS_TEMPLATE_FILEPATH, - absolute: true, -}) - -for (const tsConfigFilePath of tsConfigFilePaths) { - console.log('Transforming', tsConfigFilePath) - - const jsConfigFilePath = path.join( - path.dirname(tsConfigFilePath), - 'jsconfig.json' - ) - - fs.renameSync(tsConfigFilePath, jsConfigFilePath) - - const jsConfig = fs.readJSONSync(jsConfigFilePath) - - // This property has no meaning in JS projects. - delete jsConfig.compilerOptions.allowJs - - fs.writeJSONSync(jsConfigFilePath, jsConfig, { spaces: 2 }) -} - -console.groupEnd() diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js index 2f7e30405aeb..71817907bea5 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -1,6 +1,5 @@ -#!/usr/bin/env node - -import path from 'path' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { trace, SpanStatusCode } from '@opentelemetry/api' import checkNodeVersionCb from 'check-node-version' @@ -14,19 +13,28 @@ import yargs from 'yargs/yargs' import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@redwoodjs/tui' -import { name, version } from '../package' +import { name, version } from '../package.json' import { UID, startTelemetry, shutdownTelemetry, recordErrorViaTelemetry, -} from './telemetry' +} from './telemetry.js' const INITIAL_COMMIT_MESSAGE = 'Initial commit' -// Telemetry -const { telemetry } = Parser(hideBin(process.argv)) +// Telemetry can be disabled in two ways: +// - by passing `--telemetry false` or `--no-telemetry` +// - by setting the `REDWOOD_DISABLE_TELEMETRY` env var to `1` +const { telemetry } = Parser(hideBin(process.argv), { + boolean: ['telemetry'], + default: { + telemetry: + process.env.REDWOOD_DISABLE_TELEMETRY === undefined || + process.env.REDWOOD_DISABLE_TELEMETRY === '', + }, +}) const tui = new RedwoodTUI() @@ -67,7 +75,7 @@ async function executeCompatibilityCheck(templateDir) { }) tui.startReactive(tuiContent) - const [checksPassed, checksData] = await checkNodeAndYarnVersion(templateDir) + const [checksPassed, checksData] = await checkNodeVersion(templateDir) if (checksPassed) { tuiContent.update({ @@ -87,11 +95,6 @@ async function executeCompatibilityCheck(templateDir) { semver.minVersion(checksData.node.wanted.raw) ) - const foundYarnVersionIsLessThanRequired = semver.lt( - checksData.yarn.version.version, - semver.minVersion(checksData.yarn.wanted.raw) - ) - if (foundNodeVersionIsLessThanRequired) { tui.stopReactive(true) tui.displayError( @@ -127,41 +130,6 @@ async function executeCompatibilityCheck(templateDir) { process.exit(1) } - if (foundYarnVersionIsLessThanRequired) { - tui.stopReactive(true) - tui.displayError( - 'Compatibility checks failed', - [ - ` You need to upgrade the version of yarn you're using.`, - ` You're using ${checksData.yarn.version.version} and we currently support node ${checksData.yarn.wanted.range}.`, - '', - ` Please use tools like corepack to change to a compatible version.`, - ` See: ${terminalLink( - 'How to - Using Yarn', - 'https://redwoodjs.com/docs/how-to/using-yarn', - { - fallback: () => - 'How to - Using Yarn https://redwoodjs.com/docs/how-to/using-yarn', - } - )}`, - ` See: ${terminalLink( - 'Tutorial - Prerequisites', - 'https://redwoodjs.com/docs/tutorial/chapter1/prerequisites', - { - fallback: () => - 'Tutorial - Prerequisites https://redwoodjs.com/docs/tutorial/chapter1/prerequisites', - } - )}`, - '', - ...USE_GITPOD_TEXT, - ].join('\n') - ) - - recordErrorViaTelemetry('Compatibility checks failed') - await shutdownTelemetry() - process.exit(1) - } - tui.stopReactive(true) tui.displayWarning( 'Compatibility checks failed', @@ -220,9 +188,9 @@ async function executeCompatibilityCheck(templateDir) { * This type has to be updated if the engines field in the create redwood app template package.json is updated. * @returns [boolean, Record<'node' | 'yarn', any>] */ -function checkNodeAndYarnVersion(templateDir) { +function checkNodeVersion(templateDir) { return new Promise((resolve) => { - const { engines } = require(path.join(templateDir, 'package.json')) + const { engines } = fs.readJSONSync(path.join(templateDir, 'package.json')) checkNodeVersionCb(engines, (_error, result) => { return resolve([result.isSatisfied, result.versions]) @@ -671,57 +639,58 @@ async function handleYarnInstallPreference(yarnInstallFlag) { * - TODO - Add a list of what this function does */ async function createRedwoodApp() { - // Introductory message - tui.drawText( - [ - `${RedwoodStyling.redwood('-'.repeat(66))}`, - `${' '.repeat(16)}🌲⚡️ ${RedwoodStyling.header( - 'Welcome to RedwoodJS!' - )} ⚡️🌲`, - `${RedwoodStyling.redwood('-'.repeat(66))}`, - ].join('\n') - ) - const cli = yargs(hideBin(process.argv)) .scriptName(name) - .usage('Usage: $0 [option]') - .example('$0 newapp') - .option('typescript', { - alias: 'ts', + .usage('Usage: $0 ') + .example('$0 my-redwood-app') + .version(version) + .option('yes', { + alias: 'y', default: null, type: 'boolean', - describe: 'Generate a TypeScript project.', + describe: 'Skip prompts and use defaults', }) .option('overwrite', { default: false, type: 'boolean', describe: "Create even if target directory isn't empty", }) - .option('telemetry', { - default: true, + .option('typescript', { + alias: 'ts', + default: null, type: 'boolean', - describe: - 'Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com', + describe: 'Generate a TypeScript project', }) .option('git-init', { alias: 'git', default: null, type: 'boolean', - describe: 'Initialize a git repository.', + describe: 'Initialize a git repository', }) .option('commit-message', { alias: 'm', default: null, type: 'string', - describe: 'Commit message for the initial commit.', + describe: 'Commit message for the initial commit', }) - .option('yes', { - alias: 'y', - default: null, + .option('telemetry', { + default: true, type: 'boolean', - describe: 'Skip prompts and use defaults.', + describe: + 'Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com', }) - .version(version) + + const parsedFlags = cli.parse() + + tui.drawText( + [ + `${RedwoodStyling.redwood('-'.repeat(66))}`, + `${' '.repeat(16)}🌲⚡️ ${RedwoodStyling.header( + 'Welcome to RedwoodJS!' + )} ⚡️🌲`, + `${RedwoodStyling.redwood('-'.repeat(66))}`, + ].join('\n') + ) const _isYarnBerryOrNewer = isYarnBerryOrNewer() @@ -734,8 +703,6 @@ async function createRedwoodApp() { }) } - const parsedFlags = cli.parse() - // Extract the args as provided by the user in the command line // TODO: Make all flags have the 'flag' suffix const args = parsedFlags._ @@ -743,7 +710,6 @@ async function createRedwoodApp() { parsedFlags['yarn-install'] ?? !_isYarnBerryOrNewer ? parsedFlags.yes : null const typescriptFlag = parsedFlags.typescript ?? parsedFlags.yes const overwrite = parsedFlags.overwrite - // telemetry, // Extracted above to check if telemetry is disabled before we even reach this point const gitInitFlag = parsedFlags['git-init'] ?? parsedFlags.yes const commitMessageFlag = parsedFlags['commit-message'] ?? @@ -756,7 +722,7 @@ async function createRedwoodApp() { // Get the directory for installation from the args let targetDir = String(args).replace(/,/g, '-') - const templatesDir = path.resolve(__dirname, '../templates') + const templatesDir = fileURLToPath(new URL('../templates', import.meta.url)) // Engine check await executeCompatibilityCheck(path.join(templatesDir, 'ts')) @@ -845,32 +811,29 @@ async function createRedwoodApp() { ) } -;(async () => { - // Conditionally start telemetry - if (telemetry !== 'false' && !process.env.REDWOOD_DISABLE_TELEMETRY) { - try { - await startTelemetry() - } catch (error) { - console.error('Telemetry startup error') - console.error(error) - } - } - - // Execute create redwood app within a span - const tracer = trace.getTracer('redwoodjs') - await tracer.startActiveSpan('create-redwood-app', async (span) => { - await createRedwoodApp() - - // Span housekeeping - span?.setStatus({ code: SpanStatusCode.OK }) - span?.end() - }) - - // Shutdown telemetry, ensures data is sent before the process exits +if (telemetry) { try { - await shutdownTelemetry() + await startTelemetry() } catch (error) { - console.error('Telemetry shutdown error') + console.error('Telemetry startup error') console.error(error) } -})() +} + +// Execute create redwood app within a span +const tracer = trace.getTracer('redwoodjs') +await tracer.startActiveSpan('create-redwood-app', async (span) => { + await createRedwoodApp() + + // Span housekeeping + span?.setStatus({ code: SpanStatusCode.OK }) + span?.end() +}) + +// Shutdown telemetry, ensures data is sent before the process exits +try { + await shutdownTelemetry() +} catch (error) { + console.error('Telemetry shutdown error') + console.error(error) +} diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js index 2f183e021840..471261101029 100644 --- a/packages/create-redwood-app/src/telemetry.js +++ b/packages/create-redwood-app/src/telemetry.js @@ -12,7 +12,7 @@ import envinfo from 'envinfo' import system from 'systeminformation' import { v4 as uuidv4 } from 'uuid' -import { name as packageName, version as packageVersion } from '../package' +import { name as packageName, version as packageVersion } from '../package.json' /** * @type NodeTracerProvider diff --git a/packages/create-redwood-app/tests/e2e.test.mjs b/packages/create-redwood-app/tests/e2e.test.js similarity index 74% rename from packages/create-redwood-app/tests/e2e.test.mjs rename to packages/create-redwood-app/tests/e2e.test.js index 488f3d5178c2..7ddee4eace96 100644 --- a/packages/create-redwood-app/tests/e2e.test.mjs +++ b/packages/create-redwood-app/tests/e2e.test.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node /* eslint-env node */ import { cd, fs, $ } from 'zx' @@ -7,36 +6,31 @@ const projectPath = await fs.realpath(process.env.PROJECT_PATH) cd(projectPath) -describe('crwa', () => { +describe('create-redwood-app', () => { test('--help', async () => { const p = await $`yarn create-redwood-app --help` expect(p.exitCode).toEqual(0) expect(p.stdout).toMatchInlineSnapshot(` - "------------------------------------------------------------------ - 🌲⚡️ Welcome to RedwoodJS! ⚡️🌲 - ------------------------------------------------------------------ - Usage: create-redwood-app [option] + "Usage: create-redwood-app Options: --help Show help [boolean] - --typescript, --ts Generate a TypeScript project. - [boolean] [default: null] + --version Show version number [boolean] + -y, --yes Skip prompts and use defaults[boolean] [default: null] --overwrite Create even if target directory isn't empty [boolean] [default: false] + --typescript, --ts Generate a TypeScript project[boolean] [default: null] + --git-init, --git Initialize a git repository [boolean] [default: null] + -m, --commit-message Commit message for the initial commit + [string] [default: null] --telemetry Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com [boolean] [default: true] - --git-init, --git Initialize a git repository. [boolean] [default: null] - -m, --commit-message Commit message for the initial commit. - [string] [default: null] - -y, --yes Skip prompts and use defaults. - [boolean] [default: null] - --version Show version number [boolean] Examples: - create-redwood-app newapp + create-redwood-app my-redwood-app [?25l[?25h" `) expect(p.stderr).toMatchInlineSnapshot(`"[?25l[?25h"`) @@ -47,10 +41,7 @@ describe('crwa', () => { expect(p.exitCode).toEqual(0) expect(p.stdout).toMatchInlineSnapshot(` - "------------------------------------------------------------------ - 🌲⚡️ Welcome to RedwoodJS! ⚡️🌲 - ------------------------------------------------------------------ - 6.0.7 + "6.0.7 [?25l[?25h" `) expect(p.stderr).toMatchInlineSnapshot(`"[?25l[?25h"`) @@ -59,10 +50,6 @@ describe('crwa', () => { test('--yes, -y', async () => { const p = await $`yarn create-redwood-app ./redwood-app --yes` - // await $`yarn create-redwood-app redwood-app -y` - // # `yarn pack` seems to ignore `.yarnrc.yml` - // # cp "$SCRIPT_DIR/templates/ts/.yarnrc.yml" "$CRWA_ESM_TESTING_DIR" - expect(p.exitCode).toEqual(0) expect(p.stdout).toMatchInlineSnapshot(` "------------------------------------------------------------------ @@ -96,7 +83,7 @@ describe('crwa', () => { await fs.rm('./redwood-app', { recursive: true, force: true }) }) - test.failing('fails on unknown options', async () => { + it.failing('fails on unknown options', async () => { try { await $`yarn create-redwood-app --unknown-options`.timeout(2500) // Fail the test if the function didn't throw. diff --git a/packages/create-redwood-app/tests/e2e_prompts.sh b/packages/create-redwood-app/tests/e2e_prompts.sh index 3099ee4c5744..c017c7b184fc 100755 --- a/packages/create-redwood-app/tests/e2e_prompts.sh +++ b/packages/create-redwood-app/tests/e2e_prompts.sh @@ -17,11 +17,11 @@ expect "Where would you like to create your Redwood app?" send "$projectDirectory\n" expect "Select your preferred language" -# TypeScript +# ❯ TypeScript send "\n" expect "Do you want to initialize a git repo?" -# Yes +# ❯ Yes send "\n" expect "Enter a commit message" diff --git a/packages/create-redwood-app/tests/e2e_prompts_git.sh b/packages/create-redwood-app/tests/e2e_prompts_git.sh index e43f514a2bce..655896cc138c 100755 --- a/packages/create-redwood-app/tests/e2e_prompts_git.sh +++ b/packages/create-redwood-app/tests/e2e_prompts_git.sh @@ -17,7 +17,7 @@ expect "Where would you like to create your Redwood app?" send "$projectDirectory\n" expect "Select your preferred language" -# TypeScript +# ❯ TypeScript send "\n" expect "Enter a commit message" diff --git a/packages/create-redwood-app/tests/e2e_prompts_m.sh b/packages/create-redwood-app/tests/e2e_prompts_m.sh index c9c24613e0b1..30d5e61e6ce3 100755 --- a/packages/create-redwood-app/tests/e2e_prompts_m.sh +++ b/packages/create-redwood-app/tests/e2e_prompts_m.sh @@ -17,11 +17,11 @@ expect "Where would you like to create your Redwood app?" send "$projectDirectory\n" expect "Select your preferred language" -# TypeScript +# ❯ TypeScript send "\n" expect "Do you want to initialize a git repo?" -# Yes +# ❯ Yes send "\n" expect eof diff --git a/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh b/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh new file mode 100755 index 000000000000..73a737735458 --- /dev/null +++ b/packages/create-redwood-app/tests/e2e_prompts_node_greater.sh @@ -0,0 +1,48 @@ +#!/usr/bin/expect + +# You have to set your Node version to 21+ before running this test. + +set projectPath $env(PROJECT_PATH) + +if {$projectPath eq ""} { + puts "PROJECT_PATH is not set" + exit +} + +cd $projectPath + +set projectDirectory "redwood-app-prompt-node-greater-test" + +spawn yarn create-redwood-app + +expect "How would you like to proceed?" +# ❯ Override error and continue install +send "\n" + +expect "Where would you like to create your Redwood app?" +send "$projectDirectory\n" + +expect "Select your preferred language" +# ❯ TypeScript +send "\n" + +expect "Do you want to initialize a git repo?" +# ❯ Yes +send "\n" + +expect "Enter a commit message" +send "first\n" + +expect eof +catch wait result +set exitStatus [lindex $result 3] + +if {$exitStatus == 0} { + puts "Success" + exec rm -rf $projectDirectory + exit 0 +} else { + puts "Error: The process failed with exit status $exitStatus" + exec rm -rf $projectDirectory + exit 1 +} diff --git a/packages/create-redwood-app/tests/e2e_prompts_node_less.sh b/packages/create-redwood-app/tests/e2e_prompts_node_less.sh new file mode 100755 index 000000000000..d8187de802fe --- /dev/null +++ b/packages/create-redwood-app/tests/e2e_prompts_node_less.sh @@ -0,0 +1,30 @@ +#!/usr/bin/expect + +# You have to set your Node version to 18 before running this one. + +set projectPath $env(PROJECT_PATH) + +if {$projectPath eq ""} { + puts "PROJECT_PATH is not set" + exit +} + +cd $projectPath + +set projectDirectory "redwood-app-prompt-node-less-test" + +spawn yarn create-redwood-app + +expect eof +catch wait result +set exitStatus [lindex $result 3] + +if {$exitStatus == 1} { + puts "Success" + exec rm -rf $projectDirectory + exit 0 +} else { + puts "Error: The process didn't fail with exit status $exitStatus" + exec rm -rf $projectDirectory + exit 1 +} diff --git a/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh b/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh index ad8d890f0100..dd828aa1a7bf 100755 --- a/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh +++ b/packages/create-redwood-app/tests/e2e_prompts_overwrite.sh @@ -20,18 +20,18 @@ expect "Where would you like to create your Redwood app?" send "$projectDirectory\n" expect "Select your preferred language" -# TypeScript +# ❯ TypeScript send "\n" expect "Do you want to initialize a git repo?" -# Yes +# ❯ Yes send "\n" expect "Enter a commit message" send "first\n" expect "How would you like to proceed?" -# Quit install +# ❯ Quit install send "\n" expect eof diff --git a/packages/create-redwood-app/tests/e2e_prompts_ts.sh b/packages/create-redwood-app/tests/e2e_prompts_ts.sh index 8a8f17c52847..7a32631c8c8a 100755 --- a/packages/create-redwood-app/tests/e2e_prompts_ts.sh +++ b/packages/create-redwood-app/tests/e2e_prompts_ts.sh @@ -17,7 +17,7 @@ expect "Where would you like to create your Redwood app?" send "$projectDirectory\n" expect "Do you want to initialize a git repo?" -# Yes +# ❯ Yes send "\n" expect "Enter a commit message" diff --git a/packages/create-redwood-app/tests/templates.test.mjs b/packages/create-redwood-app/tests/templates.test.js similarity index 99% rename from packages/create-redwood-app/tests/templates.test.mjs rename to packages/create-redwood-app/tests/templates.test.js index a1bbfb76e13e..f8ef6ddf4f6c 100644 --- a/packages/create-redwood-app/tests/templates.test.mjs +++ b/packages/create-redwood-app/tests/templates.test.js @@ -1,5 +1,5 @@ +import path from 'node:path' import { fileURLToPath } from 'node:url' -import path from 'path' import klawSync from 'klaw-sync' @@ -7,7 +7,7 @@ const TS_TEMPLATE_DIR = fileURLToPath( new URL('../templates/ts', import.meta.url) ) -describe('template', () => { +describe('TS template', () => { it('files should not have changed unintentionally', () => { expect(getDirectoryStructure(TS_TEMPLATE_DIR)).toMatchInlineSnapshot(` [