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 (
+
+ );
+}
+
+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,