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: build esm modules using ipjs #865

Merged
merged 10 commits into from
Aug 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package-lock.json
yarn.lock
node_modules
/node_modules
/actions/bundle-size/node_modules
/coverage
/dist
/docs
Expand Down
2 changes: 2 additions & 0 deletions actions/bundle-size/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**
!/dist
6 changes: 6 additions & 0 deletions actions/bundle-size/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Just enough docker until github gets a new node16 runner
# see: https://github.com/actions/runner/issues/772
FROM node:16-alpine
WORKDIR /usr/src/app
COPY dist/index.js .
CMD [ "node", "index.js" ]
8 changes: 6 additions & 2 deletions actions/bundle-size/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ inputs:
description: A directory to run the bundle check in
required: false
runs:
using: 'node12'
main: 'dist/index.js'
# TODO: we need node14.14 minimum.
# https://github.com/actions/runner/issues/772
# using: 'node12'
# main: 'dist/index.js'
using: 'docker'
image: 'Dockerfile'
27 changes: 27 additions & 0 deletions md/esm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ESM support

## Setup

`aegir` leverages [ipjs](https://github.com/mikeal/ipjs) to output a build with `cjs` and `esm` for maximum compatibility. The general guidelines for writing a module in `esm` are detailed on the `ipjs` README. `aegir` will automatically identify a `esm` repo by the `module` property in `package.json`.

## Electron testing

Electron does [not support ESM](https://github.com/electron/electron/issues/21457) at the time of writing. When writing a module using ESM, we need to compile the tests to `cjs` and rely on them. For generating the build including the tests:

```bash
aegir build --esm-tests
```

## Lerna Monorepo

When using a lerna monorepo, local dependencies are symlinked by lerna on install. This means that an `esm` module will not use the resulting `dist` folder as symlink. This can become a problem if we are testing the `cjs` build of a module.

To work around the above problem, we can use `publishConfig.directory = "dist"` in `package.json` to notice lerna about the symlink path. After running the `aegir build` command, it is necessary to run `lerna link` to update the symlinks.

## Release

When releasing an `esm` module, the published content will be the generated `dist` folder content, as indicated by [ipjs](https://github.com/mikeal/ipjs).

## Examples

TODO: List examples when merged (`ipfs-unixfs`, `uint8arrays`)
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@
"esbuild-register": "^2.3.0",
"eslint": "^7.23.0",
"eslint-config-ipfs": "^2.0.0",
"execa": "^5.0.0",
"execa": "^5.1.1",
"extract-zip": "^2.0.1",
"fs-extra": "^10.0.0",
"gh-pages": "^3.1.0",
"git-authors-cli": "^1.0.33",
"globby": "^11.0.3",
"ipfs-utils": "^8.1.0",
"ipjs": "^5.0.5",
"it-glob": "~0.0.10",
"kleur": "^4.1.4",
"lilconfig": "^2.0.2",
Expand All @@ -119,7 +120,7 @@
"typedoc": "^0.21.2",
"typescript": "^4.3.5",
"update-notifier": "^5.0.0",
"yargs": "^17.0.1"
"yargs": "^17.1.1"
},
"devDependencies": {
"@types/bytes": "^3.1.0",
Expand Down
33 changes: 33 additions & 0 deletions src/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,44 @@ const build = async (argv) => {
return outfile
}

/**
* Build command
*
* @param {GlobalOptions & BuildOptions} argv
*/
const buildEsm = async (argv) => {
const dist = path.join(process.cwd(), 'dist')
// @ts-ignore no types
const ipjs = await import('ipjs')

await ipjs.default({
dist,
onConsole: (/** @type {any[]} */...args) => console.info.apply(console, args),
cwd: process.cwd(),
main: argv.esmMain,
tests: argv.esmTests
})
}

const tasks = new Listr([
{
title: 'Clean ./dist',
task: async () => del(path.join(process.cwd(), 'dist'))
},
{
title: 'Build ESM',
enabled: ctx => {
return pkg.type === 'module'
},
/**
*
* @param {GlobalOptions & BuildOptions} ctx
* @param {Task} task
*/
task: async (ctx, task) => {
await buildEsm(ctx)
}
},
{
title: 'Bundle',
enabled: ctx => ctx.bundle,
Expand Down
12 changes: 12 additions & 0 deletions src/cmds/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ module.exports = {
type: 'boolean',
describe: 'Build the Typescripts type declarations.',
default: userConfig.build.types
},
esmMain: {
alias: 'esm-main',
type: 'boolean',
describe: 'Include a main field in a built esm project',
default: userConfig.build.esmMain
},
esmTests: {
alias: 'esm-tests',
type: 'boolean',
describe: 'Include tests in a built esm project',
default: userConfig.build.esmTests
}
})
},
Expand Down
4 changes: 3 additions & 1 deletion src/config/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const defaults = {
bundlesize: false,
bundlesizeMax: '100kB',
types: true,
config: {}
config: {},
esmMain: true,
esmTests: false
},
// linter cmd options
lint: {
Expand Down
24 changes: 15 additions & 9 deletions src/release/publish.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const execa = require('execa')
const { otp } = require('../utils')
const { otp, pkg, repoDirectory } = require('../utils')
/**
* @typedef {import('./../types').ReleaseOptions} ReleaseOptions
* @typedef {import('listr').ListrTaskWrapper} ListrTask
Expand All @@ -25,16 +25,22 @@ function publish (ctx, task) {
task.title += ` (npm ${publishArgs.join(' ')})`
}

return execa('npm', publishArgs)
.catch(async (error) => {
if (error.toString().includes('provide a one-time password')) {
const code = await otp()
task.title += '. Trying again with OTP.'
return await execa('npm', publishArgs.concat('--otp', code))
// Publish from dist if ESM
const execaOptions = pkg.type === 'module'
? {
cwd: `${repoDirectory}/dist`
}
: {}

throw error
})
return execa('npm', publishArgs, execaOptions).catch(async (error) => {
if (error.toString().includes('provide a one-time password')) {
const code = await otp()
task.title += '. Trying again with OTP.'
return await execa('npm', publishArgs.concat('--otp', code), execaOptions)
}

throw error
})
}

module.exports = publish
15 changes: 15 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ interface GlobalOptions {
fileConfig: Options
}

interface ESMHooks {
onParse: () => {}
onParsed: () => {}
onDeflateStart: () => {}
onDeflateEnd: () => {}
}

interface BuildOptions {
/**
* Build the JS standalone bundle.
Expand All @@ -120,6 +127,14 @@ interface BuildOptions {
* esbuild build options
*/
config: esbuild.BuildOptions
/**
* Include tests in the ipjs output directory
*/
esmTests: boolean
/**
* Include a main field in the ipjs output package.json
*/
esmMain: boolean
}

interface TSOptions {
Expand Down
37 changes: 37 additions & 0 deletions test/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-env mocha */
'use strict'

const { expect } = require('../utils/chai')
const execa = require('execa')
const { copy, existsSync } = require('fs-extra')
const { join } = require('path')
const bin = require.resolve('../')
const tempy = require('tempy')

describe('build', () => {
describe('esm', () => {
let projectDir = ''

before(async () => {
projectDir = tempy.directory()

await copy(join(__dirname, 'fixtures', 'esm', 'an-esm-project'), projectDir)
})

it('should build an esm project', async function () {
this.timeout(20 * 1000) // slow ci is slow

await execa(bin, ['build'], {
cwd: projectDir
})

expect(existsSync(join(projectDir, 'dist', 'esm'))).to.be.true()
expect(existsSync(join(projectDir, 'dist', 'cjs'))).to.be.true()

const module = require(join(projectDir, 'dist'))

expect(module).to.have.property('useHerp').that.is.a('function')
expect(module).to.have.property('useDerp').that.is.a('function')
})
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions test/fixtures/esm/an-esm-project/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "an-esm-project",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
}
}
}
10 changes: 10 additions & 0 deletions test/fixtures/esm/an-esm-project/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import herp from 'a-cjs-dep'
import derp from 'an-esm-dep'

export const useHerp = () => {
herp()
}

export const useDerp = () => {
derp()
}
1 change: 1 addition & 0 deletions test/node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

require('./build')
require('./lint')
require('./fixtures')
require('./dependants')
Expand Down