From d2c569bf1fa2b130fdc15f621f8ddebc6673e5f3 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 20 Dec 2025 13:27:47 +0100 Subject: [PATCH 01/15] feat(cli): Generate package --- .../__snapshots__/package.test.ts.snap | 44 ++++ .../package/__tests__/package.test.ts | 93 +++++++++ .../src/commands/generate/package/package.js | 11 + .../generate/package/packageHandler.js | 189 ++++++++++++++++++ .../package/templates/README.md.template | 7 + .../package/templates/index.ts.template | 3 + .../package/templates/scenarios.ts.template | 8 + .../package/templates/test.ts.template | 7 + packages/cli/src/lib/test.js | 1 + .../src/__tests__/paths.test.ts | 1 + packages/project-config/src/paths.ts | 1 + 11 files changed, 365 insertions(+) create mode 100644 packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap create mode 100644 packages/cli/src/commands/generate/package/__tests__/package.test.ts create mode 100644 packages/cli/src/commands/generate/package/package.js create mode 100644 packages/cli/src/commands/generate/package/packageHandler.js create mode 100644 packages/cli/src/commands/generate/package/templates/README.md.template create mode 100644 packages/cli/src/commands/generate/package/templates/index.ts.template create mode 100644 packages/cli/src/commands/generate/package/templates/scenarios.ts.template create mode 100644 packages/cli/src/commands/generate/package/templates/test.ts.template diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap new file mode 100644 index 0000000000..bd5a9c61dc --- /dev/null +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -0,0 +1,44 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Single word default files > creates a single word function file > Scenario snapshot 1`] = ` +"import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://redwoodjs.com/docs/testing#scenarios +}) + +export type StandardScenario = ScenarioData +" +`; + +exports[`Single word default files > creates a single word function file > Test snapshot 1`] = ` +"import { SampleJob } from './SampleJob.js' + +describe('SampleJob', () => { + it('should not throw any errors', async () => { + await expect(SampleJob.perform()).resolves.not.toThrow() + }) +}) +" +`; + +exports[`Single word default files > creates a single word function file 1`] = ` +"import { jobs } from 'src/lib/jobs.js' + +export const SampleJob = jobs.createJob({ + queue: 'default', + perform: async () => { + jobs.logger.info('SampleJob is performing...') + }, +}) +" +`; + +exports[`multi-word files > creates a multi word function file 1`] = `undefined`; + +exports[`packageHandler > files > Single word default files > creates a single word function file > Scenario snapshot 1`] = `undefined`; + +exports[`packageHandler > files > Single word default files > creates a single word function file > Test snapshot 1`] = `undefined`; + +exports[`packageHandler > files > Single word default files > creates a single word function file 1`] = `undefined`; diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts new file mode 100644 index 0000000000..0d5366d206 --- /dev/null +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -0,0 +1,93 @@ +globalThis.__dirname = __dirname +// Load shared mocks +import '../../../../lib/test' + +import path from 'path' + +import { describe, it, expect } from 'vitest' + +import * as packageHandler from '../packageHandler.js' + +describe('packageHandler', () => { + describe('handler', () => { + it('throws on package name with two slashes', async () => { + expect(() => + packageHandler.handler({ name: 'package//name' }), + ).rejects.toThrowError( + 'Invalid package name "package//name". Package names can have at most one slash.', + ) + + await expect(() => + packageHandler.handler({ name: '@test-org/package/name' }), + ).rejects.toThrowError( + 'Invalid package name "@test-org/package/name". Package names can have at most one slash.', + ) + }) + }) + + describe('files', () => { + describe('Single word default files', async () => { + const files = await packageHandler.files({ name: '@my-org/foo' }) + + it.only('creates a single word package', () => { + const packagesPath = '/path/to/project/api/src/packages' + expect( + files[path.normalize(packagesPath + '/foo/src/index.ts')], + ).toMatchSnapshot('Package index') + + expect( + files[path.normalize(packagesPath + '/foo/src/README.md')], + ).toMatchSnapshot('README snapshot') + + expect( + files[path.normalize(packagesPath + '/foo/src/foo.test.ts')], + ).toMatchSnapshot('Test snapshot') + + expect( + files[path.normalize(packagesPath + '/foo/src/foo.scenarios.ts')], + ).toMatchSnapshot('Scenario snapshot') + }) + }) + + describe('multi-word files', () => { + it('creates a multi word function file', async () => { + const multiWordDefaultFiles = await packageHandler.files({ + name: 'send-mail', + queueName: 'default', + tests: false, + typescript: true, + }) + + expect( + multiWordDefaultFiles[ + path.normalize( + '/path/to/project/api/src/functions/SendMailJob/SendMailJob.js', + ) + ], + ).toMatchSnapshot() + }) + }) + + describe('generation of js files', async () => { + const jsFiles = await packageHandler.files({ + name: 'Sample', + queueName: 'default', + tests: true, + typescript: false, + }) + + it('returns tests, scenario and job file for JS', () => { + const fileNames = Object.keys(jsFiles) + expect(fileNames.length).toEqual(3) + + expect(fileNames).toEqual( + expect.arrayContaining([ + expect.stringContaining('SampleJob.js'), + expect.stringContaining('SampleJob.test.js'), + expect.stringContaining('SampleJob.scenarios.js'), + ]), + ) + }) + }) + }) +}) diff --git a/packages/cli/src/commands/generate/package/package.js b/packages/cli/src/commands/generate/package/package.js new file mode 100644 index 0000000000..33cd07beaa --- /dev/null +++ b/packages/cli/src/commands/generate/package/package.js @@ -0,0 +1,11 @@ +import { createHandler, createBuilder } from '../yargsCommandHelpers.js' + +export const command = 'package ' +export const description = 'Generate a workspace Package' + +export const builder = createBuilder({ + componentName: 'package', + addStories: false, +}) + +export const handler = createHandler('package') diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js new file mode 100644 index 0000000000..e809c08050 --- /dev/null +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -0,0 +1,189 @@ +import path from 'node:path' + +import { paramCase } from 'change-case' +import execa from 'execa' +import { Listr } from 'listr2' + +import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +import { errorTelemetry } from '@cedarjs/telemetry' + +import c from '../../../lib/colors.js' +import { + getPaths, + transformTSToJS, + writeFilesTask, +} from '../../../lib/index.js' +import { prepareForRollback } from '../../../lib/rollback.js' +import { + templateForComponentFile, + templateForFile, +} from '../yargsHandlerHelpers.js' + +export const files = async ({ + name: nameArg, + typescript, + tests: generateTests = true, + ...rest +}) => { + const extension = typescript ? '.ts' : '.js' + + const outputFiles = [] + + const base = path.basename(getPaths().base) + + console.log('name', nameArg) + console.log('base', base) + + const [orgName, name] = + nameArg[0] === '@' ? nameArg.split('/', 2) : ['@' + base, nameArg] + + console.log('orgName', orgName) + console.log('pkgName', name) + + const folderName = paramCase(name) + const packageName = orgName + '/' + folderName + + console.log('folderName', folderName) + console.log('packageName', packageName) + console.log('packagesPath', getPaths().packages) + + const packageFiles = await templateForFile({ + name: packageName, + componentName: packageName, + extension, + apiPathSection: 'packages', + generator: 'package', + templatePath: 'index.ts.template', + templateVars: { name, packageName, ...rest }, + outputPath: path.join( + getPaths().packages, + orgName, + `${packageName}`, + `${packageName}{extension}`, + ), + }) + + outputFiles.push(packageFiles) + + const readmeFile = await templateForComponentFile({ + name: packageName, + componentName: packageName, + extension, + apiPathSection: 'packages', + generator: 'package', + templatePath: 'README.md.template', + templateVars: { name, packageName, ...rest }, + outputPath: path.join( + getPaths().packages, + orgName, + `${packageName}`, + `${packageName}{extension}`, + ), + }) + + outputFiles.push(readmeFile) + + if (generateTests) { + const testFile = await templateForComponentFile({ + name: packageName, + componentName: packageName, + extension, + apiPathSection: 'jobs', + generator: 'job', + templatePath: 'test.ts.template', + templateVars: { ...rest }, + outputPath: path.join( + getPaths().api.jobs, + `${packageName}Job`, + `${packageName}Job.test${extension}`, + ), + }) + + const scenarioFile = await templateForComponentFile({ + name: packageName, + componentName: packageName, + extension, + apiPathSection: 'jobs', + generator: 'job', + templatePath: 'scenarios.ts.template', + templateVars: { ...rest }, + outputPath: path.join( + getPaths().api.jobs, + `${packageName}Job`, + `${packageName}Job.scenarios${extension}`, + ), + }) + + outputFiles.push(testFile) + outputFiles.push(scenarioFile) + } + + return outputFiles.reduce(async (accP, [outputPath, content]) => { + const acc = await accP + + const template = typescript + ? content + : await transformTSToJS(outputPath, content) + + return { + [outputPath]: template, + ...acc, + } + }, Promise.resolve({})) +} + +// This could be built using createYargsForComponentGeneration; +// however, we need to add a message after generating the function files +export const handler = async ({ name, force, ...rest }) => { + recordTelemetryAttributes({ + command: 'generate package', + force, + rollback: rest.rollback, + }) + + console.log('name', name) + + if (name.replaceAll('/', '').length < name.length - 1) { + throw new Error( + `Invalid package name "${name}". Package names can have at most one slash.`, + ) + } + + let packageFiles = {} + const tasks = new Listr( + [ + { + title: 'Generating package files...', + task: async () => { + packageFiles = await files({ name, ...rest }) + return writeFilesTask(packageFiles, { overwriteExisting: force }) + }, + }, + { + title: 'Cleaning up...', + task: () => { + execa.sync('yarn', [ + 'eslint', + '--fix', + '--config', + `${getPaths().base}/node_modules/@cedarjs/eslint-config/index.js`, + ...Object.keys(packageFiles), + ]) + }, + }, + ], + { rendererOptions: { collapseSubtasks: false }, exitOnError: true }, + ) + + try { + if (rest.rollback && !force) { + prepareForRollback(tasks) + } + + await tasks.run() + } catch (e) { + errorTelemetry(process.argv, e.message) + console.error(c.error(e.message)) + process.exit(e?.exitCode || 1) + } +} diff --git a/packages/cli/src/commands/generate/package/templates/README.md.template b/packages/cli/src/commands/generate/package/templates/README.md.template new file mode 100644 index 0000000000..873446e2cc --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/README.md.template @@ -0,0 +1,7 @@ +# Shared Package '${name}' + +Use code in this package by importing it into your project: + +```javascript +import { ${name} } from '${packageName}'; +``` diff --git a/packages/cli/src/commands/generate/package/templates/index.ts.template b/packages/cli/src/commands/generate/package/templates/index.ts.template new file mode 100644 index 0000000000..7d86fad9b5 --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/index.ts.template @@ -0,0 +1,3 @@ +export function ${name}() { + return 0 +} diff --git a/packages/cli/src/commands/generate/package/templates/scenarios.ts.template b/packages/cli/src/commands/generate/package/templates/scenarios.ts.template new file mode 100644 index 0000000000..a3c19a8b06 --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/scenarios.ts.template @@ -0,0 +1,8 @@ +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://cedarjs.com/docs/testing#scenarios +}) + +export type StandardScenario = ScenarioData diff --git a/packages/cli/src/commands/generate/package/templates/test.ts.template b/packages/cli/src/commands/generate/package/templates/test.ts.template new file mode 100644 index 0000000000..d5018313b4 --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/test.ts.template @@ -0,0 +1,7 @@ +import { ${name} } from './${name}.js' + +describe('${name}', () => { + it('should not throw any errors', async () => { + expect(${name}()).not.toThrow() + }) +}) diff --git a/packages/cli/src/lib/test.js b/packages/cli/src/lib/test.js index d358cba796..8da3196a7a 100644 --- a/packages/cli/src/lib/test.js +++ b/packages/cli/src/lib/test.js @@ -60,6 +60,7 @@ vi.mock('@cedarjs/project-config', async (importOriginal) => { app: path.join(BASE_PATH, '/web/src/App.js'), }, scripts: path.join(BASE_PATH, 'scripts'), + packages: path.join(BASE_PATH, 'packages'), generated: { base: path.join(BASE_PATH, '.redwood'), schema: path.join(BASE_PATH, '.redwood/schema.graphql'), diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts index c75d281241..962b299e37 100644 --- a/packages/project-config/src/__tests__/paths.test.ts +++ b/packages/project-config/src/__tests__/paths.test.ts @@ -29,6 +29,7 @@ const DEFAULT_PATHS = { }, prebuild: ['.redwood', 'prebuild'], }, + packages: ['packages'], scripts: ['scripts'], api: { base: ['api'], diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts index 540bb77690..005e128e08 100644 --- a/packages/project-config/src/paths.ts +++ b/packages/project-config/src/paths.ts @@ -155,6 +155,7 @@ export const getPaths = (BASE_DIR: string = getBaseDir()): Paths => { }, scripts: path.join(BASE_DIR, 'scripts'), + packages: path.join(BASE_DIR, 'packages'), api: { base: path.join(BASE_DIR, 'api'), From d02b920387e0ff23cd2f47bf78b0c6b918915cd5 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 22 Dec 2025 09:12:38 +0100 Subject: [PATCH 02/15] update tests --- .../package/__tests__/package.test.ts | 74 +++++++++++++------ 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index 0d5366d206..eaa6a1ceff 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -6,6 +6,7 @@ import path from 'path' import { describe, it, expect } from 'vitest' +// @ts-expect-error - Importing a JS file import * as packageHandler from '../packageHandler.js' describe('packageHandler', () => { @@ -26,11 +27,12 @@ describe('packageHandler', () => { }) describe('files', () => { - describe('Single word default files', async () => { + const packagesPath = '/path/to/project/api/src/packages' + + describe('single word package names', async () => { const files = await packageHandler.files({ name: '@my-org/foo' }) - it.only('creates a single word package', () => { - const packagesPath = '/path/to/project/api/src/packages' + it('creates a single word package', () => { expect( files[path.normalize(packagesPath + '/foo/src/index.ts')], ).toMatchSnapshot('Package index') @@ -49,42 +51,68 @@ describe('packageHandler', () => { }) }) - describe('multi-word files', () => { - it('creates a multi word function file', async () => { - const multiWordDefaultFiles = await packageHandler.files({ - name: 'send-mail', - queueName: 'default', - tests: false, - typescript: true, + // I had to decide if I wanted the folder name for multi-word packages to be + // hyphenated (kebab-case) or camelCase. I decided to use kebab-case because + // it matches what the package name is. So, just like we use PascalCase for + // folder names for React components, we use kebab-case for folder names for + // packages. + describe('multi-word package names', () => { + it('creates a multi-word package', async () => { + const files = await packageHandler.files({ + name: '@my-org/form-validators', }) - expect( - multiWordDefaultFiles[ - path.normalize( - '/path/to/project/api/src/functions/SendMailJob/SendMailJob.js', - ) - ], - ).toMatchSnapshot() + const indexPath = path.normalize( + packagesPath + '/form-validators/src/index.ts', + ) + const testPath = path.normalize( + packagesPath + '/form-validators/src/formValidators.test.ts', + ) + const scenarioPath = path.normalize( + packagesPath + '/form-validators/src/formValidators.scenarios.ts', + ) + + expect(files[indexPath]).toMatchSnapshot('Package index') + expect(files[testPath]).toMatchSnapshot('Test snapshot') + expect(files[scenarioPath]).toMatchSnapshot('Scenario snapshot') + }) + + it('creates a multiWord package', async () => { + const files = await packageHandler.files({ + name: '@my-org/formValidators', + }) + + const indexPath = path.normalize( + packagesPath + '/form-validators/src/index.ts', + ) + const testPath = path.normalize( + packagesPath + '/form-validators/src/formValidators.test.ts', + ) + const scenarioPath = path.normalize( + packagesPath + '/form-validators/src/formValidators.scenarios.ts', + ) + + expect(files[indexPath]).toMatchSnapshot('Package index') + expect(files[testPath]).toMatchSnapshot('Test snapshot') + expect(files[scenarioPath]).toMatchSnapshot('Scenario snapshot') }) }) describe('generation of js files', async () => { const jsFiles = await packageHandler.files({ name: 'Sample', - queueName: 'default', - tests: true, typescript: false, }) - it('returns tests, scenario and job file for JS', () => { + it('returns tests, scenario and main package file for JS', () => { const fileNames = Object.keys(jsFiles) expect(fileNames.length).toEqual(3) expect(fileNames).toEqual( expect.arrayContaining([ - expect.stringContaining('SampleJob.js'), - expect.stringContaining('SampleJob.test.js'), - expect.stringContaining('SampleJob.scenarios.js'), + expect.stringContaining('index.js'), + expect.stringContaining('sample.test.js'), + expect.stringContaining('sample.scenarios.js'), ]), ) }) From ef038a806303c7490a2ffacbbfe041b566ec486f Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 22 Dec 2025 10:49:10 +0100 Subject: [PATCH 03/15] Passing tests --- .../__snapshots__/package.test.ts.snap | 138 +++++++++++++++--- .../package/__tests__/package.test.ts | 72 +++++---- .../generate/package/packageHandler.js | 95 ++++-------- .../package/templates/README.md.template | 14 +- .../package/templates/index.ts.template | 2 +- .../package/templates/test.ts.template | 6 +- .../commands/generate/yargsHandlerHelpers.js | 15 +- packages/cli/src/lib/index.js | 1 - 8 files changed, 224 insertions(+), 119 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index bd5a9c61dc..24ab1db532 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -1,44 +1,148 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Single word default files > creates a single word function file > Scenario snapshot 1`] = ` +exports[`packageHandler > files > multi-word package names > creates a multi-word package 1`] = ` +"# Shared Package '@my-org/form-validators' + +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special \`workspace:*\` version, and then importing it into +your code: + +\`\`\`json + "dependencies": { + "@my-org/form-validators": "workspace:*" + } +\`\`\` + +\`\`\`javascript +import { formValidators } from '@my-org/form-validators'; +\`\`\` +" +`; + +exports[`packageHandler > files > multi-word package names > creates a multi-word package 2`] = ` +"export function formValidators() { + return 0 +} +" +`; + +exports[`packageHandler > files > multi-word package names > creates a multi-word package 3`] = ` +"import { formValidators } from './index.js' + +describe('formValidators', () => { + it('should not throw any errors', async () => { + expect(formValidators()).not.toThrow() + }) +}) +" +`; + +exports[`packageHandler > files > multi-word package names > creates a multi-word package 4`] = ` "import type { ScenarioData } from '@cedarjs/testing/api' export const standard = defineScenario({ // Define the "fixture" to write into your test database here - // See guide: https://redwoodjs.com/docs/testing#scenarios + // See guide: https://cedarjs.com/docs/testing#scenarios }) export type StandardScenario = ScenarioData " `; -exports[`Single word default files > creates a single word function file > Test snapshot 1`] = ` -"import { SampleJob } from './SampleJob.js' +exports[`packageHandler > files > multi-word package names > creates a multiWord package 1`] = ` +"# Shared Package '@my-org/form-validators' + +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special \`workspace:*\` version, and then importing it into +your code: -describe('SampleJob', () => { +\`\`\`json + "dependencies": { + "@my-org/form-validators": "workspace:*" + } +\`\`\` + +\`\`\`javascript +import { formValidators } from '@my-org/form-validators'; +\`\`\` +" +`; + +exports[`packageHandler > files > multi-word package names > creates a multiWord package 2`] = ` +"export function formValidators() { + return 0 +} +" +`; + +exports[`packageHandler > files > multi-word package names > creates a multiWord package 3`] = ` +"import { formValidators } from './index.js' + +describe('formValidators', () => { it('should not throw any errors', async () => { - await expect(SampleJob.perform()).resolves.not.toThrow() + expect(formValidators()).not.toThrow() }) }) " `; -exports[`Single word default files > creates a single word function file 1`] = ` -"import { jobs } from 'src/lib/jobs.js' +exports[`packageHandler > files > multi-word package names > creates a multiWord package 4`] = ` +"import type { ScenarioData } from '@cedarjs/testing/api' -export const SampleJob = jobs.createJob({ - queue: 'default', - perform: async () => { - jobs.logger.info('SampleJob is performing...') - }, +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://cedarjs.com/docs/testing#scenarios }) + +export type StandardScenario = ScenarioData +" +`; + +exports[`packageHandler > files > single word package names > creates a single word package 1`] = ` +"# Shared Package '@my-org/foo' + +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special \`workspace:*\` version, and then importing it into +your code: + +\`\`\`json + "dependencies": { + "@my-org/foo": "workspace:*" + } +\`\`\` + +\`\`\`javascript +import { foo } from '@my-org/foo'; +\`\`\` +" +`; + +exports[`packageHandler > files > single word package names > creates a single word package 2`] = ` +"export function foo() { + return 0 +} " `; -exports[`multi-word files > creates a multi word function file 1`] = `undefined`; +exports[`packageHandler > files > single word package names > creates a single word package 3`] = ` +"import { foo } from './index.js' -exports[`packageHandler > files > Single word default files > creates a single word function file > Scenario snapshot 1`] = `undefined`; +describe('foo', () => { + it('should not throw any errors', async () => { + expect(foo()).not.toThrow() + }) +}) +" +`; -exports[`packageHandler > files > Single word default files > creates a single word function file > Test snapshot 1`] = `undefined`; +exports[`packageHandler > files > single word package names > creates a single word package 4`] = ` +"import type { ScenarioData } from '@cedarjs/testing/api' -exports[`packageHandler > files > Single word default files > creates a single word function file 1`] = `undefined`; +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://cedarjs.com/docs/testing#scenarios +}) + +export type StandardScenario = ScenarioData +" +`; diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index eaa6a1ceff..0f42597e5b 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -1,4 +1,5 @@ globalThis.__dirname = __dirname + // Load shared mocks import '../../../../lib/test' @@ -27,27 +28,38 @@ describe('packageHandler', () => { }) describe('files', () => { - const packagesPath = '/path/to/project/api/src/packages' + const packagesPath = '/path/to/project/packages' describe('single word package names', async () => { - const files = await packageHandler.files({ name: '@my-org/foo' }) + const files = await packageHandler.files({ + name: '@my-org/foo', + typescript: true, + }) it('creates a single word package', () => { - expect( - files[path.normalize(packagesPath + '/foo/src/index.ts')], - ).toMatchSnapshot('Package index') + const fileNames = Object.keys(files) + expect(fileNames.length).toEqual(4) - expect( - files[path.normalize(packagesPath + '/foo/src/README.md')], - ).toMatchSnapshot('README snapshot') + expect(fileNames).toEqual( + expect.arrayContaining([ + expect.stringContaining('README.md'), + expect.stringContaining('index.ts'), + expect.stringContaining('foo.test.ts'), + expect.stringContaining('foo.scenarios.ts'), + ]), + ) - expect( - files[path.normalize(packagesPath + '/foo/src/foo.test.ts')], - ).toMatchSnapshot('Test snapshot') + const readmePath = path.normalize(packagesPath + '/foo/README.md') + const indexPath = path.normalize(packagesPath + '/foo/src/index.ts') + const testPath = path.normalize(packagesPath + '/foo/src/foo.test.ts') + const scenariosPath = path.normalize( + packagesPath + '/foo/src/foo.scenarios.ts', + ) - expect( - files[path.normalize(packagesPath + '/foo/src/foo.scenarios.ts')], - ).toMatchSnapshot('Scenario snapshot') + expect(files[readmePath]).toMatchSnapshot() + expect(files[indexPath]).toMatchSnapshot() + expect(files[testPath]).toMatchSnapshot() + expect(files[scenariosPath]).toMatchSnapshot() }) }) @@ -60,28 +72,37 @@ describe('packageHandler', () => { it('creates a multi-word package', async () => { const files = await packageHandler.files({ name: '@my-org/form-validators', + typescript: true, }) + const readmePath = path.normalize( + packagesPath + '/form-validators/README.md', + ) const indexPath = path.normalize( packagesPath + '/form-validators/src/index.ts', ) const testPath = path.normalize( packagesPath + '/form-validators/src/formValidators.test.ts', ) - const scenarioPath = path.normalize( + const scenariosPath = path.normalize( packagesPath + '/form-validators/src/formValidators.scenarios.ts', ) - expect(files[indexPath]).toMatchSnapshot('Package index') - expect(files[testPath]).toMatchSnapshot('Test snapshot') - expect(files[scenarioPath]).toMatchSnapshot('Scenario snapshot') + expect(files[readmePath]).toMatchSnapshot() + expect(files[indexPath]).toMatchSnapshot() + expect(files[testPath]).toMatchSnapshot() + expect(files[scenariosPath]).toMatchSnapshot() }) it('creates a multiWord package', async () => { const files = await packageHandler.files({ name: '@my-org/formValidators', + typescript: true, }) + const readmePath = path.normalize( + packagesPath + '/form-validators/README.md', + ) const indexPath = path.normalize( packagesPath + '/form-validators/src/index.ts', ) @@ -92,24 +113,23 @@ describe('packageHandler', () => { packagesPath + '/form-validators/src/formValidators.scenarios.ts', ) - expect(files[indexPath]).toMatchSnapshot('Package index') - expect(files[testPath]).toMatchSnapshot('Test snapshot') - expect(files[scenarioPath]).toMatchSnapshot('Scenario snapshot') + expect(files[readmePath]).toMatchSnapshot() + expect(files[indexPath]).toMatchSnapshot() + expect(files[testPath]).toMatchSnapshot() + expect(files[scenarioPath]).toMatchSnapshot() }) }) describe('generation of js files', async () => { - const jsFiles = await packageHandler.files({ - name: 'Sample', - typescript: false, - }) + const jsFiles = await packageHandler.files({ name: 'Sample' }) it('returns tests, scenario and main package file for JS', () => { const fileNames = Object.keys(jsFiles) - expect(fileNames.length).toEqual(3) + expect(fileNames.length).toEqual(4) expect(fileNames).toEqual( expect.arrayContaining([ + expect.stringContaining('README.md'), expect.stringContaining('index.js'), expect.stringContaining('sample.test.js'), expect.stringContaining('sample.scenarios.js'), diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index e809c08050..ab8e62a388 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -1,6 +1,6 @@ import path from 'node:path' -import { paramCase } from 'change-case' +import { paramCase, camelCase } from 'change-case' import execa from 'execa' import { Listr } from 'listr2' @@ -14,10 +14,7 @@ import { writeFilesTask, } from '../../../lib/index.js' import { prepareForRollback } from '../../../lib/rollback.js' -import { - templateForComponentFile, - templateForFile, -} from '../yargsHandlerHelpers.js' +import { templateForFile } from '../yargsHandlerHelpers.js' export const files = async ({ name: nameArg, @@ -31,86 +28,55 @@ export const files = async ({ const base = path.basename(getPaths().base) - console.log('name', nameArg) - console.log('base', base) - const [orgName, name] = nameArg[0] === '@' ? nameArg.split('/', 2) : ['@' + base, nameArg] - console.log('orgName', orgName) - console.log('pkgName', name) - const folderName = paramCase(name) const packageName = orgName + '/' + folderName - - console.log('folderName', folderName) - console.log('packageName', packageName) - console.log('packagesPath', getPaths().packages) + const fileName = camelCase(name) const packageFiles = await templateForFile({ - name: packageName, - componentName: packageName, - extension, - apiPathSection: 'packages', + name, + side: 'packages', generator: 'package', templatePath: 'index.ts.template', - templateVars: { name, packageName, ...rest }, - outputPath: path.join( - getPaths().packages, - orgName, - `${packageName}`, - `${packageName}{extension}`, - ), + templateVars: rest, + outputPath: path.join(folderName, 'src', `index${extension}`), }) outputFiles.push(packageFiles) - const readmeFile = await templateForComponentFile({ - name: packageName, - componentName: packageName, - extension, - apiPathSection: 'packages', + const readmeFile = await templateForFile({ + name, + side: 'packages', generator: 'package', templatePath: 'README.md.template', - templateVars: { name, packageName, ...rest }, - outputPath: path.join( - getPaths().packages, - orgName, - `${packageName}`, - `${packageName}{extension}`, - ), + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'README.md'), }) outputFiles.push(readmeFile) if (generateTests) { - const testFile = await templateForComponentFile({ - name: packageName, - componentName: packageName, - extension, - apiPathSection: 'jobs', - generator: 'job', + const testFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', templatePath: 'test.ts.template', - templateVars: { ...rest }, - outputPath: path.join( - getPaths().api.jobs, - `${packageName}Job`, - `${packageName}Job.test${extension}`, - ), + templateVars: rest, + outputPath: path.join(folderName, 'src', `${fileName}.test${extension}`), }) - const scenarioFile = await templateForComponentFile({ - name: packageName, - componentName: packageName, - extension, - apiPathSection: 'jobs', - generator: 'job', + const scenarioFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', templatePath: 'scenarios.ts.template', - templateVars: { ...rest }, + templateVars: rest, outputPath: path.join( - getPaths().api.jobs, - `${packageName}Job`, - `${packageName}Job.scenarios${extension}`, + folderName, + 'src', + `${fileName}.scenarios${extension}`, ), }) @@ -121,9 +87,10 @@ export const files = async ({ return outputFiles.reduce(async (accP, [outputPath, content]) => { const acc = await accP - const template = typescript - ? content - : await transformTSToJS(outputPath, content) + const template = + typescript || outputPath.endsWith('.md') || outputPath.endsWith('.json') + ? content + : await transformTSToJS(outputPath, content) return { [outputPath]: template, @@ -141,8 +108,6 @@ export const handler = async ({ name, force, ...rest }) => { rollback: rest.rollback, }) - console.log('name', name) - if (name.replaceAll('/', '').length < name.length - 1) { throw new Error( `Invalid package name "${name}". Package names can have at most one slash.`, diff --git a/packages/cli/src/commands/generate/package/templates/README.md.template b/packages/cli/src/commands/generate/package/templates/README.md.template index 873446e2cc..e31f992b7a 100644 --- a/packages/cli/src/commands/generate/package/templates/README.md.template +++ b/packages/cli/src/commands/generate/package/templates/README.md.template @@ -1,7 +1,15 @@ -# Shared Package '${name}' +# Shared Package '${packageName}' -Use code in this package by importing it into your project: +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special `workspace:*` version, and then importing it into +your code: + +```json + "dependencies": { + "${packageName}": "workspace:*" + } +``` ```javascript -import { ${name} } from '${packageName}'; +import { ${camelName} } from '${packageName}'; ``` diff --git a/packages/cli/src/commands/generate/package/templates/index.ts.template b/packages/cli/src/commands/generate/package/templates/index.ts.template index 7d86fad9b5..8ee7964ff5 100644 --- a/packages/cli/src/commands/generate/package/templates/index.ts.template +++ b/packages/cli/src/commands/generate/package/templates/index.ts.template @@ -1,3 +1,3 @@ -export function ${name}() { +export function ${camelName}() { return 0 } diff --git a/packages/cli/src/commands/generate/package/templates/test.ts.template b/packages/cli/src/commands/generate/package/templates/test.ts.template index d5018313b4..e7aad1c8fe 100644 --- a/packages/cli/src/commands/generate/package/templates/test.ts.template +++ b/packages/cli/src/commands/generate/package/templates/test.ts.template @@ -1,7 +1,7 @@ -import { ${name} } from './${name}.js' +import { ${camelName} } from './index.js' -describe('${name}', () => { +describe('${camelName}', () => { it('should not throw any errors', async () => { - expect(${name}()).not.toThrow() + expect(${camelName}()).not.toThrow() }) }) diff --git a/packages/cli/src/commands/generate/yargsHandlerHelpers.js b/packages/cli/src/commands/generate/yargsHandlerHelpers.js index d5d818898c..89d7e77aec 100644 --- a/packages/cli/src/commands/generate/yargsHandlerHelpers.js +++ b/packages/cli/src/commands/generate/yargsHandlerHelpers.js @@ -47,6 +47,10 @@ export const customOrDefaultTemplatePath = ({ templatePath, ) + if (!getPaths()[side].generators) { + return defaultPath + } + // Where a custom template *might* exist, e.g. // /path/to/app/web/generators/page/page.tsx.template const customPath = path.join( @@ -73,20 +77,25 @@ export const templateForFile = async ({ templatePath, templateVars, }) => { - const basePath = getPaths()[side][sidePathSection] + const basePath = sidePathSection + ? getPaths()[side][sidePathSection] + : getPaths()[side] + console.log('yargsHanlderHelpers basePath', basePath) const fullOutputPath = path.join(basePath, outputPath) const fullTemplatePath = customOrDefaultTemplatePath({ generator, templatePath, side, }) - const content = await generateTemplate(fullTemplatePath, { + const mergedTemplateVars = { name, outputPath: ensurePosixPath( `./${path.relative(getPaths().base, fullOutputPath)}`, ), ...templateVars, - }) + } + console.log('mergedTemplateVars', mergedTemplateVars) + const content = await generateTemplate(fullTemplatePath, mergedTemplateVars) return [fullOutputPath, content] } diff --git a/packages/cli/src/lib/index.js b/packages/cli/src/lib/index.js index 1989482d53..a5243b0004 100644 --- a/packages/cli/src/lib/index.js +++ b/packages/cli/src/lib/index.js @@ -68,7 +68,6 @@ export const generateTemplate = async (templateFilename, { name, ...rest }) => { ...nameVariants(name), ...rest, }) - return prettify(templateFilename, renderedTemplate) } catch (error) { error.message = `Error applying template at ${templateFilename} for ${name}: ${error.message}` From d8182cd91da0411dbbb6911c8eca1a61b3dc537b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 22 Dec 2025 15:51:27 +0100 Subject: [PATCH 04/15] package.json and tsconfig.json --- .../package/__tests__/package.test.ts | 33 +++++++++++++++++-- .../generate/package/packageHandler.js | 25 ++++++++++++-- .../package/templates/package.json.template | 14 ++++++++ .../package/templates/tsconfig.json.template | 14 ++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/commands/generate/package/templates/package.json.template create mode 100644 packages/cli/src/commands/generate/package/templates/tsconfig.json.template diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index 0f42597e5b..ee1f87eaa8 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -3,7 +3,8 @@ globalThis.__dirname = __dirname // Load shared mocks import '../../../../lib/test' -import path from 'path' +import fs from 'node:fs' +import path from 'node:path' import { describe, it, expect } from 'vitest' @@ -38,11 +39,13 @@ describe('packageHandler', () => { it('creates a single word package', () => { const fileNames = Object.keys(files) - expect(fileNames.length).toEqual(4) + expect(fileNames.length).toEqual(6) expect(fileNames).toEqual( expect.arrayContaining([ expect.stringContaining('README.md'), + expect.stringContaining('package.json'), + expect.stringContaining('tsconfig.json'), expect.stringContaining('index.ts'), expect.stringContaining('foo.test.ts'), expect.stringContaining('foo.scenarios.ts'), @@ -125,11 +128,14 @@ describe('packageHandler', () => { it('returns tests, scenario and main package file for JS', () => { const fileNames = Object.keys(jsFiles) - expect(fileNames.length).toEqual(4) + expect(fileNames.length).toEqual(6) expect(fileNames).toEqual( expect.arrayContaining([ expect.stringContaining('README.md'), + expect.stringContaining('package.json'), + // TODO: Make the script output jsconfig.json + expect.stringContaining('tsconfig.json'), expect.stringContaining('index.js'), expect.stringContaining('sample.test.js'), expect.stringContaining('sample.scenarios.js'), @@ -138,4 +144,25 @@ describe('packageHandler', () => { }) }) }) + + it('has the correct version of TypeScript in the generated package', () => { + const cedarPackageJsonPath = path.normalize( + path.join(__dirname, ...Array(7).fill('..'), 'package.json'), + ) + const packageJson = JSON.parse( + fs.readFileSync(cedarPackageJsonPath, 'utf8'), + ) + + const packageJsonTemplatePath = path.join( + __dirname, + '..', + 'templates', + 'package.json.template', + ) + const packageJsonTemplate = fs.readFileSync(packageJsonTemplatePath, 'utf8') + + expect(packageJsonTemplate).toContain( + `"typescript": "${packageJson.devDependencies.typescript}"`, + ) + }) }) diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index ab8e62a388..55e0a57f98 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -35,7 +35,7 @@ export const files = async ({ const packageName = orgName + '/' + folderName const fileName = camelCase(name) - const packageFiles = await templateForFile({ + const indexFile = await templateForFile({ name, side: 'packages', generator: 'package', @@ -44,8 +44,6 @@ export const files = async ({ outputPath: path.join(folderName, 'src', `index${extension}`), }) - outputFiles.push(packageFiles) - const readmeFile = await templateForFile({ name, side: 'packages', @@ -55,7 +53,28 @@ export const files = async ({ outputPath: path.join(folderName, 'README.md'), }) + const packageJsonFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'package.json.template', + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'package.json'), + }) + + const tsconfigFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'tsconfig.json.template', + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'tsconfig.json'), + }) + + outputFiles.push(indexFile) outputFiles.push(readmeFile) + outputFiles.push(packageJsonFile) + outputFiles.push(tsconfigFile) if (generateTests) { const testFile = await templateForFile({ diff --git a/packages/cli/src/commands/generate/package/templates/package.json.template b/packages/cli/src/commands/generate/package/templates/package.json.template new file mode 100644 index 0000000000..0f7454c32b --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/package.json.template @@ -0,0 +1,14 @@ +{ + "name": "${packageName}", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} diff --git a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template new file mode 100644 index 0000000000..056837f374 --- /dev/null +++ b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "composite": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + }, + "include": ["src"], +} From d1ff042e5fa9a5bbb54fd010824bdd87b88db2e8 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 27 Dec 2025 11:14:04 +0100 Subject: [PATCH 05/15] remove debug console.log --- packages/cli/src/commands/generate/yargsHandlerHelpers.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/src/commands/generate/yargsHandlerHelpers.js b/packages/cli/src/commands/generate/yargsHandlerHelpers.js index 4f96b2d5e0..4d641fa4a6 100644 --- a/packages/cli/src/commands/generate/yargsHandlerHelpers.js +++ b/packages/cli/src/commands/generate/yargsHandlerHelpers.js @@ -81,7 +81,6 @@ export const templateForFile = async ({ const basePath = sidePathSection ? getPaths()[side][sidePathSection] : getPaths()[side] - console.log('yargsHanlderHelpers basePath', basePath) const fullOutputPath = path.join(basePath, outputPath) const fullTemplatePath = customOrDefaultTemplatePath({ generator, @@ -95,7 +94,6 @@ export const templateForFile = async ({ ), ...templateVars, } - console.log('mergedTemplateVars', mergedTemplateVars) const content = await generateTemplate(fullTemplatePath, mergedTemplateVars) return [fullOutputPath, content] From b99201e0cb0de9463c35fe20b4a49532b0501501 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 27 Dec 2025 11:44:32 +0100 Subject: [PATCH 06/15] undo broken change --- .../cli/src/commands/generate/script/__tests__/script.test.ts | 2 -- packages/cli/src/commands/generate/yargsHandlerHelpers.js | 4 ---- 2 files changed, 6 deletions(-) diff --git a/packages/cli/src/commands/generate/script/__tests__/script.test.ts b/packages/cli/src/commands/generate/script/__tests__/script.test.ts index 74abf4ef6b..cce0f383c3 100644 --- a/packages/cli/src/commands/generate/script/__tests__/script.test.ts +++ b/packages/cli/src/commands/generate/script/__tests__/script.test.ts @@ -5,7 +5,6 @@ import '../../../../lib/test' import fs from 'node:fs' import path from 'node:path' -import fse from 'fs-extra' import { vi, test, expect, describe, beforeAll, afterAll } from 'vitest' import yargs from 'yargs' @@ -98,7 +97,6 @@ describe('custom template', () => { return true }) - vi.spyOn(fse, 'existsSync').mockReturnValue(true) }) afterAll(() => { diff --git a/packages/cli/src/commands/generate/yargsHandlerHelpers.js b/packages/cli/src/commands/generate/yargsHandlerHelpers.js index 4d641fa4a6..283d388b70 100644 --- a/packages/cli/src/commands/generate/yargsHandlerHelpers.js +++ b/packages/cli/src/commands/generate/yargsHandlerHelpers.js @@ -47,10 +47,6 @@ export const customOrDefaultTemplatePath = ({ templatePath, ) - if (!getPaths()[side].generators) { - return defaultPath - } - // Where a custom template *might* exist, e.g. // /path/to/app/generatorTemplates/web/page/page.tsx.template const customPath = path.join( From fb4fa3bcd82f7f9855e6a581eef4649db598341c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 27 Dec 2025 13:49:07 +0100 Subject: [PATCH 07/15] Add to generator, and add more tests --- packages/cli/src/commands/generate.js | 2 + .../__snapshots__/package.test.ts.snap | 165 ++++++++++++-- .../package/__tests__/package.test.ts | 205 ++++++++++++++---- .../generate/package/packageHandler.js | 7 +- .../package/templates/README.md.template | 4 +- .../setup/generator/generatorHandler.js | 1 + 6 files changed, 314 insertions(+), 70 deletions(-) diff --git a/packages/cli/src/commands/generate.js b/packages/cli/src/commands/generate.js index b63f21a271..d9919268b8 100644 --- a/packages/cli/src/commands/generate.js +++ b/packages/cli/src/commands/generate.js @@ -13,6 +13,7 @@ import * as generateJob from './generate/job/job.js' import * as generateLayout from './generate/layout/layout.js' import * as generateModel from './generate/model/model.js' import * as generateOgImage from './generate/ogImage/ogImage.js' +import * as generatePackage from './generate/package/package.js' import * as generatePage from './generate/page/page.js' import * as generateRealtime from './generate/realtime/realtime.js' import * as generateScaffold from './generate/scaffold/scaffold.js' @@ -48,6 +49,7 @@ export const builder = (yargs) => .command(generateLayout) .command(generateModel) .command(generateOgImage) + .command(generatePackage) .command(generatePage) .command(generateRealtime) .command(generateScaffold) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index 24ab1db532..6e36bc8de3 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -1,20 +1,20 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`packageHandler > files > multi-word package names > creates a multi-word package 1`] = ` -"# Shared Package '@my-org/form-validators' +"# Shared Package '@my-camel-case-app/form-validators' Use code in this package by adding it to the dependencies on the side you want -to use it, with the special \`workspace:*\` version, and then importing it into -your code: +to use it, with the special \`workspace:*\` version. After that you can import it +into your code: \`\`\`json "dependencies": { - "@my-org/form-validators": "workspace:*" + "@my-camel-case-app/form-validators": "workspace:*" } \`\`\` \`\`\`javascript -import { formValidators } from '@my-org/form-validators'; +import { formValidators } from '@my-camel-case-app/form-validators'; \`\`\` " `; @@ -50,20 +50,20 @@ export type StandardScenario = ScenarioData `; exports[`packageHandler > files > multi-word package names > creates a multiWord package 1`] = ` -"# Shared Package '@my-org/form-validators' +"# Shared Package '@my-camel-case-app/form-validators' Use code in this package by adding it to the dependencies on the side you want -to use it, with the special \`workspace:*\` version, and then importing it into -your code: +to use it, with the special \`workspace:*\` version. After that you can import it +into your code: \`\`\`json "dependencies": { - "@my-org/form-validators": "workspace:*" + "@my-camel-case-app/form-validators": "workspace:*" } \`\`\` \`\`\`javascript -import { formValidators } from '@my-org/form-validators'; +import { formValidators } from '@my-camel-case-app/form-validators'; \`\`\` " `; @@ -98,33 +98,38 @@ export type StandardScenario = ScenarioData " `; -exports[`packageHandler > files > single word package names > creates a single word package 1`] = ` -"# Shared Package '@my-org/foo' +exports[`packageHandler > files > single word package name > infers package scope from project path > README.md 1`] = ` +"# Shared Package '@my-cedar-app/foo' Use code in this package by adding it to the dependencies on the side you want -to use it, with the special \`workspace:*\` version, and then importing it into -your code: +to use it, with the special \`workspace:*\` version. After that you can import it +into your code: \`\`\`json "dependencies": { - "@my-org/foo": "workspace:*" + "@my-cedar-app/foo": "workspace:*" } \`\`\` \`\`\`javascript -import { foo } from '@my-org/foo'; +import { foo } from '@my-cedar-app/foo'; \`\`\` " `; -exports[`packageHandler > files > single word package names > creates a single word package 2`] = ` -"export function foo() { - return 0 -} +exports[`packageHandler > files > single word package name > infers package scope from project path > foo.scenarios.ts 1`] = ` +"import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://cedarjs.com/docs/testing#scenarios +}) + +export type StandardScenario = ScenarioData " `; -exports[`packageHandler > files > single word package names > creates a single word package 3`] = ` +exports[`packageHandler > files > single word package name > infers package scope from project path > foo.test.ts 1`] = ` "import { foo } from './index.js' describe('foo', () => { @@ -135,7 +140,94 @@ describe('foo', () => { " `; -exports[`packageHandler > files > single word package names > creates a single word package 4`] = ` +exports[`packageHandler > files > single word package name > infers package scope from project path > index.ts 1`] = ` +"export function foo() { + return 0 +} +" +`; + +exports[`packageHandler > files > single word package name > infers package scope from project path > package.json 1`] = ` +"{ + "name": "@my-cedar-app/foo", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} +" +`; + +exports[`packageHandler > files > single word package name > infers package scope from project path > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "composite": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + }, + "include": ["src"], +} +" +`; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > index 1`] = ` +"export function foo() { + return 0 +} +" +`; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > packageJson 1`] = ` +"{ + "name": "@my-org/foo", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} +" +`; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > readme 1`] = ` +"# Shared Package '@my-org/foo' + +Use code in this package by adding it to the dependencies on the side you want +to use it, with the special \`workspace:*\` version. After that you can import it +into your code: + +\`\`\`json + "dependencies": { + "@my-org/foo": "workspace:*" + } +\`\`\` + +\`\`\`javascript +import { foo } from '@my-org/foo'; +\`\`\` +" +`; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > scenarios 1`] = ` "import type { ScenarioData } from '@cedarjs/testing/api' export const standard = defineScenario({ @@ -146,3 +238,32 @@ export const standard = defineScenario({ export type StandardScenario = ScenarioData " `; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > test 1`] = ` +"import { foo } from './index.js' + +describe('foo', () => { + it('should not throw any errors', async () => { + expect(foo()).not.toThrow() + }) +}) +" +`; + +exports[`packageHandler > files > single word package name > uses the provided package scope name > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "composite": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + }, + "include": ["src"], +} +" +`; diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index ee1f87eaa8..e9d6ff4a11 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -1,13 +1,66 @@ globalThis.__dirname = __dirname // Load shared mocks -import '../../../../lib/test' +import '../../../../lib/test.js' + +const mockBase = vi.hoisted(() => ({ path: '/path/to/project' })) + +vi.mock('../../../../lib/index.js', async (importOriginal) => { + const originalProjectConfig = await importOriginal() + + return { + ...originalProjectConfig, + getPaths: () => { + return { + base: mockBase.path, + api: { + prismaConfig: path.join( + // Current test folder + globalThis.__dirname, + 'fixtures', + 'prisma.config.cjs', + ), + dataMigrations: path.join(mockBase.path, 'api/dataMigrations'), + src: path.join(mockBase.path, 'api/src'), + jobs: path.join(mockBase.path, 'api/src/jobs'), + services: path.join(mockBase.path, 'api/src/services'), + directives: path.join(mockBase.path, 'api/src/directives'), + graphql: path.join(mockBase.path, 'api/src/graphql'), + functions: path.join(mockBase.path, 'api/src/functions'), + }, + web: { + base: path.join(mockBase.path, 'web'), + config: path.join(mockBase.path, 'web/config'), + src: path.join(mockBase.path, 'web/src'), + routes: path.join(mockBase.path, 'web/src/Routes.js'), + components: path.join(mockBase.path, 'web/src/components'), + layouts: path.join(mockBase.path, 'web/src/layouts'), + pages: path.join(mockBase.path, 'web/src/pages'), + app: path.join(mockBase.path, 'web/src/App.js'), + }, + scripts: path.join(mockBase.path, 'scripts'), + packages: path.join(mockBase.path, 'packages'), + generatorTemplates: path.join(mockBase.path, 'generatorTemplates'), + generated: { + base: path.join(mockBase.path, '.redwood'), + schema: path.join(mockBase.path, '.redwood/schema.graphql'), + types: { + includes: path.join(mockBase.path, '.redwood/types/includes'), + mirror: path.join(mockBase.path, '.redwood/types/mirror'), + }, + }, + } + }, + } +}) import fs from 'node:fs' import path from 'node:path' -import { describe, it, expect } from 'vitest' +import { vi, describe, it, expect } from 'vitest' +// @ts-expect-error - Importing a JS file +import type * as LibIndex from '../../../../lib/index.js' // @ts-expect-error - Importing a JS file import * as packageHandler from '../packageHandler.js' @@ -29,15 +82,17 @@ describe('packageHandler', () => { }) describe('files', () => { - const packagesPath = '/path/to/project/packages' + describe('single word package name', () => { + it('infers package scope from project path', async () => { + console.log('executing first test') + mockBase.path = '/path/to/my-cedar-app' - describe('single word package names', async () => { - const files = await packageHandler.files({ - name: '@my-org/foo', - typescript: true, - }) + console.log('calling first method that should call out to getPaths()') + const files = await packageHandler.files({ + name: 'foo', + typescript: true, + }) - it('creates a single word package', () => { const fileNames = Object.keys(files) expect(fileNames.length).toEqual(6) @@ -52,17 +107,74 @@ describe('packageHandler', () => { ]), ) - const readmePath = path.normalize(packagesPath + '/foo/README.md') - const indexPath = path.normalize(packagesPath + '/foo/src/index.ts') - const testPath = path.normalize(packagesPath + '/foo/src/foo.test.ts') + const packageJsonPath = path.normalize( + mockBase.path + '/packages/foo/package.json', + ) + const readmePath = path.normalize( + mockBase.path + '/packages/foo/README.md', + ) + const tsconfigJsonPath = path.normalize( + mockBase.path + '/packages/foo/tsconfig.json', + ) + const indexPath = path.normalize( + mockBase.path + '/packages/foo/src/index.ts', + ) + const testPath = path.normalize( + mockBase.path + '/packages/foo/src/foo.test.ts', + ) const scenariosPath = path.normalize( - packagesPath + '/foo/src/foo.scenarios.ts', + mockBase.path + '/packages/foo/src/foo.scenarios.ts', ) - expect(files[readmePath]).toMatchSnapshot() - expect(files[indexPath]).toMatchSnapshot() - expect(files[testPath]).toMatchSnapshot() - expect(files[scenariosPath]).toMatchSnapshot() + // Both making sure the file is valid json (parsing would fail otherwise) + // and that the package name is correct + const packageJson = JSON.parse(files[packageJsonPath]) + + expect(packageJson.name).toEqual('@my-cedar-app/foo') + expect(files[packageJsonPath]).toMatchSnapshot('package.json') + expect(files[readmePath]).toMatchSnapshot('README.md') + expect(files[tsconfigJsonPath]).toMatchSnapshot('tsconfig.json') + expect(files[indexPath]).toMatchSnapshot('index.ts') + expect(files[testPath]).toMatchSnapshot('foo.test.ts') + expect(files[scenariosPath]).toMatchSnapshot('foo.scenarios.ts') + }) + + it('uses kebab-case for package scope', async () => { + // Using both a hyphen and camelCase here on purpose to make sure it's + // handled correctly + mockBase.path = '/path/to/my-camelCaseApp' + + const files = await packageHandler.files({ + name: 'foo', + typescript: true, + }) + + const packageJsonPath = path.normalize( + mockBase.path + '/packages/foo/package.json', + ) + + // Both making sure the file is valid json (parsing would fail otherwise) + // and that the package name is correct + const packageJson = JSON.parse(files[packageJsonPath]) + + expect(packageJson.name).toEqual('@my-camel-case-app/foo') + }) + + it('uses the provided package scope name', async () => { + const files = await packageHandler.files({ + name: '@my-org/foo', + typescript: true, + }) + + const packageJsonPath = path.normalize( + mockBase.path + '/packages/foo/package.json', + ) + + // Both making sure the file is valid json (parsing would fail otherwise) + // and that the package name is correct + const packageJson = JSON.parse(files[packageJsonPath]) + + expect(packageJson.name).toEqual('@my-org/foo') }) }) @@ -74,21 +186,23 @@ describe('packageHandler', () => { describe('multi-word package names', () => { it('creates a multi-word package', async () => { const files = await packageHandler.files({ - name: '@my-org/form-validators', + name: 'form-validators', typescript: true, }) const readmePath = path.normalize( - packagesPath + '/form-validators/README.md', + mockBase.path + '/packages/form-validators/README.md', ) const indexPath = path.normalize( - packagesPath + '/form-validators/src/index.ts', + mockBase.path + '/packages/form-validators/src/index.ts', ) const testPath = path.normalize( - packagesPath + '/form-validators/src/formValidators.test.ts', + mockBase.path + + '/packages/form-validators/src/formValidators.test.ts', ) const scenariosPath = path.normalize( - packagesPath + '/form-validators/src/formValidators.scenarios.ts', + mockBase.path + + '/packages/form-validators/src/formValidators.scenarios.ts', ) expect(files[readmePath]).toMatchSnapshot() @@ -98,22 +212,26 @@ describe('packageHandler', () => { }) it('creates a multiWord package', async () => { + mockBase.path = '/path/to/myCamelCaseApp' + const files = await packageHandler.files({ - name: '@my-org/formValidators', + name: 'formValidators', typescript: true, }) const readmePath = path.normalize( - packagesPath + '/form-validators/README.md', + mockBase.path + '/packages/form-validators/README.md', ) const indexPath = path.normalize( - packagesPath + '/form-validators/src/index.ts', + mockBase.path + '/packages/form-validators/src/index.ts', ) const testPath = path.normalize( - packagesPath + '/form-validators/src/formValidators.test.ts', + mockBase.path + + '/packages/form-validators/src/formValidators.test.ts', ) const scenarioPath = path.normalize( - packagesPath + '/form-validators/src/formValidators.scenarios.ts', + mockBase.path + + '/packages/form-validators/src/formValidators.scenarios.ts', ) expect(files[readmePath]).toMatchSnapshot() @@ -123,28 +241,27 @@ describe('packageHandler', () => { }) }) - describe('generation of js files', async () => { + it('returns tests, scenario and main package file for JS', async () => { const jsFiles = await packageHandler.files({ name: 'Sample' }) + const fileNames = Object.keys(jsFiles) + expect(fileNames.length).toEqual(6) - it('returns tests, scenario and main package file for JS', () => { - const fileNames = Object.keys(jsFiles) - expect(fileNames.length).toEqual(6) - - expect(fileNames).toEqual( - expect.arrayContaining([ - expect.stringContaining('README.md'), - expect.stringContaining('package.json'), - // TODO: Make the script output jsconfig.json - expect.stringContaining('tsconfig.json'), - expect.stringContaining('index.js'), - expect.stringContaining('sample.test.js'), - expect.stringContaining('sample.scenarios.js'), - ]), - ) - }) + expect(fileNames).toEqual( + expect.arrayContaining([ + expect.stringContaining('README.md'), + expect.stringContaining('package.json'), + // TODO: Make the script output jsconfig.json + expect.stringContaining('tsconfig.json'), + expect.stringContaining('index.js'), + expect.stringContaining('sample.test.js'), + expect.stringContaining('sample.scenarios.js'), + ]), + ) }) }) + // This test is to make sure we don't forget to update the template when we + // upgrade TypeScript it('has the correct version of TypeScript in the generated package', () => { const cedarPackageJsonPath = path.normalize( path.join(__dirname, ...Array(7).fill('..'), 'package.json'), diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index 55e0a57f98..a74f630082 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -29,7 +29,9 @@ export const files = async ({ const base = path.basename(getPaths().base) const [orgName, name] = - nameArg[0] === '@' ? nameArg.split('/', 2) : ['@' + base, nameArg] + nameArg[0] === '@' + ? nameArg.split('/', 2) + : ['@' + paramCase(base), nameArg] const folderName = paramCase(name) const packageName = orgName + '/' + folderName @@ -129,7 +131,8 @@ export const handler = async ({ name, force, ...rest }) => { if (name.replaceAll('/', '').length < name.length - 1) { throw new Error( - `Invalid package name "${name}". Package names can have at most one slash.`, + `Invalid package name "${name}". ` + + 'Package names can have at most one slash.', ) } diff --git a/packages/cli/src/commands/generate/package/templates/README.md.template b/packages/cli/src/commands/generate/package/templates/README.md.template index e31f992b7a..716a69a85a 100644 --- a/packages/cli/src/commands/generate/package/templates/README.md.template +++ b/packages/cli/src/commands/generate/package/templates/README.md.template @@ -1,8 +1,8 @@ # Shared Package '${packageName}' Use code in this package by adding it to the dependencies on the side you want -to use it, with the special `workspace:*` version, and then importing it into -your code: +to use it, with the special `workspace:*` version. After that you can import it +into your code: ```json "dependencies": { diff --git a/packages/cli/src/commands/setup/generator/generatorHandler.js b/packages/cli/src/commands/setup/generator/generatorHandler.js index bf84ec3da9..cc62e458fd 100644 --- a/packages/cli/src/commands/setup/generator/generatorHandler.js +++ b/packages/cli/src/commands/setup/generator/generatorHandler.js @@ -10,6 +10,7 @@ const SIDE_MAP = { web: ['cell', 'component', 'layout', 'page', 'scaffold'], api: ['function', 'sdl', 'service'], scripts: ['script'], + packages: ['package'], } const copyGenerator = (name, { force }) => { From e115756bd13ccfb8d97e64c944b00509ca606f50 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 27 Dec 2025 14:18:27 +0100 Subject: [PATCH 08/15] workspace config task --- .../generate/package/packageHandler.js | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index a74f630082..fb758cc5cf 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -1,3 +1,4 @@ +import fs from 'node:fs' import path from 'node:path' import { paramCase, camelCase } from 'change-case' @@ -26,13 +27,14 @@ export const files = async ({ const outputFiles = [] + // TODO: Extract this out into its own task that is run first, and that stores + // the name on the Listr context so that it can be used in both the + // workspaces task and the files task that calls this function const base = path.basename(getPaths().base) - const [orgName, name] = nameArg[0] === '@' ? nameArg.split('/', 2) : ['@' + paramCase(base), nameArg] - const folderName = paramCase(name) const packageName = orgName + '/' + folderName const fileName = camelCase(name) @@ -120,8 +122,6 @@ export const files = async ({ }, Promise.resolve({})) } -// This could be built using createYargsForComponentGeneration; -// however, we need to add a message after generating the function files export const handler = async ({ name, force, ...rest }) => { recordTelemetryAttributes({ command: 'generate package', @@ -139,6 +139,41 @@ export const handler = async ({ name, force, ...rest }) => { let packageFiles = {} const tasks = new Listr( [ + { + title: 'Updating workspace config...', + task: async (_ctx, task) => { + const rootPackageJsonPath = path.join(getPaths().base, 'package.json') + const packageJson = JSON.parse( + await fs.promises.readFile(rootPackageJsonPath, 'utf8'), + ) + + if (!Array.isArray(packageJson.workspaces)) { + throw new Error( + 'Invalid workspace config in ' + rootPackageJsonPath, + ) + } + + const hasAsterixPackagesWorkspace = + packageJson.workspaces.includes('packages/*') + // TODO: Skip this task if "packages/" already exists + // const hasNamedPackagesWorkspace = packageJson.workspaces.find( + // (workspace) => workspace.startsWith('packages/'), + // ) + + if (hasAsterixPackagesWorkspace) { + task.skip('Workspaces already configured') + } else { + // TODO: Push "packages/" if other "packages/pkgName" exists + // instead of the generic "packages/*" + packageJson.workspaces.push('packages/*') + + await fs.promises.writeFile( + rootPackageJsonPath, + JSON.stringify(packageJson, null, 2), + ) + } + }, + }, { title: 'Generating package files...', task: async () => { From d3810ce137978e0c3c8202623f60e3261ada28b4 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 27 Dec 2025 19:36:12 +0100 Subject: [PATCH 09/15] nameVariants --- .../__snapshots__/package.test.ts.snap | 150 ++++++++---------- .../package/__tests__/package.test.ts | 86 +++++++++- .../generate/package/packageHandler.js | 71 ++++++--- 3 files changed, 190 insertions(+), 117 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index 6e36bc8de3..0f4c597217 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -98,8 +98,33 @@ export type StandardScenario = ScenarioData " `; -exports[`packageHandler > files > single word package name > infers package scope from project path > README.md 1`] = ` -"# Shared Package '@my-cedar-app/foo' +exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > index 1`] = ` +"export function formValidatorsPkg() { + return 0 +} +" +`; + +exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > packageJson 1`] = ` +"{ + "name": "@my-org/form-validators-pkg", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} +" +`; + +exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > readme 1`] = ` +"# Shared Package '@my-org/form-validators-pkg' Use code in this package by adding it to the dependencies on the side you want to use it, with the special \`workspace:*\` version. After that you can import it @@ -107,17 +132,17 @@ into your code: \`\`\`json "dependencies": { - "@my-cedar-app/foo": "workspace:*" + "@my-org/form-validators-pkg": "workspace:*" } \`\`\` \`\`\`javascript -import { foo } from '@my-cedar-app/foo'; +import { formValidatorsPkg } from '@my-org/form-validators-pkg'; \`\`\` " `; -exports[`packageHandler > files > single word package name > infers package scope from project path > foo.scenarios.ts 1`] = ` +exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > scenario 1`] = ` "import type { ScenarioData } from '@cedarjs/testing/api' export const standard = defineScenario({ @@ -129,87 +154,19 @@ export type StandardScenario = ScenarioData " `; -exports[`packageHandler > files > single word package name > infers package scope from project path > foo.test.ts 1`] = ` -"import { foo } from './index.js' +exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > test 1`] = ` +"import { formValidatorsPkg } from './index.js' -describe('foo', () => { +describe('formValidatorsPkg', () => { it('should not throw any errors', async () => { - expect(foo()).not.toThrow() + expect(formValidatorsPkg()).not.toThrow() }) }) " `; -exports[`packageHandler > files > single word package name > infers package scope from project path > index.ts 1`] = ` -"export function foo() { - return 0 -} -" -`; - -exports[`packageHandler > files > single word package name > infers package scope from project path > package.json 1`] = ` -"{ - "name": "@my-cedar-app/foo", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "watch": "tsc --watch" - }, - "devDependencies": { - "typescript": "5.9.3" - } -} -" -`; - -exports[`packageHandler > files > single word package name > infers package scope from project path > tsconfig.json 1`] = ` -"{ - "compilerOptions": { - "composite": true, - "moduleResolution": "Bundler", - "module": "ESNext", - "baseUrl": ".", - "rootDir": "src", - "outDir": "dist", - "sourceMap": true, - "declaration": true, - "declarationMap": true, - }, - "include": ["src"], -} -" -`; - -exports[`packageHandler > files > single word package name > uses the provided package scope name > index 1`] = ` -"export function foo() { - return 0 -} -" -`; - -exports[`packageHandler > files > single word package name > uses the provided package scope name > packageJson 1`] = ` -"{ - "name": "@my-org/foo", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "watch": "tsc --watch" - }, - "devDependencies": { - "typescript": "5.9.3" - } -} -" -`; - -exports[`packageHandler > files > single word package name > uses the provided package scope name > readme 1`] = ` -"# Shared Package '@my-org/foo' +exports[`packageHandler > files > single word package name > infers package scope from project path > README.md 1`] = ` +"# Shared Package '@my-cedar-app/foo' Use code in this package by adding it to the dependencies on the side you want to use it, with the special \`workspace:*\` version. After that you can import it @@ -217,17 +174,17 @@ into your code: \`\`\`json "dependencies": { - "@my-org/foo": "workspace:*" + "@my-cedar-app/foo": "workspace:*" } \`\`\` \`\`\`javascript -import { foo } from '@my-org/foo'; +import { foo } from '@my-cedar-app/foo'; \`\`\` " `; -exports[`packageHandler > files > single word package name > uses the provided package scope name > scenarios 1`] = ` +exports[`packageHandler > files > single word package name > infers package scope from project path > foo.scenarios.ts 1`] = ` "import type { ScenarioData } from '@cedarjs/testing/api' export const standard = defineScenario({ @@ -239,7 +196,7 @@ export type StandardScenario = ScenarioData " `; -exports[`packageHandler > files > single word package name > uses the provided package scope name > test 1`] = ` +exports[`packageHandler > files > single word package name > infers package scope from project path > foo.test.ts 1`] = ` "import { foo } from './index.js' describe('foo', () => { @@ -250,7 +207,32 @@ describe('foo', () => { " `; -exports[`packageHandler > files > single word package name > uses the provided package scope name > tsconfig.json 1`] = ` +exports[`packageHandler > files > single word package name > infers package scope from project path > index.ts 1`] = ` +"export function foo() { + return 0 +} +" +`; + +exports[`packageHandler > files > single word package name > infers package scope from project path > package.json 1`] = ` +"{ + "name": "@my-cedar-app/foo", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "5.9.3" + } +} +" +`; + +exports[`packageHandler > files > single word package name > infers package scope from project path > tsconfig.json 1`] = ` "{ "compilerOptions": { "composite": true, diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index e9d6ff4a11..3320229d5c 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -81,15 +81,43 @@ describe('packageHandler', () => { }) }) + describe('nameVariants', () => { + it('uses the provided scope name', () => { + expect(packageHandler.nameVariants('@myOrg/formValidators-pkg')).toEqual({ + name: 'formValidators-pkg', + folderName: 'form-validators-pkg', + packageName: '@my-org/form-validators-pkg', + fileName: 'formValidatorsPkg', + }) + }) + + it('constructs a scope name if none is provided', () => { + expect(packageHandler.nameVariants('foo')).toEqual({ + name: 'foo', + folderName: 'foo', + packageName: '@project/foo', + fileName: 'foo', + }) + }) + + it('handles camelCase when constructing a scope name', () => { + mockBase.path = '/path/to/cedarInc' + expect(packageHandler.nameVariants('MyFoo')).toEqual({ + name: 'MyFoo', + folderName: 'my-foo', + packageName: '@cedar-inc/my-foo', + fileName: 'myFoo', + }) + }) + }) + describe('files', () => { describe('single word package name', () => { it('infers package scope from project path', async () => { - console.log('executing first test') mockBase.path = '/path/to/my-cedar-app' - console.log('calling first method that should call out to getPaths()') const files = await packageHandler.files({ - name: 'foo', + ...packageHandler.nameVariants('foo'), typescript: true, }) @@ -145,7 +173,7 @@ describe('packageHandler', () => { mockBase.path = '/path/to/my-camelCaseApp' const files = await packageHandler.files({ - name: 'foo', + ...packageHandler.nameVariants('foo'), typescript: true, }) @@ -162,7 +190,7 @@ describe('packageHandler', () => { it('uses the provided package scope name', async () => { const files = await packageHandler.files({ - name: '@my-org/foo', + ...packageHandler.nameVariants('@my-org/foo'), typescript: true, }) @@ -186,7 +214,7 @@ describe('packageHandler', () => { describe('multi-word package names', () => { it('creates a multi-word package', async () => { const files = await packageHandler.files({ - name: 'form-validators', + ...packageHandler.nameVariants('form-validators'), typescript: true, }) @@ -215,7 +243,7 @@ describe('packageHandler', () => { mockBase.path = '/path/to/myCamelCaseApp' const files = await packageHandler.files({ - name: 'formValidators', + ...packageHandler.nameVariants('formValidators'), typescript: true, }) @@ -239,10 +267,52 @@ describe('packageHandler', () => { expect(files[testPath]).toMatchSnapshot() expect(files[scenarioPath]).toMatchSnapshot() }) + + it('uses the provided scope for multiWord-package name', async () => { + const files = await packageHandler.files({ + ...packageHandler.nameVariants('@myOrg/formValidators-pkg'), + typescript: true, + }) + + const readmePath = path.normalize( + mockBase.path + '/packages/form-validators-pkg/README.md', + ) + const packageJsonPath = path.normalize( + mockBase.path + '/packages/form-validators-pkg/package.json', + ) + const indexPath = path.normalize( + mockBase.path + '/packages/form-validators-pkg/src/index.ts', + ) + const testPath = path.normalize( + mockBase.path + + '/packages/form-validators-pkg/src/formValidatorsPkg.test.ts', + ) + const scenarioPath = path.normalize( + mockBase.path + + '/packages/form-validators-pkg/src/formValidatorsPkg.scenarios.ts', + ) + + // Both making sure the file is valid json (parsing would fail otherwise) + // and that the package name is correct + const packageJson = JSON.parse(files[packageJsonPath]) + + expect(packageJson.name).toEqual('@my-org/form-validators-pkg') + expect(files[indexPath]).toMatch( + 'export function formValidatorsPkg() {', + ) + + expect(files[readmePath]).toMatchSnapshot('readme') + expect(files[packageJsonPath]).toMatchSnapshot('packageJson') + expect(files[indexPath]).toMatchSnapshot('index') + expect(files[testPath]).toMatchSnapshot('test') + expect(files[scenarioPath]).toMatchSnapshot('scenario') + }) }) it('returns tests, scenario and main package file for JS', async () => { - const jsFiles = await packageHandler.files({ name: 'Sample' }) + const jsFiles = await packageHandler.files({ + ...packageHandler.nameVariants('Sample'), + }) const fileNames = Object.keys(jsFiles) expect(fileNames.length).toEqual(6) diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index fb758cc5cf..14fe437904 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -17,8 +17,27 @@ import { import { prepareForRollback } from '../../../lib/rollback.js' import { templateForFile } from '../yargsHandlerHelpers.js' +// Exported for testing +export function nameVariants(nameArg) { + const base = path.basename(getPaths().base) + + const [orgName, name] = nameArg.startsWith('@') + ? nameArg.slice(1).split('/', 2) + : [paramCase(base), nameArg] + + const folderName = paramCase(name) + const packageName = '@' + paramCase(orgName) + '/' + folderName + const fileName = camelCase(name) + + return { name, folderName, packageName, fileName } +} + +// Exported for testing export const files = async ({ - name: nameArg, + name, + folderName, + packageName, + fileName, typescript, tests: generateTests = true, ...rest @@ -27,18 +46,6 @@ export const files = async ({ const outputFiles = [] - // TODO: Extract this out into its own task that is run first, and that stores - // the name on the Listr context so that it can be used in both the - // workspaces task and the files task that calls this function - const base = path.basename(getPaths().base) - const [orgName, name] = - nameArg[0] === '@' - ? nameArg.split('/', 2) - : ['@' + paramCase(base), nameArg] - const folderName = paramCase(name) - const packageName = orgName + '/' + folderName - const fileName = camelCase(name) - const indexFile = await templateForFile({ name, side: 'packages', @@ -139,9 +146,15 @@ export const handler = async ({ name, force, ...rest }) => { let packageFiles = {} const tasks = new Listr( [ + { + title: 'Parsing package name...', + task: (ctx) => { + ctx.nameVariants = nameVariants(name) + }, + }, { title: 'Updating workspace config...', - task: async (_ctx, task) => { + task: async (ctx, task) => { const rootPackageJsonPath = path.join(getPaths().base, 'package.json') const packageJson = JSON.parse( await fs.promises.readFile(rootPackageJsonPath, 'utf8'), @@ -153,19 +166,24 @@ export const handler = async ({ name, force, ...rest }) => { ) } - const hasAsterixPackagesWorkspace = + const packagePath = `packages/${ctx.nameVariants.folderName}` + const hasWildcardPackagesWorkspace = packageJson.workspaces.includes('packages/*') - // TODO: Skip this task if "packages/" already exists - // const hasNamedPackagesWorkspace = packageJson.workspaces.find( - // (workspace) => workspace.startsWith('packages/'), - // ) + const hasNamedPackagesWorkspace = + packageJson.workspaces.includes(packagePath) + const hasOtherNamedPackages = packageJson.workspaces.some( + (workspace) => + workspace.startsWith('packages/') && workspace !== packagePath, + ) - if (hasAsterixPackagesWorkspace) { + if (hasWildcardPackagesWorkspace || hasNamedPackagesWorkspace) { task.skip('Workspaces already configured') } else { - // TODO: Push "packages/" if other "packages/pkgName" exists - // instead of the generic "packages/*" - packageJson.workspaces.push('packages/*') + if (hasOtherNamedPackages) { + packageJson.workspaces.push(packagePath) + } else { + packageJson.workspaces.push('packages/*') + } await fs.promises.writeFile( rootPackageJsonPath, @@ -176,8 +194,11 @@ export const handler = async ({ name, force, ...rest }) => { }, { title: 'Generating package files...', - task: async () => { - packageFiles = await files({ name, ...rest }) + task: async (ctx) => { + packageFiles = await files({ + ...ctx.nameVariants, + ...rest, + }) return writeFilesTask(packageFiles, { overwriteExisting: force }) }, }, From bf36e8b4aa29e905eb52c727a7a35d5c1239dbc4 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 30 Dec 2025 22:49:53 +0100 Subject: [PATCH 10/15] Scenarios make no sense for shared packages --- .../__snapshots__/package.test.ts.snap | 56 ++------------ .../package/__tests__/package.test.ts | 29 +------- .../generate/package/packageHandler.js | 73 +++++++++++++++---- .../package/templates/package.json.template | 1 + .../package/templates/tsconfig.json.template | 6 +- 5 files changed, 73 insertions(+), 92 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index 0f4c597217..42d6309fb3 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -37,18 +37,6 @@ describe('formValidators', () => { " `; -exports[`packageHandler > files > multi-word package names > creates a multi-word package 4`] = ` -"import type { ScenarioData } from '@cedarjs/testing/api' - -export const standard = defineScenario({ - // Define the "fixture" to write into your test database here - // See guide: https://cedarjs.com/docs/testing#scenarios -}) - -export type StandardScenario = ScenarioData -" -`; - exports[`packageHandler > files > multi-word package names > creates a multiWord package 1`] = ` "# Shared Package '@my-camel-case-app/form-validators' @@ -86,18 +74,6 @@ describe('formValidators', () => { " `; -exports[`packageHandler > files > multi-word package names > creates a multiWord package 4`] = ` -"import type { ScenarioData } from '@cedarjs/testing/api' - -export const standard = defineScenario({ - // Define the "fixture" to write into your test database here - // See guide: https://cedarjs.com/docs/testing#scenarios -}) - -export type StandardScenario = ScenarioData -" -`; - exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > index 1`] = ` "export function formValidatorsPkg() { return 0 @@ -117,6 +93,7 @@ exports[`packageHandler > files > multi-word package names > uses the provided s "watch": "tsc --watch" }, "devDependencies": { + "@cedarjs/testing": "2.2.1", "typescript": "5.9.3" } } @@ -142,18 +119,6 @@ import { formValidatorsPkg } from '@my-org/form-validators-pkg'; " `; -exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > scenario 1`] = ` -"import type { ScenarioData } from '@cedarjs/testing/api' - -export const standard = defineScenario({ - // Define the "fixture" to write into your test database here - // See guide: https://cedarjs.com/docs/testing#scenarios -}) - -export type StandardScenario = ScenarioData -" -`; - exports[`packageHandler > files > multi-word package names > uses the provided scope for multiWord-package name > test 1`] = ` "import { formValidatorsPkg } from './index.js' @@ -184,18 +149,6 @@ import { foo } from '@my-cedar-app/foo'; " `; -exports[`packageHandler > files > single word package name > infers package scope from project path > foo.scenarios.ts 1`] = ` -"import type { ScenarioData } from '@cedarjs/testing/api' - -export const standard = defineScenario({ - // Define the "fixture" to write into your test database here - // See guide: https://cedarjs.com/docs/testing#scenarios -}) - -export type StandardScenario = ScenarioData -" -`; - exports[`packageHandler > files > single word package name > infers package scope from project path > foo.test.ts 1`] = ` "import { foo } from './index.js' @@ -226,6 +179,7 @@ exports[`packageHandler > files > single word package name > infers package scop "watch": "tsc --watch" }, "devDependencies": { + "@cedarjs/testing": "2.2.1", "typescript": "5.9.3" } } @@ -236,8 +190,10 @@ exports[`packageHandler > files > single word package name > infers package scop "{ "compilerOptions": { "composite": true, - "moduleResolution": "Bundler", - "module": "ESNext", + "target": "ES2024", + "module": "ES2022", + "esModuleInterop": true, + "skipLibCheck": true, "baseUrl": ".", "rootDir": "src", "outDir": "dist", diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index 3320229d5c..cb5cccd45f 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -59,9 +59,7 @@ import path from 'node:path' import { vi, describe, it, expect } from 'vitest' -// @ts-expect-error - Importing a JS file import type * as LibIndex from '../../../../lib/index.js' -// @ts-expect-error - Importing a JS file import * as packageHandler from '../packageHandler.js' describe('packageHandler', () => { @@ -122,7 +120,7 @@ describe('packageHandler', () => { }) const fileNames = Object.keys(files) - expect(fileNames.length).toEqual(6) + expect(fileNames.length).toEqual(5) expect(fileNames).toEqual( expect.arrayContaining([ @@ -131,7 +129,6 @@ describe('packageHandler', () => { expect.stringContaining('tsconfig.json'), expect.stringContaining('index.ts'), expect.stringContaining('foo.test.ts'), - expect.stringContaining('foo.scenarios.ts'), ]), ) @@ -150,9 +147,6 @@ describe('packageHandler', () => { const testPath = path.normalize( mockBase.path + '/packages/foo/src/foo.test.ts', ) - const scenariosPath = path.normalize( - mockBase.path + '/packages/foo/src/foo.scenarios.ts', - ) // Both making sure the file is valid json (parsing would fail otherwise) // and that the package name is correct @@ -164,7 +158,6 @@ describe('packageHandler', () => { expect(files[tsconfigJsonPath]).toMatchSnapshot('tsconfig.json') expect(files[indexPath]).toMatchSnapshot('index.ts') expect(files[testPath]).toMatchSnapshot('foo.test.ts') - expect(files[scenariosPath]).toMatchSnapshot('foo.scenarios.ts') }) it('uses kebab-case for package scope', async () => { @@ -228,15 +221,10 @@ describe('packageHandler', () => { mockBase.path + '/packages/form-validators/src/formValidators.test.ts', ) - const scenariosPath = path.normalize( - mockBase.path + - '/packages/form-validators/src/formValidators.scenarios.ts', - ) expect(files[readmePath]).toMatchSnapshot() expect(files[indexPath]).toMatchSnapshot() expect(files[testPath]).toMatchSnapshot() - expect(files[scenariosPath]).toMatchSnapshot() }) it('creates a multiWord package', async () => { @@ -257,15 +245,10 @@ describe('packageHandler', () => { mockBase.path + '/packages/form-validators/src/formValidators.test.ts', ) - const scenarioPath = path.normalize( - mockBase.path + - '/packages/form-validators/src/formValidators.scenarios.ts', - ) expect(files[readmePath]).toMatchSnapshot() expect(files[indexPath]).toMatchSnapshot() expect(files[testPath]).toMatchSnapshot() - expect(files[scenarioPath]).toMatchSnapshot() }) it('uses the provided scope for multiWord-package name', async () => { @@ -287,10 +270,6 @@ describe('packageHandler', () => { mockBase.path + '/packages/form-validators-pkg/src/formValidatorsPkg.test.ts', ) - const scenarioPath = path.normalize( - mockBase.path + - '/packages/form-validators-pkg/src/formValidatorsPkg.scenarios.ts', - ) // Both making sure the file is valid json (parsing would fail otherwise) // and that the package name is correct @@ -305,16 +284,15 @@ describe('packageHandler', () => { expect(files[packageJsonPath]).toMatchSnapshot('packageJson') expect(files[indexPath]).toMatchSnapshot('index') expect(files[testPath]).toMatchSnapshot('test') - expect(files[scenarioPath]).toMatchSnapshot('scenario') }) }) - it('returns tests, scenario and main package file for JS', async () => { + it('returns the corrent files for JS', async () => { const jsFiles = await packageHandler.files({ ...packageHandler.nameVariants('Sample'), }) const fileNames = Object.keys(jsFiles) - expect(fileNames.length).toEqual(6) + expect(fileNames.length).toEqual(5) expect(fileNames).toEqual( expect.arrayContaining([ @@ -324,7 +302,6 @@ describe('packageHandler', () => { expect.stringContaining('tsconfig.json'), expect.stringContaining('index.js'), expect.stringContaining('sample.test.js'), - expect.stringContaining('sample.scenarios.js'), ]), ) }) diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index 14fe437904..64f3415fff 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -32,6 +32,33 @@ export function nameVariants(nameArg) { return { name, folderName, packageName, fileName } } +/** + * Generates the file structure and content for a new package. + * + * Creates all necessary files for a package including source files, configuration, + * README, and optionally test files. Handles both TypeScript and JavaScript generation. + * + * @param {Object} options - The file generation options + * @param {string} options.name - The package name + * @param {string} options.folderName - The folder name for the package (param-case) + * @param {string} options.packageName - The full scoped package name (e.g., '@org/package') + * @param {string} options.fileName - The camelCase file name + * @param {boolean} [options.typescript] - Whether to generate TypeScript files (defaults to JS if not provided) + * @param {boolean} [options.tests=true] - Whether to generate test files + * + * @returns {Promise} A promise that resolves to an object mapping file paths to their content + * + * @example + * // Generate TypeScript package files with tests + * const fileMap = await files({ + * name: 'MyPackage', + * folderName: 'my-package', + * packageName: '@myorg/my-package', + * fileName: 'myPackage', + * typescript: true, + * tests: true + * }) + */ // Exported for testing export const files = async ({ name, @@ -97,21 +124,7 @@ export const files = async ({ outputPath: path.join(folderName, 'src', `${fileName}.test${extension}`), }) - const scenarioFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'scenarios.ts.template', - templateVars: rest, - outputPath: path.join( - folderName, - 'src', - `${fileName}.scenarios${extension}`, - ), - }) - outputFiles.push(testFile) - outputFiles.push(scenarioFile) } return outputFiles.reduce(async (accP, [outputPath, content]) => { @@ -129,6 +142,38 @@ export const files = async ({ }, Promise.resolve({})) } +/** + * Handler for the generate package command. + * + * Creates a new package in the Cedar monorepo with the specified name and + * configuration. + * Sets up the package structure including source files, tests, configuration + * files, and updates the workspace configuration. + * + * @param {Object} options - The command options + * @param {string} options.name - The package name (can be scoped like + * '@org/package' or just 'package') + * @param {boolean} [options.force] - Whether to overwrite existing files + * @param {boolean} [options.typescript] - Whether to generate TypeScript files + * (passed in rest) + * @param {boolean} [options.tests] - Whether to generate test files (passed in + * rest) + * @param {boolean} [options.rollback] - Whether to enable rollback on failure + * (passed in rest) + * + * @returns {Promise} + * + * @throws {Error} If the package name contains more than one slash + * @throws {Error} If the workspace configuration is invalid + * + * @example + * // Generate a basic package + * await handler({ name: 'my-package', force: false }) + * + * @example + * // Generate a scoped TypeScript package with tests + * await handler({ name: '@myorg/my-package', force: false, typescript: true, tests: true }) + */ export const handler = async ({ name, force, ...rest }) => { recordTelemetryAttributes({ command: 'generate package', diff --git a/packages/cli/src/commands/generate/package/templates/package.json.template b/packages/cli/src/commands/generate/package/templates/package.json.template index 0f7454c32b..48ed8ea96e 100644 --- a/packages/cli/src/commands/generate/package/templates/package.json.template +++ b/packages/cli/src/commands/generate/package/templates/package.json.template @@ -9,6 +9,7 @@ "watch": "tsc --watch" }, "devDependencies": { + "@cedarjs/testing": "2.2.1", "typescript": "5.9.3" } } diff --git a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template index 056837f374..cfe4dfb9a7 100644 --- a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template +++ b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template @@ -1,8 +1,10 @@ { "compilerOptions": { "composite": true, - "moduleResolution": "Bundler", - "module": "ESNext", + "target": "ES2024", + "module": "ES2022", + "esModuleInterop": true, + "skipLibCheck": true, "baseUrl": ".", "rootDir": "src", "outDir": "dist", From 0be2bf3fa04b5c6be23afd7cc164881f7b657186 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 31 Dec 2025 11:45:56 +0100 Subject: [PATCH 11/15] Update tsconfig --- .../__snapshots__/package.test.ts.snap | 2 +- .../package/__tests__/package.test.ts | 127 +++++++++++++++++- .../generate/package/packageHandler.js | 48 ++++++- .../package/templates/tsconfig.json.template | 2 +- 4 files changed, 168 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index 42d6309fb3..389e64d80c 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -191,7 +191,7 @@ exports[`packageHandler > files > single word package name > infers package scop "compilerOptions": { "composite": true, "target": "ES2024", - "module": "ES2022", + "module": "Node20", "esModuleInterop": true, "skipLibCheck": true, "baseUrl": ".", diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index cb5cccd45f..f44b77abdf 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -5,6 +5,12 @@ import '../../../../lib/test.js' const mockBase = vi.hoisted(() => ({ path: '/path/to/project' })) +const { memfs, ufs, vol } = await vi.hoisted(async () => { + const { vol, fs: memfs } = await import('memfs') + const { ufs } = await import('unionfs') + return { memfs, ufs, vol } +}) + vi.mock('../../../../lib/index.js', async (importOriginal) => { const originalProjectConfig = await importOriginal() @@ -14,12 +20,7 @@ vi.mock('../../../../lib/index.js', async (importOriginal) => { return { base: mockBase.path, api: { - prismaConfig: path.join( - // Current test folder - globalThis.__dirname, - 'fixtures', - 'prisma.config.cjs', - ), + base: path.join(mockBase.path, 'api'), dataMigrations: path.join(mockBase.path, 'api/dataMigrations'), src: path.join(mockBase.path, 'api/src'), jobs: path.join(mockBase.path, 'api/src/jobs'), @@ -57,11 +58,27 @@ vi.mock('../../../../lib/index.js', async (importOriginal) => { import fs from 'node:fs' import path from 'node:path' -import { vi, describe, it, expect } from 'vitest' +import { vi, describe, it, expect, afterEach } from 'vitest' import type * as LibIndex from '../../../../lib/index.js' import * as packageHandler from '../packageHandler.js' +vi.mock('node:fs', async (importOriginal) => { + const { wrapFsForUnionfs } = await import( + '../../../../__tests__/ufsFsProxy.js' + ) + const originalFs = await importOriginal() + ufs.use(wrapFsForUnionfs(originalFs)).use(memfs as any) + return { + ...ufs, + default: ufs, + } +}) + +afterEach(() => { + mockBase.path = '/path/to/project' +}) + describe('packageHandler', () => { describe('handler', () => { it('throws on package name with two slashes', async () => { @@ -206,6 +223,8 @@ describe('packageHandler', () => { // packages. describe('multi-word package names', () => { it('creates a multi-word package', async () => { + mockBase.path = '/path/to/myCamelCaseApp' + const files = await packageHandler.files({ ...packageHandler.nameVariants('form-validators'), typescript: true, @@ -307,6 +326,100 @@ describe('packageHandler', () => { }) }) + describe('updateTsconfig', () => { + const tsconfigPath = path.join(mockBase.path, 'api', 'tsconfig.json') + + const tsconfig = ` + { + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "esModuleInterop": true, + "target": "ES2023", + "module": "Node16", // This is the line to update + "moduleResolution": "Node16", + "skipLibCheck": false, + "rootDirs": ["./src", "../.redwood/types/mirror/api/src"], + "paths": { + "src/*": ["./src/*", "../.redwood/types/mirror/api/src/*"], + "types/*": ["./types/*", "../types/*"], + "@cedarjs/testing": ["../node_modules/@cedarjs/testing/api"] + }, + "typeRoots": ["../node_modules/@types", "./node_modules/@types"], + "types": ["jest"], + // No end-of-line comma here, as you don't need that in tsconfig + // files, and shouldn't insert one when editing this file + "jsx": "react-jsx" + }, + "include": [ + "src", + "../.redwood/types/includes/all-*", + "../.redwood/types/includes/api-*", + "../types" + ] + } + ` + + it('updates from Node16 to Node20', async () => { + vol.fromJSON( + { + [tsconfigPath]: tsconfig, + 'redwood.toml': '', + }, + mockBase.path, + ) + + await packageHandler.updateTsconfig({ skip: () => {} }) + + // Comments are valid in tsconfig files, we want to make sure we don't + // remove those + expect(fs.readFileSync(tsconfigPath, 'utf8')).toMatch( + /"module": "Node20", \/\/ This is the line to update/, + ) + }) + + it('skips update if "module" is already Node20', async () => { + const node20tsconfig = tsconfig.replace( + '"module": "Node16",', + '"module": "Node20",', + ) + vol.fromJSON( + { + [tsconfigPath]: node20tsconfig, + 'redwood.toml': '', + }, + mockBase.path, + ) + + const skipFn = vi.fn() + await packageHandler.updateTsconfig({ skip: skipFn }) + + expect(skipFn).toHaveBeenCalled() + expect(fs.readFileSync(tsconfigPath, 'utf8')).toEqual(node20tsconfig) + }) + + it('skips update if "module" is already NodeNext', async () => { + vol.fromJSON( + { + [tsconfigPath]: tsconfig.replace( + '"module": "Node16",', + '"module": "NodeNext",', + ), + 'redwood.toml': '', + }, + mockBase.path, + ) + + const skipFn = vi.fn() + await packageHandler.updateTsconfig({ skip: skipFn }) + + expect(skipFn).toHaveBeenCalled() + expect(fs.readFileSync(tsconfigPath, 'utf8')).toMatch( + /"module": "NodeNext"/, + ) + }) + }) + // This test is to make sure we don't forget to update the template when we // upgrade TypeScript it('has the correct version of TypeScript in the generated package', () => { diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index 64f3415fff..f1cdca63b5 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -5,6 +5,15 @@ import { paramCase, camelCase } from 'change-case' import execa from 'execa' import { Listr } from 'listr2' +/** + * @typedef {Object} ListrContext + * @property {Object} nameVariants - The parsed name variants for the package + * @property {string} nameVariants.name - The base name + * @property {string} nameVariants.folderName - The param-case folder name + * @property {string} nameVariants.packageName - The full scoped package name + * @property {string} nameVariants.fileName - The camelCase file name + */ + import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { errorTelemetry } from '@cedarjs/telemetry' @@ -142,6 +151,35 @@ export const files = async ({ }, Promise.resolve({})) } +// Exported for testing +export async function updateTsconfig(task) { + const tsconfigPath = path.join(getPaths().api.base, 'tsconfig.json') + const tsconfig = await fs.promises.readFile(tsconfigPath, 'utf8') + const tsconfigLines = tsconfig.split('\n') + + const moduleLineIndex = tsconfigLines.findIndex((line) => + /^\s*"module":\s*"/.test(line), + ) + const moduleLine = tsconfigLines[moduleLineIndex] + + if ( + moduleLine.toLowerCase().includes('node20') || + // While Cedar doesn't officially endorse the usage of NodeNext, it + // will still work here, so I won't overwrite it + moduleLine.toLowerCase().includes('nodenext') + ) { + task.skip('tsconfig already up to date') + return + } + + tsconfigLines[moduleLineIndex] = moduleLine.replace( + /":\s*"[\w\d]+"/, + '": "Node20"', + ) + + await fs.promises.writeFile(tsconfigPath, tsconfigLines.join('\n')) +} + /** * Handler for the generate package command. * @@ -190,7 +228,7 @@ export const handler = async ({ name, force, ...rest }) => { let packageFiles = {} const tasks = new Listr( - [ + /** @type {import('listr2').ListrTask[]} */ ([ { title: 'Parsing package name...', task: (ctx) => { @@ -237,6 +275,12 @@ export const handler = async ({ name, force, ...rest }) => { } }, }, + { + title: 'Updating api side tsconfig file...', + task: async (_ctx, task) => { + await updateTsconfig(task) + }, + }, { title: 'Generating package files...', task: async (ctx) => { @@ -259,7 +303,7 @@ export const handler = async ({ name, force, ...rest }) => { ]) }, }, - ], + ]), { rendererOptions: { collapseSubtasks: false }, exitOnError: true }, ) diff --git a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template index cfe4dfb9a7..e64fd64256 100644 --- a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template +++ b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template @@ -2,7 +2,7 @@ "compilerOptions": { "composite": true, "target": "ES2024", - "module": "ES2022", + "module": "Node20", "esModuleInterop": true, "skipLibCheck": true, "baseUrl": ".", From 98e9d287e7472cce67d89221fe9d88b98369e815 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 31 Dec 2025 14:31:54 +0100 Subject: [PATCH 12/15] update test --- .../commands/generate/package/__tests__/package.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index f44b77abdf..d2ebf80c53 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -348,7 +348,7 @@ describe('packageHandler', () => { "typeRoots": ["../node_modules/@types", "./node_modules/@types"], "types": ["jest"], // No end-of-line comma here, as you don't need that in tsconfig - // files, and shouldn't insert one when editing this file + // files. We shouldn't insert one when editing this file "jsx": "react-jsx" }, "include": [ @@ -376,6 +376,9 @@ describe('packageHandler', () => { expect(fs.readFileSync(tsconfigPath, 'utf8')).toMatch( /"module": "Node20", \/\/ This is the line to update/, ) + expect(fs.readFileSync(tsconfigPath, 'utf8')).toEqual( + tsconfig.replace('"module": "Node16",', '"module": "Node20",'), + ) }) it('skips update if "module" is already Node20', async () => { @@ -395,7 +398,9 @@ describe('packageHandler', () => { await packageHandler.updateTsconfig({ skip: skipFn }) expect(skipFn).toHaveBeenCalled() - expect(fs.readFileSync(tsconfigPath, 'utf8')).toEqual(node20tsconfig) + expect(fs.readFileSync(tsconfigPath, 'utf8')).toMatch( + /"module": "Node20"/, + ) }) it('skips update if "module" is already NodeNext', async () => { From 4019e3d992d1b2fe8f829ba3b87ada580a0cc65c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 1 Jan 2026 22:23:06 +0100 Subject: [PATCH 13/15] ts-expect-error in test file --- .../src/commands/generate/package/__tests__/package.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index d2ebf80c53..dc58e37f8c 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -60,11 +60,14 @@ import path from 'node:path' import { vi, describe, it, expect, afterEach } from 'vitest' +// @ts-expect-error - No types for JS files import type * as LibIndex from '../../../../lib/index.js' +// @ts-expect-error - No types for JS files import * as packageHandler from '../packageHandler.js' vi.mock('node:fs', async (importOriginal) => { const { wrapFsForUnionfs } = await import( + // @ts-expect-error - No types for JS files '../../../../__tests__/ufsFsProxy.js' ) const originalFs = await importOriginal() From 2b9eea407041327df91ec6d4143f5d41b8a2c5a8 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 1 Jan 2026 23:10:52 +0100 Subject: [PATCH 14/15] target es2023 --- .../package/__tests__/__snapshots__/package.test.ts.snap | 2 +- .../commands/generate/package/templates/tsconfig.json.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap index 389e64d80c..1d4b4c62e6 100644 --- a/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap +++ b/packages/cli/src/commands/generate/package/__tests__/__snapshots__/package.test.ts.snap @@ -190,7 +190,7 @@ exports[`packageHandler > files > single word package name > infers package scop "{ "compilerOptions": { "composite": true, - "target": "ES2024", + "target": "ES2023", "module": "Node20", "esModuleInterop": true, "skipLibCheck": true, diff --git a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template index e64fd64256..a2e259fee3 100644 --- a/packages/cli/src/commands/generate/package/templates/tsconfig.json.template +++ b/packages/cli/src/commands/generate/package/templates/tsconfig.json.template @@ -1,7 +1,7 @@ { "compilerOptions": { "composite": true, - "target": "ES2024", + "target": "ES2023", "module": "Node20", "esModuleInterop": true, "skipLibCheck": true, From bad9ba73c815d32848d8995fe96c0ec4da030b1f Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 1 Jan 2026 23:54:15 +0100 Subject: [PATCH 15/15] filesTask --- .../package/__tests__/package.test.ts | 19 ++- .../commands/generate/package/filesTask.js | 113 +++++++++++++ .../generate/package/packageHandler.js | 151 +++--------------- 3 files changed, 146 insertions(+), 137 deletions(-) create mode 100644 packages/cli/src/commands/generate/package/filesTask.js diff --git a/packages/cli/src/commands/generate/package/__tests__/package.test.ts b/packages/cli/src/commands/generate/package/__tests__/package.test.ts index dc58e37f8c..6df43facfb 100644 --- a/packages/cli/src/commands/generate/package/__tests__/package.test.ts +++ b/packages/cli/src/commands/generate/package/__tests__/package.test.ts @@ -62,6 +62,9 @@ import { vi, describe, it, expect, afterEach } from 'vitest' // @ts-expect-error - No types for JS files import type * as LibIndex from '../../../../lib/index.js' +// TODO: Separate test file for filesTask.js +// @ts-expect-error - No types for JS files +import * as filesTask from '../filesTask.js' // @ts-expect-error - No types for JS files import * as packageHandler from '../packageHandler.js' @@ -85,7 +88,7 @@ afterEach(() => { describe('packageHandler', () => { describe('handler', () => { it('throws on package name with two slashes', async () => { - expect(() => + await expect(() => packageHandler.handler({ name: 'package//name' }), ).rejects.toThrowError( 'Invalid package name "package//name". Package names can have at most one slash.', @@ -134,7 +137,7 @@ describe('packageHandler', () => { it('infers package scope from project path', async () => { mockBase.path = '/path/to/my-cedar-app' - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('foo'), typescript: true, }) @@ -185,7 +188,7 @@ describe('packageHandler', () => { // handled correctly mockBase.path = '/path/to/my-camelCaseApp' - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('foo'), typescript: true, }) @@ -202,7 +205,7 @@ describe('packageHandler', () => { }) it('uses the provided package scope name', async () => { - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('@my-org/foo'), typescript: true, }) @@ -228,7 +231,7 @@ describe('packageHandler', () => { it('creates a multi-word package', async () => { mockBase.path = '/path/to/myCamelCaseApp' - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('form-validators'), typescript: true, }) @@ -252,7 +255,7 @@ describe('packageHandler', () => { it('creates a multiWord package', async () => { mockBase.path = '/path/to/myCamelCaseApp' - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('formValidators'), typescript: true, }) @@ -274,7 +277,7 @@ describe('packageHandler', () => { }) it('uses the provided scope for multiWord-package name', async () => { - const files = await packageHandler.files({ + const files = await filesTask.files({ ...packageHandler.nameVariants('@myOrg/formValidators-pkg'), typescript: true, }) @@ -310,7 +313,7 @@ describe('packageHandler', () => { }) it('returns the corrent files for JS', async () => { - const jsFiles = await packageHandler.files({ + const jsFiles = await filesTask.files({ ...packageHandler.nameVariants('Sample'), }) const fileNames = Object.keys(jsFiles) diff --git a/packages/cli/src/commands/generate/package/filesTask.js b/packages/cli/src/commands/generate/package/filesTask.js new file mode 100644 index 0000000000..51802ea40a --- /dev/null +++ b/packages/cli/src/commands/generate/package/filesTask.js @@ -0,0 +1,113 @@ +import path from 'node:path' + +import { transformTSToJS } from '../../../lib/index.js' +import { templateForFile } from '../yargsHandlerHelpers.js' + +/** + * Generates the file structure and content for a new package. + * + * Creates all necessary files for a package including source files, configuration, + * README, and optionally test files. Handles both TypeScript and JavaScript generation. + * + * @param {Object} options - The file generation options + * @param {string} options.name - The package name + * @param {string} options.folderName - The folder name for the package (param-case) + * @param {string} options.packageName - The full scoped package name (e.g., '@org/package') + * @param {string} options.fileName - The camelCase file name + * @param {boolean} [options.typescript] - Whether to generate TypeScript files (defaults to JS if not provided) + * @param {boolean} [options.tests=true] - Whether to generate test files + * + * @returns {Promise} A promise that resolves to an object mapping file paths to their content + * + * @example + * // Generate TypeScript package files with tests + * const fileMap = await files({ + * name: 'MyPackage', + * folderName: 'my-package', + * packageName: '@myorg/my-package', + * fileName: 'myPackage', + * typescript: true, + * tests: true + * }) + */ +export const files = async ({ + name, + folderName, + packageName, + fileName, + typescript, + tests: generateTests = true, + ...rest +}) => { + const extension = typescript ? '.ts' : '.js' + + const outputFiles = [] + + const indexFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'index.ts.template', + templateVars: rest, + outputPath: path.join(folderName, 'src', `index${extension}`), + }) + + const readmeFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'README.md.template', + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'README.md'), + }) + + const packageJsonFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'package.json.template', + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'package.json'), + }) + + const tsconfigFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'tsconfig.json.template', + templateVars: { packageName, ...rest }, + outputPath: path.join(folderName, 'tsconfig.json'), + }) + + outputFiles.push(indexFile) + outputFiles.push(readmeFile) + outputFiles.push(packageJsonFile) + outputFiles.push(tsconfigFile) + + if (generateTests) { + const testFile = await templateForFile({ + name, + side: 'packages', + generator: 'package', + templatePath: 'test.ts.template', + templateVars: rest, + outputPath: path.join(folderName, 'src', `${fileName}.test${extension}`), + }) + + outputFiles.push(testFile) + } + + return outputFiles.reduce(async (accP, [outputPath, content]) => { + const acc = await accP + + const template = + typescript || outputPath.endsWith('.md') || outputPath.endsWith('.json') + ? content + : await transformTSToJS(outputPath, content) + + return { + [outputPath]: template, + ...acc, + } + }, Promise.resolve({})) +} diff --git a/packages/cli/src/commands/generate/package/packageHandler.js b/packages/cli/src/commands/generate/package/packageHandler.js index f1cdca63b5..a10edd9c82 100644 --- a/packages/cli/src/commands/generate/package/packageHandler.js +++ b/packages/cli/src/commands/generate/package/packageHandler.js @@ -5,6 +5,15 @@ import { paramCase, camelCase } from 'change-case' import execa from 'execa' import { Listr } from 'listr2' +import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +import { errorTelemetry } from '@cedarjs/telemetry' + +import c from '../../../lib/colors.js' +import { getPaths, writeFilesTask } from '../../../lib/index.js' +import { prepareForRollback } from '../../../lib/rollback.js' + +import { files } from './filesTask.js' + /** * @typedef {Object} ListrContext * @property {Object} nameVariants - The parsed name variants for the package @@ -14,18 +23,6 @@ import { Listr } from 'listr2' * @property {string} nameVariants.fileName - The camelCase file name */ -import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' -import { errorTelemetry } from '@cedarjs/telemetry' - -import c from '../../../lib/colors.js' -import { - getPaths, - transformTSToJS, - writeFilesTask, -} from '../../../lib/index.js' -import { prepareForRollback } from '../../../lib/rollback.js' -import { templateForFile } from '../yargsHandlerHelpers.js' - // Exported for testing export function nameVariants(nameArg) { const base = path.basename(getPaths().base) @@ -41,116 +38,6 @@ export function nameVariants(nameArg) { return { name, folderName, packageName, fileName } } -/** - * Generates the file structure and content for a new package. - * - * Creates all necessary files for a package including source files, configuration, - * README, and optionally test files. Handles both TypeScript and JavaScript generation. - * - * @param {Object} options - The file generation options - * @param {string} options.name - The package name - * @param {string} options.folderName - The folder name for the package (param-case) - * @param {string} options.packageName - The full scoped package name (e.g., '@org/package') - * @param {string} options.fileName - The camelCase file name - * @param {boolean} [options.typescript] - Whether to generate TypeScript files (defaults to JS if not provided) - * @param {boolean} [options.tests=true] - Whether to generate test files - * - * @returns {Promise} A promise that resolves to an object mapping file paths to their content - * - * @example - * // Generate TypeScript package files with tests - * const fileMap = await files({ - * name: 'MyPackage', - * folderName: 'my-package', - * packageName: '@myorg/my-package', - * fileName: 'myPackage', - * typescript: true, - * tests: true - * }) - */ -// Exported for testing -export const files = async ({ - name, - folderName, - packageName, - fileName, - typescript, - tests: generateTests = true, - ...rest -}) => { - const extension = typescript ? '.ts' : '.js' - - const outputFiles = [] - - const indexFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'index.ts.template', - templateVars: rest, - outputPath: path.join(folderName, 'src', `index${extension}`), - }) - - const readmeFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'README.md.template', - templateVars: { packageName, ...rest }, - outputPath: path.join(folderName, 'README.md'), - }) - - const packageJsonFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'package.json.template', - templateVars: { packageName, ...rest }, - outputPath: path.join(folderName, 'package.json'), - }) - - const tsconfigFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'tsconfig.json.template', - templateVars: { packageName, ...rest }, - outputPath: path.join(folderName, 'tsconfig.json'), - }) - - outputFiles.push(indexFile) - outputFiles.push(readmeFile) - outputFiles.push(packageJsonFile) - outputFiles.push(tsconfigFile) - - if (generateTests) { - const testFile = await templateForFile({ - name, - side: 'packages', - generator: 'package', - templatePath: 'test.ts.template', - templateVars: rest, - outputPath: path.join(folderName, 'src', `${fileName}.test${extension}`), - }) - - outputFiles.push(testFile) - } - - return outputFiles.reduce(async (accP, [outputPath, content]) => { - const acc = await accP - - const template = - typescript || outputPath.endsWith('.md') || outputPath.endsWith('.json') - ? content - : await transformTSToJS(outputPath, content) - - return { - [outputPath]: template, - ...acc, - } - }, Promise.resolve({})) -} - // Exported for testing export async function updateTsconfig(task) { const tsconfigPath = path.join(getPaths().api.base, 'tsconfig.json') @@ -180,6 +67,13 @@ export async function updateTsconfig(task) { await fs.promises.writeFile(tsconfigPath, tsconfigLines.join('\n')) } +async function installAndBuild(folderName) { + const packagePath = path.join('packages', folderName) + await execa('yarn', ['install'], { stdio: 'inherit', cwd: getPaths().base }) + // TODO: `yarn cedar build ` + await execa('yarn', ['build'], { stdio: 'inherit', cwd: packagePath }) +} + /** * Handler for the generate package command. * @@ -277,20 +171,19 @@ export const handler = async ({ name, force, ...rest }) => { }, { title: 'Updating api side tsconfig file...', - task: async (_ctx, task) => { - await updateTsconfig(task) - }, + task: (_ctx, task) => updateTsconfig(task), }, { title: 'Generating package files...', task: async (ctx) => { - packageFiles = await files({ - ...ctx.nameVariants, - ...rest, - }) + packageFiles = await files({ ...ctx.nameVariants, ...rest }) return writeFilesTask(packageFiles, { overwriteExisting: force }) }, }, + { + title: 'Installing and building...', + task: (ctx) => installAndBuild(ctx.nameVariants.folderName), + }, { title: 'Cleaning up...', task: () => {