Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions packages/cli/src/commands/__tests__/build.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
vi.mock('@cedarjs/project-config', async (importOriginal) => {
const originalProjectConfig = await importOriginal()
// mock Telemetry for CLI commands so they don't try to spawn a process
vi.mock('@cedarjs/telemetry', () => {
return {
errorTelemetry: () => vi.fn(),
timedTelemetry: (_argv, _options, callback) => {
return callback()
},
}
})

vi.mock('@cedarjs/project-config', async () => {
return {
...originalProjectConfig,
getPaths: () => {
return {
base: '/mocked/project',
api: {
dist: '/mocked/project/api/dist',
prismaConfig: '/mocked/project/api/prisma.config.js',
Expand All @@ -20,22 +29,31 @@ vi.mock('@cedarjs/project-config', async (importOriginal) => {
// the values it currently reads are optional.
}
},
resolveFile: () => {
// Used by packages/cli/src/lib/index.js
},
}
})

vi.mock('node:fs', async () => {
const actualFs = await vi.importActual('node:fs')

return {
default: {
...actualFs,
// Mock the existence of the Prisma config file
existsSync: (path) => {
if (path === '/mocked/project/api/prisma.config.js') {
// Mock the existence of the Prisma config file
return true
} else if (path.endsWith('package.json')) {
// Mock the existence of all packages/<pkg-name>/package.json files
return true
}

return actualFs.existsSync(path)
},
readFileSync: () => {
// Reading /mocked/project/package.json
// It just needs a workspace config section
return JSON.stringify({
workspaces: ['api', 'web', 'packages/*'],
})
// }
},
},
}
Expand All @@ -54,17 +72,21 @@ vi.mock('execa', () => ({
})),
}))

import { handler } from '../build.js'
import { handler } from '../buildHandler.js'

afterEach(() => {
vi.clearAllMocks()
})

test('the build tasks are in the correct sequence', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {})

await handler({})

expect(Listr.mock.calls[0][0].map((x) => x.title)).toMatchInlineSnapshot(`
[
"Generating Prisma Client...",
"Building Packages...",
"Verifying graphql schema...",
"Building API...",
"Building Web...",
Expand Down
41 changes: 36 additions & 5 deletions packages/cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ export const command = 'build [workspace..]'
export const description = 'Build for production'

export const builder = (yargs) => {
const choices = workspaces()

yargs
.positional('workspace', {
choices,
default: choices,
description: 'What workspace(s) to build',
default: ['web', 'api', 'packages/*'],
description:
'What workspace(s) to build. Valid values are: web, api, packages/*, ' +
'<package-name>',
type: 'array',
})
.option('verbose', {
Expand Down Expand Up @@ -47,6 +46,38 @@ export const builder = (yargs) => {
includeEpilogue: false,
})
})
.check((argv) => {
const workspacesArg = argv.workspace

if (!Array.isArray(workspacesArg)) {
return 'Workspace must be an array'
}

// Remove all default workspace names and then check if there are any
// remaining workspaces to build. This is an optimization to avoid calling
// `workspaces({ includePackages: true }) as that's a somewhat expensive
// method call that hits the filesystem and parses files

const filtered = workspacesArg.filter(
(item) => item !== 'api' && item !== 'web' && item !== 'packages/*',
)

if (filtered.length === 0) {
return true
}

const workspaceNames = workspaces({ includePackages: true })

if (!workspacesArg.every((item) => workspaceNames.includes(item))) {
return (
c.error(`Unknown workspace(s) ${workspacesArg.join(' ')}`) +
'\n\nValid values are: ' +
workspaceNames.join(', ')
)
}

return true
})
.epilogue(
`Also see the ${terminalLink(
'CedarJS CLI Reference',
Expand Down
69 changes: 67 additions & 2 deletions packages/cli/src/commands/buildHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'

import concurrently from 'concurrently'
import execa from 'execa'
import { Listr } from 'listr2'
import { terminalLink } from 'termi-link'
Expand All @@ -11,13 +12,14 @@ import { buildApi, cleanApiBuild } from '@cedarjs/internal/dist/build/api'
import { generate } from '@cedarjs/internal/dist/generate/generate'
import { loadAndValidateSdls } from '@cedarjs/internal/dist/validateSchema'
import { detectPrerenderRoutes } from '@cedarjs/prerender/detection'
import { timedTelemetry } from '@cedarjs/telemetry'
import { timedTelemetry, errorTelemetry } from '@cedarjs/telemetry'

import { exitWithError } from '../lib/exit.js'
import { generatePrismaCommand } from '../lib/generatePrismaClient.js'
import { getPaths, getConfig } from '../lib/index.js'

export const handler = async ({
workspace = ['api', 'web'],
workspace = ['api', 'web', 'packages/*'],
verbose = false,
prisma = true,
prerender = true,
Expand All @@ -43,6 +45,14 @@ export const handler = async ({
prismaSchemaExists &&
(workspace.includes('api') || prerenderRoutes.length > 0)

const packageJsonPath = path.join(cedarPaths.base, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const packageJsonWorkspaces = packageJson.workspaces
const restWorkspaces =
Array.isArray(packageJsonWorkspaces) && packageJsonWorkspaces.length > 2
? workspace.filter((w) => w !== 'api' && w !== 'web')
: []

const gqlFeaturesTaskTitle = `Generating types needed for ${[
useFragments && 'GraphQL Fragments',
useTrustedDocuments && 'Trusted Documents',
Expand All @@ -62,6 +72,61 @@ export const handler = async ({
})
},
},
restWorkspaces.length > 0 && {
title: 'Building Packages...',
task: async () => {
// fs.globSync requires forward slashes as path separators in patterns,
// even on Windows.
const globPattern = path
.join(cedarPaths.packages, '*')
.replaceAll('\\', '/')
const allPackagePaths = await Array.fromAsync(
fs.promises.glob(globPattern),
)

// restWorkspaces can be ['packages/*'] or
// ['@my-org/pkg-one', '@my-org/pkg-two', 'packages/pkg-three', etc]
// We need to map that to filesystem paths
const workspacePaths = restWorkspaces.some((w) => w === 'packages/*')
? allPackagePaths
: restWorkspaces.map((w) => {
const workspacePath = path.join(
cedarPaths.packages,
w.split('/').at(-1),
)

if (!fs.existsSync(workspacePath)) {
throw new Error(`Workspace not found: ${workspacePath}`)
}

return workspacePath
})

const { result } = concurrently(
workspacePaths.map((workspacePath) => {
return {
command: `yarn build`,
name: workspacePath.split('/').at(-1),
cwd: workspacePath,
}
}),
{
prefix: '{name} |',
timestampFormat: 'HH:mm:ss',
},
)

await result.catch((e) => {
if (e?.message) {
errorTelemetry(
process.argv,
`Error concurrently building sides: ${e.message}`,
)
exitWithError(e)
}
})
},
},
// If using GraphQL Fragments or Trusted Documents, then we need to use
// codegen to generate the types needed for possible types and the trusted
// document store hashes
Expand Down
29 changes: 25 additions & 4 deletions packages/cli/src/lib/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,40 @@ export const isTypeScriptProject = () => {
)
}

export const workspaces = () => {
const paths = getPaths()
export function workspaces({ includePackages = false } = {}) {
const cedarPaths = getPaths()

let workspaces = []

if (fs.existsSync(path.join(paths.web.base, 'package.json'))) {
if (fs.existsSync(path.join(cedarPaths.web.base, 'package.json'))) {
workspaces = [...workspaces, 'web']
}

if (fs.existsSync(path.join(paths.api.base, 'package.json'))) {
if (fs.existsSync(path.join(cedarPaths.api.base, 'package.json'))) {
workspaces = [...workspaces, 'api']
}

if (includePackages) {
// fs.globSync requires forward slashes as path separators in patterns,
// even on Windows.
const globPattern = path
.join(cedarPaths.packages, '*')
.replaceAll('\\', '/')
// TODO: See if we can make this async
const allPackagePaths = fs.globSync(globPattern)

workspaces = [
...workspaces,
'packages/*',
...allPackagePaths.map((p) => p.split('/').at(-1)),
...allPackagePaths.map((p) => p.split('/').slice(-2).join('/')),
...allPackagePaths.map((p) => {
const packageJsonPath = path.join(p, 'package.json')
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).name
}),
]
}

return workspaces
}

Expand Down
Loading