Skip to content

feat: adds support for detecting NX monorepos #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 11, 2025
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,5 @@ Temporary Items

.netlify
.angular
.nx
.idea
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"pretest:fixtures:angular-19-common-engine": "cd tests/fixtures/angular-19-common-engine && npm ci",
"pretest:fixtures:angular-19-app-engine": "cd tests/fixtures/angular-19-app-engine && npm ci",
"pretest:fixtures:angular-19-prerender-false": "cd tests/fixtures/angular-19-prerender-false && npm ci",
"pretest:fixtures:nx-angular-19-common-engine": "cd tests/fixtures/nx-angular-19-common-engine && npm ci",
"pretest:fixtures:nx-angular-19-app-engine": "cd tests/fixtures/nx-angular-19-app-engine && npm ci",
"pretest": "run-s pretest:*",
"test": "node --test"
},
Expand Down
22 changes: 17 additions & 5 deletions src/helpers/fixOutputDir.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ const { join } = require('path')
const getAngularJson = require('./getAngularJson')
const { getProject } = require('./setUpEdgeFunction')

const fixOutputDir = async function ({ failBuild, failPlugin, siteRoot, PUBLISH_DIR, IS_LOCAL, netlifyConfig }) {
const angularJson = getAngularJson({ failPlugin, siteRoot })
const project = getProject(angularJson, failBuild)
const fixOutputDir = async function ({
failBuild,
failPlugin,
siteRoot,
PUBLISH_DIR,
IS_LOCAL,
netlifyConfig,
workspaceType,
packagePath,
}) {
const angularJson = getAngularJson({ failPlugin, siteRoot, workspaceType, packagePath })
const project = getProject(angularJson, failBuild, workspaceType === 'nx')

const { outputPath } = project.architect.build.options
const { outputPath } = workspaceType === 'nx' ? project.targets.build.options : project.architect.build.options

const isApplicationBuilder = project.architect.build.builder.endsWith(':application')
const isApplicationBuilder =
workspaceType === 'nx'
? project.targets.build.executor.endsWith(':application')
: project.architect.build.builder.endsWith(':application')
const correctPublishDir = isApplicationBuilder ? join(outputPath, 'browser') : outputPath
if (correctPublishDir === PUBLISH_DIR) {
return
Expand Down
23 changes: 22 additions & 1 deletion src/helpers/getAngularJson.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { join } = require('node:path')
const process = require('process')

const { existsSync, readJsonSync } = require('fs-extra')

Expand All @@ -7,10 +8,30 @@ const { existsSync, readJsonSync } = require('fs-extra')
* @param {Object} obj
* @param {string} obj.siteRoot Root directory of an app
* @param {(msg: string) => never} obj.failPlugin Function to fail the plugin
* @param {'nx' | 'default'} obj.workspaceType Type of monorepo, dictates what json file to open
* @param {string} obj.packagePath The path to the package directory
*
* @returns {any}
*/
const getAngularJson = function ({ failPlugin, siteRoot }) {
const getAngularJson = function ({ failPlugin, siteRoot, workspaceType, packagePath }) {
if (workspaceType === 'nx') {
if ((packagePath ?? '').length === 0) {
return failPlugin(
`packagePath must be set to the location of the project.json being built when deploying an NX monorepo, e.g. "apps/{project-name}"`,
)
}

if (!existsSync(join(siteRoot, packagePath, 'project.json'))) {
return failPlugin(`No project.json found in ${packagePath}`)
}

try {
return readJsonSync(join(siteRoot, packagePath, 'project.json'))
} catch {
return failPlugin(`Could not parse the contents of project.json`)
}
}

if (!existsSync(join(siteRoot, 'angular.json'))) {
return failPlugin(`No angular.json found at project root`)
}
Expand Down
18 changes: 14 additions & 4 deletions src/helpers/getAngularRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,33 @@ const process = require('process')
/**
* If we're in a monorepo then the site root may not be the same as the base directory
* If there's no angular.json in the root, we instead look for it 2 levels up from the publish dir
*
* @returns {{siteRoot: string, workspaceType: 'nx' | 'default'}}
*/
const getAngularRoot = ({ failBuild, netlifyConfig }) => {
let angularRoot = process.cwd()

// This could be an NX repo, so check for the existence of nx.json too
let angularJsonExists = existsSync(path.join(angularRoot, 'angular.json'))
let nxJsonExists = existsSync(path.join(angularRoot, 'nx.json'))

if (
!existsSync(path.join(angularRoot, 'angular.json')) &&
!angularJsonExists &&
!nxJsonExists &&
netlifyConfig.build.publish &&
netlifyConfig.build.publish !== angularRoot
) {
angularRoot = path.dirname(path.resolve(path.join(netlifyConfig.build.publish, '..', '..')))
angularJsonExists = existsSync(path.join(angularRoot, 'angular.json'))
nxJsonExists = existsSync(path.join(angularRoot, 'nx.json'))

if (!existsSync(path.join(angularRoot, 'angular.json'))) {
if (!angularJsonExists && !nxJsonExists) {
return failBuild(
`Could not locate your angular.json at your project root or two levels above your publish directory. Make sure your publish directory is set to "{PATH_TO_YOUR_SITE}/dist/{YOUR_PROJECT_NAME}/browser", where {YOUR_PROJECT_NAME} is 'defaultProject' in your angular.json.`,
`Could not locate your angular.json/nx.json at your project root or two levels above your publish directory. Make sure your publish directory is set to "{PATH_TO_YOUR_SITE}/dist/{YOUR_PROJECT_NAME}/browser", where {YOUR_PROJECT_NAME} is 'defaultProject' in your angular.json.`,
)
}
}
return angularRoot
return { siteRoot: angularRoot, workspaceType: nxJsonExists ? 'nx' : 'default' }
}

module.exports = getAngularRoot
4 changes: 3 additions & 1 deletion src/helpers/knownServerTsSignatures.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
"0e451aa946aca10c9d6782ac008748fcd39236d3ad1cc9868100a2981105e010": "CommonEngine",
"577f7bc87c16bd10bac499e228ef24d23dc4dd516e469b5db3eefae4edcf6345": "CommonEngine",
"5678601ed12556305074503967b44ae42c45c268579db057c25cbf4b21a7212e": "CommonEngine",
"76419eb94b4b8672ba3bd79d34c5a66c7c30ff173995ecc6e0adc5808b86822d": "AppEngine"
"33d360cdf4819d90afeecd49952241191ee490900fa919a46f990186be3e8b5f": "CommonEngine",
"76419eb94b4b8672ba3bd79d34c5a66c7c30ff173995ecc6e0adc5808b86822d": "AppEngine",
"a5aad843a116e34ce61264117cba981cff5eea3e6672815a4db08e7b4e5599d6": "AppEngine"
}
14 changes: 7 additions & 7 deletions src/helpers/serverModuleHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,22 @@ const guessUsedEngine = function (serverModuleContents) {
* @param {string} obj.angularVersion Angular version
* @param {string} obj.siteRoot Root directory of an app
* @param {(msg: string) => never} obj.failPlugin Function to fail the plugin
* @param {'nx' | 'default'} obj.workspaceType The workspace type being parsed
* @param {string} obj.packagePath The path to the package directory
* * @param {(msg: string) => never} obj.failBuild Function to fail the build
*
* @returns {'AppEngine' | 'CommonEngine' | undefined}
*/
const fixServerTs = async function ({ angularVersion, siteRoot, failPlugin, failBuild }) {
const fixServerTs = async function ({ angularVersion, siteRoot, failPlugin, failBuild, workspaceType, packagePath }) {
if (!satisfies(angularVersion, '>=19.0.0-rc', { includePrerelease: true })) {
// for pre-19 versions, we don't need to do anything
return
}

const angularJson = getAngularJson({ failPlugin, siteRoot })
const angularJson = getAngularJson({ failPlugin, siteRoot, workspaceType, packagePath })

const project = getProject(angularJson, failBuild)
const {
architect: { build },
} = project
const project = getProject(angularJson, failBuild, workspaceType === 'nx')
const build = workspaceType === 'nx' ? project.targets.build : project.architect.build

serverModuleLocation = build?.options?.ssr?.entry
if (!serverModuleLocation || !existsSync(serverModuleLocation)) {
Expand All @@ -131,7 +131,7 @@ const fixServerTs = async function ({ angularVersion, siteRoot, failPlugin, fail
)
}

// check wether project is using stable CommonEngine or Developer Preview AppEngine
// check whether project is using stable CommonEngine or Developer Preview AppEngine
const serverModuleContents = await readFile(serverModuleLocation, 'utf8')

const usedEngineBasedOnKnownSignatures = getEngineBasedOnKnownSignatures(serverModuleContents)
Expand Down
13 changes: 9 additions & 4 deletions src/helpers/setUpEdgeFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ const getAllFilesIn = (dir) =>

const toPosix = (path) => path.split(sep).join(posix.sep)

const getProject = (angularJson, failBuild) => {
const getProject = (angularJson, failBuild, isNxWorkspace = false) => {
const selectedProject = process.env.ANGULAR_PROJECT

if (isNxWorkspace) {
return angularJson
}

if (selectedProject) {
const project = angularJson.projects[selectedProject]
if (!project) {
Expand Down Expand Up @@ -98,7 +103,7 @@ const setUpEdgeFunction = async ({ outputDir, constants, failBuild, usedEngine }
const document = Buffer.from(${JSON.stringify(
Buffer.from(html, 'utf-8').toString('base64'),
)}, 'base64').toString("utf-8");

export default async (request, context) => {
const html = await renderApplication(bootstrap, {
url: request.url,
Expand Down Expand Up @@ -155,7 +160,7 @@ const setUpEdgeFunction = async ({ outputDir, constants, failBuild, usedEngine }
// reading file is needed for inlining CSS, but failing to do so is
// not causing fatal error so we just ignore it here
}

return originalReadFile.apply(globalThis.Deno, args)
}
} catch {
Expand Down Expand Up @@ -194,7 +199,7 @@ const setUpEdgeFunction = async ({ outputDir, constants, failBuild, usedEngine }
ssrFunctionContent = /* javascript */ `
import { netlifyAppEngineHandler } from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/server.mjs";
import "./fixup-event.mjs";

export default netlifyAppEngineHandler;
`
}
Expand Down
23 changes: 15 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let usedEngine
module.exports = {
async onPreBuild({ netlifyConfig, utils, constants }) {
const { failBuild, failPlugin } = utils.build
const siteRoot = getAngularRoot({ failBuild, netlifyConfig })
const { siteRoot, workspaceType } = getAngularRoot({ failBuild, netlifyConfig })
const angularVersion = await getAngularVersion(siteRoot)
isValidAngularProject = validateAngularVersion(angularVersion)

Expand All @@ -36,9 +36,18 @@ module.exports = {
PUBLISH_DIR: constants.PUBLISH_DIR,
IS_LOCAL: constants.IS_LOCAL,
netlifyConfig,
workspaceType,
packagePath: constants.PACKAGE_PATH,
})

usedEngine = await fixServerTs({ angularVersion, siteRoot, failPlugin, failBuild })
usedEngine = await fixServerTs({
angularVersion,
siteRoot,
failPlugin,
failBuild,
workspaceType,
packagePath: constants.PACKAGE_PATH,
})
},
async onBuild({ utils, netlifyConfig, constants }) {
await revertServerTsFix()
Expand All @@ -48,13 +57,11 @@ module.exports = {

const { failBuild, failPlugin } = utils.build

const siteRoot = getAngularRoot({ failBuild, netlifyConfig })
const angularJson = getAngularJson({ failPlugin, siteRoot })
const { siteRoot, workspaceType } = getAngularRoot({ failBuild, netlifyConfig, onBuild: true })
const angularJson = getAngularJson({ failPlugin, siteRoot, workspaceType, packagePath: constants.PACKAGE_PATH })

const project = getProject(angularJson, failBuild)
const {
architect: { build },
} = project
const project = getProject(angularJson, failBuild, workspaceType === 'nx')
const build = workspaceType === 'nx' ? project.targets.build : project.architect.build
const outputDir = build?.options?.outputPath
if (!outputDir || !existsSync(outputDir)) {
return failBuild('Could not find build output directory')
Expand Down
17 changes: 17 additions & 0 deletions tests/fixtures/nx-angular-19-app-engine/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false

[*.md]
max_line_length = off
trim_trailing_whitespace = false
47 changes: 47 additions & 0 deletions tests/fixtures/nx-angular-19-app-engine/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings

# System files
.DS_Store
Thumbs.db



.nx/cache
.nx/workspace-data
5 changes: 5 additions & 0 deletions tests/fixtures/nx-angular-19-app-engine/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache
/.nx/workspace-data
3 changes: 3 additions & 0 deletions tests/fixtures/nx-angular-19-app-engine/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"recommendations": [
"angular.ng-template",
"nrwl.angular-console",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
20 changes: 20 additions & 0 deletions tests/fixtures/nx-angular-19-app-engine/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}
Loading