diff --git a/.changeset/tough-mugs-dream.md b/.changeset/tough-mugs-dream.md new file mode 100644 index 000000000..dc67559ab --- /dev/null +++ b/.changeset/tough-mugs-dream.md @@ -0,0 +1,5 @@ +--- +"modular-scripts": patch +--- + +Fix `modular build` crashing when the selected workspace(s) are not in `packages`. diff --git a/__fixtures__/custom-workspace-root/apps/alpha/package.json b/__fixtures__/custom-workspace-root/apps/alpha/package.json new file mode 100644 index 000000000..9236c58c3 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/alpha/package.json @@ -0,0 +1,9 @@ +{ + "name": "alpha", + "private": false, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/custom-workspace-root/apps/alpha/src/__tests__/index.test.ts b/__fixtures__/custom-workspace-root/apps/alpha/src/__tests__/index.test.ts new file mode 100644 index 000000000..62824a092 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/alpha/src/__tests__/index.test.ts @@ -0,0 +1,5 @@ +import add from '../index'; + +test('it should add two numbers', () => { + expect(add(0.1, 0.2)).toEqual(0.30000000000000004); +}); diff --git a/__fixtures__/custom-workspace-root/apps/alpha/src/index.ts b/__fixtures__/custom-workspace-root/apps/alpha/src/index.ts new file mode 100644 index 000000000..b92ce9fdf --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/alpha/src/index.ts @@ -0,0 +1,3 @@ +export default function add(a: number, b: number): number { + return a + b; +} diff --git a/__fixtures__/custom-workspace-root/apps/app/package.json b/__fixtures__/custom-workspace-root/apps/app/package.json new file mode 100644 index 000000000..e51745553 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "app", + "private": true, + "modular": { + "type": "app" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/custom-workspace-root/apps/app/public/favicon.ico b/__fixtures__/custom-workspace-root/apps/app/public/favicon.ico new file mode 100644 index 000000000..bcd5dfd67 Binary files /dev/null and b/__fixtures__/custom-workspace-root/apps/app/public/favicon.ico differ diff --git a/__fixtures__/custom-workspace-root/apps/app/public/index.html b/__fixtures__/custom-workspace-root/apps/app/public/index.html new file mode 100644 index 000000000..dfdafd55d --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/__fixtures__/custom-workspace-root/apps/app/public/logo192.png b/__fixtures__/custom-workspace-root/apps/app/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/__fixtures__/custom-workspace-root/apps/app/public/logo192.png differ diff --git a/__fixtures__/custom-workspace-root/apps/app/public/logo512.png b/__fixtures__/custom-workspace-root/apps/app/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/__fixtures__/custom-workspace-root/apps/app/public/logo512.png differ diff --git a/__fixtures__/custom-workspace-root/apps/app/public/manifest.json b/__fixtures__/custom-workspace-root/apps/app/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/__fixtures__/custom-workspace-root/apps/app/public/robots.txt b/__fixtures__/custom-workspace-root/apps/app/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/__fixtures__/custom-workspace-root/apps/app/src/App.css b/__fixtures__/custom-workspace-root/apps/app/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/__fixtures__/custom-workspace-root/apps/app/src/App.tsx b/__fixtures__/custom-workspace-root/apps/app/src/App.tsx new file mode 100644 index 000000000..1479a9d4a --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/App.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import logo from './logo.svg'; +import './App.css'; + +function App(): JSX.Element { + return ( +
+
+ logo +

+ Edit src/App.tsx and save to reload. +

+ + Learn React + +
+
+ ); +} + +export default App; diff --git a/__fixtures__/custom-workspace-root/apps/app/src/__tests__/App.test.tsx b/__fixtures__/custom-workspace-root/apps/app/src/__tests__/App.test.tsx new file mode 100644 index 000000000..8361e247a --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/__tests__/App.test.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from '../App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/__fixtures__/custom-workspace-root/apps/app/src/index.css b/__fixtures__/custom-workspace-root/apps/app/src/index.css new file mode 100644 index 000000000..ec2585e8c --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/__fixtures__/custom-workspace-root/apps/app/src/index.tsx b/__fixtures__/custom-workspace-root/apps/app/src/index.tsx new file mode 100644 index 000000000..54c1d799a --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/index.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import App from './App'; +import './index.css'; + +ReactDOM.render( + + + , + document.getElementById('root'), +); diff --git a/__fixtures__/custom-workspace-root/apps/app/src/logo.svg b/__fixtures__/custom-workspace-root/apps/app/src/logo.svg new file mode 100644 index 000000000..6b60c1042 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/__fixtures__/custom-workspace-root/apps/app/src/react-app-env.d.ts b/__fixtures__/custom-workspace-root/apps/app/src/react-app-env.d.ts new file mode 100644 index 000000000..a8ecff874 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/__fixtures__/custom-workspace-root/apps/app/tsconfig.json b/__fixtures__/custom-workspace-root/apps/app/tsconfig.json new file mode 100644 index 000000000..4082f16a5 --- /dev/null +++ b/__fixtures__/custom-workspace-root/apps/app/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/__fixtures__/custom-workspace-root/modular/setupEnvironment.ts b/__fixtures__/custom-workspace-root/modular/setupEnvironment.ts new file mode 100644 index 000000000..5b785ac2b --- /dev/null +++ b/__fixtures__/custom-workspace-root/modular/setupEnvironment.ts @@ -0,0 +1,2 @@ +// Allows for adding setup configuration to Jest +export {}; diff --git a/__fixtures__/custom-workspace-root/modular/setupTests.ts b/__fixtures__/custom-workspace-root/modular/setupTests.ts new file mode 100644 index 000000000..74b1a275a --- /dev/null +++ b/__fixtures__/custom-workspace-root/modular/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom/extend-expect'; diff --git a/__fixtures__/custom-workspace-root/package.json b/__fixtures__/custom-workspace-root/package.json new file mode 100644 index 000000000..50067902b --- /dev/null +++ b/__fixtures__/custom-workspace-root/package.json @@ -0,0 +1,59 @@ +{ + "name": "roots", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**", + "apps/**" + ], + "modular": { + "type": "root" + }, + "scripts": { + "start": "modular start", + "build": "modular build", + "test": "modular test", + "lint": "eslint . --ext .js,.ts,.tsx", + "prettier": "prettier --write ." + }, + "eslintConfig": { + "extends": "modular-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80, + "proseWrap": "always" + }, + "dependencies": { + "@testing-library/dom": "^8.19.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^7.2.1", + "@types/jest": "^29.2.3", + "@types/node": "^18.7.14", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "eslint-config-modular-app": "^3.0.2", + "modular-scripts": "^3.6.0", + "modular-template-app": "^1.1.0", + "prettier": "^2.7.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": ">=4.2.1 <4.5.0" + } +} diff --git a/__fixtures__/custom-workspace-root/packages/README.md b/__fixtures__/custom-workspace-root/packages/README.md new file mode 100644 index 000000000..021e98810 --- /dev/null +++ b/__fixtures__/custom-workspace-root/packages/README.md @@ -0,0 +1 @@ +This will be the readme inside /packages diff --git a/__fixtures__/custom-workspace-root/tsconfig.json b/__fixtures__/custom-workspace-root/tsconfig.json new file mode 100644 index 000000000..4fcd8edb6 --- /dev/null +++ b/__fixtures__/custom-workspace-root/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "modular-scripts/tsconfig.json", + "include": ["modular", "packages/**/src", "apps/**/src"] +} diff --git a/packages/modular-scripts/react-scripts/config/parts/baseConfig.js b/packages/modular-scripts/react-scripts/config/parts/baseConfig.js index 7ce91941c..28cffb6f9 100644 --- a/packages/modular-scripts/react-scripts/config/parts/baseConfig.js +++ b/packages/modular-scripts/react-scripts/config/parts/baseConfig.js @@ -168,7 +168,7 @@ function createConfig({ // Process application JS with esbuild. { test: /\.(js|mjs|jsx)$/, - include: paths.modularSrc, + include: paths.includeDirectories, loader: require.resolve('esbuild-loader'), options: { implementation: require('esbuild'), @@ -178,7 +178,7 @@ function createConfig({ }, { test: /\.ts$/, - include: paths.modularSrc, + include: paths.includeDirectories, loader: require.resolve('esbuild-loader'), options: { implementation: require('esbuild'), @@ -188,7 +188,7 @@ function createConfig({ }, { test: /\.tsx$/, - include: paths.modularSrc, + include: paths.includeDirectories, loader: require.resolve('esbuild-loader'), options: { implementation: require('esbuild'), diff --git a/packages/modular-scripts/react-scripts/config/paths.js b/packages/modular-scripts/react-scripts/config/paths.js index 5587b7cda..0cdcf2f27 100644 --- a/packages/modular-scripts/react-scripts/config/paths.js +++ b/packages/modular-scripts/react-scripts/config/paths.js @@ -2,6 +2,7 @@ const path = require('path'); const fs = require('fs'); +const globby = require('globby'); const getPublicUrlOrPath = require('../../react-dev-utils/getPublicUrlOrPath'); if (!process.env.MODULAR_ROOT) { @@ -77,6 +78,21 @@ const resolveModule = (resolveFn, filePath) => { return resolveFn(`${filePath}.js`); }; +// Get the workspaces field from the manifest to calculate the possible workspace directories +const rootManifest = require(resolveModular('package.json')); +const workspaceDefinitions = + (Array.isArray(rootManifest?.workspaces) + ? rootManifest?.workspaces + : rootManifest?.workspaces?.packages) || []; + +// Calculate all the possible workspace directories. We need to convert paths to posix separator to feed it into globby +// and convert back to native separator after +const workspaceDirectories = globby + .sync(workspaceDefinitions.map(resolveModular).map(toPosix), { + onlyDirectories: true, + }) + .map(fromPosix); + // config after eject: we're in ./config/ module.exports = { appPath: resolveApp('.'), @@ -86,8 +102,8 @@ module.exports = { appIndexJs: resolveModule(resolveApp, 'src/index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), - modularSrc: [ - resolveModular('packages'), + includeDirectories: [ + workspaceDirectories, resolveModular('node_modules/.modular'), ], appTsConfig: resolveApp('tsconfig.json'), @@ -101,3 +117,11 @@ module.exports = { }; module.exports.moduleFileExtensions = moduleFileExtensions; + +function toPosix(pathString) { + return pathString.split(path.sep).join(path.posix.sep); +} + +function fromPosix(pathString) { + return pathString.split(path.posix.sep).join(path.sep); +} diff --git a/packages/modular-scripts/src/__tests__/build.test.ts b/packages/modular-scripts/src/__tests__/build.test.ts index 3eeb9e578..78b3d8cfc 100644 --- a/packages/modular-scripts/src/__tests__/build.test.ts +++ b/packages/modular-scripts/src/__tests__/build.test.ts @@ -1,8 +1,16 @@ +import execa from 'execa'; import tree from 'tree-view-for-tests'; import path from 'path'; import fs from 'fs-extra'; -import { addFixturePackage, cleanup, modular } from '../test/utils'; +import { + addFixturePackage, + cleanup, + modular, + createModularTestContext, + runLocalModular, +} from '../test/utils'; + import getModularRoot from '../utils/getModularRoot'; const modularRoot = getModularRoot(); @@ -189,3 +197,68 @@ describe('WHEN building packages with private cross-package dependencies', () => `); }); }); + +describe('modular build supports custom workspaces', () => { + const fixturesFolder = path.join( + getModularRoot(), + '__fixtures__', + 'custom-workspace-root', + ); + + // Temporary test context paths set by createTempModularRepoWithTemplate() + let tempModularRepo: string; + + beforeAll(() => { + tempModularRepo = createModularTestContext(); + fs.copySync(fixturesFolder, tempModularRepo); + + // Create git repo & commit + if (process.env.GIT_AUTHOR_NAME && process.env.GIT_AUTHOR_EMAIL) { + execa.sync('git', [ + 'config', + '--global', + 'user.email', + `"${process.env.GIT_AUTHOR_EMAIL}"`, + ]); + execa.sync('git', [ + 'config', + '--global', + 'user.name', + `"${process.env.GIT_AUTHOR_NAME}"`, + ]); + } + execa.sync('git', ['init'], { + cwd: tempModularRepo, + }); + + execa.sync('yarn', { + cwd: tempModularRepo, + }); + + execa.sync('git', ['add', '.'], { + cwd: tempModularRepo, + }); + + execa.sync('git', ['commit', '-am', '"First commit"'], { + cwd: tempModularRepo, + }); + }); + + it('builds an app in a different workspace directory', () => { + const result = runLocalModular(modularRoot, tempModularRepo, [ + 'build', + 'app', + ]); + expect(result.stderr).toBeFalsy(); + expect(result.stdout).toContain('Compiled successfully.'); + }); + + it('builds a package in a different workspace directory', () => { + const result = runLocalModular(modularRoot, tempModularRepo, [ + 'build', + 'alpha', + ]); + expect(result.stderr).toBeFalsy(); + expect(result.stdout).toContain('built alpha'); + }); +}); diff --git a/packages/modular-scripts/src/build/buildPackage/makeBundle.ts b/packages/modular-scripts/src/build/buildPackage/makeBundle.ts index 1acd735f2..91cb475fb 100644 --- a/packages/modular-scripts/src/build/buildPackage/makeBundle.ts +++ b/packages/modular-scripts/src/build/buildPackage/makeBundle.ts @@ -36,6 +36,7 @@ export async function makeBundle( const modularRoot = getModularRoot(); const metadata = await getPackageMetadata(); const { + rootPackageWorkspaceDefinitions, rootPackageJsonDependencies, packageJsons, packageJsonsByPackagePath, @@ -60,6 +61,10 @@ export async function makeBundle( const target = createEsbuildBrowserslistTarget(packagePath); + const includeDirectories = Array.isArray(rootPackageWorkspaceDefinitions) + ? rootPackageWorkspaceDefinitions + : rootPackageWorkspaceDefinitions?.packages ?? []; + const bundle = await rollup.rollup({ input: path.join(modularRoot, packagePath, main), external: (id) => { @@ -86,7 +91,7 @@ export async function makeBundle( esbuild({ target, minify: false, - include: [`packages/**/*`], + include: includeDirectories, exclude: 'node_modules/**', }), postcss({ extract: false }), diff --git a/packages/modular-scripts/src/utils/getPackageMetadata.ts b/packages/modular-scripts/src/utils/getPackageMetadata.ts index a3ddbd314..afc139c04 100644 --- a/packages/modular-scripts/src/utils/getPackageMetadata.ts +++ b/packages/modular-scripts/src/utils/getPackageMetadata.ts @@ -23,13 +23,15 @@ function distinct(arr: T[]): T[] { async function getPackageMetadata() { const modularRoot = getModularRoot(); + const rootPackageJson = fse.readJSONSync( + path.join(modularRoot, 'package.json'), + ) as ModularPackageJson; + + // workspace definitions + const rootPackageWorkspaceDefinitions = rootPackageJson.workspaces; + // dependencies defined at the root - const rootPackageJsonDependencies = - ( - fse.readJSONSync( - path.join(modularRoot, 'package.json'), - ) as ModularPackageJson - ).dependencies || {}; + const rootPackageJsonDependencies = rootPackageJson.dependencies || {}; // let's populate the above three const [workspaces] = await getAllWorkspaces(); @@ -101,6 +103,7 @@ async function getPackageMetadata() { return { packageNames, + rootPackageWorkspaceDefinitions, rootPackageJsonDependencies, packageJsons, typescriptConfig,