diff --git a/.changeset/config.json b/.changeset/config.json index 5367de552..2ed1c23c6 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -6,5 +6,6 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "updateInternalDependents": "always" } diff --git a/.changeset/unlucky-guests-train.md b/.changeset/unlucky-guests-train.md new file mode 100644 index 000000000..4f8f0fbda --- /dev/null +++ b/.changeset/unlucky-guests-train.md @@ -0,0 +1,7 @@ +--- +'@modular-scripts/modular-types': major +'@modular-scripts/workspace-resolver': major +'modular-scripts': patch +--- + +Introduce @modular-scripts/modular-types and @modular-scripts/workspace-resolver diff --git a/.eslintignore b/.eslintignore index 707d8ce93..90e7b21b3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -39,4 +39,7 @@ yarn-error.log* # Used by typescript for incremental builds .tsbuildinfo -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo + +# Top level test fixtures +/__fixtures__ \ No newline at end of file diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bb496b048..ede4dccdf 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,4 +32,4 @@ jobs: - name: Install Dependencies run: yarn --frozen-lockfile - name: Run Windows tests - run: yarn test esmView.test.ts + run: yarn test esmView.test.ts workspace-resolver diff --git a/__fixtures__/clean-workspace-1/package.json b/__fixtures__/clean-workspace-1/package.json new file mode 100644 index 000000000..a037ff1d5 --- /dev/null +++ b/__fixtures__/clean-workspace-1/package.json @@ -0,0 +1,16 @@ +{ + "name": "clean-workspace-1", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/clean-workspace-1/packages/app-one/package.json b/__fixtures__/clean-workspace-1/packages/app-one/package.json new file mode 100644 index 000000000..a4f4944f8 --- /dev/null +++ b/__fixtures__/clean-workspace-1/packages/app-one/package.json @@ -0,0 +1,12 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-1/packages/package-one/package.json b/__fixtures__/clean-workspace-1/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/clean-workspace-1/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-1/packages/package-two/package.json b/__fixtures__/clean-workspace-1/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/clean-workspace-1/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-2/package.json b/__fixtures__/clean-workspace-2/package.json new file mode 100644 index 000000000..aba88a472 --- /dev/null +++ b/__fixtures__/clean-workspace-2/package.json @@ -0,0 +1,21 @@ +{ + "name": "clean-workspace-2", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": { + "packages": [ + "packages/**" + ], + "nohoist": [ + "foo" + ] + }, + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/clean-workspace-2/packages/app-one/package.json b/__fixtures__/clean-workspace-2/packages/app-one/package.json new file mode 100644 index 000000000..a4f4944f8 --- /dev/null +++ b/__fixtures__/clean-workspace-2/packages/app-one/package.json @@ -0,0 +1,12 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-2/packages/package-one/package.json b/__fixtures__/clean-workspace-2/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/clean-workspace-2/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-2/packages/package-two/package.json b/__fixtures__/clean-workspace-2/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/clean-workspace-2/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-3/package.json b/__fixtures__/clean-workspace-3/package.json new file mode 100644 index 000000000..8a93260f9 --- /dev/null +++ b/__fixtures__/clean-workspace-3/package.json @@ -0,0 +1,16 @@ +{ + "name": "clean-workspace-3", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/clean-workspace-3/packages/app-one/package.json b/__fixtures__/clean-workspace-3/packages/app-one/package.json new file mode 100644 index 000000000..17034bb06 --- /dev/null +++ b/__fixtures__/clean-workspace-3/packages/app-one/package.json @@ -0,0 +1,14 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "workspace:*", + "package-two": "workspace:^", + "package-three": "workspace:~", + "package-four": "workspace:^1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-3/packages/package-four/package.json b/__fixtures__/clean-workspace-3/packages/package-four/package.json new file mode 100644 index 000000000..e7f7c1e44 --- /dev/null +++ b/__fixtures__/clean-workspace-3/packages/package-four/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-four", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-3/packages/package-one/package.json b/__fixtures__/clean-workspace-3/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/clean-workspace-3/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-3/packages/package-three/package.json b/__fixtures__/clean-workspace-3/packages/package-three/package.json new file mode 100644 index 000000000..6327cc69d --- /dev/null +++ b/__fixtures__/clean-workspace-3/packages/package-three/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-three", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/clean-workspace-3/packages/package-two/package.json b/__fixtures__/clean-workspace-3/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/clean-workspace-3/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-1/package.json b/__fixtures__/invalid-workspace-1/package.json new file mode 100644 index 000000000..3af68a730 --- /dev/null +++ b/__fixtures__/invalid-workspace-1/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-workspace-1", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/invalid-workspace-1/packages/app-one/package.json b/__fixtures__/invalid-workspace-1/packages/app-one/package.json new file mode 100644 index 000000000..3ae2828d0 --- /dev/null +++ b/__fixtures__/invalid-workspace-1/packages/app-one/package.json @@ -0,0 +1,12 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "root" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-1/packages/package-one/package.json b/__fixtures__/invalid-workspace-1/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/invalid-workspace-1/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-1/packages/package-two/package.json b/__fixtures__/invalid-workspace-1/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/invalid-workspace-1/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-2/package.json b/__fixtures__/invalid-workspace-2/package.json new file mode 100644 index 000000000..3a03f3b1b --- /dev/null +++ b/__fixtures__/invalid-workspace-2/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-workspace-2", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/invalid-workspace-2/packages/app-one/package.json b/__fixtures__/invalid-workspace-2/packages/app-one/package.json new file mode 100644 index 000000000..6f1f2ca13 --- /dev/null +++ b/__fixtures__/invalid-workspace-2/packages/app-one/package.json @@ -0,0 +1,16 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "workspaces": [ + "foo", + "bar" + ], + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-2/packages/package-one/package.json b/__fixtures__/invalid-workspace-2/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/invalid-workspace-2/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-2/packages/package-two/package.json b/__fixtures__/invalid-workspace-2/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/invalid-workspace-2/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-3/package.json b/__fixtures__/invalid-workspace-3/package.json new file mode 100644 index 000000000..300c6d83f --- /dev/null +++ b/__fixtures__/invalid-workspace-3/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-workspace-3", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/invalid-workspace-3/packages/app-one/package.json b/__fixtures__/invalid-workspace-3/packages/app-one/package.json new file mode 100644 index 000000000..d2079fa74 --- /dev/null +++ b/__fixtures__/invalid-workspace-3/packages/app-one/package.json @@ -0,0 +1,18 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "workspaces": { + "packages": [ + "foo", + "bar" + ] + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-3/packages/package-one/package.json b/__fixtures__/invalid-workspace-3/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/invalid-workspace-3/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-3/packages/package-two/package.json b/__fixtures__/invalid-workspace-3/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/invalid-workspace-3/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-4/package.json b/__fixtures__/invalid-workspace-4/package.json new file mode 100644 index 000000000..300c6d83f --- /dev/null +++ b/__fixtures__/invalid-workspace-4/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-workspace-3", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/invalid-workspace-4/packages/app-one/package.json b/__fixtures__/invalid-workspace-4/packages/app-one/package.json new file mode 100644 index 000000000..68d70db8f --- /dev/null +++ b/__fixtures__/invalid-workspace-4/packages/app-one/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-4/packages/package-one/package.json b/__fixtures__/invalid-workspace-4/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/invalid-workspace-4/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-4/packages/package-two/package.json b/__fixtures__/invalid-workspace-4/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/invalid-workspace-4/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-5/package.json b/__fixtures__/invalid-workspace-5/package.json new file mode 100644 index 000000000..300c6d83f --- /dev/null +++ b/__fixtures__/invalid-workspace-5/package.json @@ -0,0 +1,16 @@ +{ + "name": "invalid-workspace-3", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/invalid-workspace-5/packages/app-one/package.json b/__fixtures__/invalid-workspace-5/packages/app-one/package.json new file mode 100644 index 000000000..320994483 --- /dev/null +++ b/__fixtures__/invalid-workspace-5/packages/app-one/package.json @@ -0,0 +1,11 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + } +} diff --git a/__fixtures__/invalid-workspace-5/packages/package-one/package.json b/__fixtures__/invalid-workspace-5/packages/package-one/package.json new file mode 100644 index 000000000..ee6acb268 --- /dev/null +++ b/__fixtures__/invalid-workspace-5/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/invalid-workspace-5/packages/package-two/package.json b/__fixtures__/invalid-workspace-5/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/invalid-workspace-5/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/__fixtures__/mismatched-dependency/package.json b/__fixtures__/mismatched-dependency/package.json new file mode 100644 index 000000000..f96072610 --- /dev/null +++ b/__fixtures__/mismatched-dependency/package.json @@ -0,0 +1,16 @@ +{ + "name": "mismatched-dependency", + "version": "1.0.0", + "author": "App Frameworks team", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/**" + ], + "modular": { + "type": "root" + }, + "dependencies": { + "lodash": "^4.17.21" + } +} diff --git a/__fixtures__/mismatched-dependency/packages/app-one/package.json b/__fixtures__/mismatched-dependency/packages/app-one/package.json new file mode 100644 index 000000000..a4f4944f8 --- /dev/null +++ b/__fixtures__/mismatched-dependency/packages/app-one/package.json @@ -0,0 +1,12 @@ +{ + "name": "app-one", + "private": true, + "modular": { + "type": "app" + }, + "dependencies": { + "package-one": "1.0.0", + "package-two": "1.0.0" + }, + "version": "1.0.0" +} diff --git a/__fixtures__/mismatched-dependency/packages/package-one/package.json b/__fixtures__/mismatched-dependency/packages/package-one/package.json new file mode 100644 index 000000000..cf1ce387c --- /dev/null +++ b/__fixtures__/mismatched-dependency/packages/package-one/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-one", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "0.0.9" +} diff --git a/__fixtures__/mismatched-dependency/packages/package-two/package.json b/__fixtures__/mismatched-dependency/packages/package-two/package.json new file mode 100644 index 000000000..99fbdc50a --- /dev/null +++ b/__fixtures__/mismatched-dependency/packages/package-two/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-two", + "private": true, + "modular": { + "type": "package" + }, + "main": "./src/index.ts", + "version": "1.0.0" +} diff --git a/package.json b/package.json index c3ca2742a..4615a369a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "create-modular-react-app": "ts-node packages/create-modular-react-app/src/cli.ts", "modular": "ts-node packages/modular-scripts/src/cli.ts", "test": "yarn modular test --watchAll false --runInBand", - "build": "yarn workspace create-modular-react-app build && yarn workspace modular-scripts build && yarn workspace modular-views.macro build", + "build": "yarn workspace @modular-scripts/workspace-resolver build && yarn workspace create-modular-react-app build && yarn workspace modular-scripts build && yarn workspace modular-views.macro build", "start": "yarn modular start modular-site", "prepare": "is-ci || husky install", "postinstall": "patch-package", @@ -102,7 +102,8 @@ "/node_modules/", "/modular-site/", "/modular-views.macro/" - ] + ], + "coverageProvider": "v8" }, "modular": { "type": "root" diff --git a/packages/modular-scripts/package.json b/packages/modular-scripts/package.json index 1789c3f7e..d1d55f421 100644 --- a/packages/modular-scripts/package.json +++ b/packages/modular-scripts/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@babel/code-frame": "7.16.7", + "@modular-scripts/workspace-resolver": "0.0.1", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "22.0.0", "@rollup/plugin-json": "4.1.0", @@ -116,9 +117,9 @@ "ts-morph": "^14.0.0", "update-notifier": "5.1.0", "url-loader": "4.1.1", - "webpack": "5.73.0", "webpack-dev-server": "4.9.3", "webpack-manifest-plugin": "5.0.0", + "webpack": "5.73.0", "ws": "8.8.0" }, "peerDependencies": { @@ -137,11 +138,12 @@ "*.js" ], "devDependencies": { + "@modular-scripts/modular-types": "0.0.1", "@schemastore/package": "0.0.6", "@schemastore/tsconfig": "0.0.9", "@types/js-yaml": "^4.0.5", - "react": "17.0.2", "react-dom": "17.0.2", + "react": "17.0.2", "typescript": "4.7.4" } } diff --git a/packages/modular-scripts/src/__tests__/convert.test.ts b/packages/modular-scripts/src/__tests__/convert.test.ts index 333c9257e..43b696f45 100644 --- a/packages/modular-scripts/src/__tests__/convert.test.ts +++ b/packages/modular-scripts/src/__tests__/convert.test.ts @@ -3,11 +3,12 @@ import * as path from 'path'; import * as tmp from 'tmp'; import * as fs from 'fs-extra'; -import { ModularPackageJson } from '../utils/isModularType'; import * as getModularRoot from '../utils/getModularRoot'; import { convert } from '../convert'; import tree from 'tree-view-for-tests'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + jest.mock('../utils/getModularRoot'); const mockedModularRoot = getModularRoot.default as jest.MockedFunction< diff --git a/packages/modular-scripts/src/__tests__/index.test.ts b/packages/modular-scripts/src/__tests__/index.test.ts index c0e333990..548c30b60 100644 --- a/packages/modular-scripts/src/__tests__/index.test.ts +++ b/packages/modular-scripts/src/__tests__/index.test.ts @@ -14,7 +14,8 @@ import puppeteer from 'puppeteer'; import getModularRoot from '../utils/getModularRoot'; import { startApp, DevServer } from './start-app'; -import { ModularPackageJson } from '../utils/isModularType'; + +import type { ModularPackageJson } from '@modular-scripts/modular-types'; const rimraf = promisify(_rimraf); diff --git a/packages/modular-scripts/src/__tests__/init.test.tsx b/packages/modular-scripts/src/__tests__/init.test.tsx index 29a4e7c04..cdcae95a4 100644 --- a/packages/modular-scripts/src/__tests__/init.test.tsx +++ b/packages/modular-scripts/src/__tests__/init.test.tsx @@ -3,9 +3,10 @@ import * as path from 'path'; import * as tmp from 'tmp'; import * as fs from 'fs-extra'; import { promisify } from 'util'; -import { ModularPackageJson } from '../utils/isModularType'; import { getWorkspaceInfo } from '../utils/getAllWorkspaces'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + const mktempd = promisify(tmp.dir); describe('Creating a new modular folder', () => { diff --git a/packages/modular-scripts/src/__tests__/port.test.ts b/packages/modular-scripts/src/__tests__/port.test.ts index 0003f14e7..7300eef72 100644 --- a/packages/modular-scripts/src/__tests__/port.test.ts +++ b/packages/modular-scripts/src/__tests__/port.test.ts @@ -3,11 +3,12 @@ import * as tmp from 'tmp'; import * as fs from 'fs-extra'; import rimraf from 'rimraf'; -import { ModularPackageJson } from '../utils/isModularType'; import * as getModularRoot from '../utils/getModularRoot'; import { port } from '../port'; import { initModularFolder } from '../init'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + jest.mock('../utils/getModularRoot'); const mockedModularRoot = getModularRoot.default as jest.MockedFunction< diff --git a/packages/modular-scripts/src/__tests__/utils/__snapshots__/getWorkspaceInfo.test.ts.snap b/packages/modular-scripts/src/__tests__/utils/__snapshots__/getWorkspaceInfo.test.ts.snap index f83a7b619..154497336 100644 --- a/packages/modular-scripts/src/__tests__/utils/__snapshots__/getWorkspaceInfo.test.ts.snap +++ b/packages/modular-scripts/src/__tests__/utils/__snapshots__/getWorkspaceInfo.test.ts.snap @@ -29,7 +29,10 @@ Object { "public": true, "type": "package", "version": Any, - "workspaceDependencies": Array [], + "workspaceDependencies": Array [ + "@modular-scripts/modular-types", + "@modular-scripts/workspace-resolver", + ], } `; @@ -101,7 +104,7 @@ Object { exports[`getWorkspaceInfo 10`] = ` Object { - "location": "packages/modular-views.macro", + "location": "packages/modular-types", "mismatchedWorkspaceDependencies": Array [], "public": true, "type": "package", @@ -111,6 +114,17 @@ Object { `; exports[`getWorkspaceInfo 11`] = ` +Object { + "location": "packages/modular-views.macro", + "mismatchedWorkspaceDependencies": Array [], + "public": true, + "type": "package", + "version": Any, + "workspaceDependencies": Array [], +} +`; + +exports[`getWorkspaceInfo 12`] = ` Object { "location": "packages/tree-view-for-tests", "mismatchedWorkspaceDependencies": Array [], @@ -120,3 +134,16 @@ Object { "workspaceDependencies": Array [], } `; + +exports[`getWorkspaceInfo 13`] = ` +Object { + "location": "packages/workspace-resolver", + "mismatchedWorkspaceDependencies": Array [], + "public": true, + "type": "package", + "version": Any, + "workspaceDependencies": Array [ + "@modular-scripts/modular-types", + ], +} +`; diff --git a/packages/modular-scripts/src/addPackage.ts b/packages/modular-scripts/src/addPackage.ts index 64c71b631..34b5ee3c2 100644 --- a/packages/modular-scripts/src/addPackage.ts +++ b/packages/modular-scripts/src/addPackage.ts @@ -13,7 +13,8 @@ import * as logger from './utils/logger'; import actionPreflightCheck from './utils/actionPreflightCheck'; import getAllFiles from './utils/getAllFiles'; import LineFilterOutStream from './utils/LineFilterOutStream'; -import { ModularPackageJson } from './utils/isModularType'; + +import type { ModularPackageJson } from '@modular-scripts/modular-types'; const packagesRoot = 'packages'; const CUSTOM_TEMPLATE = '__CHOOSE_MY_OWN__'; diff --git a/packages/modular-scripts/src/build/buildPackage/makeBundle.ts b/packages/modular-scripts/src/build/buildPackage/makeBundle.ts index 485023edf..1acd735f2 100644 --- a/packages/modular-scripts/src/build/buildPackage/makeBundle.ts +++ b/packages/modular-scripts/src/build/buildPackage/makeBundle.ts @@ -16,10 +16,11 @@ import { getPackageEntryPoints } from './getPackageEntryPoints'; import getPrefixedLogger from '../../utils/getPrefixedLogger'; import getPackageMetadata from '../../utils/getPackageMetadata'; import getModularRoot from '../../utils/getModularRoot'; -import { ModularPackageJson } from '../../utils/isModularType'; import getRelativeLocation from '../../utils/getRelativeLocation'; import createEsbuildBrowserslistTarget from '../../utils/createEsbuildBrowserslistTarget'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + const outputDirectory = 'dist'; const extensions = ['.ts', '.tsx', '.js', '.jsx']; diff --git a/packages/modular-scripts/src/build/index.ts b/packages/modular-scripts/src/build/index.ts index 20c371021..aeb1991f1 100644 --- a/packages/modular-scripts/src/build/index.ts +++ b/packages/modular-scripts/src/build/index.ts @@ -2,14 +2,16 @@ import { paramCase as toParamCase } from 'change-case'; import chalk from 'chalk'; import * as fs from 'fs-extra'; import * as path from 'path'; -import * as logger from '../utils/logger'; import * as minimize from 'html-minifier-terser'; +import type { CoreProperties } from '@schemastore/package'; +import type { ModularType } from '@modular-scripts/modular-types'; + +import * as logger from '../utils/logger'; import getModularRoot from '../utils/getModularRoot'; import actionPreflightCheck from '../utils/actionPreflightCheck'; import { getModularType } from '../utils/isModularType'; import { filterDependencies } from '../utils/filterDependencies'; import getWorkspaceInfo from '../utils/getWorkspaceInfo'; -import type { ModularType } from '../utils/isModularType'; import execAsync from '../utils/execAsync'; import getLocation from '../utils/getLocation'; import { setupEnvForDirectory } from '../utils/setupEnv'; @@ -35,7 +37,6 @@ import { esbuildMeasureFileSizesBeforeBuild, } from './esbuildFileSizeReporter'; import { getPackageDependencies } from '../utils/getPackageDependencies'; -import type { CoreProperties } from '@schemastore/package'; async function buildStandalone( target: string, diff --git a/packages/modular-scripts/src/check/verifyBrowserslist.ts b/packages/modular-scripts/src/check/verifyBrowserslist.ts index c9c9149fc..cfee89978 100644 --- a/packages/modular-scripts/src/check/verifyBrowserslist.ts +++ b/packages/modular-scripts/src/check/verifyBrowserslist.ts @@ -5,11 +5,12 @@ import * as path from 'path'; import prompts from 'prompts'; import * as logger from '../utils/logger'; -import { ModularPackageJson } from '../utils/isModularType'; import getModularRoot from '../utils/getModularRoot'; import getWorkspaceInfo from '../utils/getWorkspaceInfo'; import { defaultBrowsers } from '../utils/checkBrowsers'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + export async function check(): Promise { let valid = true; diff --git a/packages/modular-scripts/src/check/verifyModularRootPackageJson.ts b/packages/modular-scripts/src/check/verifyModularRootPackageJson.ts index 9c3f6e97a..ae4e502d9 100644 --- a/packages/modular-scripts/src/check/verifyModularRootPackageJson.ts +++ b/packages/modular-scripts/src/check/verifyModularRootPackageJson.ts @@ -3,9 +3,10 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import getModularRoot from '../utils/getModularRoot'; -import { ModularPackageJson } from '../utils/isModularType'; import * as logger from '../utils/logger'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + /** * @param rootPackageJson package.json object from the modular root * @returns Returns true if the packages directory is correctly included in `workspaces`, else false diff --git a/packages/modular-scripts/src/check/verifyWorkspaceStructure.ts b/packages/modular-scripts/src/check/verifyWorkspaceStructure.ts index 9815a75dd..4da9734af 100644 --- a/packages/modular-scripts/src/check/verifyWorkspaceStructure.ts +++ b/packages/modular-scripts/src/check/verifyWorkspaceStructure.ts @@ -10,7 +10,7 @@ export async function check(): Promise { const modularRoot = getModularRoot(); /** - * Validate the the worktree is valid against the globby of pacakge.json files which are found in the + * Validate the the worktree is valid against the globby of package.json files which are found in the * current working directory. They should be the same but you never know... */ diff --git a/packages/modular-scripts/src/convert.ts b/packages/modular-scripts/src/convert.ts index f2c221f45..47b0f7440 100644 --- a/packages/modular-scripts/src/convert.ts +++ b/packages/modular-scripts/src/convert.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import type { IncludeDefinition as TSConfig } from '@schemastore/tsconfig'; import type { Dependency } from '@schemastore/package'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; import execa from 'execa'; import { paramCase as toParamCase } from 'change-case'; @@ -11,7 +12,6 @@ import rimraf from 'rimraf'; import { check } from './check'; import { isValidModularRootPackageJson } from './check/verifyModularRootPackageJson'; import { cleanGit, stashChanges } from './utils/gitActions'; -import { ModularPackageJson } from './utils/isModularType'; import * as logger from './utils/logger'; process.on('SIGINT', () => { diff --git a/packages/modular-scripts/src/init.ts b/packages/modular-scripts/src/init.ts index 2e3a6286c..dd6bcb154 100644 --- a/packages/modular-scripts/src/init.ts +++ b/packages/modular-scripts/src/init.ts @@ -2,9 +2,10 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { defaultBrowsers } from './utils/checkBrowsers'; import execAsync from './utils/execAsync'; -import { ModularPackageJson } from './utils/isModularType'; import * as logger from './utils/logger'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + export async function initModularFolder( folder: string, initOverride: boolean, diff --git a/packages/modular-scripts/src/port.ts b/packages/modular-scripts/src/port.ts index 5a206e3cc..472d3f6b0 100644 --- a/packages/modular-scripts/src/port.ts +++ b/packages/modular-scripts/src/port.ts @@ -11,10 +11,11 @@ import * as logger from './utils/logger'; import getModularRoot from './utils/getModularRoot'; import getWorkspaceInfo from './utils/getWorkspaceInfo'; import actionPreflightCheck from './utils/actionPreflightCheck'; -import { ModularPackageJson } from './utils/isModularType'; import { cleanGit, stashChanges } from './utils/gitActions'; import { check } from './check'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; + process.on('SIGINT', () => { stashChanges(); process.exit(1); diff --git a/packages/modular-scripts/src/test/config.ts b/packages/modular-scripts/src/test/config.ts index 5bc0ae11c..ef0c30dea 100644 --- a/packages/modular-scripts/src/test/config.ts +++ b/packages/modular-scripts/src/test/config.ts @@ -6,7 +6,8 @@ import globby from 'globby'; import type { Config } from '@jest/types'; import { defaults } from 'jest-config'; import getModularRoot from '../utils/getModularRoot'; -import { ModularPackageJson } from '../utils/isModularType'; + +import type { ModularPackageJson } from '@modular-scripts/modular-types'; // This list may change as we learn of options where flexibility would be valuable. // Based on react-scripts supported override options @@ -19,6 +20,7 @@ const supportedOverrides = [ 'testPathIgnorePatterns', 'testRunner', 'transformIgnorePatterns', + 'coverageProvider', ]; type SetUpFilesMap = { diff --git a/packages/modular-scripts/src/typecheck.ts b/packages/modular-scripts/src/typecheck.ts index a31dd0058..4d4a49a75 100644 --- a/packages/modular-scripts/src/typecheck.ts +++ b/packages/modular-scripts/src/typecheck.ts @@ -22,6 +22,7 @@ async function typecheck(): Promise { '**/dist-cjs', '**/dist-es', 'dist', + '**/__fixtures__', ], compilerOptions: { noEmit: true, diff --git a/packages/modular-scripts/src/utils/getAllWorkspaces.ts b/packages/modular-scripts/src/utils/getAllWorkspaces.ts index f361d2341..1c2eb3cc7 100644 --- a/packages/modular-scripts/src/utils/getAllWorkspaces.ts +++ b/packages/modular-scripts/src/utils/getAllWorkspaces.ts @@ -1,15 +1,13 @@ import execa from 'execa'; import memoize from './memoize'; import getModularRoot from './getModularRoot'; + import * as logger from './logger'; import stripAnsi from 'strip-ansi'; - -interface WorkspaceObj { - location: string; - workspaceDependencies: string[]; - mismatchedWorkspaceDependencies: string[]; -} -type WorkspaceMap = Record; +import type { + WorkspaceMap, + WorkspaceObj, +} from '@modular-scripts/modular-types'; function formatYarn1Workspace(stdout: string): WorkspaceMap { return JSON.parse(stdout) as WorkspaceMap; diff --git a/packages/modular-scripts/src/utils/getWorkspaceInfo.ts b/packages/modular-scripts/src/utils/getWorkspaceInfo.ts index 4d884f7ab..480a1f157 100644 --- a/packages/modular-scripts/src/utils/getWorkspaceInfo.ts +++ b/packages/modular-scripts/src/utils/getWorkspaceInfo.ts @@ -1,7 +1,10 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { ModularType, ModularPackageJson } from './isModularType'; +import type { + ModularType, + ModularPackageJson, +} from '@modular-scripts/modular-types'; import { getAllWorkspaces } from './getAllWorkspaces'; import getModularRoot from './getModularRoot'; import memoize from './memoize'; diff --git a/packages/modular-scripts/src/utils/isModularType.ts b/packages/modular-scripts/src/utils/isModularType.ts index 8bb1e79b7..3a492885b 100644 --- a/packages/modular-scripts/src/utils/isModularType.ts +++ b/packages/modular-scripts/src/utils/isModularType.ts @@ -1,6 +1,10 @@ import * as path from 'path'; -import type { JSONSchemaForNPMPackageJsonFiles as PackageJson } from '@schemastore/package'; +import type { + ModularType, + PackageType, + ModularPackageJson, +} from '@modular-scripts/modular-types'; import * as fs from 'fs-extra'; @@ -11,41 +15,10 @@ export const packageTypes: PackageType[] = [ 'package', 'template', ]; - export const ModularTypes: ModularType[] = ( packageTypes as ModularType[] ).concat(['root']); -export type ModularTemplateType = 'app' | 'esm-view' | 'view' | 'package'; -export type PackageType = ModularTemplateType | 'template'; - -export type ModularType = PackageType | 'root'; - -// Utility type that extends type `T1` with the fields of type `T2` -type Extend = { - [k in keyof (T1 & T2)]: k extends keyof T2 - ? T2[k] - : k extends keyof T1 - ? T1[k] - : never; -}; - -type PackageJsonOverrides = { - browserslist?: Record; - modular?: { - type: ModularType; - templateType?: ModularTemplateType; - }; - workspaces?: - | string[] - | { - packages?: string[]; - nohoist?: string[]; - }; -}; - -export type ModularPackageJson = Extend; - export function getModularType(dir: string): ModularType | undefined { const packageJsonPath = path.join(dir, 'package.json'); if (fs.existsSync(packageJsonPath)) { diff --git a/packages/modular-scripts/src/utils/resolveAsBin.ts b/packages/modular-scripts/src/utils/resolveAsBin.ts index 62df53f05..940a6b200 100644 --- a/packages/modular-scripts/src/utils/resolveAsBin.ts +++ b/packages/modular-scripts/src/utils/resolveAsBin.ts @@ -1,6 +1,6 @@ import * as fs from 'fs-extra'; import * as path from 'path'; -import { ModularPackageJson } from './isModularType'; +import type { ModularPackageJson } from '@modular-scripts/modular-types'; async function getBin(packageDir: string) { const packageJson = (await fs.readJson( diff --git a/packages/modular-types/.gitignore b/packages/modular-types/.gitignore new file mode 100644 index 000000000..53c37a166 --- /dev/null +++ b/packages/modular-types/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/modular-types/package.json b/packages/modular-types/package.json new file mode 100644 index 000000000..a7f282a09 --- /dev/null +++ b/packages/modular-types/package.json @@ -0,0 +1,6 @@ +{ + "name": "@modular-scripts/modular-types", + "version": "0.0.1", + "license": "Apache-2.0", + "types": "src/types.ts" +} diff --git a/packages/modular-types/src/types.ts b/packages/modular-types/src/types.ts new file mode 100644 index 000000000..23a3216c4 --- /dev/null +++ b/packages/modular-types/src/types.ts @@ -0,0 +1,53 @@ +import type { JSONSchemaForNPMPackageJsonFiles as PackageJson } from '@schemastore/package'; + +export type ModularTemplateType = 'app' | 'esm-view' | 'view' | 'package'; +export type PackageType = ModularTemplateType | 'template'; +export type UnknownType = 'unknown'; + +export type ModularType = PackageType | UnknownType | 'root'; + +export type ModularWorkspacePackage = { + path: string; + name: string; + version: string; + workspace: boolean; + modular: { + type: ModularType; + }; + children: ModularWorkspacePackage[]; + parent: ModularWorkspacePackage | null; + dependencies: Record | undefined; +}; + +export interface WorkspaceObj { + location: string; + workspaceDependencies: string[]; + mismatchedWorkspaceDependencies: string[]; +} + +export type WorkspaceMap = Record; + +// Utility type that extends type `T1` with the fields of type `T2` +type Extend = { + [k in keyof (T1 & T2)]: k extends keyof T2 + ? T2[k] + : k extends keyof T1 + ? T1[k] + : never; +}; + +type PackageJsonOverrides = { + browserslist?: Record; + modular?: { + type: ModularType; + templateType?: ModularTemplateType; + }; + workspaces?: + | string[] + | { + packages?: string[]; + nohoist?: string[]; + }; +}; + +export type ModularPackageJson = Extend; diff --git a/packages/workspace-resolver/README.md b/packages/workspace-resolver/README.md new file mode 100644 index 000000000..579d42196 --- /dev/null +++ b/packages/workspace-resolver/README.md @@ -0,0 +1,62 @@ +# @modular-scripts/workspace-resolver + +This package encapsulates two functions: + +1. `resolveWorkspace` - Searches the filesystem (at a given modular root) for + workspace packages, returning a flat map of all packages found +2. `analyzeWorkspaceDependencies` - Analyzes `package.json` files for a set of + workspace packages, returning a flat object for each package, listing out + workspace inter-dependencies plus and mismatched dependencies. The + dependencies are analyzed according to dependencies defined in `package.json` + files. The resulting output intends to match the yarn v1 (classic) output for + `yarn workspaces info` (1) + +In most cases, the output of `resolveWorkspace` can be passed directly to +`analyzeWorkspaceDependencies`. + +(1) This package exists as a drop-in replacement for `yarn workspaces info` +because the yarn command is not consistent across other versions of yarn. + +## Example + +```TypeScript +const [workspacePackages] = resolveWorkspace('path/to/modular/project/root') + +/* +Map { + "example-package": { + path: 'packages/example-package', + name: 'example-package', + workspace: false, + version: '1.0.0', + modular: { + type: 'package' + }, + children: [], + parent: null, + dependencies: { + 'lodash': '10.0.0' + } + }, + ... +} +*/ + +const analyzed = analyzeWorkspaceDependencies(workspacePackages); + +/* +{ + "example-package": { + "location": "packages/example-package", + "workspaceDependencies": ['another-package'], + "mismatchedWorkspaceDependencies": [] + }, + "another-package": { + "location": "packages/another-package", + "workspaceDependencies": [], + "mismatchedWorkspaceDependencies": ['mismatched-dep-one'] + }, + ... +*/ + +``` diff --git a/packages/workspace-resolver/package.json b/packages/workspace-resolver/package.json new file mode 100644 index 000000000..a233aeea7 --- /dev/null +++ b/packages/workspace-resolver/package.json @@ -0,0 +1,23 @@ +{ + "name": "@modular-scripts/workspace-resolver", + "version": "0.0.1", + "license": "Apache-2.0", + "main": "dist-cjs/index.js", + "dependencies": { + "fs-extra": "^10.1.0", + "globby": "11.0.4", + "semver": "7.3.7" + }, + "devDependencies": { + "@modular-scripts/modular-types": "0.0.1", + "@types/fs-extra": "^9.0.13" + }, + "scripts": { + "build": "tsc && babel --source-maps --root-mode upward src --out-dir dist-cjs --extensions .ts --ignore **/__tests__", + "clean": "rimraf dist-cjs" + }, + "files": [ + "dist-cjs" + ], + "types": "dist-cjs/index.d.ts" +} diff --git a/packages/workspace-resolver/src/__tests__/resolve-workspace.test.ts b/packages/workspace-resolver/src/__tests__/resolve-workspace.test.ts new file mode 100644 index 000000000..0515c53db --- /dev/null +++ b/packages/workspace-resolver/src/__tests__/resolve-workspace.test.ts @@ -0,0 +1,244 @@ +import { + resolveWorkspace, + analyzeWorkspaceDependencies, +} from '../resolve-workspace'; + +import path from 'path'; + +// Find test fixtures (i.e. fake modular workspaces) 4 dirs up, in the root of the project +// This approach avoids putting fake or real packages in the packages dir, which can confuse various tools +const dirsUp = 4; +const traverseUp = Array.from({ length: dirsUp }) + .map(() => `..`) + .join(path.sep) + .concat(path.sep); +const fixturesPath = `${__dirname}${path.sep}${traverseUp}__fixtures__${path.sep}`; + +describe('@modular-scripts/workspace-resolver', () => { + describe('resolveWorkspace', () => { + it('resolves a clean workspace, detecting modular packages as appropriate', async () => { + const projectRoot = `${fixturesPath}clean-workspace-1`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + expect(allPackages.has('clean-workspace-1')).toEqual(true); + expect(allPackages.has('app-one')).toEqual(true); + expect(allPackages.has('package-one')).toEqual(true); + expect(allPackages.has('package-two')).toEqual(true); + }); + + // This covers the alternative object workspaces syntax that yarn supports + // See https://classic.yarnpkg.com/blog/2018/02/15/nohoist/ + it('resolves a clean workspace (using object workspaces syntax)', async () => { + const projectRoot = `${fixturesPath}clean-workspace-2`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + expect(allPackages.has('clean-workspace-2')).toEqual(true); + expect(allPackages.has('app-one')).toEqual(true); + expect(allPackages.has('package-one')).toEqual(true); + expect(allPackages.has('package-two')).toEqual(true); + }); + + it('resolves a clean workspace (using workspace ranges)', async () => { + const projectRoot = `${fixturesPath}clean-workspace-3`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + expect(allPackages.has('clean-workspace-3')).toEqual(true); + expect(allPackages.has('app-one')).toEqual(true); + expect(allPackages.has('package-one')).toEqual(true); + expect(allPackages.has('package-two')).toEqual(true); + expect(allPackages.has('package-three')).toEqual(true); + expect(allPackages.has('package-four')).toEqual(true); + }); + + it('does not support nested modular roots', async () => { + const projectRoot = `${fixturesPath}invalid-workspace-1`; + let thrown = false; + let message = ''; + + try { + await resolveWorkspace(projectRoot, projectRoot); + } catch (err) { + thrown = true; + if (err instanceof Error) { + message = err.message; + } + } + + expect(thrown).toEqual(true); + expect(message).toEqual( + 'Nested modular roots are currently not supported by Modular', + ); + }); + + it('does not support nested yarn workspaces (implementation 1)', async () => { + const projectRoot = `${fixturesPath}invalid-workspace-2`; + let thrown = false; + let message = ''; + + try { + await resolveWorkspace(projectRoot, projectRoot); + } catch (err) { + thrown = true; + if (err instanceof Error) { + message = err.message; + } + } + + expect(thrown).toEqual(true); + expect(message).toEqual( + 'Nested workspaces are currently not supported by Modular', + ); + }); + + it('does not support nested yarn workspaces (implementation 2)', async () => { + const projectRoot = `${fixturesPath}invalid-workspace-3`; + let thrown = false; + let message = ''; + + try { + await resolveWorkspace(projectRoot, projectRoot); + } catch (err) { + thrown = true; + if (err instanceof Error) { + message = err.message; + } + } + + expect(thrown).toEqual(true); + expect(message).toEqual( + 'Nested workspaces are currently not supported by Modular', + ); + }); + + it('does not support packages with no name', async () => { + const projectRoot = `${fixturesPath}invalid-workspace-4`; + let thrown = false; + let message = ''; + + try { + await resolveWorkspace(projectRoot, projectRoot); + } catch (err) { + thrown = true; + if (err instanceof Error) { + message = err.message; + } + } + + expect(thrown).toEqual(true); + expect(message).toEqual( + 'The package at packages/app-one/package.json does not have a valid name. Modular requires workspace packages to have a name.', + ); + }); + + it('does not support packages with no version', async () => { + const projectRoot = `${fixturesPath}invalid-workspace-5`; + let thrown = false; + let message = ''; + + try { + await resolveWorkspace(projectRoot, projectRoot); + } catch (err) { + thrown = true; + if (err instanceof Error) { + message = err.message; + } + } + + expect(thrown).toEqual(true); + expect(message).toEqual( + 'The package "app-one" has an invalid version. Modular requires workspace packages to have a version.', + ); + }); + }); + + describe('analyzeWorkspaceDependencies', () => { + it('correctly identifies workspace dependencies for a clean workspace', async () => { + const projectRoot = `${fixturesPath}clean-workspace-1`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + + const result = analyzeWorkspaceDependencies(allPackages); + const expected = { + 'app-one': { + location: 'packages/app-one', + workspaceDependencies: ['package-one', 'package-two'], + mismatchedWorkspaceDependencies: [], + }, + 'package-one': { + location: 'packages/package-one', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + 'package-two': { + location: 'packages/package-two', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + }; + expect(result).toEqual(expected); + }); + + it('correctly identifies mismatched dependencies', async () => { + const projectRoot = `${fixturesPath}mismatched-dependency`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + + const result = analyzeWorkspaceDependencies(allPackages); + const expected = { + 'app-one': { + location: 'packages/app-one', + workspaceDependencies: ['package-two'], + mismatchedWorkspaceDependencies: ['package-one'], + }, + 'package-one': { + location: 'packages/package-one', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + 'package-two': { + location: 'packages/package-two', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + }; + expect(result).toEqual(expected); + }); + + it('correctly identifies dependencies (workspace range)', async () => { + const projectRoot = `${fixturesPath}clean-workspace-3`; + const [allPackages] = await resolveWorkspace(projectRoot, projectRoot); + + // Matches explanation (version 1.0.0): + // Range of '*': matches + // Range of '^1.0.0': matches + // Range of '^': mismatches + // Range of '~': mismatches + // We rely on semver to determine this, the same way that yarn do. + + const result = analyzeWorkspaceDependencies(allPackages); + const expected = { + 'app-one': { + location: 'packages/app-one', + workspaceDependencies: ['package-one', 'package-four'], + mismatchedWorkspaceDependencies: ['package-two', 'package-three'], + }, + 'package-one': { + location: 'packages/package-one', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + 'package-two': { + location: 'packages/package-two', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + 'package-three': { + location: 'packages/package-three', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + 'package-four': { + location: 'packages/package-four', + workspaceDependencies: [], + mismatchedWorkspaceDependencies: [], + }, + }; + expect(result).toEqual(expected); + }); + }); +}); diff --git a/packages/workspace-resolver/src/index.ts b/packages/workspace-resolver/src/index.ts new file mode 100644 index 000000000..42ad5e844 --- /dev/null +++ b/packages/workspace-resolver/src/index.ts @@ -0,0 +1,4 @@ +export { + resolveWorkspace, + analyzeWorkspaceDependencies, +} from './resolve-workspace'; diff --git a/packages/workspace-resolver/src/resolve-workspace.ts b/packages/workspace-resolver/src/resolve-workspace.ts new file mode 100644 index 000000000..e693c505c --- /dev/null +++ b/packages/workspace-resolver/src/resolve-workspace.ts @@ -0,0 +1,200 @@ +import path, { join } from 'path'; +import { readJson } from 'fs-extra'; +import globby from 'globby'; +import semver from 'semver'; + +import type { + ModularWorkspacePackage, + WorkspaceMap, + WorkspaceObj, + ModularPackageJson, +} from '@modular-scripts/modular-types'; + +// See https://yarnpkg.com/features/workspaces#workspace-ranges-workspace +const YARN_WORKSPACE_RANGE_PREFIX = 'workspace:'; + +function packageJsonPath(dir: string) { + return dir.endsWith('package.json') ? dir : join(dir, 'package.json'); +} + +function resolveWorkspacesDefinition( + cwd: string, + def: ModularPackageJson['workspaces'], +): string[] { + if (!def) { + return []; + } + + if (Array.isArray(def)) { + return def.flatMap((path: string) => { + return globby.sync( + [`${path}/package.json`, '!**/node_modules/**/*', '!**/__tests__/**/*'], + { + absolute: false, + cwd, + }, + ); + }); + } + + return resolveWorkspacesDefinition(cwd, def.packages); +} + +function readPackageJson( + isRoot: boolean, + workingDir: string, + relativePath: string, +): Promise { + const jsonPath = isRoot + ? relativePath + : `${workingDir}${path.sep}${relativePath}`; + + return readJson(jsonPath) as Promise; +} + +export async function resolveWorkspace( + root: string, + workingDir: string | null = null, + parent: ModularWorkspacePackage | null = null, + collector = new Map(), +): Promise< + [Map, ModularWorkspacePackage | null] +> { + const workingDirToUse = workingDir ?? process.cwd(); + const isRoot = workingDirToUse === root; + const path = packageJsonPath(root); + const json = await readPackageJson(isRoot, workingDirToUse, path); + const isModularRoot = json.modular?.type === 'root'; + + if (!json.name) { + throw new Error( + `The package at ${path} does not have a valid name. Modular requires workspace packages to have a name.`, + ); + } + + const versionToUse = isModularRoot ? '1.0.0' : json.version; + + if (!versionToUse) { + throw new Error( + `The package "${json.name}" has an invalid version. Modular requires workspace packages to have a version.`, + ); + } + + const pkg: ModularWorkspacePackage = { + path, + name: json.name, + version: versionToUse, + workspace: !!json.workspaces, + children: [], + parent, + modular: { + type: 'unknown', + ...json.modular, + }, + // Like yarn classic `workspaces info`, we include all except peerDependencies + dependencies: { + ...json.optionalDependencies, + ...json.devDependencies, + ...json.dependencies, + }, + }; + collector.set(json.name, pkg); + + if (json.modular?.type === 'root' && !isRoot) { + throw new Error( + 'Nested modular roots are currently not supported by Modular', + ); + } + + // Allow for the `workspaces` value to be `[]` or `{}`, otherwise throw (as nested workspaces currently unsupported) + if (!isRoot && json.workspaces) { + if (Array.isArray(json.workspaces) && json.workspaces.length > 0) { + throw new Error( + 'Nested workspaces are currently not supported by Modular', + ); + } + + if ( + typeof json.workspaces === 'object' && + !Array.isArray(json.workspaces) && + Object.keys(json.workspaces).length > 0 + ) { + throw new Error( + 'Nested workspaces are currently not supported by Modular', + ); + } + } + + for (const link of resolveWorkspacesDefinition(root, json.workspaces)) { + const [, child] = await resolveWorkspace( + link, + workingDirToUse, + pkg, + collector, + ); + child && pkg.children.push(child); + } + + return [collector, pkg]; +} + +export function analyzeWorkspaceDependencies( + workspacePackages: Map, +): WorkspaceMap { + const mappedDeps = new Map(); + const exhaustivePackageNameList = Array.from(workspacePackages.keys()); + const allPackages = Array.from(workspacePackages.entries()); + + // Exclude the root when analyzing package inter-dependencies + const packagesWithoutRoot = Array.from(workspacePackages.entries()).filter( + ([, pkg]) => { + return pkg.modular.type !== 'root'; + }, + ); + + // Calculate deps and mismatches a-la Yarn classic `workspaces info` + packagesWithoutRoot.forEach(([pkgName, pkg]) => { + const packageDepNames = Object.keys(pkg.dependencies || {}).filter( + (dep) => { + return exhaustivePackageNameList.includes(dep); + }, + ); + const packageDeps = allPackages.filter(([, pkg]) => + packageDepNames.includes(pkg.name), + ); + + // Mismatched = version in packages//package.json does not satisfy the dependent's range + const mismatchedWorkspaceDependencies = Object.entries( + pkg.dependencies || {}, + ) + .filter(([dep, range]) => { + const matchingPackage = packageDeps.find( + ([matchingPackageName]) => dep === matchingPackageName, + ); + if (!matchingPackage) { + return false; + } + + const [, match] = matchingPackage; + + // Account for use of Yarn Workspace Ranges + // Note: we do not support the unstable project-relative path flavour syntax + const rangeToUse = range.includes(YARN_WORKSPACE_RANGE_PREFIX) + ? range.replace(YARN_WORKSPACE_RANGE_PREFIX, '') + : range; + + return !semver.satisfies(match.version, rangeToUse); + }) + .flatMap(([dep]) => dep); + + mappedDeps.set(pkgName, { + location: path.dirname(pkg.path), + workspaceDependencies: packageDepNames.filter( + (depName) => !mismatchedWorkspaceDependencies.includes(depName), + ), + mismatchedWorkspaceDependencies, + }); + }); + + return Object.fromEntries(mappedDeps); +} diff --git a/packages/workspace-resolver/tsconfig.json b/packages/workspace-resolver/tsconfig.json new file mode 100644 index 000000000..2577c1ff5 --- /dev/null +++ b/packages/workspace-resolver/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../modular-scripts/tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist-cjs" + }, + "include": ["src/*"], + "exclude": ["__tests__", "node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index 7812ff22c..d19699d81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,12 @@ "modular", "packages/**/src", "packages/create-modular-react-app/template/" - ] + ], + "compilerOptions": { + "paths": { + "@modular-scripts/workspace-resolver": [ + "./packages/workspace-resolver/src" + ] + } + } } diff --git a/yarn.lock b/yarn.lock index 00366caca..c9472bc7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2684,7 +2684,7 @@ dependencies: find-up "*" -"@types/fs-extra@9.0.13": +"@types/fs-extra@9.0.13", "@types/fs-extra@^9.0.13": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== @@ -6666,7 +6666,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@10.1.0, fs-extra@^10.0.0: +fs-extra@10.1.0, fs-extra@^10.0.0, fs-extra@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==