From 1c69ebd77fed9922bcbd5f44410f79b9ccf56d8b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 3 Jan 2026 15:11:09 +0100 Subject: [PATCH 1/5] chore(test-project): Convert tasks.js to TypeScript --- tasks/test-project/add-gql-fragments.ts | 16 +- ...rameworkLinking.js => frameworkLinking.ts} | 23 +- tasks/test-project/package.json | 3 + .../rebuild-test-project-fixture-esm.ts | 5 +- .../rebuild-test-project-fixture.ts | 17 +- .../test-project/set-up-trusted-documents.ts | 2 +- tasks/test-project/{tasks.js => tasks.ts} | 623 ++++++++---------- tasks/test-project/test-project.mts | 25 +- tasks/test-project/tui-tasks.ts | 160 ++--- tasks/test-project/{util.js => util.ts} | 118 +++- 10 files changed, 477 insertions(+), 515 deletions(-) rename tasks/test-project/{frameworkLinking.js => frameworkLinking.ts} (57%) create mode 100644 tasks/test-project/package.json rename tasks/test-project/{tasks.js => tasks.ts} (60%) rename tasks/test-project/{util.js => util.ts} (54%) diff --git a/tasks/test-project/add-gql-fragments.ts b/tasks/test-project/add-gql-fragments.ts index 79cddc3393..dcdb78806f 100755 --- a/tasks/test-project/add-gql-fragments.ts +++ b/tasks/test-project/add-gql-fragments.ts @@ -1,6 +1,7 @@ /* eslint-env node, es6*/ import path from 'node:path' +import { Listr } from 'listr2' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -16,14 +17,17 @@ const args = yargs(hideBin(process.argv)) */ async function runCommand() { const OUTPUT_PROJECT_PATH = path.resolve(String(args._)) - const tasks = await fragmentsTasks(OUTPUT_PROJECT_PATH, { - verbose: true, - }) + const tasks = await fragmentsTasks(OUTPUT_PROJECT_PATH) - tasks.run().catch((err: unknown) => { - console.error(err) - process.exit(1) + new Listr(tasks, { + exitOnError: true, + renderer: 'default', }) + .run() + .catch((err: unknown) => { + console.error(err) + process.exit(1) + }) } runCommand() diff --git a/tasks/test-project/frameworkLinking.js b/tasks/test-project/frameworkLinking.ts similarity index 57% rename from tasks/test-project/frameworkLinking.js rename to tasks/test-project/frameworkLinking.ts index 4e4beded5d..fd7b384655 100644 --- a/tasks/test-project/frameworkLinking.js +++ b/tasks/test-project/frameworkLinking.ts @@ -1,7 +1,11 @@ -/* eslint-env node, es6*/ -const execa = require('execa') +import execa from 'execa' +import type { StdioOption } from 'execa' -const addFrameworkDepsToProject = (frameworkPath, projectPath, stdio) => { +export const addFrameworkDepsToProject = ( + frameworkPath: string, + projectPath: string, + stdio?: StdioOption, +) => { return execa('yarn project:deps', { cwd: frameworkPath, shell: true, @@ -13,7 +17,11 @@ const addFrameworkDepsToProject = (frameworkPath, projectPath, stdio) => { }) } -const copyFrameworkPackages = (frameworkPath, projectPath, stdio) => { +export const copyFrameworkPackages = ( + frameworkPath: string, + projectPath: string, + stdio?: StdioOption, +) => { return execa('yarn project:copy', { cwd: frameworkPath, shell: true, @@ -23,9 +31,4 @@ const copyFrameworkPackages = (frameworkPath, projectPath, stdio) => { RWJS_CWD: projectPath, }, }) -} - -module.exports = { - copyFrameworkPackages, - addFrameworkDepsToProject, -} +} \ No newline at end of file diff --git a/tasks/test-project/package.json b/tasks/test-project/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/tasks/test-project/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tasks/test-project/rebuild-test-project-fixture-esm.ts b/tasks/test-project/rebuild-test-project-fixture-esm.ts index a413ba0609..0d937d7192 100755 --- a/tasks/test-project/rebuild-test-project-fixture-esm.ts +++ b/tasks/test-project/rebuild-test-project-fixture-esm.ts @@ -1,7 +1,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' @@ -24,7 +26,8 @@ import { getCfwBin, } from './util.js' -const ansis = require('ansis') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function recommendedNodeVersion() { const templatePackageJsonPath = path.join( diff --git a/tasks/test-project/rebuild-test-project-fixture.ts b/tasks/test-project/rebuild-test-project-fixture.ts index 32c91917a3..0ba901419a 100755 --- a/tasks/test-project/rebuild-test-project-fixture.ts +++ b/tasks/test-project/rebuild-test-project-fixture.ts @@ -1,7 +1,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 +14,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.js' +import { webTasks, apiTasks } from './tui-tasks.js' +import { isAwaitable, isTuiError } from './typing.js' +import type { TuiTaskDef } from './typing.js' import { getExecaOptions as utilGetExecaOptions, updatePkgJsonScripts, ExecaError, exec, getCfwBin, -} from './util' +} from './util.js' -const ansis = require('ansis') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function recommendedNodeVersion() { const templatePackageJsonPath = path.join( @@ -315,7 +318,7 @@ async function runCommand() { await tuiTask({ step: 1, title: '[link] Building Cedar framework', - content: 'yarn build:clean && yarn build', + content: 'yarn clean && yarn build', task: async () => { return exec( 'yarn build:clean && yarn build', diff --git a/tasks/test-project/set-up-trusted-documents.ts b/tasks/test-project/set-up-trusted-documents.ts index 81e5ee127c..260ea00630 100644 --- a/tasks/test-project/set-up-trusted-documents.ts +++ b/tasks/test-project/set-up-trusted-documents.ts @@ -5,7 +5,7 @@ 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.js' function getExecaOptions(cwd: string) { return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } diff --git a/tasks/test-project/tasks.js b/tasks/test-project/tasks.ts similarity index 60% rename from tasks/test-project/tasks.js rename to tasks/test-project/tasks.ts index 2388b2f6be..af5d2c1cbf 100644 --- a/tasks/test-project/tasks.js +++ b/tasks/test-project/tasks.ts @@ -1,51 +1,37 @@ -/* 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 type { ListrTask } from 'listr2' -const { +import { getExecaOptions, applyCodemod, updatePkgJsonScripts, exec, getCfwBin, -} = require('./util') - -// This variable gets used in other functions -// and is set when webTasks or apiTasks are called -let OUTPUT_PATH - -function fullPath(name, { addExtension } = { addExtension: true }) { - if (addExtension) { - if (name.startsWith('api')) { - name += '.ts' - } else if (name.startsWith('web')) { - name += '.tsx' - } - } + setOutputPath, + fullPath, + createBuilder, +} from './util.js' - return path.join(OUTPUT_PATH, name) -} - -const createBuilder = (cmd) => { - return async function createItem(positionals) { - await execa( - cmd, - Array.isArray(positionals) ? positionals : [positionals], - getExecaOptions(OUTPUT_PATH), - ) - } -} +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const createPage = createBuilder('yarn cedar g page') -async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { - OUTPUT_PATH = outputPath +interface WebTasksOptions { + linkWithLatestFwBuild: boolean +} + +export async function webTasks( + outputPath: string, + { linkWithLatestFwBuild }: WebTasksOptions, +): Promise { + setOutputPath(outputPath) - const createPages = async () => { - return new Listr([ + const createPages = async (): Promise => { + return [ { title: 'Creating home page', task: async () => { @@ -100,7 +86,7 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { import ProfilePage from './ProfilePage' - describe('ProfilePage', () => { +describe('ProfilePage', () => { it('renders successfully', async () => { mockCurrentUser({ email: 'danny@bazinga.com', @@ -156,13 +142,13 @@ 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', @@ -265,88 +251,89 @@ async function webTasks(outputPath, { linkWithLatestFwBuild, verbose }) { ) } - return new Listr( - [ - { - title: 'Creating pages', - task: () => createPages(), - }, - { - title: 'Creating layout', - task: () => createLayout(), - }, - { - title: 'Creating components', - task: () => createComponents(), - }, - { - title: 'Creating cells', - task: () => createCells(), - }, - { - title: 'Updating cell mocks', - task: () => updateCellMocks(), - }, - { - title: 'Changing routes', - task: () => applyCodemod('routes.js', fullPath('web/src/Routes')), - }, + return [ + { + title: 'Creating pages', + task: async (_ctx, task) => task.newListr(await createPages()), + }, + { + title: 'Creating layout', + task: () => createLayout(), + }, + { + title: 'Creating components', + task: () => createComponents(), + }, + { + title: 'Creating cells', + task: () => createCells(), + }, + { + title: 'Updating cell mocks', + task: () => updateCellMocks(), + }, + { + title: 'Changing routes', + task: () => applyCodemod('routes.js', fullPath('web/src/Routes')), + }, - // ====== NOTE: rufus needs this workaround for tailwind ======= - // Setup tailwind in a linked project, due to cfw we install deps manually - { - title: 'Install tailwind dependencies', - // @NOTE: use cfw, because calling the copy function doesn't seem to work here - task: () => - execa( - 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - { - title: '[link] Copy local framework files again', - // @NOTE: use cfw, because calling the copy function doesn't seem to work here - task: () => - execa( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - // ========= - { - title: 'Adding Tailwind', - task: () => { - return execa( - 'yarn cedar setup ui tailwindcss', - ['--force', linkWithLatestFwBuild && '--no-install'].filter( - Boolean, - ), - getExecaOptions(outputPath), - ) - }, - }, - ], + // ====== NOTE: rufus needs this workaround for tailwind ======= + // Setup tailwind in a linked project, due to cfw we install deps manually { - exitOnError: true, - renderer: verbose && 'verbose', + title: 'Install tailwind dependencies', + // @NOTE: use cfw, because calling the copy function doesn't seem to work here + task: () => + exec( + 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', + [], + getExecaOptions(outputPath), + ), + enabled: () => linkWithLatestFwBuild, + }, + { + title: '[link] Copy local framework files again', + // @NOTE: use cfw, because calling the copy function doesn't seem to work here + task: () => + exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ), + enabled: () => linkWithLatestFwBuild, + }, + // ========= + { + title: 'Adding Tailwind', + task: () => { + return exec( + 'yarn cedar setup ui tailwindcss', + ['--force', linkWithLatestFwBuild && '--no-install'].filter( + Boolean, + ) as string[], + getExecaOptions(outputPath), + ) + }, }, - ) + ] } -async function addModel(schema) { - const path = `${OUTPUT_PATH}/api/db/schema.prisma` +async function addModel(schema: string) { + const path = `${fullPath('api/db/schema.prisma', { addExtension: false })}` const current = fs.readFileSync(path) fs.writeFileSync(path, `${current}\n\n${schema}`) } -async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { - OUTPUT_PATH = outputPath +interface ApiTasksOptions { + linkWithLatestFwBuild: boolean +} + +export async function apiTasks( + outputPath: string, + { linkWithLatestFwBuild }: ApiTasksOptions, +): Promise { + setOutputPath(outputPath) const addDbAuth = async () => { // Temporarily disable postinstall script @@ -372,7 +359,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,20 +374,20 @@ 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', [], ) // update directive in contacts.sdl.ts - const pathContactsSdl = `${OUTPUT_PATH}/api/src/graphql/contacts.sdl.ts` + const pathContactsSdl = `${outputPath}/api/src/graphql/contacts.sdl.ts` const contentContactsSdl = fs.readFileSync(pathContactsSdl, 'utf-8') const resultsContactsSdl = contentContactsSdl .replace( @@ -414,18 +401,18 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fs.writeFileSync(pathContactsSdl, resultsContactsSdl) // update directive in posts.sdl.ts - const pathPostsSdl = `${OUTPUT_PATH}/api/src/graphql/posts.sdl.ts` + const pathPostsSdl = `${outputPath}/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 + post(id: Int!): Post @skipAuth`, // make posts accessible to all + ) 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 libAuthPath = `${outputPath}/api/src/lib/auth.ts` const libAuthContent = fs.readFileSync(libAuthPath, 'utf-8') const newLibAuthContent = libAuthContent @@ -440,7 +427,7 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fs.writeFileSync(libAuthPath, newLibAuthContent) // update requireAuth test - const pathRequireAuth = `${OUTPUT_PATH}/api/src/directives/requireAuth/requireAuth.test.ts` + const pathRequireAuth = `${outputPath}/api/src/directives/requireAuth/requireAuth.test.ts` const contentRequireAuth = fs.readFileSync(pathRequireAuth).toString() const resultsRequireAuth = contentRequireAuth.replace( /const mockExecution([^}]*){} }\)/, @@ -451,54 +438,57 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { fs.writeFileSync(pathRequireAuth, resultsRequireAuth) // add fullName input to signup form - const pathSignupPageTs = `${OUTPUT_PATH}/web/src/pages/SignupPage/SignupPage.tsx` + const pathSignupPageTs = `${outputPath}/web/src/pages/SignupPage/SignupPage.tsx` const contentSignupPageTs = fs.readFileSync(pathSignupPageTs, 'utf-8') - const usernameFields = contentSignupPageTs.match( + const usernameFieldsMatch = 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 (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 pathAuthTs = `${outputPath}/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']", + `fullName: userAttributes['full-name']`, ) fs.writeFileSync(pathAuthTs, resultsAuthTs) } // add prerender to some routes - const addPrerender = async () => { - return new Listr([ + const addPrerender = async (): Promise => { + return [ { // We need to do this here, and not where we create the other pages, to // keep it outside of BlogLayout title: 'Creating double rendering test page', task: async () => { - const createPage = createBuilder('yarn cedar g page') - await createPage('double') + const createPageBuilder = createBuilder('yarn cedar g page') + await createPageBuilder('double') const doublePageContent = `import { Metadata } from '@cedarjs/web' @@ -555,7 +545,7 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { { title: 'Update Routes.tsx', task: () => { - const pathRoutes = `${OUTPUT_PATH}/web/src/Routes.tsx` + const pathRoutes = `${outputPath}/web/src/Routes.tsx` const contentRoutes = fs.readFileSync(pathRoutes).toString() const resultsRoutesAbout = contentRoutes.replace( /name="about"/, @@ -592,152 +582,146 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { 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` + const blogPostRouteHooksPath = `${outputPath}/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` + const waterfallRouteHooksPath = `${outputPath}/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts` fs.writeFileSync(waterfallRouteHooksPath, waterfallRouteHooks) }, }, - ]) + ] } const generateScaffold = createBuilder('yarn cedar g scaffold') - return new Listr( - [ - { - title: 'Adding post model to prisma', - task: async () => { - // Need both here since they have a relation - const { post, user } = await import('./codemods/models.js') + return [ + { + title: 'Adding post model to prisma', + task: async () => { + // Need both here since they have a relation + const { post, user } = await import('./codemods/models.js') - addModel(post) - addModel(user) + addModel(post) + addModel(user) - return execa( - `yarn cedar prisma migrate dev --name create_post_user`, - [], - getExecaOptions(outputPath), - ) - }, + return exec( + `yarn cedar prisma migrate dev --name create_post_user`, + [], + getExecaOptions(outputPath), + ) }, - { - title: 'Scaffolding post', - task: async () => { - await generateScaffold('post') + }, + { + title: 'Scaffolding post', + task: async () => { + await generateScaffold('post') - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - fullPath('api/src/services/posts/posts.scenarios'), - ) + // Replace the random numbers in the scenario with consistent values + await applyCodemod( + 'scenarioValueSuffix.js', + fullPath('api/src/services/posts/posts.scenarios'), + ) - await execa( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - }, + await exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ) }, - { - title: 'Adding seed script', - task: async () => { - await applyCodemod( - 'seed.js', - fullPath('scripts/seed.ts', { addExtension: false }), - ) - }, + }, + { + title: 'Adding seed script', + task: async () => { + await applyCodemod( + 'seed.js', + fullPath('scripts/seed.ts', { addExtension: false }), + ) }, - { - title: 'Adding contact model to prisma', - task: async () => { - const { contact } = await import('./codemods/models.js') + }, + { + title: 'Adding contact model to prisma', + task: async () => { + const { contact } = await import('./codemods/models.js') - addModel(contact) + addModel(contact) - await execa( - `yarn cedar prisma migrate dev --name create_contact`, - [], - getExecaOptions(outputPath), - ) + await exec( + `yarn cedar prisma migrate dev --name create_contact`, + [], + getExecaOptions(outputPath), + ) - await generateScaffold('contacts') - }, + await generateScaffold('contacts') }, - { - // This task renames the migration folders so that we don't have to deal with duplicates/conflicts when committing to the repo - title: 'Adjust dates within migration folder names', - task: () => { - const migrationsFolderPath = path.join( - OUTPUT_PATH, - 'api', - '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() - ) - }) - .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)}`, - ), + }, + { + // This task renames the migration folders so that we don't have to deal with duplicates/conflicts when committing to the repo + title: 'Adjust dates within migration folder names', + task: () => { + const migrationsFolderPath = path.join( + outputPath, + 'api', + '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() ) - datetime.setDate(datetime.getDate() + 1) }) - }, - }, - { - title: 'Add dbAuth', - task: async () => addDbAuth(), + .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) + }) }, - { - title: 'Add users service', - task: async () => { - const generateSdl = createBuilder('yarn cedar g sdl --no-crud') + }, + { + title: 'Add dbAuth', + task: async () => addDbAuth(), + }, + { + title: 'Add users service', + task: async () => { + const generateSdl = createBuilder('yarn cedar g sdl --no-crud') - await generateSdl('user') + await generateSdl('user') - await applyCodemod( - 'usersSdl.js', - fullPath('api/src/graphql/users.sdl'), - ) + await applyCodemod('usersSdl.js', fullPath('api/src/graphql/users.sdl')) - await applyCodemod( - 'usersService.js', - fullPath('api/src/services/users/users'), - ) + await applyCodemod( + 'usersService.js', + fullPath('api/src/services/users/users'), + ) - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - fullPath('api/src/services/users/users.scenarios'), - ) + // Replace the random numbers in the scenario with consistent values + await applyCodemod( + 'scenarioValueSuffix.js', + fullPath('api/src/services/users/users.scenarios'), + ) - const test = `import { user } from './users.js' + const test = `import { user } from './users.js' import type { StandardScenario } from './users.scenarios.js' describe('users', () => { @@ -748,60 +732,53 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { }) })`.replaceAll(/ {12}/g, '') - fs.writeFileSync(fullPath('api/src/services/users/users.test'), test) + fs.writeFileSync(fullPath('api/src/services/users/users.test'), test) - return createBuilder('yarn cedar g types')() - }, + return createBuilder('yarn cedar g types')() }, - { - title: 'Add describeScenario tests', - task: async () => { - // Copy contact.scenarios.ts, because scenario tests look for the same filename - fs.copyFileSync( - fullPath('api/src/services/contacts/contacts.scenarios'), - fullPath('api/src/services/contacts/describeContacts.scenarios'), - ) + }, + { + title: 'Add describeScenario tests', + task: async () => { + // Copy contact.scenarios.ts, because scenario tests look for the same filename + fs.copyFileSync( + fullPath('api/src/services/contacts/contacts.scenarios'), + fullPath('api/src/services/contacts/describeContacts.scenarios'), + ) - // Create describeContacts.test.ts - const describeScenarioFixture = path.join( - __dirname, - 'templates', - 'api', - 'contacts.describeScenario.test.ts.template', - ) + // Create describeContacts.test.ts + const describeScenarioFixture = path.join( + __dirname, + 'templates', + 'api', + 'contacts.describeScenario.test.ts.template', + ) - fs.copyFileSync( - describeScenarioFixture, - fullPath('api/src/services/contacts/describeContacts.test'), - ) - }, - }, - { - // This is probably more of a web side task really, but the scaffolded - // pages aren't generated until we get here to the api side tasks. So - // instead of doing some up in the web side tasks, and then the rest - // here I decided to move all of them here - title: 'Add Prerender to Routes', - task: () => addPrerender(), + fs.copyFileSync( + describeScenarioFixture, + fullPath('api/src/services/contacts/describeContacts.test'), + ) }, - ], + }, { - exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { collapseSubtasks: false }, + // This is probably more of a web side task really, but the scaffolded + // pages aren't generated until we get here to the api side tasks. So + // instead of doing some up in the web side tasks, and then the rest + // here I decided to move all of them here + title: 'Add Prerender to Routes', + task: async (_ctx, task) => task.newListr(await addPrerender()), }, - ) + ] } /** * Separates the streaming-ssr related steps. These are all web tasks, * if we choose to move them later - * @param {string} outputPath */ -async function streamingTasks(outputPath, { verbose }) { - OUTPUT_PATH = outputPath +export async function streamingTasks(outputPath: string): Promise { + setOutputPath(outputPath) - const tasks = [ + return [ { title: 'Creating Delayed suspense delayed page', task: async () => { @@ -823,22 +800,16 @@ async function streamingTasks(outputPath, { verbose }) { }, }, ] - - return new Listr(tasks, { - exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { collapseSubtasks: false }, - }) } /** * Tasks to add GraphQL Fragments support to the test-project, and some queries * to test fragments */ -async function fragmentsTasks(outputPath, { verbose }) { - OUTPUT_PATH = outputPath +export async function fragmentsTasks(outputPath: string): Promise { + setOutputPath(outputPath) - const tasks = [ + return [ { title: 'Enable fragments', task: async () => { @@ -852,10 +823,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 { produce, stall } = await import('./codemods/models.js') - addModel((models.default || models).produce) - addModel((models.default || models).stall) + addModel(produce) + addModel(stall) return exec( 'yarn cedar prisma migrate dev --name create_produce_stall', @@ -893,12 +864,7 @@ async function fragmentsTasks(outputPath, { verbose }) { title: 'Copy components from templates', task: () => { const templatesPath = path.join(__dirname, 'templates', 'web') - const componentsPath = path.join( - OUTPUT_PATH, - 'web', - 'src', - 'components', - ) + const componentsPath = path.join(outputPath, 'web', 'src', 'components') for (const fileName of [ 'Card.tsx', @@ -918,8 +884,8 @@ async function fragmentsTasks(outputPath, { verbose }) { title: 'Copy sdl and service for groceries from templates', task: () => { const templatesPath = path.join(__dirname, 'templates', 'api') - const graphqlPath = path.join(OUTPUT_PATH, 'api', 'src', 'graphql') - const servicesPath = path.join(OUTPUT_PATH, 'api', 'src', 'services') + const graphqlPath = path.join(outputPath, 'api', 'src', 'graphql') + const servicesPath = path.join(outputPath, 'api', 'src', 'services') const sdlTemplatePath = path.join(templatesPath, 'groceries.sdl.ts') const sdlPath = path.join(graphqlPath, 'groceries.sdl.ts') @@ -942,17 +908,4 @@ async function fragmentsTasks(outputPath, { verbose }) { }, }, ] - - return new Listr(tasks, { - exitOnError: true, - renderer: verbose && 'verbose', - renderOptions: { 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..18c66d6210 100644 --- a/tasks/test-project/test-project.mts +++ b/tasks/test-project/test-project.mts @@ -198,27 +198,30 @@ const globalTasks = () => }, { title: 'Apply web codemods', - task: () => - webTasks(OUTPUT_PROJECT_PATH, { - verbose, - linkWithLatestFwBuild: link, - }), + task: async (_ctx, task) => + task.newListr( + await webTasks(OUTPUT_PROJECT_PATH, { + linkWithLatestFwBuild: link, + }), + ), enabled: () => !copyFromFixture, }, { // These are also web tasks... we can move them into the webTasks function // when streaming isn't experimental title: 'Enabling streaming-ssr experiment and applying codemods....', - task: () => streamingTasks(OUTPUT_PROJECT_PATH, { verbose }), + task: async (_ctx, task) => + task.newListr(await streamingTasks(OUTPUT_PROJECT_PATH)), enabled: () => streamingSsr, }, { title: 'Apply api codemods', - task: () => - apiTasks(OUTPUT_PROJECT_PATH, { - verbose, - linkWithLatestFwBuild: link, - }), + task: async (_ctx, task) => + task.newListr( + await apiTasks(OUTPUT_PROJECT_PATH, { + linkWithLatestFwBuild: link, + }), + ), enabled: () => !copyFromFixture, }, { diff --git a/tasks/test-project/tui-tasks.ts b/tasks/test-project/tui-tasks.ts index ed8b3a587f..873732214e 100644 --- a/tasks/test-project/tui-tasks.ts +++ b/tasks/test-project/tui-tasks.ts @@ -1,93 +1,40 @@ -/* eslint-env node, es2021*/ - import fs from 'node:fs' import path from 'node:path' +import { fileURLToPath } from 'node:url' -import type { Options as ExecaOptions } from 'execa' +import type { ListrTask } from 'listr2' -import type { TuiTaskList } from './typing.js' import { - getExecaOptions as utilGetExecaOptions, updatePkgJsonScripts, exec, getCfwBin, + setOutputPath, + fullPath, + createBuilder, + applyCodemod, + getExecaOptions, } from './util.js' -function getExecaOptions(cwd: string): ExecaOptions { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } -} +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: string -const RW_FRAMEWORK_PATH = path.join(__dirname, '../../') - -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) -} - -// TODO: Import from ./util.js when everything is using @rwjs/tui -async function applyCodemod(codemod: string, target: string) { - const args = [ - '--fail-on-error', - '-t', - `${path.resolve(__dirname, 'codemods', codemod)} ${target}`, - '--parser', - 'tsx', - '--verbose=2', - ] - - args.push() - - const subprocess = exec( - 'yarn jscodeshift', - args, - getExecaOptions(path.resolve(__dirname)), - ) - - return subprocess +interface WebTasksOptions { + linkWithLatestFwBuild?: boolean } -/** - * @param cmd The command to run - */ -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, - ) - - return subprocess - } -} - -export async function webTasks(outputPath: string) { +export async function webTasks( + outputPath: string, + _options?: WebTasksOptions, +): Promise { OUTPUT_PATH = outputPath + setOutputPath(outputPath) - const execaOptions = getExecaOptions(outputPath) - - const createPages = async () => { - // Passing 'web' here to test executing 'yarn redwood' in the /web directory - // to make sure it works as expected. We do the same for the /api directory - // further down in this file. + const createPages = async (): Promise => { const createPage = createBuilder('yarn cedar g page', 'web') - const tuiTaskList: TuiTaskList = [ + return [ { title: 'Creating home page', task: async () => { @@ -209,14 +156,12 @@ export async function webTasks(outputPath: string) { }, }, ] - - return tuiTaskList } 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', @@ -326,10 +271,10 @@ export async function webTasks(outputPath: string) { ) } - const tuiTaskList: TuiTaskList = [ + return [ { title: 'Creating pages', - task: () => createPages(), + task: async (_ctx, task) => task.newListr(await createPages()), }, { title: 'Creating layout', @@ -354,18 +299,14 @@ export async function webTasks(outputPath: string) { { title: 'Adding Tailwind', task: async () => { - await exec('yarn cedar setup ui tailwindcss', ['--force'], execaOptions) + await exec( + 'yarn cedar setup ui tailwindcss', + ['--force'], + getExecaOptions(outputPath), + ) }, }, - ] //, - // TODO: Figure out what to do with this. It's from Listr, but TUI doesn't - // have anything like it (yet?) - // { - // exitOnError: true, - // renderer: verbose && 'verbose', - // } - - return tuiTaskList + ] } async function addModel(schema: string) { @@ -377,15 +318,16 @@ async function addModel(schema: string) { } interface ApiTasksOptions { - linkWithLatestFwBuild: boolean - esmProject: boolean + linkWithLatestFwBuild?: boolean + esmProject?: boolean } export async function apiTasks( outputPath: string, - { linkWithLatestFwBuild, esmProject }: ApiTasksOptions, -) { + { linkWithLatestFwBuild = false, esmProject = false }: ApiTasksOptions = {}, +): Promise { OUTPUT_PATH = outputPath + setOutputPath(outputPath) const execaOptions = getExecaOptions(outputPath) @@ -403,21 +345,24 @@ export async function apiTasks( // tarballs and install them using that by setting yarn resolutions const setupPkg = path.join( - RW_FRAMEWORK_PATH, + __dirname, + '../../', 'packages', 'auth-providers', 'dbAuth', 'setup', ) const apiPkg = path.join( - RW_FRAMEWORK_PATH, + __dirname, + '../../', 'packages', 'auth-providers', 'dbAuth', 'api', ) const webPkg = path.join( - RW_FRAMEWORK_PATH, + __dirname, + '../../', 'packages', 'auth-providers', 'dbAuth', @@ -596,8 +541,8 @@ export async function apiTasks( } // add prerender to some routes - const addPrerender = async () => { - const tuiTaskList: TuiTaskList = [ + const addPrerender = async (): Promise => { + return [ { // We need to do this here, and not where we create the other pages, to // keep it outside of BlogLayout @@ -715,7 +660,7 @@ export async function apiTasks( const generateScaffold = createBuilder('yarn cedar g scaffold') - const tuiTaskList: TuiTaskList = [ + const tuiTaskList: ListrTask[] = [ { title: 'Adding post and user model to prisma', task: async () => { @@ -896,7 +841,7 @@ export async function apiTasks( // instead of doing some up in the web side tasks, and then the rest // here I decided to move all of them here title: 'Add Prerender to Routes', - task: () => addPrerender(), + task: async (_ctx, task) => task.newListr(await addPrerender()), }, { title: 'Add context tests', @@ -966,10 +911,10 @@ export async function apiTasks( * Tasks to add GraphQL Fragments support to the test-project, and some queries * to test fragments */ -export async function fragmentsTasks(outputPath: string) { - OUTPUT_PATH = outputPath +export async function fragmentsTasks(outputPath: string): Promise { + setOutputPath(outputPath) - const tuiTaskList: TuiTaskList = [ + return [ { title: 'Enable fragments', task: async () => { @@ -1024,12 +969,7 @@ export async function fragmentsTasks(outputPath: string) { title: 'Copy components from templates', task: () => { const templatesPath = path.join(__dirname, 'templates', 'web') - const componentsPath = path.join( - OUTPUT_PATH, - 'web', - 'src', - 'components', - ) + const componentsPath = path.join(outputPath, 'web', 'src', 'components') for (const fileName of [ 'Card.tsx', @@ -1049,8 +989,8 @@ export async function fragmentsTasks(outputPath: string) { title: 'Copy sdl and service for groceries from templates', task: () => { const templatesPath = path.join(__dirname, 'templates', 'api') - const graphqlPath = path.join(OUTPUT_PATH, 'api', 'src', 'graphql') - const servicesPath = path.join(OUTPUT_PATH, 'api', 'src', 'services') + const graphqlPath = path.join(outputPath, 'api', 'src', 'graphql') + const servicesPath = path.join(outputPath, 'api', 'src', 'services') const sdlTemplatePath = path.join(templatesPath, 'groceries.sdl.ts') const sdlPath = path.join(graphqlPath, 'groceries.sdl.ts') @@ -1074,6 +1014,4 @@ export async function fragmentsTasks(outputPath: string) { }, }, ] - - return tuiTaskList } diff --git a/tasks/test-project/util.js b/tasks/test-project/util.ts similarity index 54% rename from tasks/test-project/util.js rename to tasks/test-project/util.ts index 52fbd09c49..8b938df180 100644 --- a/tasks/test-project/util.js +++ b/tasks/test-project/util.ts @@ -1,13 +1,40 @@ -/* 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 + +export function setOutputPath(path: string) { + OUTPUT_PATH = path +} + +export function getOutputPath() { + return OUTPUT_PATH +} + +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,13 +44,10 @@ 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', cleanup: true, @@ -35,7 +59,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 +81,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 +107,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 +128,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 }) @@ -108,21 +152,29 @@ async function exec(...args) { }) } +/** + * @param cmd The command to run + */ +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, -} From 6fe7905518cb10af7be8bda2087dea83a5c4ac1e Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 3 Jan 2026 17:09:56 +0100 Subject: [PATCH 2/5] mts --- package.json | 4 +- ...gql-fragments.ts => add-gql-fragments.mts} | 2 +- tasks/test-project/base-tasks.mts | 361 ++++++ .../codemods/{models.ts => models.mts} | 0 ...ameworkLinking.ts => frameworkLinking.mts} | 20 +- tasks/test-project/package.json | 3 - ...s => rebuild-test-project-fixture-esm.mts} | 19 +- ...re.ts => rebuild-test-project-fixture.mts} | 21 +- ...uments.ts => set-up-trusted-documents.mts} | 8 +- tasks/test-project/tasks.mts | 273 +++++ tasks/test-project/tasks.ts | 911 --------------- tasks/test-project/test-project.mts | 6 +- tasks/test-project/tui-tasks.mts | 242 ++++ tasks/test-project/tui-tasks.ts | 1017 ----------------- tasks/test-project/{typing.ts => typing.mts} | 0 tasks/test-project/{util.ts => util.mts} | 11 +- 16 files changed, 928 insertions(+), 1970 deletions(-) rename tasks/test-project/{add-gql-fragments.ts => add-gql-fragments.mts} (94%) create mode 100644 tasks/test-project/base-tasks.mts rename tasks/test-project/codemods/{models.ts => models.mts} (100%) rename tasks/test-project/{frameworkLinking.ts => frameworkLinking.mts} (60%) delete mode 100644 tasks/test-project/package.json rename tasks/test-project/{rebuild-test-project-fixture-esm.ts => rebuild-test-project-fixture-esm.mts} (97%) rename tasks/test-project/{rebuild-test-project-fixture.ts => rebuild-test-project-fixture.mts} (97%) rename tasks/test-project/{set-up-trusted-documents.ts => set-up-trusted-documents.mts} (93%) create mode 100644 tasks/test-project/tasks.mts delete mode 100644 tasks/test-project/tasks.ts create mode 100644 tasks/test-project/tui-tasks.mts delete mode 100644 tasks/test-project/tui-tasks.ts rename tasks/test-project/{typing.ts => typing.mts} (100%) rename tasks/test-project/{util.ts => util.mts} (93%) diff --git a/package.json b/package.json index 351af814a3..9f77b71534 100644 --- a/package.json +++ b/package.json @@ -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 94% rename from tasks/test-project/add-gql-fragments.ts rename to tasks/test-project/add-gql-fragments.mts index dcdb78806f..50504dd2fb 100755 --- a/tasks/test-project/add-gql-fragments.ts +++ b/tasks/test-project/add-gql-fragments.mts @@ -5,7 +5,7 @@ import { Listr } from 'listr2' 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/base-tasks.mts b/tasks/test-project/base-tasks.mts new file mode 100644 index 0000000000..c098133d27 --- /dev/null +++ b/tasks/test-project/base-tasks.mts @@ -0,0 +1,361 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import type { ListrTask } from 'listr2' + +import { + applyCodemod, + createBuilder, + fullPath, + getCfwBin, + getExecaOptions, + setOutputPath, + updatePkgJsonScripts, + exec, +} from './util.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export interface CommonTaskOptions { + outputPath: string + linkWithLatestFwBuild?: boolean + isFixture?: boolean + esmProject?: boolean +} + +export const getCreatePagesTasks = ( + options: CommonTaskOptions, +): ListrTask[] => { + const createPage = options.isFixture + ? createBuilder('yarn cedar g page', 'web') + : createBuilder('yarn cedar g page') + + return [ + { + title: 'Creating home page', + task: async () => { + await createPage('home /') + return applyCodemod( + 'homePage.mjs', + fullPath('web/src/pages/HomePage/HomePage'), + ) + }, + }, + { + title: 'Creating about page', + task: async () => { + await createPage('about') + return applyCodemod( + 'aboutPage.mjs', + fullPath('web/src/pages/AboutPage/AboutPage'), + ) + }, + }, + { + title: 'Creating contact page', + task: async () => { + await createPage('contactUs /contact') + return applyCodemod( + 'contactUsPage.mjs', + fullPath('web/src/pages/ContactUsPage/ContactUsPage'), + ) + }, + }, + { + title: 'Creating blog post page', + task: async () => { + await createPage('blogPost /blog-post/{id:Int}') + await applyCodemod( + 'blogPostPage.mjs', + fullPath('web/src/pages/BlogPostPage/BlogPostPage'), + ) + + if (options.isFixture) { + await applyCodemod( + 'updateBlogPostPageStories.mjs', + fullPath('web/src/pages/BlogPostPage/BlogPostPage.stories'), + ) + } + }, + }, + { + title: 'Creating profile page', + task: async () => { + await createPage('profile /profile') + + 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() + }) +})` + + fs.writeFileSync( + fullPath('web/src/pages/ProfilePage/ProfilePage.test'), + testFileContent, + ) + + return applyCodemod( + 'profilePage.mjs', + fullPath('web/src/pages/ProfilePage/ProfilePage'), + ) + }, + }, + { + title: 'Creating MDX Storybook stories', + task: () => { + const cedarMdxStoryContent = fs.readFileSync( + `${path.resolve(__dirname, 'codemods', 'CedarJS.mdx')}`, + ) + fs.writeFileSync( + fullPath('web/src/CedarJS.mdx', { addExtension: false }), + cedarMdxStoryContent, + ) + }, + }, + { + title: 'Creating nested cells test page', + task: async () => { + await createPage('waterfall {id:Int}') + await applyCodemod( + 'waterfallPage.mjs', + fullPath('web/src/pages/WaterfallPage/WaterfallPage'), + ) + + if (options.isFixture) { + await applyCodemod( + 'updateWaterfallPageStories.mjs', + fullPath('web/src/pages/WaterfallPage/WaterfallPage.stories'), + ) + } + }, + }, + ] +} + +export const getCreateLayoutTasks = ( + _options: CommonTaskOptions, +): ListrTask[] => { + const createLayoutBuilder = createBuilder('yarn cedar g layout') + return [ + { + title: 'Creating layout', + task: async () => { + await createLayoutBuilder('blog') + return applyCodemod( + 'blogLayout.mjs', + fullPath('web/src/layouts/BlogLayout/BlogLayout'), + ) + }, + }, + ] +} + +export const getCreateComponentsTasks = ( + options: CommonTaskOptions, +): ListrTask[] => { + const createComponent = createBuilder('yarn cedar g component') + const tasks: ListrTask[] = [ + { + title: 'Creating components', + task: async () => { + await createComponent('blogPost') + await applyCodemod( + 'blogPost.mjs', + fullPath('web/src/components/BlogPost/BlogPost'), + ) + + await createComponent('author') + await applyCodemod( + 'author.mjs', + fullPath('web/src/components/Author/Author'), + ) + await applyCodemod( + 'updateAuthorStories.mjs', + fullPath('web/src/components/Author/Author.stories'), + ) + await applyCodemod( + 'updateAuthorTest.mjs', + fullPath('web/src/components/Author/Author.test'), + ) + + if (options.isFixture) { + await createComponent('classWithClassField') + await applyCodemod( + 'classWithClassField.ts', + fullPath( + 'web/src/components/ClassWithClassField/ClassWithClassField', + ), + ) + } + }, + }, + ] + return tasks +} + +export const getCreateCellsTasks = ( + _options: CommonTaskOptions, +): ListrTask[] => { + const createCell = createBuilder('yarn cedar g cell') + return [ + { + title: 'Creating cells', + task: async () => { + await createCell('blogPosts') + await applyCodemod( + 'blogPostsCell.mjs', + fullPath('web/src/components/BlogPostsCell/BlogPostsCell'), + ) + + await createCell('blogPost') + await applyCodemod( + 'blogPostCell.mjs', + fullPath('web/src/components/BlogPostCell/BlogPostCell'), + ) + + await createCell('author') + await applyCodemod( + 'authorCell.mjs', + fullPath('web/src/components/AuthorCell/AuthorCell'), + ) + + await createCell('waterfallBlogPost') + return applyCodemod( + 'waterfallBlogPostCell.mjs', + fullPath( + 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell', + ), + ) + }, + }, + ] +} + +export const getUpdateCellMocksTasks = ( + _options: CommonTaskOptions, +): ListrTask[] => { + return [ + { + title: 'Updating cell mocks', + task: async () => { + await applyCodemod( + 'updateBlogPostMocks.mjs', + fullPath('web/src/components/BlogPostCell/BlogPostCell.mock.ts', { + addExtension: false, + }), + ) + await applyCodemod( + 'updateBlogPostMocks.mjs', + fullPath('web/src/components/BlogPostsCell/BlogPostsCell.mock.ts', { + addExtension: false, + }), + ) + await applyCodemod( + 'updateAuthorCellMock.mjs', + fullPath('web/src/components/AuthorCell/AuthorCell.mock.ts', { + addExtension: false, + }), + ) + return applyCodemod( + 'updateWaterfallBlogPostMocks.mjs', + fullPath( + 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts', + { addExtension: false }, + ), + ) + }, + }, + ] +} + +export const getPrerenderTasks = (options: CommonTaskOptions): ListrTask[] => { + return [ + { + title: 'Creating double rendering test page', + task: async () => { + 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 RW#7757 and #317

+ Test + + ) +} +export default DoublePage` + + fs.writeFileSync( + fullPath('web/src/pages/DoublePage/DoublePage'), + doublePageContent, + ) + fs.copyFileSync( + fullPath('web/public/favicon.png', { addExtension: false }), + fullPath('web/src/pages/DoublePage/test.png', { + addExtension: false, + }), + ) + }, + }, + { + title: 'Update Routes.tsx', + task: () => { + const pathRoutes = fullPath('web/src/Routes.tsx', { + addExtension: false, + }) + let content = fs.readFileSync(pathRoutes, 'utf-8') + content = content + .replace(/name="about"/, 'name="about" prerender') + .replace(/name="home"/, 'name="home" prerender') + .replace(/name="blogPost"/, 'name="blogPost" prerender') + .replace(/page={NotFoundPage}/, 'page={NotFoundPage} prerender') + .replace(/page={WaterfallPage}/, 'page={WaterfallPage} prerender') + .replace('name="double"', 'name="double" prerender') + .replace('name="newContact"', 'name="newContact" prerender') + fs.writeFileSync(pathRoutes, content) + + const blogPostRouteHooks = `import { db } from '$api/src/lib/db.mjs' +export async function routeParameters() { + return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id })) +}` + fs.writeFileSync( + fullPath('web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts', { + addExtension: false, + }), + blogPostRouteHooks, + ) + + const waterfallRouteHooks = `export async function routeParameters() { return [{ id: 2 }] }` + fs.writeFileSync( + fullPath('web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts', { + addExtension: false, + }), + waterfallRouteHooks, + ) + }, + }, + ] +} 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.ts b/tasks/test-project/frameworkLinking.mts similarity index 60% rename from tasks/test-project/frameworkLinking.ts rename to tasks/test-project/frameworkLinking.mts index fd7b384655..5480306e06 100644 --- a/tasks/test-project/frameworkLinking.ts +++ b/tasks/test-project/frameworkLinking.mts @@ -1,20 +1,22 @@ import execa from 'execa' -import type { StdioOption } from 'execa' +import type { StdioOption, Options as ExecaOptions } from 'execa' export const addFrameworkDepsToProject = ( frameworkPath: string, projectPath: string, stdio?: StdioOption, ) => { - return execa('yarn project:deps', { + const options: ExecaOptions = { cwd: frameworkPath, shell: true, - stdio: stdio ? stdio : 'inherit', + stdio: (stdio ?? 'inherit') as any, env: { CFW_PATH: frameworkPath, RWJS_CWD: projectPath, }, - }) + } + + return execa('yarn', ['project:deps'], options) } export const copyFrameworkPackages = ( @@ -22,13 +24,15 @@ export const copyFrameworkPackages = ( projectPath: string, stdio?: StdioOption, ) => { - return execa('yarn project:copy', { + const options: ExecaOptions = { cwd: frameworkPath, shell: true, - stdio: stdio ? stdio : 'inherit', + stdio: (stdio ?? 'inherit') as any, env: { CFW_PATH: frameworkPath, RWJS_CWD: projectPath, }, - }) -} \ No newline at end of file + } + + return execa('yarn', ['project:copy'], options) +} diff --git a/tasks/test-project/package.json b/tasks/test-project/package.json deleted file mode 100644 index 3dbc1ca591..0000000000 --- a/tasks/test-project/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/tasks/test-project/rebuild-test-project-fixture-esm.ts b/tasks/test-project/rebuild-test-project-fixture-esm.mts similarity index 97% rename from tasks/test-project/rebuild-test-project-fixture-esm.ts rename to tasks/test-project/rebuild-test-project-fixture-esm.mts index 0d937d7192..83139c2999 100755 --- a/tasks/test-project/rebuild-test-project-fixture-esm.ts +++ b/tasks/test-project/rebuild-test-project-fixture-esm.mts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url' import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' +import type { Options as ExecaOptions } from 'execa' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -14,17 +15,17 @@ 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 __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -113,7 +114,7 @@ if (!startStep) { const tui = new RedwoodTUI() -function getExecaOptions(cwd: string) { +function getExecaOptions(cwd: string): ExecaOptions { return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } } @@ -163,7 +164,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { try { promise = task() - } catch (e) { + } catch (e: unknown) { // This code handles errors from synchronous tasks tui.stopReactive(true) @@ -187,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.catch((e: any) => { // This code handles errors from asynchronous tasks tui.stopReactive(true) @@ -204,7 +205,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { ) } - process.exit(e.exitCode) + process.exit(e.exitCode ?? 1) }) if (Array.isArray(result)) { diff --git a/tasks/test-project/rebuild-test-project-fixture.ts b/tasks/test-project/rebuild-test-project-fixture.mts similarity index 97% rename from tasks/test-project/rebuild-test-project-fixture.ts rename to tasks/test-project/rebuild-test-project-fixture.mts index 0ba901419a..78c0275e44 100755 --- a/tasks/test-project/rebuild-test-project-fixture.ts +++ b/tasks/test-project/rebuild-test-project-fixture.mts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url' import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' +import type { Options as ExecaOptions } from 'execa' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -14,17 +15,17 @@ 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 __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -113,7 +114,7 @@ if (!startStep) { const tui = new RedwoodTUI() -function getExecaOptions(cwd: string) { +function getExecaOptions(cwd: string): ExecaOptions { return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } } @@ -163,7 +164,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { try { promise = task() - } catch (e) { + } catch (e: unknown) { // This code handles errors from synchronous tasks tui.stopReactive(true) @@ -187,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.catch((e: any) => { // This code handles errors from asynchronous tasks tui.stopReactive(true) @@ -204,7 +205,7 @@ async function tuiTask({ step, title, content, task, parent }: TuiTaskDef) { ) } - process.exit(e.exitCode) + process.exit(e.exitCode ?? 1) }) if (Array.isArray(result)) { @@ -321,7 +322,7 @@ async function runCommand() { content: 'yarn clean && yarn build', task: async () => { return exec( - 'yarn build:clean && yarn build', + 'yarn clean && yarn build', [], getExecaOptions(RW_FRAMEWORK_PATH), ) diff --git a/tasks/test-project/set-up-trusted-documents.ts b/tasks/test-project/set-up-trusted-documents.mts similarity index 93% rename from tasks/test-project/set-up-trusted-documents.ts rename to tasks/test-project/set-up-trusted-documents.mts index 260ea00630..0cef0708d2 100644 --- a/tasks/test-project/set-up-trusted-documents.ts +++ b/tasks/test-project/set-up-trusted-documents.mts @@ -2,12 +2,13 @@ import * as fs from 'node:fs' import * as path from 'node:path' +import type { Options as ExecaOptions } from 'execa' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' -import { exec, getExecaOptions as utilGetExecaOptions } from './util.js' +import { exec, getExecaOptions as utilGetExecaOptions } from './util.mjs' -function getExecaOptions(cwd: string) { +function getExecaOptions(cwd: string): ExecaOptions { return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } } @@ -53,7 +54,8 @@ async function runCommand() { 'api/src/functions/graphql.ts', ) const graphqlHandlerContent = fs.readFileSync(graphqlHandlerPath, 'utf-8') - const storeImport = "import { store } from 'src/lib/trustedDocumentsStore.js'" + const storeImport = + "import { store } from 'src/lib/trustedDocumentsStore.mjs'" if (!graphqlHandlerContent.includes(storeImport)) { console.error( diff --git a/tasks/test-project/tasks.mts b/tasks/test-project/tasks.mts new file mode 100644 index 0000000000..e409e3ed29 --- /dev/null +++ b/tasks/test-project/tasks.mts @@ -0,0 +1,273 @@ +import type { ListrTask } from 'listr2' + +import { + getCreatePagesTasks, + getCreateLayoutTasks, + getCreateComponentsTasks, + getCreateCellsTasks, + getUpdateCellMocksTasks, + getPrerenderTasks, +} from './base-tasks.mjs' +import type { CommonTaskOptions } from './base-tasks.mjs' +import { + applyCodemod, + fullPath, + getCfwBin, + getExecaOptions, + setOutputPath, + exec, + updatePkgJsonScripts, + createBuilder, +} from './util.mjs' +import fs from 'node:fs' +import path from 'node:path' + +interface WebTasksOptions { + linkWithLatestFwBuild: boolean +} + +export async function webTasks( + outputPath: string, + { linkWithLatestFwBuild }: WebTasksOptions, +): Promise { + setOutputPath(outputPath) + const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } + + return [ + { + title: 'Creating pages', + task: async (_ctx, task) => task.newListr(getCreatePagesTasks(options)), + }, + { + title: 'Creating layout', + task: async (_ctx, task) => task.newListr(getCreateLayoutTasks(options)), + }, + { + title: 'Creating components', + task: async (_ctx, task) => + task.newListr(getCreateComponentsTasks(options)), + }, + { + title: 'Creating cells', + task: async (_ctx, task) => task.newListr(getCreateCellsTasks(options)), + }, + { + title: 'Updating cell mocks', + task: async (_ctx, task) => + task.newListr(getUpdateCellMocksTasks(options)), + }, + { + title: 'Changing routes', + task: () => applyCodemod('routes.mjs', fullPath('web/src/Routes')), + }, + { + title: 'Install tailwind dependencies', + task: () => + exec( + 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', + [], + getExecaOptions(outputPath), + ), + enabled: () => linkWithLatestFwBuild, + }, + { + title: '[link] Copy local framework files again', + task: () => + exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ), + enabled: () => linkWithLatestFwBuild, + }, + { + title: 'Adding Tailwind', + task: () => { + return exec( + 'yarn cedar setup ui tailwindcss', + ['--force', linkWithLatestFwBuild && '--no-install'].filter( + Boolean, + ) as string[], + getExecaOptions(outputPath), + ) + }, + }, + ] +} + +async function addModel(outputPath: string, schema: string) { + const prismaPath = path.join(outputPath, 'api/db/schema.prisma') + const current = fs.readFileSync(prismaPath, 'utf-8') + fs.writeFileSync(prismaPath, `${current.trim()}\n\n${schema}\n`) +} + +interface ApiTasksOptions { + linkWithLatestFwBuild: boolean +} + +export async function apiTasks( + outputPath: string, + { linkWithLatestFwBuild }: ApiTasksOptions, +): Promise { + setOutputPath(outputPath) + const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } + + const addDbAuth = async () => { + updatePkgJsonScripts({ + projectPath: outputPath, + scripts: { postinstall: '' }, + }) + + const dbAuthSetupPath = path.join( + outputPath, + 'node_modules', + '@cedarjs', + 'auth-dbauth-setup', + ) + fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) + + await exec( + 'yarn cedar setup auth dbAuth --force --no-webauthn', + [], + getExecaOptions(outputPath), + ) + + updatePkgJsonScripts({ + projectPath: outputPath, + scripts: { + postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, + }, + }) + + if (linkWithLatestFwBuild) { + await exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ) + } + + await exec( + 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', + [], + getExecaOptions(outputPath), + ) + + // Codemods for SDLs + const pathContactsSdl = path.join( + outputPath, + 'api/src/graphql/contacts.sdl.ts', + ) + let content = fs.readFileSync(pathContactsSdl, 'utf-8') + content = content + .replace( + 'createContact(input: CreateContactInput!): Contact! @requireAuth', + `createContact(input: CreateContactInput!): Contact @skipAuth`, + ) + .replace( + 'deleteContact(id: Int!): Contact! @requireAuth', + 'deleteContact(id: Int!): Contact! @requireAuth(roles:["ADMIN"])', + ) + fs.writeFileSync(pathContactsSdl, content) + + const pathPostsSdl = path.join(outputPath, 'api/src/graphql/posts.sdl.ts') + content = fs.readFileSync(pathPostsSdl, 'utf-8') + content = content.replace( + /posts: [Post!]! @requireAuth([^}]*)@requireAuth/, + `posts: [Post!]! @skipAuth\n post(id: Int!): Post @skipAuth`, + ) + fs.writeFileSync(pathPostsSdl, content) + } + + return [ + { + title: 'Adding post model to prisma', + task: async () => { + const { post, user } = await import('./codemods/models.mjs') + await addModel(outputPath, post) + await addModel(outputPath, user) + return exec( + `yarn cedar prisma migrate dev --name create_post_user`, + [], + getExecaOptions(outputPath), + ) + }, + }, + { + title: 'Scaffolding post', + task: async () => { + await createBuilder('yarn cedar g scaffold')('post') + await applyCodemod( + 'scenarioValueSuffix.mjs', + fullPath('api/src/services/posts/posts.scenarios'), + ) + await exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ) + }, + }, + { + title: 'Add dbAuth', + task: () => addDbAuth(), + }, + { + title: 'Add Prerender to Routes', + task: async (_ctx, task) => task.newListr(getPrerenderTasks(options)), + }, + ] +} + +export async function streamingTasks(outputPath: string): Promise { + return [ + { + title: 'Creating Delayed suspense delayed page', + task: async () => { + await createBuilder('yarn cedar g page')('delayed') + return applyCodemod( + 'delayedPage.mjs', + fullPath('web/src/pages/DelayedPage/DelayedPage'), + ) + }, + }, + { + title: 'Enable streaming-ssr experiment', + task: async () => { + await createBuilder('yarn cedar experimental setup-streaming-ssr')( + '--force', + ) + }, + }, + ] +} + +export async function fragmentsTasks(outputPath: string): Promise { + const options: CommonTaskOptions = { outputPath } + return [ + { + title: 'Enable fragments', + task: async () => { + const tomlPath = path.join(outputPath, 'redwood.toml') + const content = fs.readFileSync(tomlPath, 'utf-8') + fs.writeFileSync( + tomlPath, + content + '\n[graphql]\n fragments = true\n', + ) + }, + }, + { + title: 'Adding produce and stall models', + task: async () => { + const { produce, stall } = await import('./codemods/models.mjs') + await addModel(outputPath, produce) + await addModel(outputPath, stall) + return exec( + 'yarn cedar prisma migrate dev --name create_produce_stall', + [], + getExecaOptions(outputPath), + ) + }, + }, + ] +} diff --git a/tasks/test-project/tasks.ts b/tasks/test-project/tasks.ts deleted file mode 100644 index af5d2c1cbf..0000000000 --- a/tasks/test-project/tasks.ts +++ /dev/null @@ -1,911 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import type { ListrTask } from 'listr2' - -import { - getExecaOptions, - applyCodemod, - updatePkgJsonScripts, - exec, - getCfwBin, - setOutputPath, - fullPath, - createBuilder, -} from './util.js' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -const createPage = createBuilder('yarn cedar g page') - -interface WebTasksOptions { - linkWithLatestFwBuild: boolean -} - -export async function webTasks( - outputPath: string, - { linkWithLatestFwBuild }: WebTasksOptions, -): Promise { - setOutputPath(outputPath) - - const createPages = async (): Promise => { - return [ - { - title: 'Creating home page', - task: async () => { - await createPage('home /') - - return applyCodemod( - 'homePage.js', - fullPath('web/src/pages/HomePage/HomePage'), - ) - }, - }, - { - title: 'Creating about page', - task: async () => { - await createPage('about') - - return applyCodemod( - 'aboutPage.js', - fullPath('web/src/pages/AboutPage/AboutPage'), - ) - }, - }, - { - title: 'Creating contact page', - task: async () => { - await createPage('contactUs /contact') - - return applyCodemod( - 'contactUsPage.js', - fullPath('web/src/pages/ContactUsPage/ContactUsPage'), - ) - }, - }, - { - title: 'Creating blog post page', - task: async () => { - await createPage('blogPost /blog-post/{id:Int}') - - return applyCodemod( - 'blogPostPage.js', - fullPath('web/src/pages/BlogPostPage/BlogPostPage'), - ) - }, - }, - { - title: 'Creating profile page', - task: async () => { - 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() - }) - }) - ` - - fs.writeFileSync( - fullPath('web/src/pages/ProfilePage/ProfilePage.test'), - testFileContent, - ) - - return applyCodemod( - 'profilePage.js', - fullPath('web/src/pages/ProfilePage/ProfilePage'), - ) - }, - }, - { - title: 'Creating MDX Storybook stories', - task: () => { - const cedarMdxStoryContent = fs.readFileSync( - `${path.resolve(__dirname, 'codemods', 'CedarJS.mdx')}`, - ) - - fs.writeFileSync( - fullPath('web/src/CedarJS.mdx', { addExtension: false }), - cedarMdxStoryContent, - ) - - return - }, - }, - { - title: 'Creating nested cells test page', - task: async () => { - await createPage('waterfall {id:Int}') - - await applyCodemod( - 'waterfallPage.js', - fullPath('web/src/pages/WaterfallPage/WaterfallPage'), - ) - }, - }, - ] - } - - const createLayout = async () => { - const createLayoutBuilder = createBuilder('yarn cedar g layout') - - await createLayoutBuilder('blog') - - return applyCodemod( - 'blogLayout.js', - fullPath('web/src/layouts/BlogLayout/BlogLayout'), - ) - } - - const createComponents = async () => { - const createComponent = createBuilder('yarn cedar g component') - - await createComponent('blogPost') - - await applyCodemod( - 'blogPost.js', - fullPath('web/src/components/BlogPost/BlogPost'), - ) - - await createComponent('author') - - await applyCodemod( - 'author.js', - fullPath('web/src/components/Author/Author'), - ) - - await applyCodemod( - 'updateAuthorStories.js', - fullPath('web/src/components/Author/Author.stories'), - ) - - await applyCodemod( - 'updateAuthorTest.js', - fullPath('web/src/components/Author/Author.test'), - ) - } - - const createCells = async () => { - const createCell = createBuilder('yarn cedar g cell') - - await createCell('blogPosts') - - await applyCodemod( - 'blogPostsCell.js', - fullPath('web/src/components/BlogPostsCell/BlogPostsCell'), - ) - - await createCell('blogPost') - - await applyCodemod( - 'blogPostCell.js', - fullPath('web/src/components/BlogPostCell/BlogPostCell'), - ) - - await createCell('author') - - await applyCodemod( - 'authorCell.js', - fullPath('web/src/components/AuthorCell/AuthorCell'), - ) - - await createCell('waterfallBlogPost') - - return applyCodemod( - 'waterfallBlogPostCell.js', - fullPath( - 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell', - ), - ) - } - - const updateCellMocks = async () => { - await applyCodemod( - 'updateBlogPostMocks.js', - fullPath('web/src/components/BlogPostCell/BlogPostCell.mock.ts', { - addExtension: false, - }), - ) - - await applyCodemod( - 'updateBlogPostMocks.js', - fullPath('web/src/components/BlogPostsCell/BlogPostsCell.mock.ts', { - addExtension: false, - }), - ) - - await applyCodemod( - 'updateAuthorCellMock.js', - fullPath('web/src/components/AuthorCell/AuthorCell.mock.ts', { - addExtension: false, - }), - ) - - return applyCodemod( - 'updateWaterfallBlogPostMocks.js', - fullPath( - 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts', - { - addExtension: false, - }, - ), - ) - } - - return [ - { - title: 'Creating pages', - task: async (_ctx, task) => task.newListr(await createPages()), - }, - { - title: 'Creating layout', - task: () => createLayout(), - }, - { - title: 'Creating components', - task: () => createComponents(), - }, - { - title: 'Creating cells', - task: () => createCells(), - }, - { - title: 'Updating cell mocks', - task: () => updateCellMocks(), - }, - { - title: 'Changing routes', - task: () => applyCodemod('routes.js', fullPath('web/src/Routes')), - }, - - // ====== NOTE: rufus needs this workaround for tailwind ======= - // Setup tailwind in a linked project, due to cfw we install deps manually - { - title: 'Install tailwind dependencies', - // @NOTE: use cfw, because calling the copy function doesn't seem to work here - task: () => - exec( - 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - { - title: '[link] Copy local framework files again', - // @NOTE: use cfw, because calling the copy function doesn't seem to work here - task: () => - exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - // ========= - { - title: 'Adding Tailwind', - task: () => { - return exec( - 'yarn cedar setup ui tailwindcss', - ['--force', linkWithLatestFwBuild && '--no-install'].filter( - Boolean, - ) as string[], - getExecaOptions(outputPath), - ) - }, - }, - ] -} - -async function addModel(schema: string) { - const path = `${fullPath('api/db/schema.prisma', { addExtension: false })}` - - const current = fs.readFileSync(path) - - fs.writeFileSync(path, `${current}\n\n${schema}`) -} - -interface ApiTasksOptions { - linkWithLatestFwBuild: boolean -} - -export async function apiTasks( - outputPath: string, - { linkWithLatestFwBuild }: ApiTasksOptions, -): Promise { - setOutputPath(outputPath) - - const addDbAuth = async () => { - // Temporarily disable postinstall script - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: '', - }, - }) - - const dbAuthSetupPath = path.join( - outputPath, - 'node_modules', - '@cedarjs', - 'auth-dbauth-setup', - ) - - // At an earlier step we run `yarn cfw project:copy` which gives us - // auth-dbauth-setup@3.2.0 currently. We need that version to be a canary - // version for auth-dbauth-api and auth-dbauth-web package installations - // to work. So we remove the current version and add a canary version - // instead. - - fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) - - await exec( - 'yarn cedar setup auth dbAuth --force --no-webauthn', - [], - getExecaOptions(outputPath), - ) - - // Restore postinstall script - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, - }, - }) - - if (linkWithLatestFwBuild) { - await exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - } - - await exec( - 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', - [], - ) - - // update directive in contacts.sdl.ts - const pathContactsSdl = `${outputPath}/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) - - // update directive in posts.sdl.ts - const pathPostsSdl = `${outputPath}/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 - ) - - fs.writeFileSync(pathPostsSdl, resultsPostsSdl) - - // Update src/lib/auth to return roles, so tsc doesn't complain - const libAuthPath = `${outputPath}/api/src/lib/auth.ts` - 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) - - // update requireAuth test - const pathRequireAuth = `${outputPath}/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) - - // add fullName input to signup form - const pathSignupPageTs = `${outputPath}/web/src/pages/SignupPage/SignupPage.tsx` - 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) - } - - // set fullName when signing up - const pathAuthTs = `${outputPath}/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']`, - ) - - fs.writeFileSync(pathAuthTs, resultsAuthTs) - } - - // add prerender to some routes - const addPrerender = async (): Promise => { - return [ - { - // We need to do this here, and not where we create the other pages, to - // keep it outside of BlogLayout - title: 'Creating double rendering test page', - task: async () => { - 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` - - fs.writeFileSync( - fullPath('web/src/pages/DoublePage/DoublePage'), - doublePageContent, - ) - fs.copyFileSync( - fullPath('web/public/favicon.png'), - fullPath('web/src/pages/DoublePage/test.png', { - addExtension: false, - }), - ) - }, - }, - { - title: 'Update Routes.tsx', - task: () => { - const pathRoutes = `${outputPath}/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 = `${outputPath}/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts` - fs.writeFileSync(blogPostRouteHooksPath, blogPostRouteHooks) - - const waterfallRouteHooks = `export async function routeParameters() { - return [{ id: 2 }] - }` - const waterfallRouteHooksPath = `${outputPath}/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts` - fs.writeFileSync(waterfallRouteHooksPath, waterfallRouteHooks) - }, - }, - ] - } - - const generateScaffold = createBuilder('yarn cedar g scaffold') - - return [ - { - title: 'Adding post model to prisma', - task: async () => { - // Need both here since they have a relation - const { post, user } = await import('./codemods/models.js') - - addModel(post) - addModel(user) - - return exec( - `yarn cedar prisma migrate dev --name create_post_user`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Scaffolding post', - task: async () => { - await generateScaffold('post') - - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - fullPath('api/src/services/posts/posts.scenarios'), - ) - - await exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Adding seed script', - task: async () => { - await applyCodemod( - 'seed.js', - fullPath('scripts/seed.ts', { addExtension: false }), - ) - }, - }, - { - title: 'Adding contact model to prisma', - task: async () => { - const { contact } = await import('./codemods/models.js') - - addModel(contact) - - await exec( - `yarn cedar prisma migrate dev --name create_contact`, - [], - getExecaOptions(outputPath), - ) - - await generateScaffold('contacts') - }, - }, - { - // This task renames the migration folders so that we don't have to deal with duplicates/conflicts when committing to the repo - title: 'Adjust dates within migration folder names', - task: () => { - const migrationsFolderPath = path.join( - outputPath, - 'api', - '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() - ) - }) - .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) - }) - }, - }, - { - title: 'Add dbAuth', - task: async () => addDbAuth(), - }, - { - title: 'Add users service', - task: async () => { - const generateSdl = createBuilder('yarn cedar g sdl --no-crud') - - await generateSdl('user') - - await applyCodemod('usersSdl.js', fullPath('api/src/graphql/users.sdl')) - - await applyCodemod( - 'usersService.js', - fullPath('api/src/services/users/users'), - ) - - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - 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, '') - - fs.writeFileSync(fullPath('api/src/services/users/users.test'), test) - - return createBuilder('yarn cedar g types')() - }, - }, - { - title: 'Add describeScenario tests', - task: async () => { - // Copy contact.scenarios.ts, because scenario tests look for the same filename - fs.copyFileSync( - fullPath('api/src/services/contacts/contacts.scenarios'), - fullPath('api/src/services/contacts/describeContacts.scenarios'), - ) - - // Create describeContacts.test.ts - const describeScenarioFixture = path.join( - __dirname, - 'templates', - 'api', - 'contacts.describeScenario.test.ts.template', - ) - - fs.copyFileSync( - describeScenarioFixture, - fullPath('api/src/services/contacts/describeContacts.test'), - ) - }, - }, - { - // This is probably more of a web side task really, but the scaffolded - // pages aren't generated until we get here to the api side tasks. So - // instead of doing some up in the web side tasks, and then the rest - // here I decided to move all of them here - title: 'Add Prerender to Routes', - task: async (_ctx, task) => task.newListr(await addPrerender()), - }, - ] -} - -/** - * Separates the streaming-ssr related steps. These are all web tasks, - * if we choose to move them later - */ -export async function streamingTasks(outputPath: string): Promise { - setOutputPath(outputPath) - - return [ - { - title: 'Creating Delayed suspense delayed page', - task: async () => { - await createPage('delayed') - - await applyCodemod( - 'delayedPage.js', - fullPath('web/src/pages/DelayedPage/DelayedPage'), - ) - }, - }, - { - title: 'Enable streaming-ssr experiment', - task: async () => { - const setupExperiment = createBuilder( - 'yarn cedar experimental setup-streaming-ssr', - ) - await setupExperiment('--force') - }, - }, - ] -} - -/** - * Tasks to add GraphQL Fragments support to the test-project, and some queries - * to test fragments - */ -export async function fragmentsTasks(outputPath: string): Promise { - setOutputPath(outputPath) - - return [ - { - title: 'Enable fragments', - task: async () => { - const redwoodTomlPath = path.join(outputPath, 'redwood.toml') - const redwoodToml = fs.readFileSync(redwoodTomlPath).toString() - const newRedwoodToml = redwoodToml + '\n[graphql]\n fragments = true\n' - fs.writeFileSync(redwoodTomlPath, newRedwoodToml) - }, - }, - { - 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') - - addModel(produce) - addModel(stall) - - return exec( - 'yarn cedar prisma migrate dev --name create_produce_stall', - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Seed fragments data', - task: async () => { - await applyCodemod( - 'seedFragments.ts', - fullPath('scripts/seed.ts', { addExtension: false }), - ) - - await exec('yarn cedar prisma db seed', [], getExecaOptions(outputPath)) - }, - }, - { - title: 'Generate SDLs for produce and stall', - task: async () => { - const generateSdl = createBuilder('yarn cedar g sdl') - - await generateSdl('stall') - await generateSdl('produce') - - await applyCodemod( - 'producesSdl.ts', - fullPath('api/src/graphql/produces.sdl'), - ) - }, - }, - { - title: 'Copy components from templates', - task: () => { - const templatesPath = path.join(__dirname, 'templates', 'web') - const componentsPath = path.join(outputPath, 'web', 'src', 'components') - - for (const fileName of [ - 'Card.tsx', - 'FruitInfo.tsx', - 'ProduceInfo.tsx', - 'StallInfo.tsx', - 'VegetableInfo.tsx', - ]) { - const templatePath = path.join(templatesPath, fileName) - const componentPath = path.join(componentsPath, fileName) - - fs.writeFileSync(componentPath, fs.readFileSync(templatePath)) - } - }, - }, - { - title: 'Copy sdl and service for groceries from templates', - task: () => { - const templatesPath = path.join(__dirname, 'templates', 'api') - const graphqlPath = path.join(outputPath, 'api', 'src', 'graphql') - const servicesPath = path.join(outputPath, 'api', 'src', 'services') - - const sdlTemplatePath = path.join(templatesPath, 'groceries.sdl.ts') - const sdlPath = path.join(graphqlPath, 'groceries.sdl.ts') - const serviceTemplatePath = path.join(templatesPath, 'groceries.ts') - const servicePath = path.join(servicesPath, 'groceries.ts') - - fs.writeFileSync(sdlPath, fs.readFileSync(sdlTemplatePath)) - fs.writeFileSync(servicePath, fs.readFileSync(serviceTemplatePath)) - }, - }, - { - title: 'Creating Groceries page', - task: async () => { - await createPage('groceries') - - await applyCodemod( - 'groceriesPage.ts', - fullPath('web/src/pages/GroceriesPage/GroceriesPage'), - ) - }, - }, - ] -} diff --git a/tasks/test-project/test-project.mts b/tasks/test-project/test-project.mts index 18c66d6210..37d7db0b44 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) @@ -307,7 +307,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.mts b/tasks/test-project/tui-tasks.mts new file mode 100644 index 0000000000..0779b6306a --- /dev/null +++ b/tasks/test-project/tui-tasks.mts @@ -0,0 +1,242 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import type { ListrTask } from 'listr2' + +import { + getCreatePagesTasks, + getCreateLayoutTasks, + getCreateComponentsTasks, + getCreateCellsTasks, + getUpdateCellMocksTasks, + getPrerenderTasks, +} from './base-tasks.mjs' +import type { CommonTaskOptions } from './base-tasks.mjs' +import { + applyCodemod, + fullPath, + getCfwBin, + getExecaOptions, + setOutputPath, + exec, + updatePkgJsonScripts, + createBuilder, +} from './util.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +interface WebTasksOptions { + linkWithLatestFwBuild?: boolean +} + +export async function webTasks( + outputPath: string, + _options?: WebTasksOptions, +): Promise { + setOutputPath(outputPath) + const options: CommonTaskOptions = { + outputPath, + isFixture: true, + } + + return [ + { + title: 'Creating pages', + task: async () => getCreatePagesTasks(options), + }, + { + title: 'Creating layout', + task: async () => getCreateLayoutTasks(options), + }, + { + title: 'Creating components', + task: async () => getCreateComponentsTasks(options), + }, + { + title: 'Creating cells', + task: async () => getCreateCellsTasks(options), + }, + { + title: 'Updating cell mocks', + task: async () => getUpdateCellMocksTasks(options), + }, + { + title: 'Changing routes', + task: () => applyCodemod('routes.mjs', fullPath('web/src/Routes')), + }, + { + title: 'Adding Tailwind', + task: async () => { + await exec( + 'yarn cedar setup ui tailwindcss', + ['--force'], + getExecaOptions(outputPath), + ) + }, + }, + ] +} + +async function addModel(outputPath: string, schema: string) { + const prismaPath = path.join(outputPath, 'api/db/schema.prisma') + const current = fs.readFileSync(prismaPath, 'utf-8') + fs.writeFileSync(prismaPath, `${current.trim()}\n\n${schema}\n`) +} + +interface ApiTasksOptions { + linkWithLatestFwBuild?: boolean + esmProject?: boolean +} + +export async function apiTasks( + outputPath: string, + { linkWithLatestFwBuild = false, esmProject = false }: ApiTasksOptions = {}, +): Promise { + setOutputPath(outputPath) + const options: CommonTaskOptions = { + outputPath, + isFixture: true, + linkWithLatestFwBuild, + esmProject, + } + + const addDbAuth = async () => { + updatePkgJsonScripts({ + projectPath: outputPath, + scripts: { postinstall: '' }, + }) + + // Special tarball installation for fixture + const packages = ['setup', 'api', 'web'] + for (const pkg of packages) { + const pkgPath = path.join( + __dirname, + '../../', + 'packages', + 'auth-providers', + 'dbAuth', + pkg, + ) + await exec('yarn build:pack', [], getExecaOptions(pkgPath)) + const tgzDest = path.join(outputPath, `cedarjs-auth-dbauth-${pkg}.tgz`) + fs.copyFileSync( + path.join(pkgPath, `cedarjs-auth-dbauth-${pkg}.tgz`), + tgzDest, + ) + } + + const pkgJsonPath = path.join(outputPath, 'package.json') + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) + const oldResolutions = pkgJson.resolutions + pkgJson.resolutions = { + ...pkgJson.resolutions, + '@cedarjs/auth-dbauth-setup': './cedarjs-auth-dbauth-setup.tgz', + '@cedarjs/auth-dbauth-api': './cedarjs-auth-dbauth-api.tgz', + '@cedarjs/auth-dbauth-web': './cedarjs-auth-dbauth-web.tgz', + } + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) + + await exec('yarn install', [], getExecaOptions(outputPath)) + await exec( + 'yarn cedar setup auth dbAuth --force --no-webauthn --no-createUserModel --no-generateAuthPages', + [], + getExecaOptions(outputPath), + ) + + if (oldResolutions) { + pkgJson.resolutions = oldResolutions + } else { + delete pkgJson.resolutions + } + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) + + updatePkgJsonScripts({ + projectPath: outputPath, + scripts: { + postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, + }, + }) + + await exec( + 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', + [], + getExecaOptions(outputPath), + ) + } + + return [ + { + title: 'Adding post and user model to prisma', + task: async () => { + const { post, user } = await import('./codemods/models.mjs') + await addModel(outputPath, post) + await addModel(outputPath, user) + return exec( + `yarn cedar prisma migrate dev --name create_post_user`, + [], + getExecaOptions(outputPath), + ) + }, + }, + { + title: 'Scaffolding post', + task: async () => { + await createBuilder('yarn cedar g scaffold')('post') + await applyCodemod( + 'scenarioValueSuffix.mjs', + fullPath('api/src/services/posts/posts.scenarios'), + ) + await exec( + `yarn ${getCfwBin(outputPath)} project:copy`, + [], + getExecaOptions(outputPath), + ) + }, + }, + { + title: 'Add dbAuth', + task: async () => addDbAuth(), + }, + { + title: 'Add users service', + task: async () => { + await createBuilder('yarn cedar g sdl --no-crud', 'api')('user') + await applyCodemod( + 'usersSdl.mjs', + fullPath('api/src/graphql/users.sdl'), + ) + await applyCodemod( + 'usersService.mjs', + fullPath('api/src/services/users/users'), + ) + await createBuilder('yarn cedar g types')() + }, + }, + { + title: 'Add Prerender to Routes', + task: async () => getPrerenderTasks(options), + }, + { + title: 'Add context tests', + task: () => { + const templatePath = path.join( + __dirname, + 'templates', + 'api', + 'context.test.ts.template', + ) + const projectPath = path.join( + outputPath, + 'api', + 'src', + '__tests__', + 'context.test.ts', + ) + fs.mkdirSync(path.dirname(projectPath), { recursive: true }) + fs.writeFileSync(projectPath, fs.readFileSync(templatePath)) + }, + }, + ] +} diff --git a/tasks/test-project/tui-tasks.ts b/tasks/test-project/tui-tasks.ts deleted file mode 100644 index 873732214e..0000000000 --- a/tasks/test-project/tui-tasks.ts +++ /dev/null @@ -1,1017 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -import type { ListrTask } from 'listr2' - -import { - updatePkgJsonScripts, - exec, - getCfwBin, - setOutputPath, - fullPath, - createBuilder, - applyCodemod, - getExecaOptions, -} from './util.js' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -let OUTPUT_PATH: string - -interface WebTasksOptions { - linkWithLatestFwBuild?: boolean -} - -export async function webTasks( - outputPath: string, - _options?: WebTasksOptions, -): Promise { - OUTPUT_PATH = outputPath - setOutputPath(outputPath) - - const createPages = async (): Promise => { - const createPage = createBuilder('yarn cedar g page', 'web') - - return [ - { - title: 'Creating home page', - task: async () => { - await createPage('home /') - - await applyCodemod( - 'homePage.js', - fullPath('web/src/pages/HomePage/HomePage'), - ) - }, - }, - { - title: 'Creating about page', - task: async () => { - await createPage('about') - - await applyCodemod( - 'aboutPage.js', - fullPath('web/src/pages/AboutPage/AboutPage'), - ) - }, - }, - { - title: 'Creating contact page', - task: async () => { - await createPage('contactUs /contact') - - await applyCodemod( - 'contactUsPage.js', - fullPath('web/src/pages/ContactUsPage/ContactUsPage'), - ) - }, - }, - { - title: 'Creating blog post page', - task: async () => { - await createPage('blogPost /blog-post/{id:Int}') - - await applyCodemod( - 'blogPostPage.js', - fullPath('web/src/pages/BlogPostPage/BlogPostPage'), - ) - - return applyCodemod( - 'updateBlogPostPageStories.js', - fullPath('web/src/pages/BlogPostPage/BlogPostPage.stories'), - ) - }, - }, - { - title: 'Creating profile page', - task: async () => { - 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() - }) - }) - ` - - fs.writeFileSync( - fullPath('web/src/pages/ProfilePage/ProfilePage.test'), - testFileContent, - ) - - return applyCodemod( - 'profilePage.js', - fullPath('web/src/pages/ProfilePage/ProfilePage'), - ) - }, - }, - { - title: 'Creating MDX Storybook stories', - task: () => { - const cedarMdxStoryContent = fs.readFileSync( - `${path.resolve(__dirname, 'codemods', 'CedarJS.mdx')}`, - ) - - fs.writeFileSync( - fullPath('web/src/CedarJS.mdx', { addExtension: false }), - cedarMdxStoryContent, - ) - - return - }, - }, - { - title: 'Creating nested cells test page', - task: async () => { - await createPage('waterfall {id:Int}') - - await applyCodemod( - 'waterfallPage.js', - fullPath('web/src/pages/WaterfallPage/WaterfallPage'), - ) - - await applyCodemod( - 'updateWaterfallPageStories.js', - fullPath('web/src/pages/WaterfallPage/WaterfallPage.stories'), - ) - }, - }, - ] - } - - const createLayout = async () => { - const createLayoutBuilder = createBuilder('yarn cedar g layout') - - await createLayoutBuilder('blog') - - return applyCodemod( - 'blogLayout.js', - fullPath('web/src/layouts/BlogLayout/BlogLayout'), - ) - } - - const createComponents = async () => { - const createComponent = createBuilder('yarn cedar g component') - - await createComponent('blogPost') - - await applyCodemod( - 'blogPost.js', - fullPath('web/src/components/BlogPost/BlogPost'), - ) - - await createComponent('author') - - await applyCodemod( - 'author.js', - fullPath('web/src/components/Author/Author'), - ) - - await applyCodemod( - 'updateAuthorStories.js', - fullPath('web/src/components/Author/Author.stories'), - ) - - await applyCodemod( - 'updateAuthorTest.js', - fullPath('web/src/components/Author/Author.test'), - ) - - await createComponent('classWithClassField') - - await applyCodemod( - 'classWithClassField.ts', - fullPath('web/src/components/ClassWithClassField/ClassWithClassField'), - ) - } - - const createCells = async () => { - const createCell = createBuilder('yarn cedar g cell') - - await createCell('blogPosts') - - await applyCodemod( - 'blogPostsCell.js', - fullPath('web/src/components/BlogPostsCell/BlogPostsCell'), - ) - - await createCell('blogPost') - - await applyCodemod( - 'blogPostCell.js', - fullPath('web/src/components/BlogPostCell/BlogPostCell'), - ) - - await createCell('author') - - await applyCodemod( - 'authorCell.js', - fullPath('web/src/components/AuthorCell/AuthorCell'), - ) - - await createCell('waterfallBlogPost') - - return applyCodemod( - 'waterfallBlogPostCell.js', - fullPath( - 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell', - ), - ) - } - - const updateCellMocks = async () => { - await applyCodemod( - 'updateBlogPostMocks.js', - fullPath('web/src/components/BlogPostCell/BlogPostCell.mock.ts', { - addExtension: false, - }), - ) - - await applyCodemod( - 'updateBlogPostMocks.js', - fullPath('web/src/components/BlogPostsCell/BlogPostsCell.mock.ts', { - addExtension: false, - }), - ) - - await applyCodemod( - 'updateAuthorCellMock.js', - fullPath('web/src/components/AuthorCell/AuthorCell.mock.ts', { - addExtension: false, - }), - ) - - return applyCodemod( - 'updateWaterfallBlogPostMocks.js', - fullPath( - 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts', - { - addExtension: false, - }, - ), - ) - } - - return [ - { - title: 'Creating pages', - task: async (_ctx, task) => task.newListr(await createPages()), - }, - { - title: 'Creating layout', - task: () => createLayout(), - }, - { - title: 'Creating components', - task: () => createComponents(), - }, - { - title: 'Creating cells', - task: () => createCells(), - }, - { - title: 'Updating cell mocks', - task: () => updateCellMocks(), - }, - { - title: 'Changing routes', - task: () => applyCodemod('routes.js', fullPath('web/src/Routes')), - }, - { - title: 'Adding Tailwind', - task: async () => { - await exec( - 'yarn cedar setup ui tailwindcss', - ['--force'], - getExecaOptions(outputPath), - ) - }, - }, - ] -} - -async function addModel(schema: string) { - const path = `${OUTPUT_PATH}/api/db/schema.prisma` - - const current = fs.readFileSync(path, 'utf-8') - - fs.writeFileSync(path, `${current.trim()}\n\n${schema}\n`) -} - -interface ApiTasksOptions { - linkWithLatestFwBuild?: boolean - esmProject?: boolean -} - -export async function apiTasks( - outputPath: string, - { linkWithLatestFwBuild = false, esmProject = false }: ApiTasksOptions = {}, -): Promise { - OUTPUT_PATH = outputPath - setOutputPath(outputPath) - - const execaOptions = getExecaOptions(outputPath) - - const addDbAuth = async () => { - // Temporarily disable postinstall script - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: '', - }, - }) - - // We want to use the latest version of the auth-dbauth-{setup,api,web} - // packages. But they're not published yet. So let's package them up as - // tarballs and install them using that by setting yarn resolutions - - const setupPkg = path.join( - __dirname, - '../../', - 'packages', - 'auth-providers', - 'dbAuth', - 'setup', - ) - const apiPkg = path.join( - __dirname, - '../../', - 'packages', - 'auth-providers', - 'dbAuth', - 'api', - ) - const webPkg = path.join( - __dirname, - '../../', - 'packages', - 'auth-providers', - 'dbAuth', - 'web', - ) - - await exec('yarn build:pack', [], getExecaOptions(setupPkg)) - await exec('yarn build:pack', [], getExecaOptions(apiPkg)) - await exec('yarn build:pack', [], getExecaOptions(webPkg)) - - const setupTgz = path.join(setupPkg, 'cedarjs-auth-dbauth-setup.tgz') - const apiTgz = path.join(apiPkg, 'cedarjs-auth-dbauth-api.tgz') - const webTgz = path.join(webPkg, 'cedarjs-auth-dbauth-web.tgz') - - const setupTgzDest = path.join(outputPath, 'cedarjs-auth-dbauth-setup.tgz') - const apiTgzDest = path.join(outputPath, 'cedarjs-auth-dbauth-api.tgz') - const webTgzDest = path.join(outputPath, 'cedarjs-auth-dbauth-web.tgz') - - fs.copyFileSync(setupTgz, setupTgzDest) - fs.copyFileSync(apiTgz, apiTgzDest) - fs.copyFileSync(webTgz, webTgzDest) - - const projectPackageJsonPath = path.join(outputPath, 'package.json') - const projectPackageJson = JSON.parse( - fs.readFileSync(projectPackageJsonPath, 'utf-8'), - ) - - const existingResolutions = projectPackageJson.resolutions - ? { ...projectPackageJson.resolutions } - : undefined - - projectPackageJson.resolutions ??= {} - projectPackageJson.resolutions = { - ...projectPackageJson.resolutions, - '@cedarjs/auth-dbauth-setup': './cedarjs-auth-dbauth-setup.tgz', - '@cedarjs/auth-dbauth-api': './cedarjs-auth-dbauth-api.tgz', - '@cedarjs/auth-dbauth-web': './cedarjs-auth-dbauth-web.tgz', - } - - fs.writeFileSync( - projectPackageJsonPath, - JSON.stringify(projectPackageJson, null, 2), - ) - - // Run `yarn install` to have the resolutions take effect and install the - // tarballs we copied over - await exec('yarn install', [], execaOptions) - - await exec( - 'yarn cedar setup auth dbAuth --force --no-webauthn --no-createUserModel --no-generateAuthPages', - [], - execaOptions, - ) - - // Restore old resolutions - if (existingResolutions) { - projectPackageJson.resolutions = existingResolutions - } - - fs.writeFileSync( - projectPackageJsonPath, - JSON.stringify(projectPackageJson, null, 2), - ) - - // Remove tarballs - fs.unlinkSync(setupTgzDest) - fs.unlinkSync(apiTgzDest) - fs.unlinkSync(webTgzDest) - - // Restore postinstall script - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, - }, - }) - - if (linkWithLatestFwBuild) { - await exec(`yarn ${getCfwBin(outputPath)} project:copy`, [], execaOptions) - } - - await exec( - 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', - [], - execaOptions, - ) - - // 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(?=\s)/, - '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 - - 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') - - 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) - - // 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']", - ) - - 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']", - ) - - fs.writeFileSync(pathAuthTs, resultsAuthTs) - } - - // add prerender to some routes - const addPrerender = async (): Promise => { - return [ - { - // We need to do this here, and not where we create the other pages, to - // 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` - - fs.writeFileSync( - fullPath('web/src/pages/DoublePage/DoublePage'), - doublePageContent, - ) - fs.copyFileSync( - fullPath('web/public/favicon.png', { addExtension: false }), - fullPath('web/src/pages/DoublePage/test.png', { - addExtension: false, - }), - ) - }, - }, - { - 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 }] - }` - const waterfallRouteHooksPath = `${OUTPUT_PATH}/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts` - fs.writeFileSync(waterfallRouteHooksPath, waterfallRouteHooks) - }, - }, - ] - - return tuiTaskList - } - - const generateScaffold = createBuilder('yarn cedar g scaffold') - - const tuiTaskList: ListrTask[] = [ - { - 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') - - addModel(post) - addModel(user) - - return exec( - `yarn cedar prisma migrate dev --name create_post_user`, - [], - execaOptions, - ) - }, - }, - { - title: 'Scaffolding post', - task: async () => { - await generateScaffold('post') - - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - fullPath('api/src/services/posts/posts.scenarios'), - ) - - await exec( - `yarn ${getCfwBin(OUTPUT_PATH)} project:copy`, - [], - execaOptions, - ) - }, - }, - { - title: 'Adding seed script', - task: async () => { - await applyCodemod( - 'seed.js', - fullPath('scripts/seed.ts', { addExtension: false }), - ) - }, - }, - { - title: 'Adding contact model to prisma', - task: async () => { - const { contact } = await import('./codemods/models.js') - - addModel(contact) - - await exec( - `yarn cedar prisma migrate dev --name create_contact`, - [], - execaOptions, - ) - - await generateScaffold('contacts') - - const contactsServicePath = fullPath( - 'api/src/services/contacts/contacts', - ) - fs.writeFileSync( - contactsServicePath, - fs - .readFileSync(contactsServicePath, 'utf-8') - .replace( - "import { db } from 'src/lib/db'", - '// Testing aliased imports with extensions\n' + - "import { db } from 'src/lib/db.js'", - ), - ) - }, - }, - { - // This task renames the migration folders so that we don't have to deal with duplicates/conflicts when committing to the repo - title: 'Adjust dates within migration folder names', - task: () => { - const migrationsFolderPath = path.join( - OUTPUT_PATH, - 'api', - '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() - ) - }) - .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) + - '120000' // Time hardcoded to 12:00:00 to limit TZ issues - fs.renameSync( - path.join(migrationsFolderPath, name), - path.join( - migrationsFolderPath, - `${datetimeInCorrectFormat}${name.substring(14)}`, - ), - ) - datetime.setDate(datetime.getDate() + 1) - }) - }, - }, - { - title: 'Add users service', - task: async () => { - const generateSdl = createBuilder('yarn cedar g sdl --no-crud', 'api') - - await generateSdl('user') - - await applyCodemod('usersSdl.js', fullPath('api/src/graphql/users.sdl')) - - await applyCodemod( - 'usersService.js', - fullPath('api/src/services/users/users'), - ) - - // Replace the random numbers in the scenario with consistent values - await applyCodemod( - 'scenarioValueSuffix.js', - 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, '') - - fs.writeFileSync(fullPath('api/src/services/users/users.test'), test) - - return createBuilder('yarn cedar g types')() - }, - }, - { - title: 'Add dbAuth', - task: async () => addDbAuth(), - }, - { - title: 'Add describeScenario tests', - task: () => { - // Copy contact.scenarios.ts, because scenario tests look for the same filename - fs.copyFileSync( - fullPath('api/src/services/contacts/contacts.scenarios'), - fullPath('api/src/services/contacts/describeContacts.scenarios'), - ) - - // Create describeContacts.test.ts - const describeScenarioFixture = path.join( - __dirname, - 'templates', - 'api', - 'contacts.describeScenario.test.ts.template', - ) - - fs.copyFileSync( - describeScenarioFixture, - fullPath('api/src/services/contacts/describeContacts.test'), - ) - }, - }, - { - // This is probably more of a web side task really, but the scaffolded - // pages aren't generated until we get here to the api side tasks. So - // instead of doing some up in the web side tasks, and then the rest - // here I decided to move all of them here - title: 'Add Prerender to Routes', - task: async (_ctx, task) => task.newListr(await addPrerender()), - }, - { - title: 'Add context tests', - task: () => { - const templatePath = path.join( - __dirname, - 'templates', - 'api', - 'context.test.ts.template', - ) - const projectPath = path.join( - OUTPUT_PATH, - 'api', - 'src', - '__tests__', - 'context.test.ts', - ) - - fs.mkdirSync(path.dirname(projectPath), { recursive: true }) - fs.writeFileSync(projectPath, fs.readFileSync(templatePath)) - }, - }, - { - title: 'Add vitest db import tracking tests for ESM test project', - task: () => { - if (!esmProject) { - return - } - - const templatesDir = path.join(__dirname, 'templates', 'api') - const templatePath1 = path.join(templatesDir, '1-db-import.test.ts') - const templatePath2 = path.join(templatesDir, '2-db-import.test.ts') - const templatePath3 = path.join(templatesDir, '3-db-import.test.ts') - - const testsDir = path.join(OUTPUT_PATH, 'api', 'src', '__tests__') - const testFilePath1 = path.join(testsDir, '1-db-import.test.ts') - const testFilePath2 = path.join(testsDir, '2-db-import.test.ts') - const testFilePath3 = path.join(testsDir, '3-db-import.test.ts') - - fs.mkdirSync(testsDir, { recursive: true }) - fs.copyFileSync(templatePath1, testFilePath1) - fs.copyFileSync(templatePath2, testFilePath2) - fs.copyFileSync(templatePath3, testFilePath3) - - // I opted to add an additional vitest config file rather than modifying - // the existing one because I wanted to keep one looking exactly the - // same as it'll look in user's projects. - fs.copyFileSync( - path.join(templatesDir, 'vitest-sort.config.ts'), - path.join(OUTPUT_PATH, 'api', 'vitest-sort.config.ts'), - ) - }, - }, - ] - // ], - // TODO: Figure out what to do with this. It's from Listr, but TUI doesn't - // have anything like it (yet?) - // { - // exitOnError: true, - // renderer: verbose && 'verbose', - // renderOptions: { collapseSubtasks: false }, - // } - return tuiTaskList -} - -/** - * Tasks to add GraphQL Fragments support to the test-project, and some queries - * to test fragments - */ -export async function fragmentsTasks(outputPath: string): Promise { - setOutputPath(outputPath) - - return [ - { - title: 'Enable fragments', - task: async () => { - const redwoodTomlPath = path.join(outputPath, 'redwood.toml') - const redwoodToml = fs.readFileSync(redwoodTomlPath).toString() - const newRedwoodToml = redwoodToml + '\n[graphql]\n fragments = true\n' - fs.writeFileSync(redwoodTomlPath, newRedwoodToml) - }, - }, - { - 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') - - addModel(produce) - addModel(stall) - - return exec( - 'yarn cedar prisma migrate dev --name create_produce_stall', - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Seed fragments data', - task: async () => { - await applyCodemod( - 'seedFragments.ts', - fullPath('scripts/seed.ts', { addExtension: false }), - ) - - await exec('yarn cedar prisma db seed', [], getExecaOptions(outputPath)) - }, - }, - { - title: 'Generate SDLs for produce and stall', - task: async () => { - const generateSdl = createBuilder('yarn cedar g sdl') - - await generateSdl('stall') - await generateSdl('produce') - - await applyCodemod( - 'producesSdl.ts', - fullPath('api/src/graphql/produces.sdl'), - ) - }, - }, - { - title: 'Copy components from templates', - task: () => { - const templatesPath = path.join(__dirname, 'templates', 'web') - const componentsPath = path.join(outputPath, 'web', 'src', 'components') - - for (const fileName of [ - 'Card.tsx', - 'FruitInfo.tsx', - 'ProduceInfo.tsx', - 'StallInfo.tsx', - 'VegetableInfo.tsx', - ]) { - const templatePath = path.join(templatesPath, fileName) - const componentPath = path.join(componentsPath, fileName) - - fs.writeFileSync(componentPath, fs.readFileSync(templatePath)) - } - }, - }, - { - title: 'Copy sdl and service for groceries from templates', - task: () => { - const templatesPath = path.join(__dirname, 'templates', 'api') - const graphqlPath = path.join(outputPath, 'api', 'src', 'graphql') - const servicesPath = path.join(outputPath, 'api', 'src', 'services') - - const sdlTemplatePath = path.join(templatesPath, 'groceries.sdl.ts') - const sdlPath = path.join(graphqlPath, 'groceries.sdl.ts') - const serviceTemplatePath = path.join(templatesPath, 'groceries.ts') - const servicePath = path.join(servicesPath, 'groceries.ts') - - fs.writeFileSync(sdlPath, fs.readFileSync(sdlTemplatePath)) - fs.writeFileSync(servicePath, fs.readFileSync(serviceTemplatePath)) - }, - }, - { - title: 'Creating Groceries page', - task: async () => { - const createPage = createBuilder('yarn cedar g page') - await createPage('groceries') - - await applyCodemod( - 'groceriesPage.ts', - fullPath('web/src/pages/GroceriesPage/GroceriesPage'), - ) - }, - }, - ] -} 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.ts b/tasks/test-project/util.mts similarity index 93% rename from tasks/test-project/util.ts rename to tasks/test-project/util.mts index 8b938df180..9cbc491212 100644 --- a/tasks/test-project/util.ts +++ b/tasks/test-project/util.mts @@ -130,9 +130,14 @@ export class ExecaError extends Error { export async function exec( file: string, - args?: string[], - options?: ExecaOptions, + argsOrOptions?: string[] | ExecaOptions, + maybeOptions?: ExecaOptions, ) { + const args = Array.isArray(argsOrOptions) ? argsOrOptions : [] + const options = Array.isArray(argsOrOptions) + ? maybeOptions + : (argsOrOptions as ExecaOptions) + return execa(file, args, options) .then(({ stdout, stderr, exitCode }) => { if (exitCode !== 0) { @@ -146,7 +151,7 @@ export async function exec( // Rethrow ExecaError throw error } else { - const { stdout, stderr, exitCode } = error + const { stdout = '', stderr = '', exitCode = 1 } = error throw new ExecaError({ stdout, stderr, exitCode }) } }) From a0104bccadc3cd0c2f254b05980ab2ed40d90d80 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 3 Jan 2026 20:43:36 +0100 Subject: [PATCH 3/5] rebuild-test-project-fixture now sort of works --- tasks/test-project/base-tasks.mts | 356 ++++++++++++++++-- .../rebuild-test-project-fixture-esm.mts | 3 +- .../rebuild-test-project-fixture.mts | 5 +- .../test-project/set-up-trusted-documents.mts | 3 +- tasks/test-project/tasks.mts | 225 ++--------- tasks/test-project/tui-tasks.mts | 233 ++---------- tasks/test-project/util.mts | 11 +- 7 files changed, 412 insertions(+), 424 deletions(-) diff --git a/tasks/test-project/base-tasks.mts b/tasks/test-project/base-tasks.mts index c098133d27..23edfeec6b 100644 --- a/tasks/test-project/base-tasks.mts +++ b/tasks/test-project/base-tasks.mts @@ -25,6 +25,316 @@ export interface CommonTaskOptions { esmProject?: boolean } +export interface HighLevelTask { + title: string + /** + * Use this to create subtasks. The return value should be compatible with + * ListrTask[] + */ + tasksGetter?: ( + options: CommonTaskOptions, + ) => ListrTask[] | Promise + /** + * Use this for a single task that doesn't have subtasks + */ + task?: (options: CommonTaskOptions) => void | Promise | Promise + enabled?: boolean | ((options: CommonTaskOptions) => boolean) +} + +export const getWebTasks = (options: CommonTaskOptions): HighLevelTask[] => { + return [ + { + title: 'Creating pages', + tasksGetter: (opts) => getCreatePagesTasks(opts), + }, + { + title: 'Creating layout', + tasksGetter: (opts) => getCreateLayoutTasks(opts), + }, + { + title: 'Creating components', + tasksGetter: (opts) => getCreateComponentsTasks(opts), + }, + { + title: 'Creating cells', + tasksGetter: (opts) => getCreateCellsTasks(opts), + }, + { + title: 'Updating cell mocks', + tasksGetter: (opts) => getUpdateCellMocksTasks(opts), + }, + { + title: 'Changing routes', + task: () => applyCodemod('routes.js', fullPath('web/src/Routes')), + }, + { + title: 'Install tailwind dependencies', + task: () => + exec( + 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', + [], + getExecaOptions(options.outputPath), + ), + enabled: (opts) => !!opts.linkWithLatestFwBuild, + }, + { + title: '[link] Copy local framework files again', + task: () => + exec( + `yarn ${getCfwBin(options.outputPath)} project:copy`, + [], + getExecaOptions(options.outputPath), + ), + enabled: (opts) => !!opts.linkWithLatestFwBuild, + }, + { + title: 'Adding Tailwind', + task: async (opts) => { + await exec( + 'yarn cedar setup ui tailwindcss', + ['--force', opts.linkWithLatestFwBuild && '--no-install'].filter( + Boolean, + ) as string[], + getExecaOptions(opts.outputPath), + ) + }, + }, + ] +} + +export async function addModel(outputPath: string, schema: string) { + const prismaPath = path.join(outputPath, 'api/db/schema.prisma') + const current = fs.readFileSync(prismaPath, 'utf-8') + fs.writeFileSync(prismaPath, `${current.trim()}\n\n${schema}\n`) +} + +export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { + const addDbAuth = async () => { + updatePkgJsonScripts({ + projectPath: options.outputPath, + scripts: { postinstall: '' }, + }) + + if (options.isFixture) { + // Special tarball installation for fixture + const packages = ['setup', 'api', 'web'] + for (const pkg of packages) { + const pkgPath = path.join( + __dirname, + '../../', + 'packages', + 'auth-providers', + 'dbAuth', + pkg, + ) + await exec('yarn build:pack', [], getExecaOptions(pkgPath)) + const tgzDest = path.join( + options.outputPath, + `cedarjs-auth-dbauth-${pkg}.tgz`, + ) + fs.copyFileSync( + path.join(pkgPath, `cedarjs-auth-dbauth-${pkg}.tgz`), + tgzDest, + ) + } + + const pkgJsonPath = path.join(options.outputPath, 'package.json') + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) + const oldResolutions = pkgJson.resolutions + pkgJson.resolutions = { + ...pkgJson.resolutions, + '@cedarjs/auth-dbauth-setup': './cedarjs-auth-dbauth-setup.tgz', + '@cedarjs/auth-dbauth-api': './cedarjs-auth-dbauth-api.tgz', + '@cedarjs/auth-dbauth-web': './cedarjs-auth-dbauth-web.tgz', + } + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) + + await exec('yarn install', [], getExecaOptions(options.outputPath)) + await exec( + 'yarn cedar setup auth dbAuth --force --no-webauthn --no-createUserModel --no-generateAuthPages', + [], + getExecaOptions(options.outputPath), + ) + + if (oldResolutions) { + pkgJson.resolutions = oldResolutions + } else { + delete pkgJson.resolutions + } + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) + } else { + const dbAuthSetupPath = path.join( + options.outputPath, + 'node_modules', + '@cedarjs', + 'auth-dbauth-setup', + ) + fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) + + await exec( + 'yarn cedar setup auth dbAuth --force --no-webauthn', + [], + getExecaOptions(options.outputPath), + ) + } + + updatePkgJsonScripts({ + projectPath: options.outputPath, + scripts: { + postinstall: `yarn ${getCfwBin(options.outputPath)} project:copy`, + }, + }) + + if (options.linkWithLatestFwBuild) { + await exec( + `yarn ${getCfwBin(options.outputPath)} project:copy`, + [], + getExecaOptions(options.outputPath), + ) + } + + await exec( + 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', + [], + getExecaOptions(options.outputPath), + ) + + // Codemods for SDLs + const pathContactsSdl = path.join( + options.outputPath, + 'api/src/graphql/contacts.sdl.ts', + ) + if (fs.existsSync(pathContactsSdl)) { + let content = fs.readFileSync(pathContactsSdl, 'utf-8') + content = content + .replace( + 'createContact(input: CreateContactInput!): Contact! @requireAuth', + `createContact(input: CreateContactInput!): Contact @skipAuth`, + ) + .replace( + 'deleteContact(id: Int!): Contact! @requireAuth', + 'deleteContact(id: Int!): Contact! @requireAuth(roles:["ADMIN"])', + ) + fs.writeFileSync(pathContactsSdl, content) + } + + const pathPostsSdl = path.join( + options.outputPath, + 'api/src/graphql/posts.sdl.ts', + ) + if (fs.existsSync(pathPostsSdl)) { + let content = fs.readFileSync(pathPostsSdl, 'utf-8') + content = content.replace( + /posts: \[Post!\]! @requireAuth([^}]*)@requireAuth/, + `posts: [Post!]! @skipAuth\n post(id: Int!): Post @skipAuth`, + ) + fs.writeFileSync(pathPostsSdl, content) + } + } + + return [ + { + title: 'Adding models to prisma', + task: async () => { + const { post, user, contact } = await import('./codemods/models.mjs') + await addModel(options.outputPath, post) + await addModel(options.outputPath, user) + if (options.isFixture) { + await addModel(options.outputPath, contact) + return exec( + `yarn cedar prisma migrate dev --name create_models`, + [], + getExecaOptions(options.outputPath), + ) + } else { + return exec( + `yarn cedar prisma migrate dev --name create_post_user`, + [], + getExecaOptions(options.outputPath), + ) + } + }, + }, + { + title: 'Scaffolding post and contacts', + task: async () => { + await createBuilder('yarn cedar g scaffold')('post') + await applyCodemod( + 'scenarioValueSuffix.js', + fullPath('api/src/services/posts/posts.scenarios'), + ) + if (options.isFixture) { + await createBuilder('yarn cedar g scaffold')('contacts') + } + await exec( + `yarn ${getCfwBin(options.outputPath)} project:copy`, + [], + getExecaOptions(options.outputPath), + ) + }, + }, + { + title: 'Add dbAuth', + task: async () => addDbAuth(), + }, + { + title: 'Add users service', + task: async () => { + await createBuilder('yarn cedar g sdl --no-crud', 'api')('user') + await applyCodemod('usersSdl.js', fullPath('api/src/graphql/users.sdl')) + await applyCodemod( + 'usersService.js', + fullPath('api/src/services/users/users'), + ) + + const testPath = fullPath('api/src/services/users/users.test.ts', { + addExtension: false, + }) + if (fs.existsSync(testPath)) { + let content = fs.readFileSync(testPath, 'utf-8') + content = content.replace( + "import type { User } from '@prisma/client'", + '', + ) + fs.writeFileSync(testPath, content) + } + + await createBuilder('yarn cedar g types')() + }, + // Assuming this is also for fixture mainly, or generally useful? + // tui-tasks.mts had it. tasks.mts did not. + // I'll enable it for fixture for now, or maybe always if safe? + // "usersSdl.js" codemod exists? tui-tasks.mts used it. + enabled: (opts) => !!opts.isFixture, + }, + { + title: 'Add Prerender to Routes', + tasksGetter: (opts) => getPrerenderTasks(opts), + }, + { + title: 'Add context tests', + task: () => { + const templatePath = path.join( + __dirname, + 'templates', + 'api', + 'context.test.ts.template', + ) + const projectPath = path.join( + options.outputPath, + 'api', + 'src', + '__tests__', + 'context.test.ts', + ) + fs.mkdirSync(path.dirname(projectPath), { recursive: true }) + fs.writeFileSync(projectPath, fs.readFileSync(templatePath)) + }, + enabled: (opts) => !!opts.isFixture, + }, + ] +} + export const getCreatePagesTasks = ( options: CommonTaskOptions, ): ListrTask[] => { @@ -38,7 +348,7 @@ export const getCreatePagesTasks = ( task: async () => { await createPage('home /') return applyCodemod( - 'homePage.mjs', + 'homePage.js', fullPath('web/src/pages/HomePage/HomePage'), ) }, @@ -48,7 +358,7 @@ export const getCreatePagesTasks = ( task: async () => { await createPage('about') return applyCodemod( - 'aboutPage.mjs', + 'aboutPage.js', fullPath('web/src/pages/AboutPage/AboutPage'), ) }, @@ -58,7 +368,7 @@ export const getCreatePagesTasks = ( task: async () => { await createPage('contactUs /contact') return applyCodemod( - 'contactUsPage.mjs', + 'contactUsPage.js', fullPath('web/src/pages/ContactUsPage/ContactUsPage'), ) }, @@ -68,13 +378,13 @@ export const getCreatePagesTasks = ( task: async () => { await createPage('blogPost /blog-post/{id:Int}') await applyCodemod( - 'blogPostPage.mjs', + 'blogPostPage.js', fullPath('web/src/pages/BlogPostPage/BlogPostPage'), ) if (options.isFixture) { await applyCodemod( - 'updateBlogPostPageStories.mjs', + 'updateBlogPostPageStories.js', fullPath('web/src/pages/BlogPostPage/BlogPostPage.stories'), ) } @@ -112,7 +422,7 @@ describe('ProfilePage', () => { ) return applyCodemod( - 'profilePage.mjs', + 'profilePage.js', fullPath('web/src/pages/ProfilePage/ProfilePage'), ) }, @@ -134,13 +444,13 @@ describe('ProfilePage', () => { task: async () => { await createPage('waterfall {id:Int}') await applyCodemod( - 'waterfallPage.mjs', + 'waterfallPage.js', fullPath('web/src/pages/WaterfallPage/WaterfallPage'), ) if (options.isFixture) { await applyCodemod( - 'updateWaterfallPageStories.mjs', + 'updateWaterfallPageStories.js', fullPath('web/src/pages/WaterfallPage/WaterfallPage.stories'), ) } @@ -159,7 +469,7 @@ export const getCreateLayoutTasks = ( task: async () => { await createLayoutBuilder('blog') return applyCodemod( - 'blogLayout.mjs', + 'blogLayout.js', fullPath('web/src/layouts/BlogLayout/BlogLayout'), ) }, @@ -177,21 +487,21 @@ export const getCreateComponentsTasks = ( task: async () => { await createComponent('blogPost') await applyCodemod( - 'blogPost.mjs', + 'blogPost.js', fullPath('web/src/components/BlogPost/BlogPost'), ) await createComponent('author') await applyCodemod( - 'author.mjs', + 'author.js', fullPath('web/src/components/Author/Author'), ) await applyCodemod( - 'updateAuthorStories.mjs', + 'updateAuthorStories.js', fullPath('web/src/components/Author/Author.stories'), ) await applyCodemod( - 'updateAuthorTest.mjs', + 'updateAuthorTest.js', fullPath('web/src/components/Author/Author.test'), ) @@ -220,25 +530,25 @@ export const getCreateCellsTasks = ( task: async () => { await createCell('blogPosts') await applyCodemod( - 'blogPostsCell.mjs', + 'blogPostsCell.js', fullPath('web/src/components/BlogPostsCell/BlogPostsCell'), ) await createCell('blogPost') await applyCodemod( - 'blogPostCell.mjs', + 'blogPostCell.js', fullPath('web/src/components/BlogPostCell/BlogPostCell'), ) await createCell('author') await applyCodemod( - 'authorCell.mjs', + 'authorCell.js', fullPath('web/src/components/AuthorCell/AuthorCell'), ) await createCell('waterfallBlogPost') return applyCodemod( - 'waterfallBlogPostCell.mjs', + 'waterfallBlogPostCell.js', fullPath( 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell', ), @@ -256,25 +566,25 @@ export const getUpdateCellMocksTasks = ( title: 'Updating cell mocks', task: async () => { await applyCodemod( - 'updateBlogPostMocks.mjs', + 'updateBlogPostMocks.js', fullPath('web/src/components/BlogPostCell/BlogPostCell.mock.ts', { addExtension: false, }), ) await applyCodemod( - 'updateBlogPostMocks.mjs', + 'updateBlogPostMocks.js', fullPath('web/src/components/BlogPostsCell/BlogPostsCell.mock.ts', { addExtension: false, }), ) await applyCodemod( - 'updateAuthorCellMock.mjs', + 'updateAuthorCellMock.js', fullPath('web/src/components/AuthorCell/AuthorCell.mock.ts', { addExtension: false, }), ) return applyCodemod( - 'updateWaterfallBlogPostMocks.mjs', + 'updateWaterfallBlogPostMocks.js', fullPath( 'web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts', { addExtension: false }, @@ -301,7 +611,7 @@ const DoublePage = () => { <>

DoublePage

-

This page exists to make sure we don't regress on RW#7757 and #317

+

This page exists to make sure we don't regress on RW#7757 and #317

Test ) @@ -337,7 +647,7 @@ export default DoublePage` .replace('name="newContact"', 'name="newContact" prerender') fs.writeFileSync(pathRoutes, content) - const blogPostRouteHooks = `import { db } from '$api/src/lib/db.mjs' + 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 })) }` diff --git a/tasks/test-project/rebuild-test-project-fixture-esm.mts b/tasks/test-project/rebuild-test-project-fixture-esm.mts index 83139c2999..a228a5713e 100755 --- a/tasks/test-project/rebuild-test-project-fixture-esm.mts +++ b/tasks/test-project/rebuild-test-project-fixture-esm.mts @@ -352,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), ) }, diff --git a/tasks/test-project/rebuild-test-project-fixture.mts b/tasks/test-project/rebuild-test-project-fixture.mts index 78c0275e44..fbd51ae460 100755 --- a/tasks/test-project/rebuild-test-project-fixture.mts +++ b/tasks/test-project/rebuild-test-project-fixture.mts @@ -349,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), ) }, @@ -594,7 +595,7 @@ async function runCommand() { // removes existing Fixture and replaces with newly built project, // then removes new Project temp directory - await copyProject() + // await copyProject() }, }) diff --git a/tasks/test-project/set-up-trusted-documents.mts b/tasks/test-project/set-up-trusted-documents.mts index 0cef0708d2..4484143400 100644 --- a/tasks/test-project/set-up-trusted-documents.mts +++ b/tasks/test-project/set-up-trusted-documents.mts @@ -54,8 +54,7 @@ async function runCommand() { 'api/src/functions/graphql.ts', ) const graphqlHandlerContent = fs.readFileSync(graphqlHandlerPath, 'utf-8') - const storeImport = - "import { store } from 'src/lib/trustedDocumentsStore.mjs'" + const storeImport = "import { store } from 'src/lib/trustedDocumentsStore.js'" if (!graphqlHandlerContent.includes(storeImport)) { console.error( diff --git a/tasks/test-project/tasks.mts b/tasks/test-project/tasks.mts index e409e3ed29..f52f5fd201 100644 --- a/tasks/test-project/tasks.mts +++ b/tasks/test-project/tasks.mts @@ -1,29 +1,51 @@ +import fs from 'node:fs' +import path from 'node:path' + import type { ListrTask } from 'listr2' import { - getCreatePagesTasks, - getCreateLayoutTasks, - getCreateComponentsTasks, - getCreateCellsTasks, - getUpdateCellMocksTasks, - getPrerenderTasks, + getWebTasks, + getApiTasks, + addModel, + type HighLevelTask, + type CommonTaskOptions, } from './base-tasks.mjs' -import type { CommonTaskOptions } from './base-tasks.mjs' import { applyCodemod, fullPath, - getCfwBin, getExecaOptions, setOutputPath, exec, - updatePkgJsonScripts, createBuilder, } from './util.mjs' -import fs from 'node:fs' -import path from 'node:path' interface WebTasksOptions { - linkWithLatestFwBuild: boolean + linkWithLatestFwBuild?: boolean +} + +function mapToListrTask( + t: HighLevelTask, + options: CommonTaskOptions, +): ListrTask { + const enabled = + typeof t.enabled === 'function' ? t.enabled(options) : t.enabled + + return { + title: t.title, + task: async (_ctx, task) => { + if (t.tasksGetter) { + const subtasks = await t.tasksGetter(options) + return task.newListr(subtasks) + } + + if (t.task) { + return t.task(options) + } + + throw new Error('Unexpected task') + }, + enabled, + } } export async function webTasks( @@ -33,76 +55,12 @@ export async function webTasks( setOutputPath(outputPath) const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } - return [ - { - title: 'Creating pages', - task: async (_ctx, task) => task.newListr(getCreatePagesTasks(options)), - }, - { - title: 'Creating layout', - task: async (_ctx, task) => task.newListr(getCreateLayoutTasks(options)), - }, - { - title: 'Creating components', - task: async (_ctx, task) => - task.newListr(getCreateComponentsTasks(options)), - }, - { - title: 'Creating cells', - task: async (_ctx, task) => task.newListr(getCreateCellsTasks(options)), - }, - { - title: 'Updating cell mocks', - task: async (_ctx, task) => - task.newListr(getUpdateCellMocksTasks(options)), - }, - { - title: 'Changing routes', - task: () => applyCodemod('routes.mjs', fullPath('web/src/Routes')), - }, - { - title: 'Install tailwind dependencies', - task: () => - exec( - 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - { - title: '[link] Copy local framework files again', - task: () => - exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ), - enabled: () => linkWithLatestFwBuild, - }, - { - title: 'Adding Tailwind', - task: () => { - return exec( - 'yarn cedar setup ui tailwindcss', - ['--force', linkWithLatestFwBuild && '--no-install'].filter( - Boolean, - ) as string[], - getExecaOptions(outputPath), - ) - }, - }, - ] -} - -async function addModel(outputPath: string, schema: string) { - const prismaPath = path.join(outputPath, 'api/db/schema.prisma') - const current = fs.readFileSync(prismaPath, 'utf-8') - fs.writeFileSync(prismaPath, `${current.trim()}\n\n${schema}\n`) + const tasks = getWebTasks(options) + return tasks.map((t) => mapToListrTask(t, options)) } interface ApiTasksOptions { - linkWithLatestFwBuild: boolean + linkWithLatestFwBuild?: boolean } export async function apiTasks( @@ -112,111 +70,8 @@ export async function apiTasks( setOutputPath(outputPath) const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } - const addDbAuth = async () => { - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { postinstall: '' }, - }) - - const dbAuthSetupPath = path.join( - outputPath, - 'node_modules', - '@cedarjs', - 'auth-dbauth-setup', - ) - fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) - - await exec( - 'yarn cedar setup auth dbAuth --force --no-webauthn', - [], - getExecaOptions(outputPath), - ) - - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, - }, - }) - - if (linkWithLatestFwBuild) { - await exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - } - - await exec( - 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', - [], - getExecaOptions(outputPath), - ) - - // Codemods for SDLs - const pathContactsSdl = path.join( - outputPath, - 'api/src/graphql/contacts.sdl.ts', - ) - let content = fs.readFileSync(pathContactsSdl, 'utf-8') - content = content - .replace( - 'createContact(input: CreateContactInput!): Contact! @requireAuth', - `createContact(input: CreateContactInput!): Contact @skipAuth`, - ) - .replace( - 'deleteContact(id: Int!): Contact! @requireAuth', - 'deleteContact(id: Int!): Contact! @requireAuth(roles:["ADMIN"])', - ) - fs.writeFileSync(pathContactsSdl, content) - - const pathPostsSdl = path.join(outputPath, 'api/src/graphql/posts.sdl.ts') - content = fs.readFileSync(pathPostsSdl, 'utf-8') - content = content.replace( - /posts: [Post!]! @requireAuth([^}]*)@requireAuth/, - `posts: [Post!]! @skipAuth\n post(id: Int!): Post @skipAuth`, - ) - fs.writeFileSync(pathPostsSdl, content) - } - - return [ - { - title: 'Adding post model to prisma', - task: async () => { - const { post, user } = await import('./codemods/models.mjs') - await addModel(outputPath, post) - await addModel(outputPath, user) - return exec( - `yarn cedar prisma migrate dev --name create_post_user`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Scaffolding post', - task: async () => { - await createBuilder('yarn cedar g scaffold')('post') - await applyCodemod( - 'scenarioValueSuffix.mjs', - fullPath('api/src/services/posts/posts.scenarios'), - ) - await exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Add dbAuth', - task: () => addDbAuth(), - }, - { - title: 'Add Prerender to Routes', - task: async (_ctx, task) => task.newListr(getPrerenderTasks(options)), - }, - ] + const tasks = getApiTasks(options) + return tasks.map((t) => mapToListrTask(t, options)) } export async function streamingTasks(outputPath: string): Promise { @@ -226,7 +81,7 @@ export async function streamingTasks(outputPath: string): Promise { task: async () => { await createBuilder('yarn cedar g page')('delayed') return applyCodemod( - 'delayedPage.mjs', + 'delayedPage.js', fullPath('web/src/pages/DelayedPage/DelayedPage'), ) }, diff --git a/tasks/test-project/tui-tasks.mts b/tasks/test-project/tui-tasks.mts index 0779b6306a..1a3909775b 100644 --- a/tasks/test-project/tui-tasks.mts +++ b/tasks/test-project/tui-tasks.mts @@ -1,36 +1,38 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - import type { ListrTask } from 'listr2' import { - getCreatePagesTasks, - getCreateLayoutTasks, - getCreateComponentsTasks, - getCreateCellsTasks, - getUpdateCellMocksTasks, - getPrerenderTasks, + getWebTasks, + getApiTasks, + type HighLevelTask, + type CommonTaskOptions, } from './base-tasks.mjs' -import type { CommonTaskOptions } from './base-tasks.mjs' -import { - applyCodemod, - fullPath, - getCfwBin, - getExecaOptions, - setOutputPath, - exec, - updatePkgJsonScripts, - createBuilder, -} from './util.mjs' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +import { setOutputPath } from './util.mjs' interface WebTasksOptions { linkWithLatestFwBuild?: boolean } +function mapToTuiTask(t: HighLevelTask, options: CommonTaskOptions): ListrTask { + const enabled = + typeof t.enabled === 'function' ? t.enabled(options) : t.enabled + + return { + title: t.title, + task: async () => { + if (t.tasksGetter) { + return t.tasksGetter(options) + } + + if (t.task) { + return t.task(options) + } + + throw new Error('Unexpected task') + }, + enabled, + } +} + export async function webTasks( outputPath: string, _options?: WebTasksOptions, @@ -41,48 +43,8 @@ export async function webTasks( isFixture: true, } - return [ - { - title: 'Creating pages', - task: async () => getCreatePagesTasks(options), - }, - { - title: 'Creating layout', - task: async () => getCreateLayoutTasks(options), - }, - { - title: 'Creating components', - task: async () => getCreateComponentsTasks(options), - }, - { - title: 'Creating cells', - task: async () => getCreateCellsTasks(options), - }, - { - title: 'Updating cell mocks', - task: async () => getUpdateCellMocksTasks(options), - }, - { - title: 'Changing routes', - task: () => applyCodemod('routes.mjs', fullPath('web/src/Routes')), - }, - { - title: 'Adding Tailwind', - task: async () => { - await exec( - 'yarn cedar setup ui tailwindcss', - ['--force'], - getExecaOptions(outputPath), - ) - }, - }, - ] -} - -async function addModel(outputPath: string, schema: string) { - const prismaPath = path.join(outputPath, 'api/db/schema.prisma') - const current = fs.readFileSync(prismaPath, 'utf-8') - fs.writeFileSync(prismaPath, `${current.trim()}\n\n${schema}\n`) + const tasks = getWebTasks(options) + return tasks.map((t) => mapToTuiTask(t, options)) } interface ApiTasksOptions { @@ -102,141 +64,6 @@ export async function apiTasks( esmProject, } - const addDbAuth = async () => { - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { postinstall: '' }, - }) - - // Special tarball installation for fixture - const packages = ['setup', 'api', 'web'] - for (const pkg of packages) { - const pkgPath = path.join( - __dirname, - '../../', - 'packages', - 'auth-providers', - 'dbAuth', - pkg, - ) - await exec('yarn build:pack', [], getExecaOptions(pkgPath)) - const tgzDest = path.join(outputPath, `cedarjs-auth-dbauth-${pkg}.tgz`) - fs.copyFileSync( - path.join(pkgPath, `cedarjs-auth-dbauth-${pkg}.tgz`), - tgzDest, - ) - } - - const pkgJsonPath = path.join(outputPath, 'package.json') - const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) - const oldResolutions = pkgJson.resolutions - pkgJson.resolutions = { - ...pkgJson.resolutions, - '@cedarjs/auth-dbauth-setup': './cedarjs-auth-dbauth-setup.tgz', - '@cedarjs/auth-dbauth-api': './cedarjs-auth-dbauth-api.tgz', - '@cedarjs/auth-dbauth-web': './cedarjs-auth-dbauth-web.tgz', - } - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) - - await exec('yarn install', [], getExecaOptions(outputPath)) - await exec( - 'yarn cedar setup auth dbAuth --force --no-webauthn --no-createUserModel --no-generateAuthPages', - [], - getExecaOptions(outputPath), - ) - - if (oldResolutions) { - pkgJson.resolutions = oldResolutions - } else { - delete pkgJson.resolutions - } - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) - - updatePkgJsonScripts({ - projectPath: outputPath, - scripts: { - postinstall: `yarn ${getCfwBin(outputPath)} project:copy`, - }, - }) - - await exec( - 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', - [], - getExecaOptions(outputPath), - ) - } - - return [ - { - title: 'Adding post and user model to prisma', - task: async () => { - const { post, user } = await import('./codemods/models.mjs') - await addModel(outputPath, post) - await addModel(outputPath, user) - return exec( - `yarn cedar prisma migrate dev --name create_post_user`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Scaffolding post', - task: async () => { - await createBuilder('yarn cedar g scaffold')('post') - await applyCodemod( - 'scenarioValueSuffix.mjs', - fullPath('api/src/services/posts/posts.scenarios'), - ) - await exec( - `yarn ${getCfwBin(outputPath)} project:copy`, - [], - getExecaOptions(outputPath), - ) - }, - }, - { - title: 'Add dbAuth', - task: async () => addDbAuth(), - }, - { - title: 'Add users service', - task: async () => { - await createBuilder('yarn cedar g sdl --no-crud', 'api')('user') - await applyCodemod( - 'usersSdl.mjs', - fullPath('api/src/graphql/users.sdl'), - ) - await applyCodemod( - 'usersService.mjs', - fullPath('api/src/services/users/users'), - ) - await createBuilder('yarn cedar g types')() - }, - }, - { - title: 'Add Prerender to Routes', - task: async () => getPrerenderTasks(options), - }, - { - title: 'Add context tests', - task: () => { - const templatePath = path.join( - __dirname, - 'templates', - 'api', - 'context.test.ts.template', - ) - const projectPath = path.join( - outputPath, - 'api', - 'src', - '__tests__', - 'context.test.ts', - ) - fs.mkdirSync(path.dirname(projectPath), { recursive: true }) - fs.writeFileSync(projectPath, fs.readFileSync(templatePath)) - }, - }, - ] + const tasks = getApiTasks(options) + return tasks.map((t) => mapToTuiTask(t, options)) } diff --git a/tasks/test-project/util.mts b/tasks/test-project/util.mts index 9cbc491212..5c52588402 100644 --- a/tasks/test-project/util.mts +++ b/tasks/test-project/util.mts @@ -130,15 +130,10 @@ export class ExecaError extends Error { export async function exec( file: string, - argsOrOptions?: string[] | ExecaOptions, - maybeOptions?: ExecaOptions, + args?: string[], + options?: ExecaOptions, ) { - const args = Array.isArray(argsOrOptions) ? argsOrOptions : [] - const options = Array.isArray(argsOrOptions) - ? maybeOptions - : (argsOrOptions as ExecaOptions) - - return execa(file, args, options) + return execa(file, args ?? [], options) .then(({ stdout, stderr, exitCode }) => { if (exitCode !== 0) { throw new ExecaError({ stdout, stderr, exitCode }) From de503b1f0887a5e91965244e656aa4f7af31b365 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 4 Jan 2026 00:23:56 +0100 Subject: [PATCH 4/5] fix verbosity --- tasks/test-project/base-tasks.mts | 10 ++------ .../rebuild-test-project-fixture-esm.mts | 23 +++++++++---------- .../rebuild-test-project-fixture.mts | 18 +++++++-------- tasks/test-project/test-project.mts | 9 +++++++- tasks/test-project/util.mts | 11 ++++++++- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/tasks/test-project/base-tasks.mts b/tasks/test-project/base-tasks.mts index 23edfeec6b..cd9b999835 100644 --- a/tasks/test-project/base-tasks.mts +++ b/tasks/test-project/base-tasks.mts @@ -10,7 +10,6 @@ import { fullPath, getCfwBin, getExecaOptions, - setOutputPath, updatePkgJsonScripts, exec, } from './util.mjs' @@ -27,16 +26,11 @@ export interface CommonTaskOptions { export interface HighLevelTask { title: string - /** - * Use this to create subtasks. The return value should be compatible with - * ListrTask[] - */ + /** Use this to create subtasks */ tasksGetter?: ( options: CommonTaskOptions, ) => ListrTask[] | Promise - /** - * Use this for a single task that doesn't have subtasks - */ + /** Use this for a single task that doesn't have subtasks */ task?: (options: CommonTaskOptions) => void | Promise | Promise enabled?: boolean | ((options: CommonTaskOptions) => boolean) } diff --git a/tasks/test-project/rebuild-test-project-fixture-esm.mts b/tasks/test-project/rebuild-test-project-fixture-esm.mts index a228a5713e..4b5f7da07f 100755 --- a/tasks/test-project/rebuild-test-project-fixture-esm.mts +++ b/tasks/test-project/rebuild-test-project-fixture-esm.mts @@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url' import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' -import type { Options as ExecaOptions } from 'execa' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -20,8 +19,9 @@ import { webTasks, apiTasks } from './tui-tasks.mjs' import { isAwaitable, isTuiError } from './typing.mjs' import type { TuiTaskDef } from './typing.mjs' import { - getExecaOptions as utilGetExecaOptions, + getExecaOptions, updatePkgJsonScripts, + setVerbose, ExecaError, exec, getCfwBin, @@ -84,6 +84,8 @@ const args = yargs(hideBin(process.argv)) const { verbose, resume, resumePath, resumeStep } = args +setVerbose(verbose) + const RW_FRAMEWORK_PATH = path.join(__dirname, '../../') const OUTPUT_PROJECT_PATH = resumePath ? /* path.resolve(String(resumePath)) */ resumePath @@ -114,10 +116,6 @@ if (!startStep) { const tui = new RedwoodTUI() -function getExecaOptions(cwd: string): ExecaOptions { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } -} - function beginStep(step: string) { fs.mkdirSync(OUTPUT_PROJECT_PATH, { recursive: true }) fs.writeFileSync(path.join(OUTPUT_PROJECT_PATH, 'step.txt'), '' + step) @@ -322,10 +320,10 @@ async function runCommand() { await tuiTask({ step: 1, title: '[link] Building Cedar framework', - content: 'yarn build:clean && yarn build', + content: 'yarn clean && yarn build', task: async () => { return exec( - 'yarn build:clean && yarn build', + 'yarn clean && yarn build', [], getExecaOptions(RW_FRAMEWORK_PATH), ) @@ -341,7 +339,7 @@ async function runCommand() { return addFrameworkDepsToProject( RW_FRAMEWORK_PATH, OUTPUT_PROJECT_PATH, - 'pipe', // TODO: Remove this when everything is using @rwjs/tui + 'pipe', ) }, }) @@ -355,8 +353,9 @@ async function runCommand() { await exec('yarn install', [], getExecaOptions(OUTPUT_PROJECT_PATH)) // TODO: Now that I've added this, I wonder what other steps I can remove + const CFW_BIN = getCfwBin(OUTPUT_PROJECT_PATH) return exec( - `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:tarsync`, + `yarn ${CFW_BIN} project:tarsync`, [], getExecaOptions(OUTPUT_PROJECT_PATH), ) @@ -406,15 +405,15 @@ async function runCommand() { step: 6, title: '[link] Add cfw project:copy postinstall', task: () => { + const CFW_BIN = getCfwBin(OUTPUT_PROJECT_PATH) return updatePkgJsonScripts({ projectPath: OUTPUT_PROJECT_PATH, scripts: { - postinstall: `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:copy`, + postinstall: `yarn ${CFW_BIN} project:copy`, }, }) }, }) - await tuiTask({ step: 7, title: 'Apply web codemods', diff --git a/tasks/test-project/rebuild-test-project-fixture.mts b/tasks/test-project/rebuild-test-project-fixture.mts index fbd51ae460..9def183966 100755 --- a/tasks/test-project/rebuild-test-project-fixture.mts +++ b/tasks/test-project/rebuild-test-project-fixture.mts @@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url' import ansis from 'ansis' import { rimraf } from 'rimraf' import semver from 'semver' -import type { Options as ExecaOptions } from 'execa' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -20,8 +19,9 @@ import { webTasks, apiTasks } from './tui-tasks.mjs' import { isAwaitable, isTuiError } from './typing.mjs' import type { TuiTaskDef } from './typing.mjs' import { - getExecaOptions as utilGetExecaOptions, + getExecaOptions, updatePkgJsonScripts, + setVerbose, ExecaError, exec, getCfwBin, @@ -84,6 +84,8 @@ const args = yargs(hideBin(process.argv)) const { verbose, resume, resumePath, resumeStep } = args +setVerbose(verbose) + const RW_FRAMEWORK_PATH = path.join(__dirname, '../../') const OUTPUT_PROJECT_PATH = resumePath ? /* path.resolve(String(resumePath)) */ resumePath @@ -114,10 +116,6 @@ if (!startStep) { const tui = new RedwoodTUI() -function getExecaOptions(cwd: string): ExecaOptions { - return { ...utilGetExecaOptions(cwd), stdio: 'pipe' } -} - function beginStep(step: string) { fs.mkdirSync(OUTPUT_PROJECT_PATH, { recursive: true }) fs.writeFileSync(path.join(OUTPUT_PROJECT_PATH, 'step.txt'), '' + step) @@ -338,7 +336,7 @@ async function runCommand() { return addFrameworkDepsToProject( RW_FRAMEWORK_PATH, OUTPUT_PROJECT_PATH, - 'pipe', // TODO: Remove this when everything is using @rwjs/tui + 'pipe', ) }, }) @@ -352,8 +350,9 @@ async function runCommand() { await exec('yarn install', [], getExecaOptions(OUTPUT_PROJECT_PATH)) // TODO: Now that I've added this, I wonder what other steps I can remove + const CFW_BIN = getCfwBin(OUTPUT_PROJECT_PATH) return exec( - `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:tarsync`, + `yarn ${CFW_BIN} project:tarsync`, [], getExecaOptions(OUTPUT_PROJECT_PATH), ) @@ -403,10 +402,11 @@ async function runCommand() { step: 6, title: '[link] Add cfw project:copy postinstall', task: () => { + const CFW_BIN = getCfwBin(OUTPUT_PROJECT_PATH) return updatePkgJsonScripts({ projectPath: OUTPUT_PROJECT_PATH, scripts: { - postinstall: `yarn ${getCfwBin(OUTPUT_PROJECT_PATH)} project:copy`, + postinstall: `yarn ${CFW_BIN} project:copy`, }, }) }, diff --git a/tasks/test-project/test-project.mts b/tasks/test-project/test-project.mts index 37d7db0b44..4c3eaa7767 100644 --- a/tasks/test-project/test-project.mts +++ b/tasks/test-project/test-project.mts @@ -11,7 +11,12 @@ import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' import { apiTasks, streamingTasks, webTasks } from './tasks.mjs' -import { confirmNoFixtureNoLink, getExecaOptions, getCfwBin } from './util.mjs' +import { + confirmNoFixtureNoLink, + getExecaOptions, + getCfwBin, + setVerbose, +} from './util.mjs' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -69,6 +74,8 @@ const { streamingSsr, } = args +setVerbose(verbose) + if (args._.length > 1) { console.log( ansis.red.bold( diff --git a/tasks/test-project/util.mts b/tasks/test-project/util.mts index 5c52588402..8f4a42e7fe 100644 --- a/tasks/test-project/util.mts +++ b/tasks/test-project/util.mts @@ -10,6 +10,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) let OUTPUT_PATH: string +let VERBOSE = false export function setOutputPath(path: string) { OUTPUT_PATH = path @@ -19,6 +20,14 @@ 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 }, @@ -49,7 +58,7 @@ export async function applyCodemod(codemod: string, target: string) { export const getExecaOptions = (cwd: string): ExecaOptions => ({ shell: true, - stdio: 'inherit', + stdio: VERBOSE ? 'inherit' : 'pipe', cleanup: true, cwd, env: { From d664a72a3eeb1419e757e60080c972f781dff820 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 4 Jan 2026 12:21:45 +0100 Subject: [PATCH 5/5] try to minimize diff --- tasks/test-project/add-gql-fragments.mts | 16 +- tasks/test-project/base-tasks.mts | 408 ++++++++++++++++++----- tasks/test-project/tasks.mts | 53 ++- tasks/test-project/test-project.mts | 36 +- tasks/test-project/tui-tasks.mts | 2 + tasks/test-project/util.mts | 3 - 6 files changed, 382 insertions(+), 136 deletions(-) diff --git a/tasks/test-project/add-gql-fragments.mts b/tasks/test-project/add-gql-fragments.mts index 50504dd2fb..d11eecb5fb 100755 --- a/tasks/test-project/add-gql-fragments.mts +++ b/tasks/test-project/add-gql-fragments.mts @@ -1,7 +1,6 @@ /* eslint-env node, es6*/ import path from 'node:path' -import { Listr } from 'listr2' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -17,17 +16,14 @@ const args = yargs(hideBin(process.argv)) */ async function runCommand() { const OUTPUT_PROJECT_PATH = path.resolve(String(args._)) - const tasks = await fragmentsTasks(OUTPUT_PROJECT_PATH) + const tasks = await fragmentsTasks(OUTPUT_PROJECT_PATH, { + verbose: true, + }) - new Listr(tasks, { - exitOnError: true, - renderer: 'default', + tasks.run().catch((err: unknown) => { + console.error(err) + process.exit(1) }) - .run() - .catch((err: unknown) => { - console.error(err) - process.exit(1) - }) } runCommand() diff --git a/tasks/test-project/base-tasks.mts b/tasks/test-project/base-tasks.mts index cd9b999835..e8833ea9ba 100644 --- a/tasks/test-project/base-tasks.mts +++ b/tasks/test-project/base-tasks.mts @@ -3,10 +3,10 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import type { ListrTask } from 'listr2' +import type { Options, StdioOption } from 'execa' import { applyCodemod, - createBuilder, fullPath, getCfwBin, getExecaOptions, @@ -22,6 +22,7 @@ export interface CommonTaskOptions { linkWithLatestFwBuild?: boolean isFixture?: boolean esmProject?: boolean + stdio?: Options['stdio'] } export interface HighLevelTask { @@ -35,6 +36,25 @@ export interface HighLevelTask { enabled?: boolean | ((options: CommonTaskOptions) => boolean) } +export function createBuilder(cmd: string, dir = '', opts: CommonTaskOptions) { + return function (positionalArguments?: string | string[]) { + const execaOptions = { + ...getExecaOptions(path.join(opts.outputPath, dir)), + stdio: opts.stdio, + } + + const args = Array.isArray(positionalArguments) + ? positionalArguments + : positionalArguments + ? [positionalArguments] + : [] + + const subprocess = exec(cmd, args, execaOptions) + + return subprocess + } +} + export const getWebTasks = (options: CommonTaskOptions): HighLevelTask[] => { return [ { @@ -63,22 +83,21 @@ export const getWebTasks = (options: CommonTaskOptions): HighLevelTask[] => { }, { title: 'Install tailwind dependencies', - task: () => + task: (opts) => exec( 'yarn workspace web add -D postcss postcss-loader tailwindcss autoprefixer prettier-plugin-tailwindcss@^0.5.12', [], - getExecaOptions(options.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ), enabled: (opts) => !!opts.linkWithLatestFwBuild, }, { title: '[link] Copy local framework files again', - task: () => - exec( - `yarn ${getCfwBin(options.outputPath)} project:copy`, - [], - getExecaOptions(options.outputPath), - ), + task: (opts) => + exec(`yarn ${getCfwBin(opts.outputPath)} project:copy`, [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }), enabled: (opts) => !!opts.linkWithLatestFwBuild, }, { @@ -89,7 +108,7 @@ export const getWebTasks = (options: CommonTaskOptions): HighLevelTask[] => { ['--force', opts.linkWithLatestFwBuild && '--no-install'].filter( Boolean, ) as string[], - getExecaOptions(opts.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ) }, }, @@ -103,13 +122,13 @@ export async function addModel(outputPath: string, schema: string) { } export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { - const addDbAuth = async () => { + const addDbAuth = async (opts: CommonTaskOptions) => { updatePkgJsonScripts({ - projectPath: options.outputPath, + projectPath: opts.outputPath, scripts: { postinstall: '' }, }) - if (options.isFixture) { + if (opts.isFixture) { // Special tarball installation for fixture const packages = ['setup', 'api', 'web'] for (const pkg of packages) { @@ -121,9 +140,12 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { 'dbAuth', pkg, ) - await exec('yarn build:pack', [], getExecaOptions(pkgPath)) + await exec('yarn build:pack', [], { + ...getExecaOptions(pkgPath), + stdio: opts.stdio, + }) const tgzDest = path.join( - options.outputPath, + opts.outputPath, `cedarjs-auth-dbauth-${pkg}.tgz`, ) fs.copyFileSync( @@ -132,7 +154,7 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { ) } - const pkgJsonPath = path.join(options.outputPath, 'package.json') + const pkgJsonPath = path.join(opts.outputPath, 'package.json') const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) const oldResolutions = pkgJson.resolutions pkgJson.resolutions = { @@ -143,11 +165,14 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { } fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) - await exec('yarn install', [], getExecaOptions(options.outputPath)) + await exec('yarn install', [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }) await exec( 'yarn cedar setup auth dbAuth --force --no-webauthn --no-createUserModel --no-generateAuthPages', [], - getExecaOptions(options.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ) if (oldResolutions) { @@ -158,44 +183,42 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)) } else { const dbAuthSetupPath = path.join( - options.outputPath, + opts.outputPath, 'node_modules', '@cedarjs', 'auth-dbauth-setup', ) fs.rmSync(dbAuthSetupPath, { recursive: true, force: true }) - await exec( - 'yarn cedar setup auth dbAuth --force --no-webauthn', - [], - getExecaOptions(options.outputPath), - ) + await exec('yarn cedar setup auth dbAuth --force --no-webauthn', [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }) } updatePkgJsonScripts({ - projectPath: options.outputPath, + projectPath: opts.outputPath, scripts: { - postinstall: `yarn ${getCfwBin(options.outputPath)} project:copy`, + postinstall: `yarn ${getCfwBin(opts.outputPath)} project:copy`, }, }) - if (options.linkWithLatestFwBuild) { - await exec( - `yarn ${getCfwBin(options.outputPath)} project:copy`, - [], - getExecaOptions(options.outputPath), - ) + if (opts.linkWithLatestFwBuild) { + await exec(`yarn ${getCfwBin(opts.outputPath)} project:copy`, [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }) } await exec( 'yarn cedar g dbAuth --no-webauthn --username-label=username --password-label=password', [], - getExecaOptions(options.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ) // Codemods for SDLs const pathContactsSdl = path.join( - options.outputPath, + opts.outputPath, 'api/src/graphql/contacts.sdl.ts', ) if (fs.existsSync(pathContactsSdl)) { @@ -213,7 +236,7 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { } const pathPostsSdl = path.join( - options.outputPath, + opts.outputPath, 'api/src/graphql/posts.sdl.ts', ) if (fs.existsSync(pathPostsSdl)) { @@ -224,57 +247,209 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { ) fs.writeFileSync(pathPostsSdl, content) } + + // Update src/lib/auth to return roles, so tsc doesn't complain + const libAuthPath = path.join(opts.outputPath, 'api/src/lib/auth.ts') + if (fs.existsSync(libAuthPath)) { + let content = fs.readFileSync(libAuthPath, 'utf-8') + content = content + .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, content) + } + + // update requireAuth test + const pathRequireAuth = path.join( + opts.outputPath, + 'api/src/directives/requireAuth/requireAuth.test.ts', + ) + if (fs.existsSync(pathRequireAuth)) { + let content = fs.readFileSync(pathRequireAuth, 'utf-8') + content = content.replace( + /const mockExecution([^}]*){} }\)/, + `const mockExecution = mockRedwoodDirective(requireAuth, { + context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } }, + })`, + ) + fs.writeFileSync(pathRequireAuth, content) + } + + // add fullName input to signup form + const pathSignupPageTs = path.join( + opts.outputPath, + 'web/src/pages/SignupPage/SignupPage.tsx', + ) + if (fs.existsSync(pathSignupPageTs)) { + let content = fs.readFileSync(pathSignupPageTs, 'utf-8') + const usernameFieldsMatch = content.match( + /\s*/, + ) + if (usernameFieldsMatch) { + const usernameFields = usernameFieldsMatch[0] + const fullNameFields = usernameFields + .replace(/\s*ref=\{usernameRef}/, '') + .replaceAll('username', 'full-name') + .replaceAll('Username', 'Full Name') + + content = content + .replace( + '', + '\n' + + fullNameFields, + ) + .replace( + 'password: data.password', + "password: data.password, 'full-name': data['full-name']", + ) + fs.writeFileSync(pathSignupPageTs, content) + } + } + + // set fullName when signing up + const pathAuthTs = path.join(opts.outputPath, 'api/src/functions/auth.ts') + if (fs.existsSync(pathAuthTs)) { + let content = fs.readFileSync(pathAuthTs, 'utf-8') + content = content + .replace('name: string', "'full-name': string") + .replace('userAttributes: _userAttributes', 'userAttributes') + .replace( + '// name: userAttributes.name', + "fullName: userAttributes['full-name']", + ) + fs.writeFileSync(pathAuthTs, content) + } } return [ { title: 'Adding models to prisma', - task: async () => { + task: async (opts) => { const { post, user, contact } = await import('./codemods/models.mjs') - await addModel(options.outputPath, post) - await addModel(options.outputPath, user) - if (options.isFixture) { - await addModel(options.outputPath, contact) + await addModel(opts.outputPath, post) + await addModel(opts.outputPath, user) + if (opts.isFixture) { + await addModel(opts.outputPath, contact) return exec( `yarn cedar prisma migrate dev --name create_models`, [], - getExecaOptions(options.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ) } else { return exec( `yarn cedar prisma migrate dev --name create_post_user`, [], - getExecaOptions(options.outputPath), + { ...getExecaOptions(opts.outputPath), stdio: opts.stdio }, ) } }, }, { title: 'Scaffolding post and contacts', - task: async () => { - await createBuilder('yarn cedar g scaffold')('post') + task: async (opts) => { + await createBuilder('yarn cedar g scaffold', '', opts)('post') await applyCodemod( 'scenarioValueSuffix.js', fullPath('api/src/services/posts/posts.scenarios'), ) - if (options.isFixture) { - await createBuilder('yarn cedar g scaffold')('contacts') + if (opts.isFixture) { + await createBuilder('yarn cedar g scaffold', '', opts)('contacts') } - await exec( - `yarn ${getCfwBin(options.outputPath)} project:copy`, - [], - getExecaOptions(options.outputPath), + await exec(`yarn ${getCfwBin(opts.outputPath)} project:copy`, [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }) + }, + }, + { + title: 'Adding seed script', + task: async () => { + await applyCodemod( + 'seed.js', + fullPath('scripts/seed.ts', { addExtension: false }), ) }, }, { - title: 'Add dbAuth', - task: async () => addDbAuth(), + title: 'Adding contact model to prisma', + task: async (opts) => { + const { contact } = await import('./codemods/models.mjs') + await addModel(opts.outputPath, contact) + await exec(`yarn cedar prisma migrate dev --name create_contact`, [], { + ...getExecaOptions(opts.outputPath), + stdio: opts.stdio, + }) + await createBuilder('yarn cedar g scaffold', '', opts)('contacts') + + const contactsServicePath = fullPath( + 'api/src/services/contacts/contacts', + ) + if (fs.existsSync(contactsServicePath)) { + fs.writeFileSync( + contactsServicePath, + fs + .readFileSync(contactsServicePath, 'utf-8') + .replace( + "import { db } from 'src/lib/db'", + '// Testing aliased imports with extensions\n' + + "import { db } from 'src/lib/db.js'", + ), + ) + } + }, + enabled: (opts) => !!opts.isFixture, + }, + { + // This task renames the migration folders so that we don't have to deal with duplicates/conflicts when committing to the repo + title: 'Adjust dates within migration folder names', + task: (opts) => { + const migrationsFolderPath = path.join( + opts.outputPath, + 'api', + 'db', + 'migrations', + ) + if (!fs.existsSync(migrationsFolderPath)) { + return + } + + // 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) + + '120000' // Time hardcoded to 12:00:00 to limit TZ issues + fs.renameSync( + path.join(migrationsFolderPath, name), + path.join( + migrationsFolderPath, + `${datetimeInCorrectFormat}${name.substring(14)}`, + ), + ) + datetime.setDate(datetime.getDate() + 1) + }) + }, }, { title: 'Add users service', - task: async () => { - await createBuilder('yarn cedar g sdl --no-crud', 'api')('user') + task: async (opts) => { + await createBuilder('yarn cedar g sdl --no-crud', 'api', opts)('user') await applyCodemod('usersSdl.js', fullPath('api/src/graphql/users.sdl')) await applyCodemod( 'usersService.js', @@ -293,12 +468,8 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { fs.writeFileSync(testPath, content) } - await createBuilder('yarn cedar g types')() + await createBuilder('yarn cedar g types', '', opts)() }, - // Assuming this is also for fixture mainly, or generally useful? - // tui-tasks.mts had it. tasks.mts did not. - // I'll enable it for fixture for now, or maybe always if safe? - // "usersSdl.js" codemod exists? tui-tasks.mts used it. enabled: (opts) => !!opts.isFixture, }, { @@ -307,7 +478,7 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { }, { title: 'Add context tests', - task: () => { + task: (opts) => { const templatePath = path.join( __dirname, 'templates', @@ -315,7 +486,7 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { 'context.test.ts.template', ) const projectPath = path.join( - options.outputPath, + opts.outputPath, 'api', 'src', '__tests__', @@ -326,21 +497,74 @@ export const getApiTasks = (options: CommonTaskOptions): HighLevelTask[] => { }, enabled: (opts) => !!opts.isFixture, }, + { + title: 'Add describeScenario tests', + task: (opts) => { + // Copy contact.scenarios.ts, because scenario tests look for the same filename + fs.copyFileSync( + fullPath('api/src/services/contacts/contacts.scenarios'), + fullPath('api/src/services/contacts/describeContacts.scenarios'), + ) + + // Create describeContacts.test.ts + const describeScenarioFixture = path.join( + __dirname, + 'templates', + 'api', + 'contacts.describeScenario.test.ts.template', + ) + + fs.copyFileSync( + describeScenarioFixture, + fullPath('api/src/services/contacts/describeContacts.test'), + ) + }, + enabled: (opts) => !!opts.isFixture, + }, + { + title: 'Add vitest db import tracking tests for ESM test project', + task: (opts) => { + const templatesDir = path.join(__dirname, 'templates', 'api') + const templatePath1 = path.join(templatesDir, '1-db-import.test.ts') + const templatePath2 = path.join(templatesDir, '2-db-import.test.ts') + const templatePath3 = path.join(templatesDir, '3-db-import.test.ts') + + const testsDir = path.join(opts.outputPath, 'api', 'src', '__tests__') + const testFilePath1 = path.join(testsDir, '1-db-import.test.ts') + const testFilePath2 = path.join(testsDir, '2-db-import.test.ts') + const testFilePath3 = path.join(testsDir, '3-db-import.test.ts') + + fs.mkdirSync(testsDir, { recursive: true }) + fs.copyFileSync(templatePath1, testFilePath1) + fs.copyFileSync(templatePath2, testFilePath2) + fs.copyFileSync(templatePath3, testFilePath3) + + // I opted to add an additional vitest config file rather than modifying + // the existing one because I wanted to keep one looking exactly the + // same as it'll look in user's projects. + fs.copyFileSync( + path.join(templatesDir, 'vitest-sort.config.ts'), + path.join(opts.outputPath, 'api', 'vitest-sort.config.ts'), + ) + }, + enabled: (opts) => !!opts.esmProject, + }, ] } export const getCreatePagesTasks = ( options: CommonTaskOptions, ): ListrTask[] => { - const createPage = options.isFixture - ? createBuilder('yarn cedar g page', 'web') - : createBuilder('yarn cedar g page') + const createPage = (opts: CommonTaskOptions) => + opts.isFixture + ? createBuilder('yarn cedar g page', 'web', opts) + : createBuilder('yarn cedar g page', '', opts) return [ { title: 'Creating home page', - task: async () => { - await createPage('home /') + task: async (opts) => { + await createPage(opts)('home /') return applyCodemod( 'homePage.js', fullPath('web/src/pages/HomePage/HomePage'), @@ -349,8 +573,8 @@ export const getCreatePagesTasks = ( }, { title: 'Creating about page', - task: async () => { - await createPage('about') + task: async (opts) => { + await createPage(opts)('about') return applyCodemod( 'aboutPage.js', fullPath('web/src/pages/AboutPage/AboutPage'), @@ -359,8 +583,8 @@ export const getCreatePagesTasks = ( }, { title: 'Creating contact page', - task: async () => { - await createPage('contactUs /contact') + task: async (opts) => { + await createPage(opts)('contactUs /contact') return applyCodemod( 'contactUsPage.js', fullPath('web/src/pages/ContactUsPage/ContactUsPage'), @@ -369,14 +593,14 @@ export const getCreatePagesTasks = ( }, { title: 'Creating blog post page', - task: async () => { - await createPage('blogPost /blog-post/{id:Int}') + task: async (opts) => { + await createPage(opts)('blogPost /blog-post/{id:Int}') await applyCodemod( 'blogPostPage.js', fullPath('web/src/pages/BlogPostPage/BlogPostPage'), ) - if (options.isFixture) { + if (opts.isFixture) { await applyCodemod( 'updateBlogPostPageStories.js', fullPath('web/src/pages/BlogPostPage/BlogPostPage.stories'), @@ -386,8 +610,8 @@ export const getCreatePagesTasks = ( }, { title: 'Creating profile page', - task: async () => { - await createPage('profile /profile') + task: async (opts) => { + await createPage(opts)('profile /profile') const testFileContent = `import { render, waitFor, screen } from '@cedarjs/testing/web' import ProfilePage from './ProfilePage' @@ -408,7 +632,8 @@ describe('ProfilePage', () => { expect(await screen.findByText('danny@bazinga.com')).toBeInTheDocument() }) -})` +}) +` fs.writeFileSync( fullPath('web/src/pages/ProfilePage/ProfilePage.test'), @@ -435,14 +660,14 @@ describe('ProfilePage', () => { }, { title: 'Creating nested cells test page', - task: async () => { - await createPage('waterfall {id:Int}') + task: async (opts) => { + await createPage(opts)('waterfall {id:Int}') await applyCodemod( 'waterfallPage.js', fullPath('web/src/pages/WaterfallPage/WaterfallPage'), ) - if (options.isFixture) { + if (opts.isFixture) { await applyCodemod( 'updateWaterfallPageStories.js', fullPath('web/src/pages/WaterfallPage/WaterfallPage.stories'), @@ -456,12 +681,11 @@ describe('ProfilePage', () => { export const getCreateLayoutTasks = ( _options: CommonTaskOptions, ): ListrTask[] => { - const createLayoutBuilder = createBuilder('yarn cedar g layout') return [ { title: 'Creating layout', - task: async () => { - await createLayoutBuilder('blog') + task: async (opts) => { + await createBuilder('yarn cedar g layout', '', opts)('blog') return applyCodemod( 'blogLayout.js', fullPath('web/src/layouts/BlogLayout/BlogLayout'), @@ -474,11 +698,15 @@ export const getCreateLayoutTasks = ( export const getCreateComponentsTasks = ( options: CommonTaskOptions, ): ListrTask[] => { - const createComponent = createBuilder('yarn cedar g component') const tasks: ListrTask[] = [ { title: 'Creating components', - task: async () => { + task: async (opts) => { + const createComponent = createBuilder( + 'yarn cedar g component', + '', + opts, + ) await createComponent('blogPost') await applyCodemod( 'blogPost.js', @@ -499,7 +727,7 @@ export const getCreateComponentsTasks = ( fullPath('web/src/components/Author/Author.test'), ) - if (options.isFixture) { + if (opts.isFixture) { await createComponent('classWithClassField') await applyCodemod( 'classWithClassField.ts', @@ -517,11 +745,11 @@ export const getCreateComponentsTasks = ( export const getCreateCellsTasks = ( _options: CommonTaskOptions, ): ListrTask[] => { - const createCell = createBuilder('yarn cedar g cell') return [ { title: 'Creating cells', - task: async () => { + task: async (opts) => { + const createCell = createBuilder('yarn cedar g cell', '', opts) await createCell('blogPosts') await applyCodemod( 'blogPostsCell.js', @@ -593,8 +821,8 @@ export const getPrerenderTasks = (options: CommonTaskOptions): ListrTask[] => { return [ { title: 'Creating double rendering test page', - task: async () => { - const createPageBuilder = createBuilder('yarn cedar g page') + task: async (opts) => { + const createPageBuilder = createBuilder('yarn cedar g page', '', opts) await createPageBuilder('double') const doublePageContent = `import { Metadata } from '@cedarjs/web' @@ -626,7 +854,7 @@ export default DoublePage` }, { title: 'Update Routes.tsx', - task: () => { + task: (opts) => { const pathRoutes = fullPath('web/src/Routes.tsx', { addExtension: false, }) diff --git a/tasks/test-project/tasks.mts b/tasks/test-project/tasks.mts index f52f5fd201..055d6d5a1f 100644 --- a/tasks/test-project/tasks.mts +++ b/tasks/test-project/tasks.mts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' +import { Listr } from 'listr2' import type { ListrTask } from 'listr2' import { @@ -21,6 +22,7 @@ import { interface WebTasksOptions { linkWithLatestFwBuild?: boolean + verbose?: boolean } function mapToListrTask( @@ -50,32 +52,49 @@ function mapToListrTask( export async function webTasks( outputPath: string, - { linkWithLatestFwBuild }: WebTasksOptions, -): Promise { + { linkWithLatestFwBuild, verbose }: WebTasksOptions, +) { setOutputPath(outputPath) const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } const tasks = getWebTasks(options) - return tasks.map((t) => mapToListrTask(t, options)) + return new Listr( + tasks.map((t) => mapToListrTask(t, options)), + { + exitOnError: true, + renderer: verbose ? 'verbose' : 'default', + }, + ) } interface ApiTasksOptions { linkWithLatestFwBuild?: boolean + verbose?: boolean } export async function apiTasks( outputPath: string, - { linkWithLatestFwBuild }: ApiTasksOptions, -): Promise { + { linkWithLatestFwBuild, verbose }: ApiTasksOptions, +) { setOutputPath(outputPath) const options: CommonTaskOptions = { outputPath, linkWithLatestFwBuild } const tasks = getApiTasks(options) - return tasks.map((t) => mapToListrTask(t, options)) + return new Listr( + tasks.map((t) => mapToListrTask(t, options)), + { + exitOnError: true, + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, + }, + ) } -export async function streamingTasks(outputPath: string): Promise { - return [ +export async function streamingTasks( + outputPath: string, + { verbose }: { verbose?: boolean }, +) { + const tasks: ListrTask[] = [ { title: 'Creating Delayed suspense delayed page', task: async () => { @@ -95,11 +114,20 @@ export async function streamingTasks(outputPath: string): Promise { }, }, ] + + return new Listr(tasks, { + exitOnError: true, + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, + }) } -export async function fragmentsTasks(outputPath: string): Promise { +export async function fragmentsTasks( + outputPath: string, + { verbose }: { verbose?: boolean }, +) { const options: CommonTaskOptions = { outputPath } - return [ + const tasks: ListrTask[] = [ { title: 'Enable fragments', task: async () => { @@ -125,4 +153,9 @@ export async function fragmentsTasks(outputPath: string): Promise { }, }, ] + + return new Listr(tasks, { + exitOnError: true, + renderer: verbose ? 'verbose' : 'default', + }) } diff --git a/tasks/test-project/test-project.mts b/tasks/test-project/test-project.mts index 4c3eaa7767..40b8f8eddb 100644 --- a/tasks/test-project/test-project.mts +++ b/tasks/test-project/test-project.mts @@ -11,12 +11,7 @@ import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' import { apiTasks, streamingTasks, webTasks } from './tasks.mjs' -import { - confirmNoFixtureNoLink, - getExecaOptions, - getCfwBin, - setVerbose, -} from './util.mjs' +import { confirmNoFixtureNoLink, getExecaOptions, getCfwBin } from './util.mjs' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -74,8 +69,6 @@ const { streamingSsr, } = args -setVerbose(verbose) - if (args._.length > 1) { console.log( ansis.red.bold( @@ -205,30 +198,27 @@ const globalTasks = () => }, { title: 'Apply web codemods', - task: async (_ctx, task) => - task.newListr( - await webTasks(OUTPUT_PROJECT_PATH, { - linkWithLatestFwBuild: link, - }), - ), + task: () => + webTasks(OUTPUT_PROJECT_PATH, { + verbose, + linkWithLatestFwBuild: link, + }), enabled: () => !copyFromFixture, }, { // These are also web tasks... we can move them into the webTasks function // when streaming isn't experimental title: 'Enabling streaming-ssr experiment and applying codemods....', - task: async (_ctx, task) => - task.newListr(await streamingTasks(OUTPUT_PROJECT_PATH)), + task: () => streamingTasks(OUTPUT_PROJECT_PATH, { verbose }), enabled: () => streamingSsr, }, { title: 'Apply api codemods', - task: async (_ctx, task) => - task.newListr( - await apiTasks(OUTPUT_PROJECT_PATH, { - linkWithLatestFwBuild: link, - }), - ), + task: () => + apiTasks(OUTPUT_PROJECT_PATH, { + verbose, + linkWithLatestFwBuild: link, + }), enabled: () => !copyFromFixture, }, { @@ -314,7 +304,7 @@ async function runCommand() { try { await globalTasks().run() - } catch (err: any) { + } catch (err) { console.error(err) process.exit(1) } diff --git a/tasks/test-project/tui-tasks.mts b/tasks/test-project/tui-tasks.mts index 1a3909775b..f15a83314b 100644 --- a/tasks/test-project/tui-tasks.mts +++ b/tasks/test-project/tui-tasks.mts @@ -41,6 +41,7 @@ export async function webTasks( const options: CommonTaskOptions = { outputPath, isFixture: true, + stdio: 'pipe', } const tasks = getWebTasks(options) @@ -62,6 +63,7 @@ export async function apiTasks( isFixture: true, linkWithLatestFwBuild, esmProject, + stdio: 'pipe', } const tasks = getApiTasks(options) diff --git a/tasks/test-project/util.mts b/tasks/test-project/util.mts index 8f4a42e7fe..d1b207ffb5 100644 --- a/tasks/test-project/util.mts +++ b/tasks/test-project/util.mts @@ -161,9 +161,6 @@ export async function exec( }) } -/** - * @param cmd The command to run - */ export function createBuilder(cmd: string, dir = '') { return function (positionalArguments?: string | string[]) { const execaOptions = getExecaOptions(path.join(OUTPUT_PATH, dir))