Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add workspace support #21

Merged
merged 10 commits into from
Dec 9, 2021
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ single devDependency.

**CAUTION: THESE CHANGES WILL OVERWRITE ANY LOCAL FILES AND SETTINGS**

### Configuration

Configure the use of `template-oss` in the root `package.json`.

```js
{
name: 'my-package',
// ...
templateOSS: {
// copy repo specific files for the root pkg
applyRootRepoFiles: true,
// modify package.json and copy module specific files for the root pkg
applyRootModuleFiles: true,
// copy repo files for each whitelisted workspaces
applyWorkspaceRepoFiles: true,
// whitelist workspace by package name to modify package.json
// and copy module files
workspaces: ['workspace-package-name'],
version: '2.3.1'
}
}

### `package.json` patches

These fields will be set in the project's `package.json`:
Expand Down Expand Up @@ -58,13 +80,18 @@ These files will be copied, overwriting any existing files:
- `LICENSE.md`
- `SECURITY.md`

### Dynamic Files

Currently, the only dynamic file generated is a github workflow for a given workspace.
`.github/workflows/ci-$$package-name$$.yml`

#### Extending

Place files in the `lib/content/` directory, use only the file name and remove
any leading `.` characters (i.e. `.github/workflows/ci.yml` becomes `ci.yml`
and `.gitignore` becomes `gitignore`).

Modify the `content` object at the top of `lib/postinstall/copy-content.js` to include
Modify the `repoFiles` and `moduleFiles` objects at the top of `lib/postinstall/copy-content.js` to include
your new file. The object keys are destination paths, and values are source.

### `package.json` checks
Expand All @@ -76,4 +103,4 @@ is not configured properly, with steps to run to correct any problems.

Add any unwanted packages to `unwantedPackages` in `lib/check.js`. Currently
the best way to install any packages is to include them as `peerDependencies`
in this repo.
in this repo.
18 changes: 14 additions & 4 deletions bin/npm-template-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const checkPackage = require('../lib/postlint/check-package.js')
const checkGitIgnore = require('../lib/postlint/check-gitignore.js')
const getConfig = require('../lib/config.js')

const main = async () => {
const {
Expand All @@ -12,10 +13,19 @@ const main = async () => {
throw new Error('This package requires npm >7.21.1')
}

const problems = [
...(await checkPackage(root)),
...(await checkGitIgnore(root)),
]
const config = await getConfig(root)

const problemSets = []
for (const path of config.paths) {
if (path !== root || config.applyRootModuleFiles) {
problemSets.push(await checkPackage(path))
}
if (path !== root || config.applyRootRepoFiles) {
problemSets.push(await checkGitIgnore(path))
}
}

const problems = problemSets.flat()
wraithgar marked this conversation as resolved.
Show resolved Hide resolved

if (problems.length) {
console.error('Some problems were detected:')
Expand Down
13 changes: 8 additions & 5 deletions bin/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const copyContent = require('../lib/postinstall/copy-content.js')
const patchPackage = require('../lib/postinstall/update-package.js')
const getConfig = require('../lib/config.js')

const main = async () => {
const {
Expand All @@ -14,12 +15,14 @@ const main = async () => {
return
}

const needsAction = await patchPackage(root)
if (!needsAction) {
return
}
const config = await getConfig(root)
for (const path of config.paths) {
if (!await patchPackage(path, root, config)) {
continue
}

await copyContent(root)
await copyContent(path, root, config)
}
}

module.exports = main().catch((err) => {
Expand Down
48 changes: 48 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const PackageJson = require('@npmcli/package-json')
const mapWorkspaces = require('@npmcli/map-workspaces')

const defaultConfig = {
applyRootRepoFiles: true,
applyWorkspaceRepoFiles: true,
applyRootModuleFiles: true,
workspaces: [],
paths: [],
}

module.exports = async (root) => {
let pkg
let pkgError = false
try {
pkg = (await PackageJson.load(root)).content
} catch (e) {
pkgError = true
}
if (pkgError || !pkg.templateOSS) {
return {
...defaultConfig,
paths: [root],
}
}
const config = {
...defaultConfig,
...pkg.templateOSS,
}
const workspaceMap = await mapWorkspaces({
pkg,
cwd: root,
})
const wsPaths = []
const workspaceSet = new Set(config.workspaces)
for (const [name, path] of workspaceMap.entries()) {
if (workspaceSet.has(name)) {
wsPaths.push(path)
}
}
config.workspacePaths = wsPaths

config.paths = config.paths.concat(config.workspacePaths)

config.paths.push(root)

return config
}
76 changes: 76 additions & 0 deletions lib/content/ci-workspace.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Node Workspace CI %%pkgname%%

on:
pull_request:
paths:
- %%pkgpath%%/**
branches:
- '*'
push:
paths:
- %%pkgpath%%/**
branches:
- release-next
- latest
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest
steps:
# Checkout the npm/cli repo
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: npm
- name: Install dependencies
run: |
node ./bin/npm-cli.js install --ignore-scripts --no-audit
node ./bin/npm-cli.js rebuild
- name: Run linting
run: node ./bin/npm-cli.js run posttest -w libnpmdiff
env:
DEPLOY_VERSION: testing

test:
strategy:
fail-fast: false
matrix:
node-version: ['12.13.0', 12.x, '14.15.0', 14.x, '16.0.0', 16.x]
platform:
- os: ubuntu-latest
shell: bash
- os: macos-latest
shell: bash
- os: windows-latest
shell: bash
- os: windows-latest
shell: powershell

runs-on: ${{ matrix.platform.os }}
defaults:
run:
shell: ${{ matrix.platform.shell }}

steps:
# Checkout the npm/cli repo
- uses: actions/checkout@v2

# Installs the specific version of Node.js
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: npm

# Run the installer script
- name: Install dependencies
run: |
node ./bin/npm-cli.js install --ignore-scripts --no-audit
node ./bin/npm-cli.js rebuild

# Run the tests, but not if we're just gonna do coveralls later anyway
- name: Run Tap tests
run: node ./bin/npm-cli.js run -w libnpmdiff --ignore-scripts test -- -t600 -Rbase -c
97 changes: 76 additions & 21 deletions lib/postinstall/copy-content.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
const { dirname, join, resolve } = require('path')
const fs = require('@npmcli/fs')
const PackageJson = require('@npmcli/package-json')

const contentDir = resolve(__dirname, '..', 'content')

// keys are destination paths in the target project
// values are paths to contents relative to '../content/'
const content = {
const moduleFiles = {
'.eslintrc.js': './eslintrc.js',
'.gitignore': './gitignore',
'SECURITY.md': './SECURITY.md',
}

const repoFiles = {
'.github/workflows/ci.yml': './ci.yml',
'.github/ISSUE_TEMPLATE/bug.yml': './bug.yml',
'.github/ISSUE_TEMPLATE/config.yml': './config.yml',
'.github/CODEOWNERS': './CODEOWNERS',
'.gitignore': './gitignore',
'LICENSE.md': './LICENSE.md',
'SECURITY.md': './SECURITY.md',
}

// currently no workspace moduleFiles
// const workspaceContent = {}
// const workspaceRootContent = {}

const filesToDelete = [
// remove any other license files
/^LICENSE*/,
// remove any eslint config files that aren't local to the project
/^\.eslintrc\.(?!(local\.)).*/,
]

// given a root directory, copy all files in the content map
// after purging any files we need to delete
const copyContent = async (root) => {
const contents = await fs.readdir(root)

for (const file of contents) {
if (filesToDelete.some((p) => p.test(file))) {
await fs.rm(join(root, file))
}
}
const defaultConfig = {
applyRootRepoFiles: true,
applyWorkspaceRepoFiles: true,
applyRootModuleFiles: true,
}

for (let [target, source] of Object.entries(content)) {
const copyFiles = async (targetDir, files) => {
for (let [target, source] of Object.entries(files)) {
source = join(contentDir, source)
target = join(root, target)
// if the target is a subdirectory of the root, mkdirp it first
if (dirname(target) !== root) {
target = join(targetDir, target)
// if the target is a subdirectory of the path, mkdirp it first
if (dirname(target) !== targetDir) {
await fs.mkdir(dirname(target), {
owner: 'inherit',
recursive: true,
Expand All @@ -48,6 +49,60 @@ const copyContent = async (root) => {
await fs.copyFile(source, target, { owner: 'inherit' })
}
}
copyContent.content = content

// given a root directory, copy all files in the content map
// after purging any files we need to delete
const copyContent = async (path, rootPath, config) => {
config = { ...defaultConfig, ...config }
const isWorkspace = path !== rootPath

const contents = await fs.readdir(path)

if (isWorkspace || config.applyRootModuleFiles) {
// delete files and copy moduleFiles if it's a workspace
// or if we enabled doing so for the root
for (const file of contents) {
if (filesToDelete.some((p) => p.test(file))) {
await fs.rm(join(path, file))
}
}
await copyFiles(path, moduleFiles)
}

if (!isWorkspace) {
if (config.applyRootRepoFiles) {
await copyFiles(rootPath, repoFiles)
}
return
} // only workspace now

// TODO: await copyFiles(path, workspaceFiles)
// if we ever have workspace specific files

if (config.applyWorkspaceRepoFiles) {
// copy and edit workspace repo file (ci github action)
const workspacePkg = (await PackageJson.load(path)).content
const workspaceName = workspacePkg.name
const workflowPath = join(rootPath, '.github', 'workflows')
await fs.mkdir(workflowPath, {
owner: 'inherit',
recursive: true,
force: true,
})

let workflowData = await fs.readFile(
join(contentDir, './ci-workspace.yml'),
{ encoding: 'utf-8' }
)

const relPath = path.substring(rootPath.length + 1)
workflowData = workflowData.replace(/%%pkgpath%%/g, relPath)
workflowData = workflowData.replace(/%%pkgname%%/g, workspaceName)

await fs.writeFile(join(workflowPath, `ci-${workspaceName}.yml`), workflowData)
}
}
copyContent.moduleFiles = moduleFiles
copyContent.repoFiles = repoFiles

module.exports = copyContent