diff --git a/__fixtures__/test-project/package.json b/__fixtures__/test-project/package.json index 1ed6603e14..ec57e79fc2 100644 --- a/__fixtures__/test-project/package.json +++ b/__fixtures__/test-project/package.json @@ -2,7 +2,8 @@ "private": true, "workspaces": [ "api", - "web" + "web", + "packages/*" ], "devDependencies": { "@cedarjs/core": "2.3.0", diff --git a/__fixtures__/test-project/packages/validators/README.md b/__fixtures__/test-project/packages/validators/README.md new file mode 100644 index 0000000000..ecfd785633 --- /dev/null +++ b/__fixtures__/test-project/packages/validators/README.md @@ -0,0 +1,15 @@ +# Shared Package '@my-org/validators' + +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special `workspace:*` version. After that you can import it +into your code: + +```json + "dependencies": { + "@my-org/validators": "workspace:*" + } +``` + +```javascript +import { validators } from '@my-org/validators' +``` diff --git a/__fixtures__/test-project/packages/validators/package.json b/__fixtures__/test-project/packages/validators/package.json new file mode 100644 index 0000000000..e52bec0137 --- /dev/null +++ b/__fixtures__/test-project/packages/validators/package.json @@ -0,0 +1,22 @@ +{ + "name": "@my-org/validators", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "@cedarjs/testing": "2.2.1", + "typescript": "5.9.3" + } +} diff --git a/__fixtures__/test-project/packages/validators/src/index.ts b/__fixtures__/test-project/packages/validators/src/index.ts new file mode 100644 index 0000000000..22aefe648c --- /dev/null +++ b/__fixtures__/test-project/packages/validators/src/index.ts @@ -0,0 +1,5 @@ +export function validateEmail(email: string) { + return email.includes('@') && + email.includes('.') && + email.lastIndexOf('.') > email.indexOf('@') + 1 +} diff --git a/__fixtures__/test-project/packages/validators/src/validators.test.ts b/__fixtures__/test-project/packages/validators/src/validators.test.ts new file mode 100644 index 0000000000..110ad35ba3 --- /dev/null +++ b/__fixtures__/test-project/packages/validators/src/validators.test.ts @@ -0,0 +1,7 @@ +import { validateEmail } from './index.js' + +describe('validators', () => { + it('should not throw any errors', async () => { + expect(validateEmail('valid@email.com')).not.toThrow() + }) +}) diff --git a/__fixtures__/test-project/packages/validators/tsconfig.json b/__fixtures__/test-project/packages/validators/tsconfig.json new file mode 100644 index 0000000000..a405d33430 --- /dev/null +++ b/__fixtures__/test-project/packages/validators/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2023", + "module": "Node20", + "esModuleInterop": true, + "skipLibCheck": true, + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + }, + "include": ["src"], + "exclude": ["**/*.test.ts"], +} diff --git a/__fixtures__/test-project/redwood.toml b/__fixtures__/test-project/redwood.toml index fd4787af4b..5d80e61f18 100644 --- a/__fixtures__/test-project/redwood.toml +++ b/__fixtures__/test-project/redwood.toml @@ -19,3 +19,6 @@ open = true [notifications] versionUpdates = ["latest"] + +[experimental.packagesWorkspace] + enabled = true diff --git a/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json index fb781a867d..21a3055ff1 100644 --- a/__fixtures__/test-project/web/package.json +++ b/__fixtures__/test-project/web/package.json @@ -15,6 +15,7 @@ "@cedarjs/forms": "2.3.0", "@cedarjs/router": "2.3.0", "@cedarjs/web": "2.3.0", + "@my-org/validators": "workspace:*", "humanize-string": "2.1.0", "react": "19.2.3", "react-dom": "19.2.3" diff --git a/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx b/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx index de6488e1a7..3f523f7717 100644 --- a/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx +++ b/__fixtures__/test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' +import { validateEmail } from '@my-org/validators' import { useForm } from 'react-hook-form' import { @@ -94,9 +95,16 @@ const ContactUsPage = () => { name="email" validation={{ required: true, - pattern: { - value: /[^@]+@[^.]+..+/, - message: 'Please enter a valid email address', + validate: (value) => { + if (!value) { + return 'Email is required' + } + + if (!validateEmail(value)) { + return 'Please enter a valid email address' + } + + return true }, }} className="rounded-sm border px-2 py-1" diff --git a/tasks/test-project/codemods/contactUsPage.js b/tasks/test-project/codemods/contactUsPage.js index 6f21dce2fc..6b90251b9d 100644 --- a/tasks/test-project/codemods/contactUsPage.js +++ b/tasks/test-project/codemods/contactUsPage.js @@ -39,9 +39,16 @@ const body = ` name="email" validation={{ required: true, - pattern: { - value: /[^@]+@[^.]+\..+/, - message: 'Please enter a valid email address', + validate: (value) => { + if (!value) { + return 'Email is required' + } + + if (!validateEmail(value)) { + return 'Please enter a valid email address' + } + + return true }, }} className="border rounded-sm px-2 py-1" @@ -157,6 +164,15 @@ export default (file, api) => { ], j.stringLiteral('@cedarjs/router'), ), + j.importDeclaration( + [ + j.importSpecifier( + j.identifier('validateEmail'), + j.identifier('validateEmail'), + ), + ], + j.stringLiteral('@my-org/validators'), + ), ] // Remove the `{ Link, routes }` imports that are generated and unused diff --git a/tasks/test-project/rebuild-test-project-fixture.mts b/tasks/test-project/rebuild-test-project-fixture.mts index 3b0aae4578..5983774886 100755 --- a/tasks/test-project/rebuild-test-project-fixture.mts +++ b/tasks/test-project/rebuild-test-project-fixture.mts @@ -498,6 +498,125 @@ async function runCommand() { await tuiTask({ step: 10, + title: 'Add workspace packages', + task: async () => { + const tomlPath = path.join(OUTPUT_PROJECT_PATH, 'redwood.toml') + const redwoodToml = fs.readFileSync(tomlPath, 'utf-8') + const newRedwoodToml = + redwoodToml + '\n[experimental.packagesWorkspace]\n enabled = true\n' + + fs.writeFileSync(tomlPath, newRedwoodToml) + + await exec( + 'yarn cedar g package @my-org/validators', + [], + getExecaOptions(OUTPUT_PROJECT_PATH), + ) + + const packagePath = path.join( + OUTPUT_PROJECT_PATH, + 'packages', + 'validators', + ) + + fs.writeFileSync( + path.join(packagePath, 'src', 'index.ts'), + 'export function validateEmail(email: string) {\n' + + " return email.includes('@') &&\n" + + " email.includes('.') &&\n" + + " email.lastIndexOf('.') > email.indexOf('@') + 1\n" + + '}\n', + ) + + fs.writeFileSync( + path.join(packagePath, 'src', 'validators.test.ts'), + "import { validateEmail } from './index.js'\n" + + '\n' + + "describe('validators', () => {\n" + + " it('should not throw any errors', async () => {\n" + + " expect(validateEmail('valid@email.com')).not.toThrow()\n" + + ' })\n' + + '})\n', + ) + + const webPackageJson = JSON.parse( + fs.readFileSync( + path.join(OUTPUT_PROJECT_PATH, 'web', 'package.json'), + 'utf8', + ), + ) + + webPackageJson.dependencies['@my-org/validators'] = 'workspace:*' + + fs.writeFileSync( + path.join(OUTPUT_PROJECT_PATH, 'web', 'package.json'), + JSON.stringify(webPackageJson, null, 2), + ) + + await exec('yarn install', [], getExecaOptions(OUTPUT_PROJECT_PATH)) + + const build = await exec( + 'yarn cedar build', + [], + getExecaOptions(OUTPUT_PROJECT_PATH), + ) + + const distFiles = fs.readdirSync( + path.join(OUTPUT_PROJECT_PATH, 'packages', 'validators', 'dist'), + ) + + if (distFiles.some((file) => file.includes('test'))) { + console.error('distFiles', distFiles) + throw new Error( + 'Unexpected test file in validators package dist directory', + ) + } + + // TODO: Update this when we refine the build process + if (!build.stdout.includes('yarn build exited with code 0')) { + console.error('yarn cedar build output', build.stdout, build.stderr) + throw new Error('Unexpected output from `yarn cedar build`') + } + + // Verify that `yarn cedar ` works inside package directories + // Starting with `yarn cedar info` + // TODO: Enable code below + // const info = await exec( + // 'yarn cedar info', + // [], + // getExecaOptions(OUTPUT_PROJECT_PATH), + // ) + + // if ( + // !info.stdout.includes('Binaries:') || + // !info.stdout.includes('Node:') || + // !info.stdout.includes('npmPackages:') || + // !info.stdout.includes('@cedarjs/core') + // ) { + // console.error('yarn cedar info output', info.stdout, info.stderr) + + // throw new Error('Unexpected output from `yarn cedar info`') + // } + + // Continue testing `yarn cedar ` by running `yarn cedar test` + // const test = await exec( + // 'yarn cedar test @my-org/validators', + // [], + // getExecaOptions(OUTPUT_PROJECT_PATH), + // ) + + // Validate that only the tests for this package ran + // Verify that all tests passed + // TODO: Implement functionality according to the comment above + + // The package we've generated (@my-org/validators) is used in the test + // project on both the web and the api side and is further tested by our + // playwright tests that trigger the files that import the package. + }, + }) + + await tuiTask({ + step: 11, title: 'Running prisma migrate reset', task: () => { return exec( @@ -509,7 +628,7 @@ async function runCommand() { }) await tuiTask({ - step: 11, + step: 12, title: 'Lint --fix all the things', task: async () => { try { @@ -540,7 +659,7 @@ async function runCommand() { }) await tuiTask({ - step: 12, + step: 13, title: 'Replace and Cleanup Fixture', task: async () => { // @TODO: This only works on UNIX, we should use path.join everywhere @@ -567,10 +686,13 @@ async function runCommand() { await rimraf(`${OUTPUT_PROJECT_PATH}/.nx`) await rimraf(`${OUTPUT_PROJECT_PATH}/tarballs`) - // Copy over package.json from template, so we remove the extra dev dependencies, and cfw postinstall script - // that we added in "Adding framework dependencies to project" + // Copy over package.json from template, so we remove the extra dev + // dependencies, and cfw postinstall script that we added in "Adding + // framework dependencies to project" // There's one devDep we actually do want in there though, and that's the // prettier plugin for Tailwind CSS + // We also want the `packages/*` workspace config that was added when + // adding the validators package const rootPackageJson = JSON.parse( fs.readFileSync(path.join(OUTPUT_PROJECT_PATH, 'package.json'), 'utf8'), ) @@ -583,6 +705,7 @@ async function runCommand() { ) newRootPackageJson.devDependencies['prettier-plugin-tailwindcss'] = rootPackageJson.devDependencies['prettier-plugin-tailwindcss'] + newRootPackageJson.workspaces.push('packages/*') fs.writeFileSync( path.join(OUTPUT_PROJECT_PATH, 'package.json'), JSON.stringify(newRootPackageJson, null, 2) + '\n', @@ -595,7 +718,7 @@ async function runCommand() { }) await tuiTask({ - step: 13, + step: 14, title: 'All done!', task: () => { console.log('-'.repeat(30))