bevry / base

Base files for new projects
Other
30 stars 14 forks source link

Use editions #21

Closed balupton closed 8 years ago

balupton commented 8 years ago

It would be nice to introduce a concept of editions for packages that include multiple targets. Something like:

{
  "editions": {
    "source": {
      "directory": "src",
      "entry": "src/lib/index.js"
    },
    "esnext": {
      "directory": "esnext",
      "entry": "esnext/lib/index.js",
      "command": "babel src --out-dir esnext --presets es2015 --plugins syntax-flow transform-flow-strip-types"
    },
    "es2015": {
      "directory": "es2015",
      "entry": "es2015/lib/index.js",
      "command": "babel src --out-dir es2015 --presets es2015 --plugins syntax-flow"
    },
    "prefer": "esnext",
    "fallback": "es2015"
  },
  "esnext": {
    "sourceDirectory": "esnext",
    "compiledDirectory": "es5",
    "guardianEntry": "esnextguardian.js",
    "mainEntryRelative": "lib/index.js",
    "testEntryRelative": "test/index.js"
  }
}

Which using something like the following inside projectz:

        // Babel
        if ( opts.esnext ) {
            const sourceDirectoryPath = `${opts.name}/${opts.esnext.sourceDirectory}`
            const compiledDirectoryPath = `${opts.name}/${opts.esnext.compiledDirectory}`
            const sourceEntryPath = `${sourceDirectoryPath}/${opts.esnext.mainEntryRelative}`
            const compiledEntryPath = `${compiledDirectoryPath}/${opts.esnext.mainEntryRelative}`
            const guardianEntryPath = `${opts.name}/${opts.esnext.guardianEntry}`
            const guardianLink = projectzUtil.getLink({text: 'ESNextGuardian', url: 'https://www.npmjs.com/package/esnextguardian', title: `Loads ES6+ files if the user's environment supports it, otherwise gracefully fallback to ES5 files.`})
            const polyfillLink = projectzUtil.getLink({text: `Babel's Polyfill`, url: 'https://babeljs.io/docs/usage/polyfill/', title: 'A polyfill by the Babel project that emulates missing ECMAScript environment features'})
            const babelLink = projectzUtil.getLink({text: 'Babel', url: 'https://babeljs.io', title: 'The compiler for writing next generation JavaScript'})
            const esLink = projectzUtil.getLink({text: 'latest ECMAScript standards', url: 'https://babeljs.io/docs/learn-es2015/'})

            parts.push([
                `<h3>ESNext</h3>`,
                `<p>This project is written using the ${esLink} and compiled using ${babelLink}.<br/>`,
                `<ul><li>The source code is at <code>${sourceDirectoryPath}</code> and entry is at <code>${sourceEntryPath}</code></li>`,
                `<li>The compiled code is at <code>${compiledDirectoryPath}</code> and entry is at <code>${compiledEntryPath}</code></li>`,
                `<li>The default entry uses ${guardianLink} and is at <code>${guardianEntryPath}</code></li></ul>`,
                `Older ECMAScript environments may need ${polyfillLink} or something else.</p>`
            ].join('\n'))
        }

Will generate a README entry like:

ESNext

This project is written using the latest ECMAScript standards and compiled using Babel.

  • The source code is at projectz/esnext and entry is at projectz/esnext/lib/index.js
  • The compiled code is at projectz/es5 and entry is at projectz/es5/lib/index.js
  • The default entry uses ESNextGuardian and is at projectz/esnextguardian.js
Older ECMAScript environments may need Babel's Polyfill or something else.

balupton commented 8 years ago

Another take. This time using Flow Type syntax and using npm scripts for the build.

{
  "devDependencies": {
    "assert-helpers": "^4.0.0",
    "babel-cli": "^6.4.5",
    "babel-plugin-syntax-flow": "^6.3.13",
    "babel-plugin-transform-flow-strip-types": "^6.4.0",
    "babel-preset-es2015": "^6.3.13",
    "eslint": "^1.10.3",
    "eslint-plugin-flowtype": "^1.0.0",
    "flow-bin": "^0.20.1",
    "joe": "^1.6.0",
    "joe-reporter-console": "^1.2.1",
    "safeps": "^6.0.0",
    "yuidocjs": "^0.10.0"
  },
  "scripts": {
    "clean": "rm -Rf es2015 esnext",
    "setup": "npm install",
    "setup:build": "npm install --save-dev babel-cli babel-preset-es2015 babel-plugin-syntax-flow babel-plugin-transform-flow-strip-types eslint eslint-plugin-flowtype flow-bin yuidocjs",
    "compile": "npm run compile:esnext && npm run compile:es2015",
    "compile:esnext": "babel source --out-dir esnext --plugins syntax-flow,transform-flow-strip-types",
    "compile:es2015": "babel source --out-dir es2015 --presets es2015 --plugins syntax-flow,transform-flow-strip-types",
    "verify": "npm run compile && npm run verify:eslint && npm run verify:flow",
    "verify:eslint": "eslint source --plugin flowtype",
    "verify:flow": "flow check",
    "meta": "npm run meta:yuidoc && npm run meta:projectz",
    "meta:yuidoc": "yuidoc -o yuidoc --syntaxtype js -e .js esnext",
    "meta:projectz": "$npm_package_bin compile",
    "prepare": "npm run clean && npm run verify && npm run meta",
    "release": "npm run prepare && npm run release:tag && npm run release:push",
    "release:tag": "git tag v$npm_package_version -a",
    "release:push": "git push origin master && git push origin --tags",
    "pretest": "npm run verify",
    "test": "node --harmony $npm_package_test"
  },
  "editions": [
    {
      "description": "source ESNext code with Flow Type",
      "directory": "source",
      "entry": "source/lib/index.js",
      "source": true
    },
    {
      "description": "Babel compiled ESNext code",
      "directory": "esnext",
      "entry": "esnext/lib/index.js"
    },
    {
      "description": "Babel compiled ES2015 code",
      "directory": "es2015",
      "entry": "es2015/lib/index.js",
      "browser": true
    }
  ],
  "engines": {
    "node": ">=0.12"
  },
  "preferGlobal": true,
  "bin": "./bin/projectz",
  "main": "./esnextguardian.js",
  "test": "./es2015/test/index.js"
}

Will remove in favour of flow type comments which no longer needed a source directory.

balupton commented 8 years ago

A cut down version of the above without flow types:

{
    "scripts": {
        "clean": "rimraf es2015 yuidoc",
        "setup": "npm install",
        "setup:build": "npm install --save-dev babel-cli babel-preset-es2015 eslint flow-bin rimraf yuidocjs && flow init",
        "compile": "npm run compile:es2015",
        "compile:es2015": "babel ./esnext --out-dir ./es2015 --presets es2015",
        "meta": "npm run meta:yuidoc && npm run meta:projectz",
        "meta:yuidoc": "yuidoc ./esnext -o yuidoc --syntaxtype js -e .js",
        "meta:projectz": "./bin/projectz compile",
        "prepare": "npm run clean && npm run compile && npm run test && npm run meta",
        "release": "npm run prepare && npm run release:tag && npm run release:push",
        "release:tag": "git tag v$npm_package_version -a",
        "release:push": "git push origin master && git push origin --tags",
        "pretest": "npm run test:eslint && npm run test:flow",
        "test:eslint": "eslint ./esnext",
        "test:flow": "flow check",
        "test": "node --harmony ./es2015/test/index.js"
    },
    "editions": [
        {
            "description": "source ESNext code",
            "directory": "esnext",
            "entry": "esnext/lib/index.js",
            "source": true,
            "prefer": true
        },
        {
            "description": "Babel compiled ES2015 code",
            "directory": "es2015",
            "entry": "es2015/lib/index.js",
            "browser": true,
            "fallback": true
        }
    ]
}
balupton commented 8 years ago

Turns out node actually supports creating a file at project/esnext/index.js then including it via require('project/esnext'). Which can simplify a lot of this.


The directory property here can probably be optional, determined either from the edition name (if using an object hash, e.g. "editions": { "esnext": { /* ... */ } }), or as the first directory of the entry property. However, if optional, will require parsers to do more.

balupton commented 8 years ago

Here is another take. Uses a syntax array for parsers to decide if they can support that edition (useful for bundlers, and parsers). Uses source to indicate which one is the original source (useful for bundlers). Uses the order in which they appear as the require preference (first is most preferred require).

Expanded:

{
    "editions": {
        "source": {
            "source": true, // meta information only, not used by parsers
            "syntax": ["ESNext", "Flow Type", "JSX"],
            "directory": "source", // meta information only, not used by parsers
            "entry": "source/index.js"
        },
        "esnext": {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ESNext"],
            "directory": "esnext", // meta information only, not used by parsers
            "entry": "esnext/index.js"
        },
        "es2015": {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ES2015"],
            "directory": "es2015", // meta information only, not used by parsers
            "entry": "es2015/index.js"
        },
        "es5": {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ES5"],
            "directory": "es5", // meta information only, not used by parsers
            "entry": "es5/index.js"
        }
    }
}

Shortended:

{
    "editions": {
        "source": {
            "syntax": ["ESNext", "Flow Type", "JSX"]
        },
        "esnext": {
            "syntax": ["ESNext"]
        },
        "es2015": {
            "syntax": ["ES2015"]
        },
        "es5": {
            "syntax": ["ES5"],
        }
    }
}

Using hashes for the editions could provide the false assumption that the edition hashes actually mean something, which isn't true, would just be for the convenience for the developer. So using an array like so would be better.

Expanded:

{
    "editions": [
        {
            "source": true,
            "syntax": ["ESNext", "Flow Type", "JSX"],
            "directory": "source",
            "entry": "source/index.js"
        }, {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ESNext"],
            "directory": "esnext", // meta information only, not used by parsers
            "entry": "esnext/index.js"
        }, {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ES2015"],
            "directory": "es2015", // meta information only, not used by parsers
            "entry": "es2015/index.js"
        }, {
            "compiled": true, // meta information only, not used by parsers
            "syntax": ["ES5"],
            "directory": "es5", // meta information only, not used by parsers
            "entry": "es5/index.js"
        }
    ]
}

Shortened:

{
    "editions": [
        {
            "syntax": ["ESNext", "Flow Type", "JSX"],
            "directory": "source"
        }, {
            "syntax": ["ESNext"],
            "directory": "esnext"
        }, {
            "syntax": ["ES2015"],
            "directory": "es2015"
        }, {
            "syntax": ["ES5"],
            "directory": "es5"
        }
    ]
}

There should probably also be an optional default property that could be auto-detected, but when specified will say this edition is the one main points to. If main points to something else, like esnextguardian or whatever, it is suitable for no edition to have a default property. The README generator can detect this appropriately when outputting human readable usage instructions.

There could also be the syntax features ES Modules and CJS Modules. However, that will require different builds for each. Maybe the proposed jsm file extension here could work (the proposal being where each compiled file comes in two files, a .js file for CJS Modules and a .jsm file for ES Modules) however syntax may need to support something like ["ESNext", ["CJS Modules", "ES Modules"]] to say it supports either, however that is really messy and complex, and could mean multiple things - are they both supported in the same file? does it use jsm files (which the parser may or may not actually support)? Maybe using jsm files will need a new optionalSyntax so something like "syntax": ["ESNext", "CJS Modules"], "optionalSyntax": ["JSM Modules"]

Another point, is the directory actually useful here, or could we just have the entry. And could the entry just be say dir when dir/index.js is used, considering one can just do require('dir') rather than require('dir/index.js') - or should it always point to the full file path, rather than the possible shorter require path.


Output into READMEs for this proposal would be something like the following when using esnextguardian:

Editions

  • require('project') is an alias for require('project/esnextguardian.js') which uses esnextguardian to load the correct edition for your environment
  • require('project/source') is the source files with ESNext, Flow Type, and JSX syntax
  • require('project/esnext') is compiled files with ESNext syntax
  • require('project/es2015') is compiled files with ES2015 syntax
  • require('project/es5') is compiled files with ES5 syntax

And if not using esnextguardian and main just goes to the ES2015 edition:

Editions

  • require('project') is an alias for require('project/es2015')
  • require('project/source') is the source files with ESNext, Flow Type, and JSX syntax
  • require('project/esnext') is compiled files with ESNext syntax
  • require('project/es2015') is compiled files with ES2015 syntax
  • require('project/es5') is compiled files with ES5 syntax

Using the syntax feature, tools like projectz could automatically add the necessary browser properties:

{
    "browser": "./es5/index.js",  // browserify, and perhaps webpack
    "jsnext:main": "./esnext/index.js",  // rollup
    "jspm": { // jspm
        "main": "./es5/index.js"  // when jspm 0.17 comes out, projectz can automatically update this to point to esnext
    }
}

The es5 edition here is babel compiled with 2015-loose preset, which is needed for things like node 0.10 and IE8 support

Other syntaxes instead of ESNext could also be things like TypeScript or CoffeeScript or whatever.

balupton commented 8 years ago

Enacted at https://github.com/bevry/editions

balupton commented 8 years ago

Base files updated.