diff --git a/.gitignore b/.gitignore index 491fc35..4ae6bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules lib +/index.js +/stdlib.js diff --git a/package.json b/package.json index bc6da3a..a8d4e2a 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "babel-plugin-lightscript", "version": "0.2.3", "description": "Compile LightScript to JavaScript.", - "main": "lib/index.js", + "main": "index.js", "scripts": { "test": "node node_modules/.bin/mocha test", - "build": "babel src --out-dir lib" + "build": "babel src --out-dir ." }, "author": "Alex Rattray (http://alexrattray.com/)", "homepage": "http://lightscript.org/", @@ -14,7 +14,8 @@ "babylon-lightscript": "^0.2.1" }, "files": [ - "lib" + "./index.js", + "./stdlib.js" ], "devDependencies": { "babel-cli": "^6.22.2", diff --git a/src/index.js b/src/index.js index 47060b3..1059f73 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import { parse } from "babylon-lightscript"; - +import { defaultImports, lightscriptImports, lodashImports, reactImports, } from "./stdlib"; export default function (babel) { const { types: t } = babel; @@ -472,6 +472,109 @@ export default function (babel) { ); } + // eg; 'react', 'react *', 'react default' + type ImportShorthand = string; + + // eg; 'react', 'lodash/fp', './actions' + type ImportPath = string; + + // eg; "React", "PropTypes" + type Specifier = string; + + type ImportObj = { + default: ?Specifier, + namespace: ?Specifier, + others: [Specifier], + }; + + type Imports = { + [ImportPath]: ImportObj, + }; + + type Stdlib = false | { + [Specifier]: ImportShorthand, + }; + + function initializeStdlib(opts) { + let stdlib: Stdlib; + + + if (opts.stdlib === false) { + stdlib = false; + } else if (typeof opts.stdlib === "object") { + stdlib = Object.assign({}, + opts.stdlib.lodash === false ? {} : lodashImports, + opts.stdlib.react === false ? {} : reactImports, + opts.stdlib.lightscript === false ? {} : lightscriptImports, + opts.stdlib + ); + } else { + stdlib = defaultImports; + } + return stdlib; + } + + function collectStdlibImport(stdlib: Stdlib, imports: Imports, specifier: Specifier) { + const [ importPath, suffix ] = stdlib[specifier].split(" "); + + const isDefault = suffix === "default"; + const isNamespace = suffix === "*"; + + // initialize import setup for filepath + if (!imports[importPath]) { + imports[importPath] = { + default: null, + namespace: null, + others: [], + }; + } + + if (isDefault) { + const existingDefault = imports[importPath].default; + if (!existingDefault) { + imports[importPath].default = specifier; + } else if (existingDefault !== specifier) { + throw new SyntaxError( + `Multiple "default" imports declared for "${importPath}": "${existingDefault}" and "${specifier}".` + ); + } + } else if (isNamespace) { + const existingNamespace = imports[importPath].namespace; + if (!existingNamespace) { + imports[importPath].namespace = specifier; + } else if (existingNamespace !== specifier) { + throw new SyntaxError( + `Multiple "* as" imports declared for "${importPath}": "${existingNamespace}" and "${specifier}".` + ); + } + } else { + if (imports[importPath].others.indexOf(specifier) < 0) { + imports[importPath].others.push(specifier); + } + } + } + + function insertStdlibImports(path, imports: Imports) { + for (const importPath: ImportPath in imports) { + const imp: ImportObj = imports[importPath]; + const specifiers = []; + if (imp.default) { + specifiers.push(t.importDefaultSpecifier(t.identifier(imp.default))); + } + if (imp.namespace) { + specifiers.push(t.importNamespaceSpecifier(t.identifier(imp.namespace))); + } + if (imp.others) { + for (const specifierName of imp.others) { + const importIdentifier = t.identifier(specifierName); + specifiers.push(t.importSpecifier(importIdentifier, importIdentifier)); + } + } + const importDeclaration = t.importDeclaration(specifiers, t.stringLiteral(importPath)); + path.unshiftContainer("body", importDeclaration); + } + } + // TYPE DEFINITIONS definePluginType("ForFromArrayStatement", { @@ -669,6 +772,11 @@ export default function (babel) { // (and avoid traversing any of their output) function Program(path, state) { if (!shouldParseAsLightScript(state.file)) return; + + const stdlib: Stdlib = initializeStdlib(state.opts); + const imports: Imports = {}; + let hasJSX = false; + path.traverse({ ForFromArrayStatement(path) { @@ -994,8 +1102,30 @@ export default function (babel) { } }, + // collect functions to be imported for the stdlib + ReferencedIdentifier(path) { + if (stdlib === false) return; + + const { node, scope } = path; + if (stdlib[node.name] && !scope.hasBinding(node.name)) { + collectStdlibImport(stdlib, imports, node.name); + } + }, + + // track whether JSX is used for React in stdlib + JSXElement() { + if (stdlib === false) return; + hasJSX = true; + }, + }); + + if (hasJSX && stdlib.React) { + // TODO: allow custom pragmas + collectStdlibImport(stdlib, imports, "React"); + } + insertStdlibImports(path, imports); } return { diff --git a/src/stdlib.js b/src/stdlib.js new file mode 100644 index 0000000..b1aba8f --- /dev/null +++ b/src/stdlib.js @@ -0,0 +1,343 @@ + +export function looseEq(a, b) { + return a == b; +} + +export function looseNotEq(a, b) { + return a != b; +} + +export function bitwiseNot(a) { + return ~a; +} + +export const everyLodashMethod = [ + "add", + "after", + "ary", + "assign", + "assignIn", + "assignInWith", + "assignWith", + "at", + "attempt", + "before", + "bind", + "bindAll", + "bindKey", + "camelCase", + "capitalize", + "castArray", + "ceil", + "chain", + "chunk", + "clamp", + "clone", + "cloneDeep", + "cloneDeepWith", + "cloneWith", + "compact", + "concat", + "cond", + "conforms", + "conformsTo", + "constant", + "countBy", + "create", + "curry", + "curryRight", + "debounce", + "deburr", + "defaultTo", + "defaults", + "defaultsDeep", + "defer", + "delay", + "difference", + "differenceBy", + "differenceWith", + "divide", + "drop", + "dropRight", + "dropRightWhile", + "dropWhile", + "each", + "eachRight", + "endsWith", + "entries", + "entriesIn", + "eq", + "escape", + "escapeRegExp", + "every", + "extend", + "extendWith", + "fill", + "filter", + "find", + "findIndex", + "findKey", + "findLast", + "findLastIndex", + "findLastKey", + "first", + "flatMap", + "flatMapDeep", + "flatMapDepth", + "flatten", + "flattenDeep", + "flattenDepth", + "flip", + "floor", + "flow", + "flowRight", + "forEach", + "forEachRight", + "forIn", + "forInRight", + "forOwn", + "forOwnRight", + "fromPairs", + "functions", + "functionsIn", + "get", + "groupBy", + "gt", + "gte", + "has", + "hasIn", + "head", + "identity", + "inRange", + "includes", + "indexOf", + "initial", + "intersection", + "intersectionBy", + "intersectionWith", + "invert", + "invertBy", + "invoke", + "invokeMap", + "isArguments", + "isArray", + "isArrayBuffer", + "isArrayLike", + "isArrayLikeObject", + "isBoolean", + "isBuffer", + "isDate", + "isElement", + "isEmpty", + "isEqual", + "isEqualWith", + "isError", + "isFinite", + "isFunction", + "isInteger", + "isLength", + "isMap", + "isMatch", + "isMatchWith", + "isNaN", + "isNative", + "isNil", + "isNull", + "isNumber", + "isObject", + "isObjectLike", + "isPlainObject", + "isRegExp", + "isSafeInteger", + "isSet", + "isString", + "isSymbol", + "isTypedArray", + "isUndefined", + "isWeakMap", + "isWeakSet", + "iteratee", + "join", + "kebabCase", + "keyBy", + "keys", + "keysIn", + "last", + "lastIndexOf", + "lowerCase", + "lowerFirst", + "lt", + "lte", + "map", + "mapKeys", + "mapValues", + "matches", + "matchesProperty", + "max", + "maxBy", + "mean", + "meanBy", + "memoize", + "merge", + "mergeWith", + "method", + "methodOf", + "min", + "minBy", + "mixin", + "multiply", + "negate", + "noConflict", + "noop", + "now", + "nth", + "nthArg", + "omit", + "omitBy", + "once", + "orderBy", + "over", + "overArgs", + "overEvery", + "overSome", + "pad", + "padEnd", + "padStart", + "parseInt", + "partial", + "partialRight", + "partition", + "pick", + "pickBy", + "property", + "propertyOf", + "pull", + "pullAll", + "pullAllBy", + "pullAllWith", + "pullAt", + "random", + "range", + "rangeRight", + "rearg", + "reduce", + "reduceRight", + "reject", + "remove", + "repeat", + "replace", + "rest", + "result", + "reverse", + "round", + "runInContext", + "sample", + "sampleSize", + "set", + "setWith", + "shuffle", + "size", + "slice", + "snakeCase", + "some", + "sortBy", + "sortedIndex", + "sortedIndexBy", + "sortedIndexOf", + "sortedLastIndex", + "sortedLastIndexBy", + "sortedLastIndexOf", + "sortedUniq", + "sortedUniqBy", + "split", + "spread", + "startCase", + "startsWith", + "stubArray", + "stubFalse", + "stubObject", + "stubString", + "stubTrue", + "subtract", + "sum", + "sumBy", + "tail", + "take", + "takeRight", + "takeRightWhile", + "takeWhile", + "tap", + "template", + "templateSettings", + "throttle", + "thru", + "times", + "toArray", + "toFinite", + "toInteger", + "toLength", + "toLower", + "toNumber", + "toPairs", + "toPairsIn", + "toPath", + "toPlainObject", + "toSafeInteger", + "toString", + "toUpper", + "transform", + "trim", + "trimEnd", + "trimStart", + "truncate", + "unary", + "unescape", + "union", + "unionBy", + "unionWith", + "uniq", + "uniqBy", + "uniqWith", + "uniqueId", + "unset", + "unzip", + "unzipWith", + "update", + "updateWith", + "upperCase", + "upperFirst", + "values", + "valuesIn", + "without", + "words", + "wrap", + "xor", + "xorBy", + "xorWith", + "zip", + "zipObject", + "zipObjectDeep", + "zipWith", +]; + +export const lightscriptImports = { + "looseEq": "babel-plugin-lightscript/stdlib", + "looseNotEq": "babel-plugin-lightscript/stdlib", + "bitwiseNot": "babel-plugin-lightscript/stdlib", +}; + +export const lodashImports = everyLodashMethod.reduce((obj, methodName) => { + obj[methodName] = "lodash"; + return obj; +}, {}); + +export const reactImports = { + "React": "react default", + "PropTypes": "react", +}; + +export const defaultImports = Object.assign({}, + lightscriptImports, + lodashImports, + reactImports, +); diff --git a/test/fixtures/examples/jsx/expected.js b/test/fixtures/examples/jsx/expected.js index f402a91..d079dee 100644 --- a/test/fixtures/examples/jsx/expected.js +++ b/test/fixtures/examples/jsx/expected.js @@ -1,3 +1,5 @@ +import React from "react"; + function otherCondition() { return true; } diff --git a/test/fixtures/stdlib/disabled/actual.js b/test/fixtures/stdlib/disabled/actual.js new file mode 100644 index 0000000..54bb2dc --- /dev/null +++ b/test/fixtures/stdlib/disabled/actual.js @@ -0,0 +1,2 @@ +map() +looseEq() diff --git a/test/fixtures/stdlib/disabled/expected.js b/test/fixtures/stdlib/disabled/expected.js new file mode 100644 index 0000000..53e5582 --- /dev/null +++ b/test/fixtures/stdlib/disabled/expected.js @@ -0,0 +1,2 @@ +map(); +looseEq(); diff --git a/test/fixtures/stdlib/disabled/options.json b/test/fixtures/stdlib/disabled/options.json new file mode 100644 index 0000000..fa84cb0 --- /dev/null +++ b/test/fixtures/stdlib/disabled/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + ["lightscript", { + "stdlib": false + }] + ] +} diff --git a/test/fixtures/stdlib/extended/actual.js b/test/fixtures/stdlib/extended/actual.js new file mode 100644 index 0000000..ae83a7c --- /dev/null +++ b/test/fixtures/stdlib/extended/actual.js @@ -0,0 +1,7 @@ + +Foo +FooChild +FooSecondChild + +Bar +BarRest diff --git a/test/fixtures/stdlib/extended/expected.js b/test/fixtures/stdlib/extended/expected.js new file mode 100644 index 0000000..1034d08 --- /dev/null +++ b/test/fixtures/stdlib/extended/expected.js @@ -0,0 +1,9 @@ +import Bar, * as BarRest from "bar"; +import Foo, { FooChild, FooSecondChild } from "foo"; + +Foo; +FooChild; +FooSecondChild; + +Bar; +BarRest; diff --git a/test/fixtures/stdlib/extended/options.json b/test/fixtures/stdlib/extended/options.json new file mode 100644 index 0000000..458fd9a --- /dev/null +++ b/test/fixtures/stdlib/extended/options.json @@ -0,0 +1,13 @@ +{ + "plugins": [ + ["lightscript", { + "stdlib": { + "Foo": "foo default", + "FooChild": "foo", + "FooSecondChild": "foo", + "Bar": "bar default", + "BarRest": "bar *" + } + }] + ] +} diff --git a/test/fixtures/stdlib/jsx-react-disabled/actual.js b/test/fixtures/stdlib/jsx-react-disabled/actual.js new file mode 100644 index 0000000..91e060f --- /dev/null +++ b/test/fixtures/stdlib/jsx-react-disabled/actual.js @@ -0,0 +1 @@ +; diff --git a/test/fixtures/stdlib/jsx-react-disabled/expected.js b/test/fixtures/stdlib/jsx-react-disabled/expected.js new file mode 100644 index 0000000..91e060f --- /dev/null +++ b/test/fixtures/stdlib/jsx-react-disabled/expected.js @@ -0,0 +1 @@ +; diff --git a/test/fixtures/stdlib/jsx-react-disabled/options.json b/test/fixtures/stdlib/jsx-react-disabled/options.json new file mode 100644 index 0000000..613d4ed --- /dev/null +++ b/test/fixtures/stdlib/jsx-react-disabled/options.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + ["lightscript", { + "stdlib": { + "React": false + } + }] + ] +} diff --git a/test/fixtures/stdlib/jsx/actual.js b/test/fixtures/stdlib/jsx/actual.js new file mode 100644 index 0000000..91e060f --- /dev/null +++ b/test/fixtures/stdlib/jsx/actual.js @@ -0,0 +1 @@ +; diff --git a/test/fixtures/stdlib/jsx/expected.js b/test/fixtures/stdlib/jsx/expected.js new file mode 100644 index 0000000..b2dc7b3 --- /dev/null +++ b/test/fixtures/stdlib/jsx/expected.js @@ -0,0 +1,2 @@ +import React from "react"; +; diff --git a/test/fixtures/stdlib/kitchen-sink/actual.js b/test/fixtures/stdlib/kitchen-sink/actual.js new file mode 100644 index 0000000..7143ec1 --- /dev/null +++ b/test/fixtures/stdlib/kitchen-sink/actual.js @@ -0,0 +1,8 @@ +looseEq(1, '1') +looseNotEq(1, '1') +foo(1, '1') +uniq(x) -> x +map([.1, .2, .5, .9], round)~uniq() +React +PropTypes +redux.createStore() diff --git a/test/fixtures/stdlib/kitchen-sink/expected.js b/test/fixtures/stdlib/kitchen-sink/expected.js new file mode 100644 index 0000000..322b0bc --- /dev/null +++ b/test/fixtures/stdlib/kitchen-sink/expected.js @@ -0,0 +1,16 @@ +import * as redux from 'redux'; +import React, { PropTypes } from 'react'; +import { map, round } from 'lodash'; +import { looseEq, looseNotEq } from 'babel-plugin-lightscript/stdlib'; +looseEq(1, '1'); +looseNotEq(1, '1'); +foo(1, '1'); + +function uniq(x) { + return x; +} + +uniq(map([.1, .2, .5, .9], round)); +React; +PropTypes; +redux.createStore(); diff --git a/test/fixtures/stdlib/kitchen-sink/options.json b/test/fixtures/stdlib/kitchen-sink/options.json new file mode 100644 index 0000000..c87bcb8 --- /dev/null +++ b/test/fixtures/stdlib/kitchen-sink/options.json @@ -0,0 +1,10 @@ +{ + "plugins": [ + ["lightscript", { + "stdlib": { + "PropTypes": "react", + "redux": "redux *" + } + }] + ] +} diff --git a/test/fixtures/stdlib/lightscript-stdlib-works/exec.js b/test/fixtures/stdlib/lightscript-stdlib-works/exec.js new file mode 100644 index 0000000..b5f8e02 --- /dev/null +++ b/test/fixtures/stdlib/lightscript-stdlib-works/exec.js @@ -0,0 +1,14 @@ +assert.equal(looseEq(1, '1'), true) +assert.equal(looseEq(1, 1), true) +assert.equal(looseEq(1, '2'), false) +assert.equal(looseEq(1, 2), false) +assert.equal(looseEq(1, true), true) +assert.equal(looseEq(2, true), false) + +assert.equal(looseNotEq(1, '1'), false) +assert.equal(looseNotEq(1, '2'), true) +assert.equal(looseNotEq(1, 1), false) +assert.equal(looseNotEq('hi', false), true) + +assert.equal(bitwiseNot(1), -2) +assert.equal(bitwiseNot(bitwiseNot(1)), 1) diff --git a/test/fixtures/stdlib/lightscript-stdlib-works/options.json b/test/fixtures/stdlib/lightscript-stdlib-works/options.json new file mode 100644 index 0000000..77ff339 --- /dev/null +++ b/test/fixtures/stdlib/lightscript-stdlib-works/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["lightscript", "transform-es2015-modules-commonjs"] +} diff --git a/test/fixtures/stdlib/lodash-disabled/actual.js b/test/fixtures/stdlib/lodash-disabled/actual.js new file mode 100644 index 0000000..987c513 --- /dev/null +++ b/test/fixtures/stdlib/lodash-disabled/actual.js @@ -0,0 +1 @@ +uniq() diff --git a/test/fixtures/stdlib/lodash-disabled/expected.js b/test/fixtures/stdlib/lodash-disabled/expected.js new file mode 100644 index 0000000..d379283 --- /dev/null +++ b/test/fixtures/stdlib/lodash-disabled/expected.js @@ -0,0 +1 @@ +uniq(); diff --git a/test/fixtures/stdlib/lodash-disabled/options.json b/test/fixtures/stdlib/lodash-disabled/options.json new file mode 100644 index 0000000..714a4a3 --- /dev/null +++ b/test/fixtures/stdlib/lodash-disabled/options.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + ["lightscript", { + "stdlib": { + "lodash": false + } + }] + ] +} diff --git a/test/fixtures/stdlib/overridden/actual.js b/test/fixtures/stdlib/overridden/actual.js new file mode 100644 index 0000000..d8630c9 --- /dev/null +++ b/test/fixtures/stdlib/overridden/actual.js @@ -0,0 +1,3 @@ +uniq() -> 1 +uniq() +map() diff --git a/test/fixtures/stdlib/overridden/expected.js b/test/fixtures/stdlib/overridden/expected.js new file mode 100644 index 0000000..718ba98 --- /dev/null +++ b/test/fixtures/stdlib/overridden/expected.js @@ -0,0 +1,8 @@ +import { map } from "lodash"; + +function uniq() { + return 1; +} + +uniq(); +map();