diff --git a/package.json b/package.json index 351af814a3..1fa4f9ee7b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "nx run-many -t build", "build:clean": "node ./tasks/clean-build.mts", "build:pack": "nx run-many -t build:pack", - "build:test-project": "node ./tasks/test-project/test-project.mts", + "build:test-project": "tsx ./tasks/test-project/test-project.mts", "build:watch": "lerna run build:watch --parallel; tsc --build", "changesets": "tsx ./tasks/changesets/changesets.mts", "check": "cross-env yarn constraints && yarn dedupe --check", @@ -44,8 +44,8 @@ "project:deps": "node ./tasks/framework-tools/frameworkDepsToProject.mjs", "project:sync": "node ./tasks/framework-tools/frameworkSyncToProject.mjs", "project:tarsync": "tsx ./tasks/framework-tools/tarsync/bin.mts", - "rebuild-test-project-fixture": "tsx ./tasks/test-project/rebuild-test-project-fixture.ts", - "rebuild-test-project-fixture-esm": "tsx ./tasks/test-project/rebuild-test-project-fixture-esm.ts", + "rebuild-test-project-fixture": "tsx ./tasks/test-project/rebuild-test-project-fixture.mts", + "rebuild-test-project-fixture-esm": "tsx ./tasks/test-project/rebuild-test-project-fixture-esm.mts", "smoke-tests": "node ./tasks/smoke-tests/smoke-tests.mjs", "test": "nx run-many -t test -- --minWorkers=1 --maxWorkers=4", "test:k6": "tsx ./tasks/k6-test/run-k6-tests.mts", diff --git a/tasks/test-project/add-gql-fragments.ts b/tasks/test-project/add-gql-fragments.mts similarity index 93% rename from tasks/test-project/add-gql-fragments.ts rename to tasks/test-project/add-gql-fragments.mts index 79cddc3393..d11eecb5fb 100755 --- a/tasks/test-project/add-gql-fragments.ts +++ b/tasks/test-project/add-gql-fragments.mts @@ -4,7 +4,7 @@ import path from 'node:path' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' -import { fragmentsTasks } from './tasks.js' +import { fragmentsTasks } from './tasks.mjs' const args = yargs(hideBin(process.argv)) .usage('Usage: $0 ') diff --git a/tasks/test-project/codemods/delayedPage.js b/tasks/test-project/codemods/delayedPage.js index fbc5f030ac..cd256d03e8 100644 --- a/tasks/test-project/codemods/delayedPage.js +++ b/tasks/test-project/codemods/delayedPage.js @@ -1,5 +1,3 @@ -// @ts-check - const delayedComponents = ` function DelayedComponent({ time, diff --git a/tasks/test-project/codemods/models.ts b/tasks/test-project/codemods/models.mts similarity index 100% rename from tasks/test-project/codemods/models.ts rename to tasks/test-project/codemods/models.mts diff --git a/tasks/test-project/frameworkLinking.js b/tasks/test-project/frameworkLinking.js deleted file mode 100644 index 4e4beded5d..0000000000 --- a/tasks/test-project/frameworkLinking.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-env node, es6*/ -const execa = require('execa') - -const addFrameworkDepsToProject = (frameworkPath, projectPath, stdio) => { - return execa('yarn project:deps', { - cwd: frameworkPath, - shell: true, - stdio: stdio ? stdio : 'inherit', - env: { - CFW_PATH: frameworkPath, - RWJS_CWD: projectPath, - }, - }) -} - -const copyFrameworkPackages = (frameworkPath, projectPath, stdio) => { - return execa('yarn project:copy', { - cwd: frameworkPath, - shell: true, - stdio: stdio ? stdio : 'inherit', - env: { - CFW_PATH: frameworkPath, - RWJS_CWD: projectPath, - }, - }) -} - -module.exports = { - copyFrameworkPackages, - addFrameworkDepsToProject, -} diff --git a/tasks/test-project/frameworkLinking.mts b/tasks/test-project/frameworkLinking.mts new file mode 100644 index 0000000000..5480306e06 --- /dev/null +++ b/tasks/test-project/frameworkLinking.mts @@ -0,0 +1,38 @@ +import execa from 'execa' +import type { StdioOption, Options as ExecaOptions } from 'execa' + +export const addFrameworkDepsToProject = ( + frameworkPath: string, + projectPath: string, + stdio?: StdioOption, +) => { + const options: ExecaOptions = { + cwd: frameworkPath, + shell: true, + stdio: (stdio ?? 'inherit') as any, + env: { + CFW_PATH: frameworkPath, + RWJS_CWD: projectPath, + }, + } + + return execa('yarn', ['project:deps'], options) +} + +export const copyFrameworkPackages = ( + frameworkPath: string, + projectPath: string, + stdio?: StdioOption, +) => { + const options: ExecaOptions = { + cwd: frameworkPath, + shell: true, + stdio: (stdio ?? 'inherit') as any, + env: { + CFW_PATH: frameworkPath, + RWJS_CWD: projectPath, + }, + } + + return execa('yarn', ['project:copy'], options) +} diff --git a/tasks/test-project/rebuild-test-project-fixture-esm.ts b/tasks/test-project/rebuild-test-project-fixture-esm.mts similarity index 96% rename from tasks/test-project/rebuild-test-project-fixture-esm.ts rename to tasks/test-project/rebuild-test-project-fixture-esm.mts index f3e5bf76b5..e4420c1d4a 100755 --- a/tasks/test-project/rebuild-test-project-fixture-esm.ts +++ b/tasks/test-project/rebuild-test-project-fixture-esm.mts @@ -2,6 +2,9 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' import { hideBin } from 'yargs/helpers' @@ -12,19 +15,20 @@ import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@cedarjs/tui' import { addFrameworkDepsToProject, copyFrameworkPackages, -} from './frameworkLinking.js' -import { webTasks, apiTasks } from './tui-tasks.js' -import { isAwaitable, isTuiError } from './typing.js' -import type { TuiTaskDef } from './typing.js' +} from './frameworkLinking.mjs' +import { webTasks, apiTasks } from './tui-tasks.mjs' +import { isAwaitable, isTuiError } from './typing.mjs' +import type { TuiTaskDef } from './typing.mjs' import { getExecaOptions as utilGetExecaOptions, updatePkgJsonScripts, ExecaError, exec, getCfwBin, -} from './util.js' +} from './util.mjs' -const ansis = require('ansis') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function recommendedNodeVersion() { const templatePackageJsonPath = path.join( @@ -111,7 +115,7 @@ if (!startStep) { const tui = new RedwoodTUI() function getExecaOptions(cwd: string) { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } + return { ...utilGetExecaOptions(cwd), stdio: 'pipe' as const } } function beginStep(step: string) { @@ -160,7 +164,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { try { promise = task() - } catch (e) { + } catch (e: any) { // This code handles errors from synchronous tasks tui.stopReactive(true) @@ -184,7 +188,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { } if (isAwaitable(promise)) { - const result = await promise.catch((e) => { + const result = await (promise as Promise).catch((e: any) => { // This code handles errors from asynchronous tasks tui.stopReactive(true) @@ -348,11 +352,12 @@ async function runCommand() { content: 'yarn install', task: async () => { // TODO: See if this is needed now with tarsync - await exec('yarn install', getExecaOptions(OUTPUT_PROJECT_PATH)) + await exec('yarn install', [], getExecaOptions(OUTPUT_PROJECT_PATH)) // TODO: Now that I've added this, I wonder what other steps I can remove return exec( `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:tarsync`, + [], getExecaOptions(OUTPUT_PROJECT_PATH), ) }, @@ -521,7 +526,7 @@ async function runCommand() { RW_PATH: path.join(__dirname, '../../'), }, }) - } catch (e) { + } catch (e: any) { if ( e instanceof ExecaError && !e.stderr && diff --git a/tasks/test-project/rebuild-test-project-fixture.ts b/tasks/test-project/rebuild-test-project-fixture.mts similarity index 96% rename from tasks/test-project/rebuild-test-project-fixture.ts rename to tasks/test-project/rebuild-test-project-fixture.mts index 0610a10db8..c0f816bbb6 100755 --- a/tasks/test-project/rebuild-test-project-fixture.ts +++ b/tasks/test-project/rebuild-test-project-fixture.mts @@ -2,6 +2,9 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' import { hideBin } from 'yargs/helpers' @@ -12,19 +15,20 @@ import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@cedarjs/tui' import { addFrameworkDepsToProject, copyFrameworkPackages, -} from './frameworkLinking' -import { webTasks, apiTasks } from './tui-tasks' -import { isAwaitable, isTuiError } from './typing' -import type { TuiTaskDef } from './typing' +} from './frameworkLinking.mjs' +import { webTasks, apiTasks } from './tui-tasks.mjs' +import { isAwaitable, isTuiError } from './typing.mjs' +import type { TuiTaskDef } from './typing.mjs' import { getExecaOptions as utilGetExecaOptions, updatePkgJsonScripts, ExecaError, exec, getCfwBin, -} from './util' +} from './util.mjs' -const ansis = require('ansis') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function recommendedNodeVersion() { const templatePackageJsonPath = path.join( @@ -111,7 +115,7 @@ if (!startStep) { const tui = new RedwoodTUI() function getExecaOptions(cwd: string) { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } + return { ...utilGetExecaOptions(cwd), stdio: 'pipe' as const } } function beginStep(step: string) { @@ -160,7 +164,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { try { promise = task() - } catch (e) { + } catch (e: any) { // This code handles errors from synchronous tasks tui.stopReactive(true) @@ -184,7 +188,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { } if (isAwaitable(promise)) { - const result = await promise.catch((e) => { + const result = await (promise as Promise).catch((e: any) => { // This code handles errors from asynchronous tasks tui.stopReactive(true) @@ -345,11 +349,12 @@ async function runCommand() { content: 'yarn install', task: async () => { // TODO: See if this is needed now with tarsync - await exec('yarn install', getExecaOptions(OUTPUT_PROJECT_PATH)) + await exec('yarn install', [], getExecaOptions(OUTPUT_PROJECT_PATH)) // TODO: Now that I've added this, I wonder what other steps I can remove return exec( `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:tarsync`, + [], getExecaOptions(OUTPUT_PROJECT_PATH), ) }, @@ -521,7 +526,7 @@ async function runCommand() { RW_PATH: path.join(__dirname, '../../'), }, }) - } catch (e) { + } catch (e: any) { if ( e instanceof ExecaError && !e.stderr && diff --git a/tasks/test-project/set-up-trusted-documents.ts b/tasks/test-project/set-up-trusted-documents.mts similarity index 97% rename from tasks/test-project/set-up-trusted-documents.ts rename to tasks/test-project/set-up-trusted-documents.mts index 81e5ee127c..0390cdfe01 100644 --- a/tasks/test-project/set-up-trusted-documents.ts +++ b/tasks/test-project/set-up-trusted-documents.mts @@ -5,10 +5,10 @@ import * as path from 'node:path' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' -import { exec, getExecaOptions as utilGetExecaOptions } from './util' +import { exec, getExecaOptions as utilGetExecaOptions } from './util.mjs' function getExecaOptions(cwd: string) { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } + return { ...utilGetExecaOptions(cwd), stdio: 'pipe' as const } } const args = yargs(hideBin(process.argv)) diff --git a/tasks/test-project/tasks.js b/tasks/test-project/tasks.mts similarity index 59% rename from tasks/test-project/tasks.js rename to tasks/test-project/tasks.mts index 2388b2f6be..5574fd856e 100644 --- a/tasks/test-project/tasks.js +++ b/tasks/test-project/tasks.mts @@ -1,23 +1,30 @@ /* eslint-env node, es6*/ -const fs = require('node:fs') -const path = require('path') +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const execa = require('execa') -const Listr = require('listr2').Listr +import execa from 'execa' +import { Listr } from 'listr2' -const { +import { getExecaOptions, applyCodemod, updatePkgJsonScripts, exec, getCfwBin, -} = require('./util') +} from './util.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) // This variable gets used in other functions // and is set when webTasks or apiTasks are called -let OUTPUT_PATH +let OUTPUT_PATH: string -function fullPath(name, { addExtension } = { addExtension: true }) { +function fullPath( + name: string, + { addExtension } = { addExtension: true }, +): string { if (addExtension) { if (name.startsWith('api')) { name += '.ts' @@ -29,11 +36,15 @@ function fullPath(name, { addExtension } = { addExtension: true }) { return path.join(OUTPUT_PATH, name) } -const createBuilder = (cmd) => { - return async function createItem(positionals) { +const createBuilder = (cmd: string) => { + return async function createItem(positionals?: string | string[]) { await execa( cmd, - Array.isArray(positionals) ? positionals : [positionals], + positionals + ? Array.isArray(positionals) + ? positionals + : [positionals] + : [], getExecaOptions(OUTPUT_PATH), ) } @@ -41,7 +52,15 @@ const createBuilder = (cmd) => { const createPage = createBuilder('yarn cedar g page') -async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { +interface WebTasksOptions { + linkWithLatestFwBuild: boolean + verbose: boolean +} + +export async function webTasks( + outputPath: string, + { linkWithLatestFwBuild, verbose }: WebTasksOptions, +) { OUTPUT_PATH = outputPath const createPages = async () => { @@ -96,28 +115,28 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { await createPage('profile /profile') // Update the profile page test - const testFileContent = `import { render, waitFor, screen } from '@cedarjs/testing/web' - - import ProfilePage from './ProfilePage' - - describe('ProfilePage', () => { - it('renders successfully', async () => { - mockCurrentUser({ - email: 'danny@bazinga.com', - id: 84849020, - roles: 'BAZINGA', - }) - - await waitFor(async () => { - expect(() => { - render() - }).not.toThrow() - }) - - expect(await screen.findByText('danny@bazinga.com')).toBeInTheDocument() - }) - }) - ` + const testFileContent = [ + "import { render, waitFor, screen } from '@cedarjs/testing/web'", + "import ProfilePage from './ProfilePage'", + '', + "describe('ProfilePage', () => {", + " it('renders successfully', async () => {", + " mockCurrentUser({", + " email: 'danny@bazinga.com',", + " id: 84849020,", + " roles: 'BAZINGA',", + " })", + '', + " await waitFor(async () => {", + " expect(() => {", + " render()", + " }).not.toThrow()", + " })", + '', + " expect(await screen.findByText('danny@bazinga.com')).toBeInTheDocument()", + " })", + "})", + ].join('\n') fs.writeFileSync( fullPath('web/src/pages/ProfilePage/ProfilePage.test'), @@ -160,9 +179,9 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { } const createLayout = async () => { - const createLayout = createBuilder('yarn cedar g layout') + const createLayoutBuilder = createBuilder('yarn cedar g layout') - await createLayout('blog') + await createLayoutBuilder('blog') return applyCodemod( 'blogLayout.js', @@ -298,7 +317,7 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { title: 'Install tailwind dependencies', // @NOTE: use cfw, because calling the copy function doesn't seem to work here task: () => - execa( + exec( 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', [], getExecaOptions(outputPath), @@ -309,7 +328,7 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { title: '[link] Copy local framework files again', // @NOTE: use cfw, because calling the copy function doesn't seem to work here task: () => - execa( + exec( `yarn ${getCfwBin(outputPath)} project:copy`, [], getExecaOptions(outputPath), @@ -320,11 +339,11 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { { title: 'Adding Tailwind', task: () => { - return execa( + return exec( 'yarn cedar setup ui tailwindcss', ['--force', linkWithLatestFwBuild && '--no-install'].filter( Boolean, - ), + ) as string[], getExecaOptions(outputPath), ) }, @@ -332,20 +351,28 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { ], { exitOnError: true, - renderer: verbose && 'verbose', + renderer: verbose ? 'verbose' : 'default', }, ) } -async function addModel(schema) { - const path = `${OUTPUT_PATH}/api/db/schema.prisma` +export async function addModel(schema: string) { + const prismaPath = `${OUTPUT_PATH}/api/db/schema.prisma` - const current = fs.readFileSync(path) + const current = fs.readFileSync(prismaPath) + + fs.writeFileSync(prismaPath, `${current}\n\n${schema}`) +} - fs.writeFileSync(path, `${current}\n\n${schema}`) +interface ApiTasksOptions { + verbose: boolean + linkWithLatestFwBuild: boolean } -async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { +export async function apiTasks( + outputPath: string, + { verbose, linkWithLatestFwBuild }: ApiTasksOptions, +) { OUTPUT_PATH = outputPath const addDbAuth = async () => { @@ -372,7 +399,7 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) - await execa( + await exec( 'yarn cedar setup auth dbAuth --force --no-webauthn', [], getExecaOptions(outputPath), @@ -387,106 +414,120 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { }) if (linkWithLatestFwBuild) { - await execa( + await exec( `yarn ${getCfwBin(outputPath)} project:copy`, [], getExecaOptions(outputPath), ) } - await execa( + await exec( 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', [], + getExecaOptions(outputPath), ) // update directive in contacts.sdl.ts const pathContactsSdl = `${OUTPUT_PATH}/api/src/graphql/contacts.sdl.ts` - const contentContactsSdl = fs.readFileSync(pathContactsSdl, 'utf-8') - const resultsContactsSdl = contentContactsSdl - .replace( - 'createContact(input: CreateContactInput!): Contact! @requireAuth', - `createContact(input: CreateContactInput!): Contact @skipAuth`, - ) - .replace( - 'deleteContact(id: Int!): Contact! @requireAuth', - 'deleteContact(id: Int!): Contact! @requireAuth(roles:["ADMIN"])', - ) // make deleting contacts admin only - fs.writeFileSync(pathContactsSdl, resultsContactsSdl) + if (fs.existsSync(pathContactsSdl)) { + const contentContactsSdl = fs.readFileSync(pathContactsSdl, 'utf-8') + const resultsContactsSdl = contentContactsSdl + .replace( + 'createContact(input: CreateContactInput!): Contact! @requireAuth', + 'createContact(input: CreateContactInput!): Contact @skipAuth', + ) + .replace( + 'deleteContact(id: Int!): Contact! @requireAuth', + 'deleteContact(id: Int!): Contact! @requireAuth(roles:["ADMIN"])', + ) // make deleting contacts admin only' + fs.writeFileSync(pathContactsSdl, resultsContactsSdl) + } // update directive in posts.sdl.ts const pathPostsSdl = `${OUTPUT_PATH}/api/src/graphql/posts.sdl.ts` - const contentPostsSdl = fs.readFileSync(pathPostsSdl, 'utf-8') - const resultsPostsSdl = contentPostsSdl.replace( - /posts: \[Post!\]! @requireAuth([^}]*)@requireAuth/, - `posts: [Post!]! @skipAuth - post(id: Int!): Post @skipAuth`, - ) // make posts accessible to all + if (fs.existsSync(pathPostsSdl)) { + const contentPostsSdl = fs.readFileSync(pathPostsSdl, 'utf-8') + const resultsPostsSdl = contentPostsSdl.replace( + /posts: [Post!]! @requireAuth([^}]*)@requireAuth/, + 'posts: [Post!]! @skipAuth\n post(id: Int!): Post @skipAuth', // make posts accessible to all + + ) - fs.writeFileSync(pathPostsSdl, resultsPostsSdl) + fs.writeFileSync(pathPostsSdl, resultsPostsSdl) + } // Update src/lib/auth to return roles, so tsc doesn't complain const libAuthPath = `${OUTPUT_PATH}/api/src/lib/auth.ts` - const libAuthContent = fs.readFileSync(libAuthPath, 'utf-8') + if (fs.existsSync(libAuthPath)) { + const libAuthContent = fs.readFileSync(libAuthPath, 'utf-8') - const newLibAuthContent = libAuthContent - .replace( - 'select: { id: true }', - 'select: { id: true, roles: true, email: true}', - ) - .replace( - 'const currentUserRoles = context.currentUser?.roles', - 'const currentUserRoles = context.currentUser?.roles as string | string[]', - ) - fs.writeFileSync(libAuthPath, newLibAuthContent) + const newLibAuthContent = libAuthContent + .replace( + 'select: { id: true }', + 'select: { id: true, roles: true, email: true}', + ) + .replace( + 'const currentUserRoles = context.currentUser?.roles', + 'const currentUserRoles = context.currentUser?.roles as string | string[]', + ) + fs.writeFileSync(libAuthPath, newLibAuthContent) + } // update requireAuth test const pathRequireAuth = `${OUTPUT_PATH}/api/src/directives/requireAuth/requireAuth.test.ts` - const contentRequireAuth = fs.readFileSync(pathRequireAuth).toString() - const resultsRequireAuth = contentRequireAuth.replace( - /const mockExecution([^}]*){} }\)/, - `const mockExecution = mockRedwoodDirective(requireAuth, { - context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } }, - })`, - ) - fs.writeFileSync(pathRequireAuth, resultsRequireAuth) + if (fs.existsSync(pathRequireAuth)) { + const contentRequireAuth = fs.readFileSync(pathRequireAuth).toString() + const resultsRequireAuth = contentRequireAuth.replace( + /const mockExecution([^}]*){} }\)/, + "const mockExecution = mockRedwoodDirective(requireAuth, {\n context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } },\n })", + ) + fs.writeFileSync(pathRequireAuth, resultsRequireAuth) + } // add fullName input to signup form const pathSignupPageTs = `${OUTPUT_PATH}/web/src/pages/SignupPage/SignupPage.tsx` - const contentSignupPageTs = fs.readFileSync(pathSignupPageTs, 'utf-8') - const usernameFields = contentSignupPageTs.match( - /\s*/, - )[0] - const fullNameFields = usernameFields - .replace(/\s*ref=\{usernameRef}/, '') - .replaceAll('username', 'full-name') - .replaceAll('Username', 'Full Name') - - const newContentSignupPageTs = contentSignupPageTs - .replace( - '', - '\n' + - fullNameFields, - ) - // include full-name in the data we pass to `signUp()` - .replace( - 'password: data.password', - "password: data.password, 'full-name': data['full-name']", + if (fs.existsSync(pathSignupPageTs)) { + const contentSignupPageTs = fs.readFileSync(pathSignupPageTs, 'utf-8') + const usernameFieldsMatch = contentSignupPageTs.match( + /\s*/, ) + if (usernameFieldsMatch) { + const usernameFields = usernameFieldsMatch[0] + const fullNameFields = usernameFields + .replace(/\s*ref={usernameRef}/, '') + .replaceAll('username', 'full-name') + .replaceAll('Username', 'Full Name') + + const newContentSignupPageTs = contentSignupPageTs + .replace( + '', + '\n' + + fullNameFields, + ) + // include full-name in the data we pass to `signUp()` + .replace( + 'password: data.password', + `password: data.password, 'full-name': data['full-name']`, + ) - fs.writeFileSync(pathSignupPageTs, newContentSignupPageTs) + fs.writeFileSync(pathSignupPageTs, newContentSignupPageTs) + } + } // set fullName when signing up const pathAuthTs = `${OUTPUT_PATH}/api/src/functions/auth.ts` - const contentAuthTs = fs.readFileSync(pathAuthTs).toString() - const resultsAuthTs = contentAuthTs - .replace('name: string', "'full-name': string") - .replace('userAttributes: _userAttributes', 'userAttributes') - .replace( - '// name: userAttributes.name', - "fullName: userAttributes['full-name']", - ) + if (fs.existsSync(pathAuthTs)) { + const contentAuthTs = fs.readFileSync(pathAuthTs).toString() + const resultsAuthTs = contentAuthTs + .replace('name: string', "'full-name': string") + .replace('userAttributes: _userAttributes', 'userAttributes') + .replace( + '// name: userAttributes.name', + `fullName: userAttributes['full-name']`, + ) - fs.writeFileSync(pathAuthTs, resultsAuthTs) + fs.writeFileSync(pathAuthTs, resultsAuthTs) + } } // add prerender to some routes @@ -497,48 +538,50 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { // keep it outside of BlogLayout title: 'Creating double rendering test page', task: async () => { - const createPage = createBuilder('yarn cedar g page') - await createPage('double') - - const doublePageContent = `import { Metadata } from '@cedarjs/web' - - import test from './test.png' - - const DoublePage = () => { - return ( - <> - - -

DoublePage

-

- This page exists to make sure we don't regress on{' '} - - #7757 - -

-

For RW#7757 it needs to be a page that is not wrapped in a Set

-

- We also use this page to make sure we don't regress on{' '} - - #317 - -

- Test - - ) - } - - export default DoublePage` + const createPageBuilder = createBuilder('yarn cedar g page') + await createPageBuilder('double') + + const doublePageContent = [ + "import { Metadata } from '@cedarjs/web'", + "", + "import test from './test.png'", + "", + "const DoublePage = () => {", + " return (", + " <>", + " ", + "", + "

DoublePage

", + "

", + " This page exists to make sure we don't regress on{' '}", + " ", + " #7757", + " ", + "

", + "

For RW#7757 it needs to be a page that is not wrapped in a Set

", + "

", + " We also use this page to make sure we don't regress on{' '}", + " ", + " #317", + " ", + "

", + " \"Test\"", + " ", + " )", + "}", + "", + "export default DoublePage", + ].join("\n") fs.writeFileSync( fullPath('web/src/pages/DoublePage/DoublePage'), @@ -556,48 +599,49 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { title: 'Update Routes.tsx', task: () => { const pathRoutes = `${OUTPUT_PATH}/web/src/Routes.tsx` - const contentRoutes = fs.readFileSync(pathRoutes).toString() - const resultsRoutesAbout = contentRoutes.replace( - /name="about"/, - `name="about" prerender`, - ) - const resultsRoutesHome = resultsRoutesAbout.replace( - /name="home"/, - `name="home" prerender`, - ) - const resultsRoutesBlogPost = resultsRoutesHome.replace( - /name="blogPost"/, - `name="blogPost" prerender`, - ) - const resultsRoutesNotFound = resultsRoutesBlogPost.replace( - /page={NotFoundPage}/, - `page={NotFoundPage} prerender`, - ) - const resultsRoutesWaterfall = resultsRoutesNotFound.replace( - /page={WaterfallPage}/, - `page={WaterfallPage} prerender`, - ) - const resultsRoutesDouble = resultsRoutesWaterfall.replace( - 'name="double"', - 'name="double" prerender', - ) - const resultsRoutesNewContact = resultsRoutesDouble.replace( - 'name="newContact"', - 'name="newContact" prerender', - ) - fs.writeFileSync(pathRoutes, resultsRoutesNewContact) - - const blogPostRouteHooks = `import { db } from '$api/src/lib/db.js' - - export async function routeParameters() { - return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id })) - }` - const blogPostRouteHooksPath = `${OUTPUT_PATH}/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts` - fs.writeFileSync(blogPostRouteHooksPath, blogPostRouteHooks) - - const waterfallRouteHooks = `export async function routeParameters() { - return [{ id: 2 }] - }` + if (fs.existsSync(pathRoutes)) { + const contentRoutes = fs.readFileSync(pathRoutes).toString() + const resultsRoutesAbout = contentRoutes.replace( + /name="about"/, + 'name="about" prerender', + ) + const resultsRoutesHome = resultsRoutesAbout.replace( + /name="home"/, + 'name="home" prerender', + ) + const resultsRoutesBlogPost = resultsRoutesHome.replace( + /name="blogPost"/, + 'name="blogPost" prerender', + ) + const resultsRoutesNotFound = resultsRoutesBlogPost.replace( + /page={NotFoundPage}/, + 'page={NotFoundPage} prerender', + ) + const resultsRoutesWaterfall = resultsRoutesNotFound.replace( + /page={WaterfallPage}/, + 'page={WaterfallPage} prerender', + ) + const resultsRoutesDouble = resultsRoutesWaterfall.replace( + 'name="double"', + 'name="double" prerender', + ) + const resultsRoutesNewContact = resultsRoutesDouble.replace( + 'name="newContact"', + 'name="newContact" prerender', + ) + fs.writeFileSync(pathRoutes, resultsRoutesNewContact) + } + + const blogPostRouteHooks = [ + "import { db } from '$api/src/lib/db.js'", + "", + "export async function routeParameters() {", + " return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id }))", + "}", + ].join("\n") + const blogPostRouteHooksPath = `${OUTPUT_PATH}/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts` + fs.writeFileSync(blogPostRouteHooksPath, blogPostRouteHooks) + const waterfallRouteHooks = 'export async function routeParameters() { return [{ id: 2 }] }' const waterfallRouteHooksPath = `${OUTPUT_PATH}/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts` fs.writeFileSync(waterfallRouteHooksPath, waterfallRouteHooks) }, @@ -613,13 +657,13 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { title: 'Adding post model to prisma', task: async () => { // Need both here since they have a relation - const { post, user } = await import('./codemods/models.js') + const { post, user } = await import('./codemods/models.mjs') addModel(post) addModel(user) - return execa( - `yarn cedar prisma migrate dev --name create_post_user`, + return exec( + 'yarn cedar prisma migrate dev --name create_post_user', [], getExecaOptions(outputPath), ) @@ -636,7 +680,7 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fullPath('api/src/services/posts/posts.scenarios'), ) - await execa( + await exec( `yarn ${getCfwBin(outputPath)} project:copy`, [], getExecaOptions(outputPath), @@ -655,12 +699,12 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { { title: 'Adding contact model to prisma', task: async () => { - const { contact } = await import('./codemods/models.js') + const { contact } = await import('./codemods/models.mjs') addModel(contact) - await execa( - `yarn cedar prisma migrate dev --name create_contact`, + await exec( + 'yarn cedar prisma migrate dev --name create_contact', [], getExecaOptions(outputPath), ) @@ -678,36 +722,38 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { 'db', 'migrations', ) - // Migration folders are folders which start with 14 digits because they have a yyyymmddhhmmss - const migrationFolders = fs - .readdirSync(migrationsFolderPath) - .filter((name) => { - return ( - name.match(/\d{14}.+/) && - fs - .lstatSync(path.join(migrationsFolderPath, name)) - .isDirectory() + if (fs.existsSync(migrationsFolderPath)) { + // Migration folders are folders which start with 14 digits because they have a yyyymmddhhmmss + const migrationFolders = fs + .readdirSync(migrationsFolderPath) + .filter((name) => { + return ( + name.match(/\d{14}.+/) && + fs + .lstatSync(path.join(migrationsFolderPath, name)) + .isDirectory() + ) + }) + .sort() + const datetime = new Date('2022-01-01T12:00:00.000Z') + migrationFolders.forEach((name) => { + const datetimeInCorrectFormat = + datetime.getFullYear() + + ('0' + (datetime.getMonth() + 1)).slice(-2) + + ('0' + datetime.getDate()).slice(-2) + + ('0' + datetime.getHours()).slice(-2) + + ('0' + datetime.getMinutes()).slice(-2) + + ('0' + datetime.getSeconds()).slice(-2) + fs.renameSync( + path.join(migrationsFolderPath, name), + path.join( + migrationsFolderPath, + `${datetimeInCorrectFormat}${name.substring(14)}`, + ), ) + datetime.setDate(datetime.getDate() + 1) }) - .sort() - const datetime = new Date('2022-01-01T12:00:00.000Z') - migrationFolders.forEach((name) => { - const datetimeInCorrectFormat = - datetime.getFullYear() + - ('0' + (datetime.getMonth() + 1)).slice(-2) + - ('0' + datetime.getDate()).slice(-2) + - ('0' + datetime.getHours()).slice(-2) + - ('0' + datetime.getMinutes()).slice(-2) + - ('0' + datetime.getSeconds()).slice(-2) - fs.renameSync( - path.join(migrationsFolderPath, name), - path.join( - migrationsFolderPath, - `${datetimeInCorrectFormat}${name.substring(14)}`, - ), - ) - datetime.setDate(datetime.getDate() + 1) - }) + } }, }, { @@ -737,16 +783,18 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fullPath('api/src/services/users/users.scenarios'), ) - const test = `import { user } from './users.js' - import type { StandardScenario } from './users.scenarios.js' - - describe('users', () => { - scenario('returns a single user', async (scenario: StandardScenario) => { - const result = await user({ id: scenario.user.one.id }) - - expect(result).toEqual(scenario.user.one) - }) - })`.replaceAll(/ {12}/g, '') + const test = [ + "import { user } from './users.js'", + "import type { StandardScenario } from './users.scenarios.js'", + "", + "describe('users', () => {", + " scenario('returns a single user', async (scenario: StandardScenario) => {", + " const result = await user({ id: scenario.user.one.id })", + "", + " expect(result).toEqual(scenario.user.one)", + " })", + "})", + ].join("\n") fs.writeFileSync(fullPath('api/src/services/users/users.test'), test) @@ -787,8 +835,8 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { ], { exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { collapseSubtasks: false }, + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, }, ) } @@ -798,7 +846,10 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { * if we choose to move them later * @param {string} outputPath */ -async function streamingTasks(outputPath, { verbose }) { +export async function streamingTasks( + outputPath: string, + { verbose }: { verbose: boolean }, +) { OUTPUT_PATH = outputPath const tasks = [ @@ -826,8 +877,8 @@ async function streamingTasks(outputPath, { verbose }) { return new Listr(tasks, { exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { collapseSubtasks: false }, + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, }) } @@ -835,7 +886,10 @@ async function streamingTasks(outputPath, { verbose }) { * Tasks to add GraphQL Fragments support to the test-project, and some queries * to test fragments */ -async function fragmentsTasks(outputPath, { verbose }) { +export async function fragmentsTasks( + outputPath: string, + { verbose }: { verbose: boolean }, +) { OUTPUT_PATH = outputPath const tasks = [ @@ -852,10 +906,10 @@ async function fragmentsTasks(outputPath, { verbose }) { title: 'Adding produce and stall models to prisma', task: async () => { // Need both here since they have a relation - const models = await import('./codemods/models.js') + const models = await import('./codemods/models.mjs') - addModel((models.default || models).produce) - addModel((models.default || models).stall) + addModel(models.produce) + addModel(models.stall) return exec( 'yarn cedar prisma migrate dev --name create_produce_stall', @@ -945,14 +999,7 @@ async function fragmentsTasks(outputPath, { verbose }) { return new Listr(tasks, { exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { collapseSubtasks: false }, + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, }) } - -module.exports = { - apiTasks, - webTasks, - streamingTasks, - fragmentsTasks, -} diff --git a/tasks/test-project/test-project.mts b/tasks/test-project/test-project.mts index 928afe6494..e2b6c5c736 100644 --- a/tasks/test-project/test-project.mts +++ b/tasks/test-project/test-project.mts @@ -10,8 +10,8 @@ import { rimraf } from 'rimraf' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' -import { apiTasks, streamingTasks, webTasks } from './tasks.js' -import { confirmNoFixtureNoLink, getExecaOptions, getCfwBin } from './util.js' +import { apiTasks, streamingTasks, webTasks } from './tasks.mjs' +import { confirmNoFixtureNoLink, getExecaOptions, getCfwBin } from './util.mjs' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -304,7 +304,7 @@ async function runCommand() { try { await globalTasks().run() - } catch (err) { + } catch (err: any) { console.error(err) process.exit(1) } diff --git a/tasks/test-project/tui-tasks.ts b/tasks/test-project/tui-tasks.mts similarity index 98% rename from tasks/test-project/tui-tasks.ts rename to tasks/test-project/tui-tasks.mts index ed8b3a587f..4e54fcb93f 100644 --- a/tasks/test-project/tui-tasks.ts +++ b/tasks/test-project/tui-tasks.mts @@ -1,17 +1,22 @@ /* eslint-env node, es2021*/ +import { fileURLToPath } from 'node:url' + import fs from 'node:fs' import path from 'node:path' import type { Options as ExecaOptions } from 'execa' -import type { TuiTaskList } from './typing.js' +import type { TuiTaskList } from './typing.mjs' import { getExecaOptions as utilGetExecaOptions, updatePkgJsonScripts, exec, getCfwBin, -} from './util.js' +} from './util.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function getExecaOptions(cwd: string): ExecaOptions { return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } @@ -64,13 +69,13 @@ function createBuilder(cmd: string, dir = '') { const execaOptions = getExecaOptions(path.join(OUTPUT_PATH, dir)) return function (positionalArguments?: string | string[]) { - const subprocess = exec( - cmd, - Array.isArray(positionalArguments) - ? positionalArguments - : [positionalArguments], - execaOptions, - ) + const args = Array.isArray(positionalArguments) + ? positionalArguments + : positionalArguments + ? [positionalArguments] + : [] + + const subprocess = exec(cmd, args, execaOptions) return subprocess } @@ -720,7 +725,7 @@ export async function apiTasks( title: 'Adding post and user model to prisma', task: async () => { // Need both here since they have a relation - const { post, user } = await import('./codemods/models.js') + const { post, user } = await import('./codemods/models.mjs') addModel(post) addModel(user) @@ -762,7 +767,7 @@ export async function apiTasks( { title: 'Adding contact model to prisma', task: async () => { - const { contact } = await import('./codemods/models.js') + const { contact } = await import('./codemods/models.mjs') addModel(contact) @@ -983,7 +988,7 @@ export async function fragmentsTasks(outputPath: string) { title: 'Adding produce and stall models to prisma', task: async () => { // Need both here since they have a relation - const { produce, stall } = await import('./codemods/models.js') + const { produce, stall } = await import('./codemods/models.mjs') addModel(produce) addModel(stall) diff --git a/tasks/test-project/typing.ts b/tasks/test-project/typing.mts similarity index 100% rename from tasks/test-project/typing.ts rename to tasks/test-project/typing.mts diff --git a/tasks/test-project/util.js b/tasks/test-project/util.mts similarity index 50% rename from tasks/test-project/util.js rename to tasks/test-project/util.mts index 52fbd09c49..b67db76077 100644 --- a/tasks/test-project/util.js +++ b/tasks/test-project/util.mts @@ -1,13 +1,49 @@ -/* eslint-env node, es6*/ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const fs = require('node:fs') -const path = require('path') -const stream = require('stream') +import execa from 'execa' +import type { Options as ExecaOptions } from 'execa' +import prompts from 'prompts' -const execa = require('execa') -const prompts = require('prompts') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -async function applyCodemod(codemod, target) { +let OUTPUT_PATH: string +let VERBOSE = false + +export function setOutputPath(path: string) { + OUTPUT_PATH = path +} + +export function getOutputPath() { + return OUTPUT_PATH +} + +export function setVerbose(verbose: boolean) { + VERBOSE = verbose +} + +export function getVerbose() { + return VERBOSE +} + +export function fullPath( + name: string, + { addExtension } = { addExtension: true }, +) { + if (addExtension) { + if (name.startsWith('api')) { + name += '.ts' + } else if (name.startsWith('web')) { + name += '.tsx' + } + } + + return path.join(OUTPUT_PATH, name) +} + +export async function applyCodemod(codemod: string, target: string) { const args = [ '--fail-on-error', '-t', @@ -17,15 +53,12 @@ async function applyCodemod(codemod, target) { '--verbose=2', ] - args.push() - await exec('yarn jscodeshift', args, getExecaOptions(path.resolve(__dirname))) } -/** @type {(string) => import('execa').Options} */ -const getExecaOptions = (cwd) => ({ +export const getExecaOptions = (cwd: string): ExecaOptions => ({ shell: true, - stdio: 'inherit', + stdio: VERBOSE ? 'inherit' : 'pipe', cleanup: true, cwd, env: { @@ -35,7 +68,13 @@ const getExecaOptions = (cwd) => ({ }, }) -const updatePkgJsonScripts = ({ projectPath, scripts }) => { +export const updatePkgJsonScripts = ({ + projectPath, + scripts, +}: { + projectPath: string + scripts: Record +}) => { const projectPackageJsonPath = path.join(projectPath, 'package.json') const projectPackageJson = JSON.parse( fs.readFileSync(projectPackageJsonPath, 'utf-8'), @@ -51,7 +90,10 @@ const updatePkgJsonScripts = ({ projectPath, scripts }) => { } // Confirmation prompt when using --no-copyFromFixture --no-link' -async function confirmNoFixtureNoLink(copyFromFixtureOption, linkOption) { +export async function confirmNoFixtureNoLink( + copyFromFixtureOption: boolean, + linkOption: boolean, +) { if (!copyFromFixtureOption && !linkOption) { const { checkNoLink } = await prompts( { @@ -74,13 +116,20 @@ async function confirmNoFixtureNoLink(copyFromFixtureOption, linkOption) { } } -const nullStream = new stream.Writable() -nullStream._write = (_chunk, _encoding, next) => { - next() -} +export class ExecaError extends Error { + stdout: string + stderr: string + exitCode: number -class ExecaError extends Error { - constructor({ stdout, stderr, exitCode }) { + constructor({ + stdout, + stderr, + exitCode, + }: { + stdout: string + stderr: string + exitCode: number + }) { super(`execa failed with exit code ${exitCode}`) this.stdout = stdout this.stderr = stderr @@ -88,8 +137,12 @@ class ExecaError extends Error { } } -async function exec(...args) { - return execa(...args) +export async function exec( + file: string, + args?: string[], + options?: ExecaOptions, +) { + return execa(file, args ?? [], options) .then(({ stdout, stderr, exitCode }) => { if (exitCode !== 0) { throw new ExecaError({ stdout, stderr, exitCode }) @@ -97,32 +150,37 @@ async function exec(...args) { return { stdout, stderr, exitCode } }) - .catch((error) => { + .catch((error: any) => { if (error instanceof ExecaError) { // Rethrow ExecaError throw error } else { - const { stdout, stderr, exitCode } = error + const { stdout = '', stderr = '', exitCode = 1 } = error throw new ExecaError({ stdout, stderr, exitCode }) } }) } +export function createBuilder(cmd: string, dir = '') { + return function (positionalArguments?: string | string[]) { + const execaOptions = getExecaOptions(path.join(OUTPUT_PATH, dir)) + + const args = Array.isArray(positionalArguments) + ? positionalArguments + : positionalArguments + ? [positionalArguments] + : [] + + const subprocess = exec(cmd, args, execaOptions) + + return subprocess + } +} + // TODO: Remove this as soon as cfw is part of a stable Cedar release, and then // instead just use `cfw` directly everywhere -function getCfwBin(projectPath) { +export function getCfwBin(projectPath: string) { return fs.existsSync(path.join(projectPath, 'node_modules/.bin/cfw')) ? 'cfw' : 'rwfw' } - -module.exports = { - getExecaOptions, - applyCodemod, - updatePkgJsonScripts, - confirmNoFixtureNoLink, - nullStream, - ExecaError, - exec, - getCfwBin, -} diff --git a/tasks/tsconfig.json b/tasks/tsconfig.json index abf89a7803..184444a524 100644 --- a/tasks/tsconfig.json +++ b/tasks/tsconfig.json @@ -4,5 +4,6 @@ "moduleResolution": "NodeNext", "module": "NodeNext", "allowJs": true - } + }, + "exclude": ["test-project/templates"] }