From 96db3235d46b196e394cfe7ff2c0e0886c16f5f3 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 10:54:45 -0300 Subject: [PATCH 1/6] feat(create-email): caching of fetch to fallback to when fetch fails this is particularly useful for when the user doesn't have connectivity but has already used the starter once --- packages/create-email/src/index.js | 96 +++++++++++++++++++----------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/packages/create-email/src/index.js b/packages/create-email/src/index.js index bfb739ed45..9c9aa80752 100755 --- a/packages/create-email/src/index.js +++ b/packages/create-email/src/index.js @@ -1,5 +1,6 @@ #!/usr/bin/env node +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Command } from 'commander'; @@ -16,10 +17,27 @@ const packageJson = JSON.parse( ); const getLatestVersionOfTag = async (packageName, tag) => { - const response = await fetch( - `https://registry.npmjs.org/${packageName}/${tag}`, + const cachePath = path.resolve( + os.tmpdir(), + `${packageName.replaceAll('/', '-')}-${tag}.json`, ); - const data = await response.json(); + let data; + try { + const response = await fetch( + `https://registry.npmjs.org/${packageName}/${tag}`, + ); + data = await response.json(); + await fse.writeFile(cachePath, JSON.stringify(data)); + } catch (exception) { + if (fse.existsSync(cachePath)) { + console.warn( + `${logSymbols.warning} Failed to fetch the latest version from npm, using a cache`, + ); + data = await fse.readJson(cachePath); + } else { + throw exception; + } + } if (typeof data === 'string' && data.startsWith('version not found')) { console.error(`Tag ${tag} does not exist for ${packageName}.`); @@ -61,38 +79,46 @@ const init = async (name, { tag }) => { fse.copySync(templatePath, resolvedProjectPath, { recursive: true, }); - const templatePackageJsonPath = path.resolve( - resolvedProjectPath, - './package.json', - ); - const templatePackageJson = fse.readFileSync(templatePackageJsonPath, 'utf8'); - fse.writeFileSync( - templatePackageJsonPath, - templatePackageJson - .replace( - 'INSERT_COMPONENTS_VERSION', - await getLatestVersionOfTag('@react-email/components', tag), - ) - .replace( - 'INSERT_REACT_EMAIL_VERSION', - await getLatestVersionOfTag('react-email', tag), - ), - 'utf8', - ); - - spinner.stopAndPersist({ - symbol: logSymbols.success, - text: 'React Email Starter files ready', - }); - - // eslint-disable-next-line no-console - console.info( - await tree(resolvedProjectPath, 4, (dirent) => { - return !path - .join(dirent.parentPath, dirent.name) - .includes('node_modules'); - }), - ); + try { + const templatePackageJsonPath = path.resolve( + resolvedProjectPath, + './package.json', + ); + const templatePackageJson = fse.readFileSync( + templatePackageJsonPath, + 'utf8', + ); + fse.writeFileSync( + templatePackageJsonPath, + templatePackageJson + .replace( + 'INSERT_COMPONENTS_VERSION', + await getLatestVersionOfTag('@react-email/components', tag), + ) + .replace( + 'INSERT_REACT_EMAIL_VERSION', + await getLatestVersionOfTag('react-email', tag), + ), + 'utf8', + ); + + spinner.stopAndPersist({ + symbol: logSymbols.success, + text: 'React Email Starter files ready', + }); + + // eslint-disable-next-line no-console + console.info( + await tree(resolvedProjectPath, 4, (dirent) => { + return !path + .join(dirent.parentPath, dirent.name) + .includes('node_modules'); + }), + ); + } catch (exception) { + fse.removeSync(resolvedProjectPath); + throw exception; + } }; new Command() From a8e91671350430cbdc416abfb8a96e234349092c Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 10:57:21 -0300 Subject: [PATCH 2/6] add changeset --- .changeset/mighty-views-kneel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-views-kneel.md diff --git a/.changeset/mighty-views-kneel.md b/.changeset/mighty-views-kneel.md new file mode 100644 index 0000000000..042e93cc81 --- /dev/null +++ b/.changeset/mighty-views-kneel.md @@ -0,0 +1,5 @@ +--- +"create-email": patch +--- + +caching of versions to use when fetch of latest fails From c8a0dc0f630a5f7526a65c70e5f38e3e28b47c3e Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 11:02:06 -0300 Subject: [PATCH 3/6] remove try-catch from this pull request --- packages/create-email/src/index.js | 70 +++++++++++++----------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/create-email/src/index.js b/packages/create-email/src/index.js index 9c9aa80752..b4e1bae766 100755 --- a/packages/create-email/src/index.js +++ b/packages/create-email/src/index.js @@ -79,46 +79,38 @@ const init = async (name, { tag }) => { fse.copySync(templatePath, resolvedProjectPath, { recursive: true, }); - try { - const templatePackageJsonPath = path.resolve( - resolvedProjectPath, - './package.json', - ); - const templatePackageJson = fse.readFileSync( - templatePackageJsonPath, - 'utf8', - ); - fse.writeFileSync( - templatePackageJsonPath, - templatePackageJson - .replace( - 'INSERT_COMPONENTS_VERSION', - await getLatestVersionOfTag('@react-email/components', tag), - ) - .replace( - 'INSERT_REACT_EMAIL_VERSION', - await getLatestVersionOfTag('react-email', tag), - ), - 'utf8', - ); + const templatePackageJsonPath = path.resolve( + resolvedProjectPath, + './package.json', + ); + const templatePackageJson = fse.readFileSync(templatePackageJsonPath, 'utf8'); + fse.writeFileSync( + templatePackageJsonPath, + templatePackageJson + .replace( + 'INSERT_COMPONENTS_VERSION', + await getLatestVersionOfTag('@react-email/components', tag), + ) + .replace( + 'INSERT_REACT_EMAIL_VERSION', + await getLatestVersionOfTag('react-email', tag), + ), + 'utf8', + ); - spinner.stopAndPersist({ - symbol: logSymbols.success, - text: 'React Email Starter files ready', - }); - - // eslint-disable-next-line no-console - console.info( - await tree(resolvedProjectPath, 4, (dirent) => { - return !path - .join(dirent.parentPath, dirent.name) - .includes('node_modules'); - }), - ); - } catch (exception) { - fse.removeSync(resolvedProjectPath); - throw exception; - } + spinner.stopAndPersist({ + symbol: logSymbols.success, + text: 'React Email Starter files ready', + }); + + // eslint-disable-next-line no-console + console.info( + await tree(resolvedProjectPath, 4, (dirent) => { + return !path + .join(dirent.parentPath, dirent.name) + .includes('node_modules'); + }), + ); }; new Command() From 446a7aab43fb5d654c61f3088777082f5dbc6a28 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 11:23:46 -0300 Subject: [PATCH 4/6] store the cache in a .cache directory in the package's location, and use safer permissions for the files --- packages/create-email/.gitignore | 1 + packages/create-email/src/index.js | 74 ++++++++++++++++-------------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/create-email/.gitignore b/packages/create-email/.gitignore index 960dd8addf..5880e67058 100644 --- a/packages/create-email/.gitignore +++ b/packages/create-email/.gitignore @@ -1 +1,2 @@ .test +.cache diff --git a/packages/create-email/src/index.js b/packages/create-email/src/index.js index b4e1bae766..eac2c28808 100755 --- a/packages/create-email/src/index.js +++ b/packages/create-email/src/index.js @@ -1,45 +1,51 @@ #!/usr/bin/env node -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { Command } from 'commander'; -import fse from 'fs-extra'; -import logSymbols from 'log-symbols'; -import ora from 'ora'; -import { tree } from './tree.js'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Command } from "commander"; +import fse from "fs-extra"; +import logSymbols from "log-symbols"; +import ora from "ora"; +import { tree } from "./tree.js"; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); const packageJson = JSON.parse( - fse.readFileSync(path.resolve(dirname, '../package.json'), 'utf8'), + fse.readFileSync(path.resolve(dirname, "../package.json"), "utf8"), ); const getLatestVersionOfTag = async (packageName, tag) => { - const cachePath = path.resolve( - os.tmpdir(), - `${packageName.replaceAll('/', '-')}-${tag}.json`, - ); + const cacheFilename = `${packageName.replaceAll("/", "-")}-${tag}.json`; + + const cacheDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '.cache'); + + if (!fse.existsSync(cacheDir)) { + fse.mkdirSync(cacheDir, { recursive: true, mode: 0o700 }); + } + + const cachePath = path.join(cacheDir, cacheFilename); + let data; try { const response = await fetch( `https://registry.npmjs.org/${packageName}/${tag}`, ); data = await response.json(); - await fse.writeFile(cachePath, JSON.stringify(data)); + + fse.writeFileSync(cachePath, JSON.stringify(data), { mode: 0o600 }); } catch (exception) { if (fse.existsSync(cachePath)) { console.warn( `${logSymbols.warning} Failed to fetch the latest version from npm, using a cache`, ); - data = await fse.readJson(cachePath); + data = fse.readJSONSync(cachePath); } else { throw exception; } } - if (typeof data === 'string' && data.startsWith('version not found')) { + if (typeof data === "string" && data.startsWith("version not found")) { console.error(`Tag ${tag} does not exist for ${packageName}.`); process.exit(1); } @@ -47,9 +53,9 @@ const getLatestVersionOfTag = async (packageName, tag) => { const { version } = data; if (!/^\d+\.\d+\.\d+.*$/.test(version)) { - console.error('Invalid version received, something has gone very wrong.'); + console.error("Invalid version received, something has gone very wrong."); } - + return version; }; @@ -57,14 +63,14 @@ const init = async (name, { tag }) => { let projectPath = name; if (!projectPath) { - projectPath = path.join(process.cwd(), 'react-email-starter'); + projectPath = path.join(process.cwd(), "react-email-starter"); } - if (typeof projectPath === 'string') { + if (typeof projectPath === "string") { projectPath = projectPath.trim(); } - const templatePath = path.resolve(dirname, '../template'); + const templatePath = path.resolve(dirname, "../template"); const resolvedProjectPath = path.resolve(projectPath); if (fse.existsSync(resolvedProjectPath)) { @@ -73,7 +79,7 @@ const init = async (name, { tag }) => { } const spinner = ora({ - text: 'Preparing files...\n', + text: "Preparing files...\n", }).start(); fse.copySync(templatePath, resolvedProjectPath, { @@ -81,26 +87,26 @@ const init = async (name, { tag }) => { }); const templatePackageJsonPath = path.resolve( resolvedProjectPath, - './package.json', + "./package.json", ); - const templatePackageJson = fse.readFileSync(templatePackageJsonPath, 'utf8'); + const templatePackageJson = fse.readFileSync(templatePackageJsonPath, "utf8"); fse.writeFileSync( templatePackageJsonPath, templatePackageJson .replace( - 'INSERT_COMPONENTS_VERSION', - await getLatestVersionOfTag('@react-email/components', tag), + "INSERT_COMPONENTS_VERSION", + await getLatestVersionOfTag("@react-email/components", tag), ) .replace( - 'INSERT_REACT_EMAIL_VERSION', - await getLatestVersionOfTag('react-email', tag), + "INSERT_REACT_EMAIL_VERSION", + await getLatestVersionOfTag("react-email", tag), ), - 'utf8', + "utf8", ); spinner.stopAndPersist({ symbol: logSymbols.success, - text: 'React Email Starter files ready', + text: "React Email Starter files ready", }); // eslint-disable-next-line no-console @@ -108,7 +114,7 @@ const init = async (name, { tag }) => { await tree(resolvedProjectPath, 4, (dirent) => { return !path .join(dirent.parentPath, dirent.name) - .includes('node_modules'); + .includes("node_modules"); }), ); }; @@ -116,8 +122,8 @@ const init = async (name, { tag }) => { new Command() .name(packageJson.name) .version(packageJson.version) - .description('The easiest way to get started with React Email') - .arguments('[dir]', 'Path to initialize the project') - .option('-t, --tag ', 'Tag of React Email versions to use', 'latest') + .description("The easiest way to get started with React Email") + .arguments("[dir]", "Path to initialize the project") + .option("-t, --tag ", "Tag of React Email versions to use", "latest") .action(init) .parse(process.argv); From e2fc90056175304dcb8492941f13a0560d3db2a6 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Mon, 23 Jun 2025 11:32:37 -0300 Subject: [PATCH 5/6] lint --- packages/create-email/src/index.js | 72 ++++++++++++++++-------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/packages/create-email/src/index.js b/packages/create-email/src/index.js index eac2c28808..88595130e5 100755 --- a/packages/create-email/src/index.js +++ b/packages/create-email/src/index.js @@ -1,38 +1,42 @@ #!/usr/bin/env node -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { Command } from "commander"; -import fse from "fs-extra"; -import logSymbols from "log-symbols"; -import ora from "ora"; -import { tree } from "./tree.js"; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Command } from 'commander'; +import fse from 'fs-extra'; +import logSymbols from 'log-symbols'; +import ora from 'ora'; +import { tree } from './tree.js'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); const packageJson = JSON.parse( - fse.readFileSync(path.resolve(dirname, "../package.json"), "utf8"), + fse.readFileSync(path.resolve(dirname, '../package.json'), 'utf8'), ); const getLatestVersionOfTag = async (packageName, tag) => { - const cacheFilename = `${packageName.replaceAll("/", "-")}-${tag}.json`; - - const cacheDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '.cache'); - + const cacheFilename = `${packageName.replaceAll('/', '-')}-${tag}.json`; + + const cacheDir = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '..', + '.cache', + ); + if (!fse.existsSync(cacheDir)) { fse.mkdirSync(cacheDir, { recursive: true, mode: 0o700 }); } - + const cachePath = path.join(cacheDir, cacheFilename); - + let data; try { const response = await fetch( `https://registry.npmjs.org/${packageName}/${tag}`, ); data = await response.json(); - + fse.writeFileSync(cachePath, JSON.stringify(data), { mode: 0o600 }); } catch (exception) { if (fse.existsSync(cachePath)) { @@ -45,7 +49,7 @@ const getLatestVersionOfTag = async (packageName, tag) => { } } - if (typeof data === "string" && data.startsWith("version not found")) { + if (typeof data === 'string' && data.startsWith('version not found')) { console.error(`Tag ${tag} does not exist for ${packageName}.`); process.exit(1); } @@ -53,9 +57,9 @@ const getLatestVersionOfTag = async (packageName, tag) => { const { version } = data; if (!/^\d+\.\d+\.\d+.*$/.test(version)) { - console.error("Invalid version received, something has gone very wrong."); + console.error('Invalid version received, something has gone very wrong.'); } - + return version; }; @@ -63,14 +67,14 @@ const init = async (name, { tag }) => { let projectPath = name; if (!projectPath) { - projectPath = path.join(process.cwd(), "react-email-starter"); + projectPath = path.join(process.cwd(), 'react-email-starter'); } - if (typeof projectPath === "string") { + if (typeof projectPath === 'string') { projectPath = projectPath.trim(); } - const templatePath = path.resolve(dirname, "../template"); + const templatePath = path.resolve(dirname, '../template'); const resolvedProjectPath = path.resolve(projectPath); if (fse.existsSync(resolvedProjectPath)) { @@ -79,7 +83,7 @@ const init = async (name, { tag }) => { } const spinner = ora({ - text: "Preparing files...\n", + text: 'Preparing files...\n', }).start(); fse.copySync(templatePath, resolvedProjectPath, { @@ -87,26 +91,26 @@ const init = async (name, { tag }) => { }); const templatePackageJsonPath = path.resolve( resolvedProjectPath, - "./package.json", + './package.json', ); - const templatePackageJson = fse.readFileSync(templatePackageJsonPath, "utf8"); + const templatePackageJson = fse.readFileSync(templatePackageJsonPath, 'utf8'); fse.writeFileSync( templatePackageJsonPath, templatePackageJson .replace( - "INSERT_COMPONENTS_VERSION", - await getLatestVersionOfTag("@react-email/components", tag), + 'INSERT_COMPONENTS_VERSION', + await getLatestVersionOfTag('@react-email/components', tag), ) .replace( - "INSERT_REACT_EMAIL_VERSION", - await getLatestVersionOfTag("react-email", tag), + 'INSERT_REACT_EMAIL_VERSION', + await getLatestVersionOfTag('react-email', tag), ), - "utf8", + 'utf8', ); spinner.stopAndPersist({ symbol: logSymbols.success, - text: "React Email Starter files ready", + text: 'React Email Starter files ready', }); // eslint-disable-next-line no-console @@ -114,7 +118,7 @@ const init = async (name, { tag }) => { await tree(resolvedProjectPath, 4, (dirent) => { return !path .join(dirent.parentPath, dirent.name) - .includes("node_modules"); + .includes('node_modules'); }), ); }; @@ -122,8 +126,8 @@ const init = async (name, { tag }) => { new Command() .name(packageJson.name) .version(packageJson.version) - .description("The easiest way to get started with React Email") - .arguments("[dir]", "Path to initialize the project") - .option("-t, --tag ", "Tag of React Email versions to use", "latest") + .description('The easiest way to get started with React Email') + .arguments('[dir]', 'Path to initialize the project') + .option('-t, --tag ', 'Tag of React Email versions to use', 'latest') .action(init) .parse(process.argv); From 0084c9b3a82b27f22575ca085eb7c3035b8a1ee5 Mon Sep 17 00:00:00 2001 From: gabriel miranda Date: Wed, 25 Jun 2025 10:01:48 -0300 Subject: [PATCH 6/6] try to ignore only local .cache --- packages/create-email/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-email/.gitignore b/packages/create-email/.gitignore index 5880e67058..3bf3c07c9c 100644 --- a/packages/create-email/.gitignore +++ b/packages/create-email/.gitignore @@ -1,2 +1,2 @@ .test -.cache +./.cache