diff --git a/packages/lwc-compiler/.npmignore b/packages/lwc-compiler/.npmignore old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/package.json b/packages/lwc-compiler/package.json old mode 100755 new mode 100644 index d4afe286c4..ca016952ab --- a/packages/lwc-compiler/package.json +++ b/packages/lwc-compiler/package.json @@ -3,21 +3,19 @@ "version": "0.20.0", "description": "LWC compiler", "main": "dist/commonjs/index.js", - "typings": "dist/types/index.d.ts", "author": "", "license": "ISC", "scripts": { "clean": "rm -rf dist", - "build": "yarn compile && yarn update:version", - "compile": "echo 'Building compiler...' && DIR=`pwd` && cd ../../ && tsc -p $DIR/tsconfig.json", - "update:version": "node ./scripts/update-compiler-version.js", + "build": "echo 'Building compiler...' && DIR=`pwd` && cd ../../ && tsc -p $DIR/tsconfig.json", + "build:all": "", + "build:umd": "webpack --progress", "test": "DIR=`pwd` && cd ../../ && jest $DIR" }, "dependencies": { "@babel/core": "7.0.0-beta.40", "@babel/plugin-proposal-class-properties": "7.0.0-beta.40", "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.40", - "@types/chokidar": "^1.7.5", "babel-plugin-transform-lwc-class": "0.20.0", "babel-preset-compat": "0.17.40", "babel-preset-minify": "0.4.0-alpha.caaefb4c", @@ -27,12 +25,10 @@ "postcss": "~6.0.19", "postcss-plugin-lwc": "0.20.0", "postcss-selector-parser": "^3.1.1", - "rollup": "~0.56.5", + "rollup": "~0.56.2", "rollup-plugin-replace": "^2.0.0" }, "devDependencies": { - "@types/babel-core": "^6.25.3", - "magic-string": "^0.22.4", "string-replace-webpack-plugin": "0.1.3", "webpack": "^3.11.0" } diff --git a/packages/lwc-compiler/scripts/update-compiler-version.js b/packages/lwc-compiler/scripts/update-compiler-version.js deleted file mode 100644 index a61306bfc0..0000000000 --- a/packages/lwc-compiler/scripts/update-compiler-version.js +++ /dev/null @@ -1,34 +0,0 @@ -const path = require("path"); -const replace = require("rollup-plugin-replace"); -const { rollup } = require("rollup"); -const { version } = require("../package.json"); - -async function updateVersion(version) { - const sourcePath = path.resolve("dist/commonjs/index.js"); - - const result = await rollup({ - input: sourcePath, - plugins: [ - replace({ - __VERSION__: version - }) - ] - }); - - await result.write({ - file: sourcePath, - format: "cjs", - sourcemap: false, // keep typescript generated map to stay consistent with the rest of the files. - }); - - console.log("Compiler version: ", version); -} - -if (!version || typeof version !== "string") { - throw new Error( - "Failed to update compiler version. Expected version value as a string, received: " + - version - ); -} - -updateVersion(version); diff --git a/packages/lwc-compiler/src/__tests__/fixtures.spec.js b/packages/lwc-compiler/src/__tests__/fixtures.spec.js new file mode 100644 index 0000000000..e907be2ac0 --- /dev/null +++ b/packages/lwc-compiler/src/__tests__/fixtures.spec.js @@ -0,0 +1,440 @@ +/* eslint-env node, jest */ + +const { compile } = require('../index'); +const { fixturePath, readFixture, pretify } = require('./utils'); + +describe('validate options', () => { + it('should validate entry type', () => { + expect(() => compile()).toThrow(/Expected a string for entry/); + }); + + it('should validate mode', () => { + expect(() => + compile('/x/foo/foo.js', { + mode: 'foo', + }), + ).toThrow( + /Expected a mode in dev, prod, compat, prod_compat, all. Received instead foo/, + ); + }); + + it('should validate sources option format', () => { + expect(() => + compile('/x/foo/foo.js', { + sources: { + '/x/foo/foo.js': true, + }, + }), + ).toThrow( + /in-memory module resolution expects values to be string. Received true for key \/x\/foo\/foo.js/, + ); + }); +}); + +describe('stylesheet', () => { + it('should import the associated stylesheet by default', async () => { + const { code } = await compile( + fixturePath('namespaced_folder/styled/styled.js'), + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-styled.js')), + ); + }); + + it('should import compress css in prod mode', async () => { + const { code } = await compile( + fixturePath('namespaced_folder/styled/styled.js'), + { + mode: 'prod' + } + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-styled-prod.js')), + ); + }); +}); + +describe('component name and namespace override', () => { + it('should be able to override module name', async () => { + const { code } = await compile('/x/foo/foo.js', { + componentName: 'bar', + format: 'amd', + mode: 'prod', + sources: { + '/x/foo/foo.js': `console.log('foo')`, + }, + }); + + expect(pretify(code)).toBe( + pretify(`define("x-bar",function(){console.log("foo")});`), + ); + }); + + it('should be able to override module namespace', async () => { + const { code } = await compile('/x/foo/foo.js', { + componentNamespace: 'bar', + format: 'amd', + mode: 'prod', + sources: { + '/x/foo/foo.js': `console.log('foo')`, + }, + }); + + expect(pretify(code)).toBe( + pretify(`define("bar-foo",function(){console.log("foo")});`), + ); + }); +}); + +describe('compile from file system', () => { + it('compiles module with no option and default namespace', async () => { + const { code, metadata } = await compile( + fixturePath('namespaced_folder/default/default.js'), + ); + + expect(pretify(code)).toBe( + pretify( + readFixture( + 'expected-compile-with-no-options-and-default-namespace.js', + ), + ), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + {name: 'engine', type: 'module' } + ] + }); + }); + + it('compiles with namespace mapping', async () => { + const { code, metadata } = await compile( + fixturePath('namespaced_folder/ns1/cmp1/cmp1.js'), + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-mapping-namespace-from-path.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); +}); + +describe('compile from in-memory', () => { + it('compiles to ESModule by deafult', async () => { + const { code, metadata } = await compile('/x/foo/foo.js', { + mapNamespaceFromPath: true, + sources: { + '/x/foo/foo.js': readFixture( + 'class_and_template/class_and_template.js', + ), + '/x/foo/foo.html': readFixture( + 'class_and_template/class_and_template.html', + ), + }, + }); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-sources-namespaced.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); + + it('respects the output format', async () => { + const { code, metadata } = await compile('/x/foo/foo.js', { + format: 'amd', + mapNamespaceFromPath: true, + sources: { + '/x/foo/foo.js': readFixture( + 'class_and_template/class_and_template.js', + ), + '/x/foo/foo.html': readFixture( + 'class_and_template/class_and_template.html', + ), + }, + }); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-sources-namespaced-format.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); + + it('respects the output format', async () => { + const { + code, + metadata, + } = await compile('myns/relative_import/relative_import.js', { + format: 'amd', + mapNamespaceFromPath: true, + sources: { + 'myns/relative_import/relative_import.html': readFixture( + 'relative_import/relative_import.html', + ), + 'myns/relative_import/relative_import.js': readFixture( + 'relative_import/relative_import.js', + ), + 'myns/relative_import/relative.js': readFixture( + 'relative_import/relative.js', + ), + 'myns/relative_import/other/relative2.js': readFixture( + 'relative_import/other/relative2.js', + ), + 'myns/relative_import/other/relative3.js': readFixture( + 'relative_import/other/relative3.js', + ), + }, + }); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-relative-import.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); +}); + +describe('mode generation', () => { + it('handles prod mode', async () => { + const { code, metadata } = await compile( + fixturePath('class_and_template/class_and_template.js'), + { + format: 'amd', + mode: 'prod', + }, + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-prod-mode.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); + + it('handles compat mode', async () => { + const { code, metadata } = await compile( + fixturePath('class_and_template/class_and_template.js'), + { + format: 'amd', + mode: 'compat', + }, + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-compat-mode.js')), + ); + + expect(metadata).toMatchObject({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); + + it('handles prod-compat mode', async () => { + const { code, metadata } = await compile( + fixturePath('class_and_template/class_and_template.js'), + { + format: 'amd', + mode: 'prod_compat', + }, + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-prod_compat-mode.js')), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + }); + + it('handles all modes', async () => { + const res = await compile( + fixturePath('class_and_template/class_and_template.js'), + { + format: 'amd', + mode: 'all', + }, + ); + + expect(Object.keys(res)).toEqual([ + 'dev', + 'prod', + 'compat', + 'prod_compat', + ]); + + for (let mode of Object.keys(res)) { + const { code, metadata } = res[mode]; + + expect(pretify(code)).toBe( + pretify(readFixture(`expected-${mode}-mode.js`)), + ); + + expect(metadata).toEqual({ + decorators: [], + references: [ + { name: 'engine', type: 'module' } + ] + }); + } + }); +}); + +describe('node env', function () { + it('does not remove production code when no NODE_ENV option is specified', async () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = undefined; + const { code, metadata } = await compile( + fixturePath('node_env/node_env.js'), + { + mode: 'dev', + }, + ); + process.env.NODE_ENV = previous; + + expect(pretify(code)).toBe( + pretify(readFixture('expected-node-env-dev.js')), + ); + }); + + it('does removes production code when process.env.NODE_ENV is production', async () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + const { code, metadata } = await compile( + fixturePath('node_env/node_env.js'), + { + mode: 'dev', + }, + ); + process.env.NODE_ENV = previous; + + expect(pretify(code)).toBe( + pretify(readFixture('expected-node-env-prod.js')), + ); + }); + + it('removes production code when NODE_ENV option is production', async () => { + const { code, metadata } = await compile( + fixturePath('node_env/node_env.js'), + { + mode: 'dev', + env: { + NODE_ENV: 'production', + } + }, + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-node-env-prod.js')), + ); + }); + + it('does not remove production code when in NODE_ENV option is development', async () => { + const { code, metadata } = await compile( + fixturePath('node_env/node_env.js'), + { + mode: 'dev', + env: { + NODE_ENV: 'development', + } + }, + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-node-env-dev.js')), + ); + }); +}); + +describe('metadata output', () => { + it('decorators and references', async () => { + const { code, metadata } = await compile('/x/foo/foo.js', { + mapNamespaceFromPath: true, + sources: { + '/x/foo/foo.js': readFixture( + 'metadata/metadata.js', + ), + '/x/foo/foo.html': readFixture( + 'metadata/metadata.html', + ), + }, + }); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-sources-metadata.js')), + ); + + expect(metadata).toEqual({ + decorators: [ + { + type: 'api', + targets: [ + { type: 'property', name: 'publicProp' }, + { type: 'method', name: 'publicMethod' } + ] + }, + { + type: 'wire', + targets: [ + { + type: 'property', + adapter: { name: 'getTodo', reference: 'todo' }, + name: 'wiredProp', + params: {}, + static: {} + }, + { + type: 'method', + adapter: { name: 'getHello', reference: '@schema/foo.bar' }, + name: 'wiredMethod', + params: { name: 'publicProp' }, + static: { 'fields': ['one', 'two'] } + } + ] + } + ], + references: [ + { name: 'x-bar', type: 'component' }, + { name: 'engine', type: 'module' }, + { name: 'todo', type: 'module' }, + { name: '@schema/foo.bar', type: 'module' } + ] + }); + }); +}); diff --git a/packages/lwc-compiler/src/__tests__/fixtures/class_and_template/class_and_template.html b/packages/lwc-compiler/src/__tests__/fixtures/class_and_template/class_and_template.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/class_and_template/class_and_template.js b/packages/lwc-compiler/src/__tests__/fixtures/class_and_template/class_and_template.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-all-modes.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-all-modes.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-babel-compat.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-babel-compat.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-babel.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-babel.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-compat-mode.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-compat-mode.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-individual-resources.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-individual-resources.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-compile-with-no-options-and-default-namespace.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-dev-mode.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-external-dependency.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-mapping-namespace-from-path.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-dev.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-node-env-prod.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-prod-mode.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-prod-mode.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-prod_compat-mode.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-prod_compat-mode.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-relative-import.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-metadata.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced-format.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-sources-namespaced.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled-prod.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js b/packages/lwc-compiler/src/__tests__/fixtures/expected-styled.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/app.js b/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/app.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/simpleClass1.html b/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/simpleClass1.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/simpleClass1.js b/packages/lwc-compiler/src/__tests__/fixtures/flat_structure/simpleClass1.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/metadata/metadata.html b/packages/lwc-compiler/src/__tests__/fixtures/metadata/metadata.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/metadata/metadata.js b/packages/lwc-compiler/src/__tests__/fixtures/metadata/metadata.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/babel/babel.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/babel/babel.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/custom/foo-bar.html b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/custom/foo-bar.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/custom/foo-bar.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/custom/foo-bar.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/default/default.html b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/default/default.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/default/default.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/default/default.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns1/cmp1/cmp1.html b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns1/cmp1/cmp1.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns1/cmp1/cmp1.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns1/cmp1/cmp1.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns2/components/cmp1/cmp1.html b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns2/components/cmp1/cmp1.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns2/components/cmp1/cmp1.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/ns2/components/cmp1/cmp1.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.css b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.css old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.html b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.js b/packages/lwc-compiler/src/__tests__/fixtures/namespaced_folder/styled/styled.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/node_env/node_env.html b/packages/lwc-compiler/src/__tests__/fixtures/node_env/node_env.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/node_env/node_env.js b/packages/lwc-compiler/src/__tests__/fixtures/node_env/node_env.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/relative_import/other/relative2.js b/packages/lwc-compiler/src/__tests__/fixtures/relative_import/other/relative2.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/relative_import/other/relative3.js b/packages/lwc-compiler/src/__tests__/fixtures/relative_import/other/relative3.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative.js b/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative_import.html b/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative_import.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative_import.js b/packages/lwc-compiler/src/__tests__/fixtures/relative_import/relative_import.js old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/fixtures/template/template.html b/packages/lwc-compiler/src/__tests__/fixtures/template/template.html old mode 100755 new mode 100644 diff --git a/packages/lwc-compiler/src/__tests__/index.spec.js b/packages/lwc-compiler/src/__tests__/index.spec.js new file mode 100644 index 0000000000..deb2ad776c --- /dev/null +++ b/packages/lwc-compiler/src/__tests__/index.spec.js @@ -0,0 +1,194 @@ +/* eslint-env node, jest */ + +const { compile, transform } = require('../index'); +const { pretify } = require('./utils'); + +const VALID_TEST_JS = ` +import label from '@label/mylabel'; +function isTrue() { + return label; +} +isTrue(); +`.trim(); + +describe('compile', () => { + it('should validate entry type', () => { + expect(() => compile()).toThrow(/Expected a string for entry/); + }); + + it('should validate mode', () => { + expect(() => + compile('/x/foo/foo.js', { + mode: 'foo', + }), + ).toThrow( + /Expected a mode in dev, prod, compat, prod_compat, all. Received instead foo/, + ); + }); + + it('should validate sources option format', () => { + expect(() => + compile('/x/foo/foo.js', { + sources: { + '/x/foo/foo.js': true, + }, + }), + ).toThrow( + /in-memory module resolution expects values to be string. Received true for key \/x\/foo\/foo.js/, + ); + }); + // TODO: uncomment once compile output format changes + // it.only('should return status, diagnostics, references, and bundles', () => { + // const config = { + // sources: { + // '/x/foo/foo.js': VALID_TEST_JS, + // }, + // entry: '/x/foo/foo.js', + // moduleName: 'foo', + // moduleNamespace: 'x', + // format: 'whoknows', + // }; + // const { status, diagnostics, references, bundles } = compile(); + // expect(status).toBe('ok'); + // expect(diagnostics.length).toBe(0); + // expect(references.length > 0).toBe(true); + // expect(bundles.length).toBe(1); + // }); +}); + +describe('transform', () => { + it('should validate presence of src', () => { + expect(() => transform()).toThrow( + /Expect a string for source. Received undefined/, + ); + }); + + it('should validate presence of id', () => { + expect(() => transform(`console.log('Hello')`)).toThrow( + /Expect a string for id. Received undefined/, + ); + }); + + it('should apply transformation for javascript file', async () => { + const actual = ` + import { Element } from 'engine'; + export default class Foo extends Element {} + `; + + const expected = ` + import _tmpl from "./foo.html"; + import { Element } from 'engine'; + export default class Foo extends Element { + render() { + return _tmpl; + } + } + Foo.style = _tmpl.style; + `; + + const { code } = await transform(actual, 'foo.js', { + moduleNamespace: 'x', + moduleName: 'foo', + }); + + expect(pretify(code)).toBe(pretify(expected)); + }); + + it('should apply transformation for template file', async () => { + const actual = ` + + `; + + const expected = ` + import style from './foo.css' + + export default function tmpl($api, $cmp, $slotset, $ctx) { + const { + t: api_text, + h: api_element + } = $api; + + return [api_element("div", { + key: 1 + }, [api_text("Hello")])]; + } + + if (style) { + const tagName = 'x-foo'; + const token = 'x-foo_foo'; + tmpl.token = token; + tmpl.style = style(tagName, token); + } + `; + + const { code } = await transform(actual, 'foo.html', { + moduleNamespace: 'x', + moduleName: 'foo', + }); + + expect(pretify(code)).toBe(pretify(expected)); + }); + + it('should apply transformation for stylesheet file', async () => { + const actual = ` + div { + background-color: red; + } + `; + + const expected = ` + function style(tagName, token) { + return \` + div[\${token}] { + background-color: red; + } + \`; + } + export default style; + `; + + const { code } = await transform(actual, 'foo.css', { + moduleNamespace: 'x', + moduleName: 'foo', + }); + + expect(pretify(code)).toBe(pretify(expected)); + }); + + it('javascript metadata', async () => { + const content = ` + import { Element, api } from 'engine'; + /** Foo doc */ + export default class Foo extends Element { + _privateTodo; + @api get todo () { + return this._privateTodo; + } + @api set todo (val) { + return this._privateTodo = val; + } + @api + index; + } + `; + + const result = await transform(content, 'foo.js', { + moduleNamespace: 'x', + moduleName: 'foo', + }); + const metadata = result.metadata; + expect(metadata.decorators).toEqual([ + { + type: 'api', + targets: [ + { type: 'property', name: 'todo' }, + { type: 'property', name: 'index' } + ] + } + ]); + expect(metadata.doc).toBe('Foo doc'); + expect(metadata.declarationLoc).toEqual({ start: { line: 4, column: 12 }, end: { line: 14, column: 13 } }); + }); +}); diff --git a/packages/lwc-compiler/src/__tests__/index.spec.ts b/packages/lwc-compiler/src/__tests__/index.spec.ts deleted file mode 100755 index f09502b8b4..0000000000 --- a/packages/lwc-compiler/src/__tests__/index.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { compile, transform, version } from "../index"; - -const COMPILER_CONFIG = { - name: "foo", - namespace: "x", - files: { "foo.js": "debugger" }, - outputConfig: { - env: { NODE_ENV: "development" }, - minify: false, - compat: false - } -}; - -describe("test index entry points", () => { - test("able to invoke compiler", async () => { - const { result, success } = await compile(COMPILER_CONFIG); - expect(success).toBe(true); - expect(result).toBeDefined(); - }); - - test("able to invoke transformer", async () => { - // transform should normalize options and append outputConfig - const config = { - name: "foo", - namespace: "x", - files: { "foo.js": "debugger" }, - }; - const { code } = await transform("debugger", "file.js", config); - expect(code).toBe("debugger;"); - }); - - test("able to retrieve version", () => { - expect(version).toBeDefined(); - }); -}); diff --git a/packages/lwc-compiler/src/__tests__/regression.spec.js b/packages/lwc-compiler/src/__tests__/regression.spec.js new file mode 100644 index 0000000000..66ded3481f --- /dev/null +++ b/packages/lwc-compiler/src/__tests__/regression.spec.js @@ -0,0 +1,46 @@ +const { compile } = require('../index'); +const { fixturePath, readFixture, pretify } = require('./utils'); + +describe('resgression test', () => { + it('#743 - Object rest spread throwing', async () => { + const actual = `const base = { foo: true }; const res = { ...base, bar: false };`; + const expected = `const base = { + foo: true +}; +const res = Object.assign({}, base, { + bar: false +});`; + + const { code } = await compile('/x/foo/foo.js', { + sources: { + '/x/foo/foo.js': actual, + }, + }); + + expect(pretify(code)).toBe(pretify(expected)); + }); +}); + +describe('smoke test for babel transform', () => { + it('should transpile none stage-4 syntax features in none-compat', async () => { + const { code } = await compile( + fixturePath('namespaced_folder/babel/babel.js'), + { + mode: 'dev' + } + ); + expect(pretify(code)).toBe( + pretify(readFixture('expected-babel.js')), + ); + }); + + it('should transpile back to es5 in compat mode', async () => { + const { code } = await compile( + fixturePath('namespaced_folder/babel/babel.js'), { mode: 'compat' } + ); + + expect(pretify(code)).toBe( + pretify(readFixture('expected-babel-compat.js')), + ); + }); +}); diff --git a/packages/lwc-compiler/src/__tests__/regression.spec.ts b/packages/lwc-compiler/src/__tests__/regression.spec.ts deleted file mode 100755 index 4517ca3e82..0000000000 --- a/packages/lwc-compiler/src/__tests__/regression.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { compile } from "../index"; -import { readFixture, pretify } from "./utils"; - -describe("regression test", () => { - it("#743 - Object rest spread throwing", async () => { - const actual = `const base = { foo: true }; const res = { ...base, bar: false };`; - const expected = `define('x-foo', function () { - const base = { - foo: true - }; - const res = Object.assign({}, base, { - bar: false - }); - });`; - - const { result } = await compile({ - name: "foo", - namespace: "x", - files: { - "foo.js": actual - }, - }); - expect(pretify(result.code)).toBe(pretify(expected)); - }); -}); - -describe("smoke test for babel transform", () => { - it("should transpile none stage-4 syntax features in none-compat", async () => { - const { result } = await compile({ - name: 'babel', - namespace: 'x', - files: { - 'babel.js': readFixture("namespaced_folder/babel/babel.js") - }, - outputConfig: { format: 'es' } - } - ); - - expect(pretify(result.code)).toBe(pretify(readFixture("expected-babel.js"))); - }); - - it("should transpile back to es5 in compat mode", async () => { - const { result } = await compile({ - name: 'babel', - namespace: 'x', - files: { - 'babel.js': readFixture("namespaced_folder/babel/babel.js") - }, - outputConfig: { - compat: true, - format: 'es', - } - }); - - expect(pretify(result.code)).toBe( - pretify(readFixture("expected-babel-compat.js")) - ); - }); -}); diff --git a/packages/lwc-compiler/src/__tests__/utils.ts b/packages/lwc-compiler/src/__tests__/utils.js old mode 100755 new mode 100644 similarity index 58% rename from packages/lwc-compiler/src/__tests__/utils.ts rename to packages/lwc-compiler/src/__tests__/utils.js index fb1160cc40..8b4d84b473 --- a/packages/lwc-compiler/src/__tests__/utils.ts +++ b/packages/lwc-compiler/src/__tests__/utils.js @@ -1,17 +1,19 @@ -import * as fs from 'fs'; -import * as path from 'path'; +/* eslint-env node */ + +const fs = require('fs'); +const path = require('path'); const FIXTURE_DIR = path.join(__dirname, 'fixtures'); -export function fixturePath(location: string): string { +function fixturePath(location) { return path.join(FIXTURE_DIR, location); } -export function readFixture(location: string): string { +function readFixture(location) { return fs.readFileSync(fixturePath(location), 'utf-8'); } -export function pretify(str: string): string { +function pretify(str) { return str.toString() .replace(/^\s+|\s+$/, '') .split('\n') @@ -19,3 +21,9 @@ export function pretify(str: string): string { .filter(line => line.length) .join('\n'); } + +module.exports = { + fixturePath, + readFixture, + pretify +}; diff --git a/packages/lwc-compiler/src/babel-plugins.ts b/packages/lwc-compiler/src/babel-plugins.js old mode 100755 new mode 100644 similarity index 83% rename from packages/lwc-compiler/src/babel-plugins.ts rename to packages/lwc-compiler/src/babel-plugins.js index bf81232103..4e355fbe76 --- a/packages/lwc-compiler/src/babel-plugins.ts +++ b/packages/lwc-compiler/src/babel-plugins.js @@ -33,11 +33,11 @@ * modules: false, * }; */ -import * as transformPublicFields from '@babel/plugin-proposal-class-properties'; -import * as transformObjectRestSpread from '@babel/plugin-proposal-object-rest-spread'; + +import transformPublicFields from '@babel/plugin-proposal-class-properties'; +import transformObjectRestSpread from '@babel/plugin-proposal-object-rest-spread'; // Base babel configuration -// TODO: Need to remove * on the parserOpts plugin - not advised by babel-core export const BABEL_CONFIG_BASE = { babelrc: false, sourceMaps: true, diff --git a/packages/lwc-compiler/src/bundle.js b/packages/lwc-compiler/src/bundle.js new file mode 100644 index 0000000000..8189b6fb21 --- /dev/null +++ b/packages/lwc-compiler/src/bundle.js @@ -0,0 +1,72 @@ +import { rollup } from 'rollup'; +import * as rollupPluginReplace from 'rollup-plugin-replace'; + +import { isCompat, isProd } from './modes'; +import rollupModuleResolver from './rollup-plugins/module-resolver'; +import rollupTransfrom from './rollup-plugins/transform'; +import rollupCompat from './rollup-plugins/compat'; +import rollupMinify from './rollup-plugins/minify'; + +function rollupWarningOverride(warning) { + if (warning.code && warning.code === 'UNRESOLVED_IMPORT') { + return; + } + + console.warn(warning.message); +} + +function mergeMetadata(metadata) { + const dependencies = new Map((metadata.rollupDependencies || []).map(d => ([d, 'module']))); + const decorators = []; + + for (let i in metadata) { + (metadata[i].templateDependencies || []).forEach(td => (dependencies.set(td, 'component'))); + decorators.push(...(metadata[i].decorators || [])); + } + + return { + decorators, + references: Array.from(dependencies).map(d => ({name: d[0], type: d[1]})) + }; +} + + +export default function bundle(entry, options = {}) { + const environment = options.env.NODE_ENV || process.env.NODE_ENV; + const plugins = [ + rollupPluginReplace({ 'process.env.NODE_ENV': JSON.stringify(environment) }), + rollupModuleResolver(options), + rollupTransfrom(options) + ]; + + if (isCompat(options.mode)) { + plugins.push(rollupCompat(options)); + } + + if (isProd(options.mode)) { + plugins.push(rollupMinify(options)); + } + + return rollup({ + input: entry, + plugins: plugins, + onwarn: rollupWarningOverride, + }) + .then(bundle => { + return bundle.generate({ + amd: { id: options.normalizedModuleName }, + interop: false, + strict: false, + format: options.format, + globals: options.globals + }); + }) + .then(result => { + return { + code: result.code, + map: result.map, + metadata: mergeMetadata(options.$metadata), + rawMetadata: options.$metadata + }; + }); +} diff --git a/packages/lwc-compiler/src/bundler/__tests__/bundler.spec.ts b/packages/lwc-compiler/src/bundler/__tests__/bundler.spec.ts deleted file mode 100755 index 4587a44c66..0000000000 --- a/packages/lwc-compiler/src/bundler/__tests__/bundler.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { bundle } from "../bundler"; -import { pretify } from "../../__tests__/utils"; - -describe('bundler', () => { - test('throws when invoked without configurations', async () => { - try { - const { diagnostics, code, metadata } = await bundle(); - } catch (error) { - expect(error.message).toBe("Expected options object, received \"undefined\"."); - } - }); -}); diff --git a/packages/lwc-compiler/src/bundler/__tests__/import-locations.spec.ts b/packages/lwc-compiler/src/bundler/__tests__/import-locations.spec.ts deleted file mode 100644 index 04cb11b077..0000000000 --- a/packages/lwc-compiler/src/bundler/__tests__/import-locations.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { collectImportLocations } from "../../bundler/import-location-collector"; - -describe("import locations", () => { - test("location collector should return empty array if incoming code isn't module", () => { - const locs = collectImportLocations("debugger"); - expect(locs.length).toBe(0); - }); - - test("location collector should return empty array if no imports were specified", () => { - const src = `define('x-foo', function () {});` - const locs = collectImportLocations("debugger"); - expect(locs.length).toBe(0); - }); - - test("location collector should return location object for each import", () => { - const src = `define('x-foo', ['x-bar', '@xfoose', 'xy/zoolaf'], function (xBar, xFoose, xZoolaf) { - xBoo(); - xFoose(); - xZoolaf(); - });`; - const locs = collectImportLocations(src); - - expect(locs.length).toBe(3); - expect(locs[0]).toMatchObject({ - name: "x-bar", - location: { - start: 18, - length: 5 - } - }); - expect(locs[1]).toMatchObject({ - name: "@xfoose", - location: { - start: 27, - length: 7 - } - }); - expect(locs[2]).toMatchObject({ - name: "xy/zoolaf", - location: { - start: 38, - length: 9 - } - }); - }); -}); diff --git a/packages/lwc-compiler/src/bundler/bundler.ts b/packages/lwc-compiler/src/bundler/bundler.ts deleted file mode 100755 index d44203b6e5..0000000000 --- a/packages/lwc-compiler/src/bundler/bundler.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { rollup } from "rollup"; -import * as rollupPluginReplace from "rollup-plugin-replace"; - -import { MetadataCollector, BundleMetadata } from "./meta-collector"; -import rollupModuleResolver from "../rollup-plugins/module-resolver"; - -import rollupTransform from "../rollup-plugins/transform"; -import rollupCompat from "../rollup-plugins/compat"; -import rollupMinify from "../rollup-plugins/minify"; - -import { - NormalizedCompilerOptions, - validateNormalizedOptions -} from "../compiler/options"; - -import { Diagnostic, DiagnosticLevel } from "../diagnostics/diagnostic"; - -import { collectImportLocations } from "./import-location-collector"; - -export interface BundleReport { - code: string; - diagnostics: Diagnostic[]; - map: null; - metadata: BundleMetadata; -} - -interface RollupWarning { - message: string; - frame?: string; - loc?: { - file: string; - line: number; - column: number; - }; -} - -const DEFAULT_FORMAT = "amd"; - -function handleRollupWarning(diagnostics: Diagnostic[]) { - return function onwarn({ message, loc }: RollupWarning) { - const filename = loc && loc.file; - - diagnostics.push({ - level: DiagnosticLevel.Warning, - message, - filename - }); - }; -} - -export async function bundle( - options: NormalizedCompilerOptions -): Promise { - validateNormalizedOptions(options); - - const { outputConfig, name, namespace } = options; - - // TODO: remove format option once tests are converted to 'amd' format - const format = (outputConfig as any).format || DEFAULT_FORMAT; - - const diagnostics: Diagnostic[] = []; - - const metadataCollector = new MetadataCollector(); - - const plugins = [ - rollupPluginReplace({ - "process.env.NODE_ENV": JSON.stringify(outputConfig.env.NODE_ENV) - }), - rollupModuleResolver({ - metadataCollector, - options - }), - rollupTransform({ - metadataCollector, - options - }) - ]; - - if (outputConfig.compat) { - plugins.push(rollupCompat(outputConfig.resolveProxyCompat)); - } - - if (outputConfig.minify) { - plugins.push(rollupMinify()); - } - - const rollupBundler = await rollup({ - input: name, - plugins, - onwarn: handleRollupWarning(diagnostics) - }); - - const { code } = await rollupBundler.generate({ - amd: { id: namespace + "-" + name }, - interop: false, - strict: false, - format - }); - - metadataCollector.collectImportLocations( - collectImportLocations(code) || [] - ); - - return { - diagnostics, - code, - map: null, - metadata: metadataCollector.getMetadata() - }; -} diff --git a/packages/lwc-compiler/src/bundler/import-location-collector.ts b/packages/lwc-compiler/src/bundler/import-location-collector.ts deleted file mode 100644 index a8ef998eec..0000000000 --- a/packages/lwc-compiler/src/bundler/import-location-collector.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Location } from "../common-interfaces/location"; - -export interface ModuleImportLocation { - name: string; - location: Location; -} - -const MODULE_IMPORT_REGEX = /(?:define\([(['|"][\w-]+['|"],?\s*)(?:\[((?:['|"][@\w-/]+['|"],?\s*)+)\])?,?\s*function/; - -export function collectImportLocations(code: string) { - const matches = new RegExp(MODULE_IMPORT_REGEX).exec(code); - - // assert amd - if (!matches || !matches.length) { - return []; - } - - const searchSubstring: string = matches[0]; - - // format: `'x-bar', 'x-foo'` - const rawImports = matches[1]; - if (!rawImports) { - return []; - } - - // split result: ["'x-bar', 'x-foo'"] - const imports = rawImports.split(/,\s*/) || []; - - return imports.map((moduleImport) => { - const normalizedName = moduleImport.replace(/'/g, ""); - const position = searchSubstring.indexOf(normalizedName); - - return { - name: normalizedName, - location: { - start: position, - length: normalizedName.length - } - }; - }); -} diff --git a/packages/lwc-compiler/src/bundler/meta-collector.ts b/packages/lwc-compiler/src/bundler/meta-collector.ts deleted file mode 100755 index d27445562f..0000000000 --- a/packages/lwc-compiler/src/bundler/meta-collector.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - ApiDecorator, - TrackDecorator, - WireDecorator -} from "babel-plugin-transform-lwc-class"; - -import { ModuleImportLocation } from "./import-location-collector"; - -export type MetadataDecorators = Array< - ApiDecorator | TrackDecorator | WireDecorator ->; - -export interface BundleMetadata { - decorators: MetadataDecorators; - importLocations: ModuleImportLocation[]; -} - -export class MetadataCollector { - private decorators: Array< - ApiDecorator | TrackDecorator | WireDecorator - > = []; - private importLocations: ModuleImportLocation[] = []; - - public collectDecorator( - decorator: ApiDecorator | TrackDecorator | WireDecorator - ) { - this.decorators.push(decorator); - } - - public collectImportLocations(importLocations: ModuleImportLocation[]) { - this.importLocations.push(...importLocations); - } - - public getMetadata(): BundleMetadata { - return { - decorators: this.decorators, - importLocations: this.importLocations - }; - } -} diff --git a/packages/lwc-compiler/src/common-interfaces/location.ts b/packages/lwc-compiler/src/common-interfaces/location.ts deleted file mode 100644 index e5f3ba043d..0000000000 --- a/packages/lwc-compiler/src/common-interfaces/location.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Location { - /** 0-base character index in the file */ - start: number; - - /** Number of character after the start index */ - length: number; -} diff --git a/packages/lwc-compiler/src/compiler/__tests__/modes.spec.ts b/packages/lwc-compiler/src/compiler/__tests__/modes.spec.ts deleted file mode 100644 index 7b68b10f6f..0000000000 --- a/packages/lwc-compiler/src/compiler/__tests__/modes.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { readFile } from "fs"; -import { compile } from "../../index"; -import { fixturePath, readFixture, pretify } from "../../__tests__/utils"; - -const NODE_ENV_CONFIG = { - name: "node_env", - namespace: "x", - files: { - "node_env.js": readFixture("node_env/node_env.js"), - "node_env.html": readFixture("node_env/node_env.html") - }, - outputConfig: { format: "es" } -}; - -describe("node env", function() { - it('sets env.NODE_ENV to "development" by default', async () => { - const config = { - name: "foo", - namespace: "x", - files: { - "foo.js": "export const env = process.env.NODE_ENV" - }, - outputConfig: { format: "es" } - }; - const { result: { code, metadata } } = await compile(config); - - expect(pretify(code)).toBe( - 'const env = "development";\nexport { env };' - ); - }); - - it("removes production code when NODE_ENV option is production", async () => { - const config = { - ...NODE_ENV_CONFIG, - outputConfig: { format: "es", env: { NODE_ENV: "production" } } - }; - const { result: { code, metadata } } = await compile(config); - - expect(pretify(code)).toBe( - pretify(readFixture("expected-node-env-prod.js")) - ); - }); - - it("does not remove production code when in NODE_ENV option is development", async () => { - const config = { - ...NODE_ENV_CONFIG, - outputConfig: { format: "es", env: { NODE_ENV: "development" } } - }; - const { result: { code, metadata } } = await compile(config); - - expect(pretify(code)).toBe( - pretify(readFixture("expected-node-env-dev.js")) - ); - }); -}); diff --git a/packages/lwc-compiler/src/compiler/__tests__/options.spec.ts b/packages/lwc-compiler/src/compiler/__tests__/options.spec.ts deleted file mode 100644 index ba3ba842f7..0000000000 --- a/packages/lwc-compiler/src/compiler/__tests__/options.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { compile } from "../compiler"; -import { pretify, readFixture } from "../../__tests__/utils"; - -const VALID_CONFIG = { - outputConfig: { - env: {}, - minify: false, - compat: false, - format: "amd" - }, - name: "class_and_template", - namespace: "x", - files: { - "class_and_template.js": readFixture( - "class_and_template/class_and_template.js" - ), - "class_and_template.html": readFixture( - "class_and_template/class_and_template.html" - ) - } -}; - -describe("compiler options", () => { - it("should validate presence of options", async () => { - await expect(compile()).rejects.toMatchObject({ - message: 'Expected options object, received "undefined".' - }); - }); - - it("should validate bundle name option", async () => { - await expect(compile({})).rejects.toMatchObject({ - message: 'Expected a string for name, received "undefined".' - }); - }); - - it("should validate bundle namespace option", async () => { - await expect(compile({ name: "foo" })).rejects.toMatchObject({ - message: 'Expected a string for namespace, received "undefined".' - }); - }); - - it("should validate presence of files option", async () => { - await expect( - compile({ - name: "/x/foo/foo.js", - namespace: "x", - files: {} - }) - ).rejects.toMatchObject({ - message: "Expected an object with files to be compiled." - }); - }); - - it("should validate files option value type", async () => { - await expect( - compile({ - name: "foo", - namespace: "x", - files: { - "foo.js": true - } - }) - ).rejects.toMatchObject({ - message: - 'Unexpected file content for "foo.js". Expected a string, received "true".' - }); - }); - - it("should validate outputConfig.minify", async () => { - await expect( - compile({ - name: "foo", - namespace: "x", - files: { x: "foo" }, - outputConfig: { - minify: "true" - } - }) - ).rejects.toMatchObject({ - message: - 'Expected a boolean for outputConfig.minify, received "true".' - }); - }); - - it("should validate outputConfig.compat", async () => { - await expect( - compile({ - name: "foo", - namespace: "x", - files: { x: "foo" }, - outputConfig: { - compat: "true" - } - }) - ).rejects.toMatchObject({ - message: - 'Expected a boolean for outputConfig.compat, received "true".' - }); - }); -}); diff --git a/packages/lwc-compiler/src/compiler/__tests__/result.spec.ts b/packages/lwc-compiler/src/compiler/__tests__/result.spec.ts deleted file mode 100755 index 3426ee432e..0000000000 --- a/packages/lwc-compiler/src/compiler/__tests__/result.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { compile } from "../compiler"; -import { pretify, readFixture } from "../../__tests__/utils"; - -const VALID_CONFIG = { - outputConfig: { - env: {}, - minify: false, - compat: false, - format: "amd" - }, - name: "class_and_template", - namespace: "x", - files: { - "class_and_template.js": readFixture( - "class_and_template/class_and_template.js" - ), - "class_and_template.html": readFixture( - "class_and_template/class_and_template.html" - ) - } -}; - -describe("compiler result", () => { - test("compiler should return bundle result default output configuration ", async () => { - const noOutputConfig = { ...VALID_CONFIG, outputConfig: undefined }; - const { result: { outputConfig } } = await compile(noOutputConfig); - expect(outputConfig).toMatchObject({ - env: { - NODE_ENV: "development" - }, - minify: false, - compat: false - }); - }); - test("compiler should return bundle result with normalized DEV output config", async () => { - const config = Object.assign({}, VALID_CONFIG, { - outputConfig: { - minify: false, - compat: false - } - }); - const { result: { outputConfig } } = await compile(config); - expect(outputConfig).toMatchObject({ - env: { - NODE_ENV: "development" - }, - minify: false, - compat: false - }); - }); - test("compiler should return bundle result with normalized PROD output config", async () => { - const config = Object.assign({}, VALID_CONFIG, { - outputConfig: { - minify: true, - compat: false, - env: { - NODE_ENV: "production" - } - } - }); - const { result: { outputConfig } } = await compile(config); - expect(outputConfig).toMatchObject({ - env: { - NODE_ENV: "production" - }, - minify: true, - compat: false - }); - }); - test("compiler should return bundle result with normalized COMPAT output config", async () => { - const config = Object.assign({}, VALID_CONFIG, { - outputConfig: { - minify: false, - compat: true - } - }); - const { result: { outputConfig } } = await compile(config); - expect(outputConfig).toMatchObject({ - env: { - NODE_ENV: "development" - }, - minify: false, - compat: true - }); - }); - test("compiler should return bundle result with normalized PROD_COMPAT output config", async () => { - const config = Object.assign({}, VALID_CONFIG, { - outputConfig: { - minify: false, - compat: true, - env: { - NODE_ENV: "production" - } - } - }); - const { result: { outputConfig } } = await compile(config); - expect(outputConfig).toMatchObject({ - env: { - NODE_ENV: "production" - }, - minify: false, - compat: true - }); - }); - test("should return output object with expected properties", async () => { - const output = await compile(VALID_CONFIG); - const { success, diagnostics, result, version } = output; - const { code, metadata } = result; - - expect(code).toBeDefined(); - expect(diagnostics).toBeDefined(); - expect(version).toBeDefined(); - expect(metadata).toBeDefined(); - expect(success).toBeDefined(); - }); -}); - -describe("comiler metadata", () => { - it("decorators and import locations", async () => { - const { result: { code, metadata } } = await compile({ - name: "foo", - namespace: "x", - files: { - "foo.js": readFixture("metadata/metadata.js"), - "foo.html": readFixture("metadata/metadata.html") - }, - outputConfig: { format: "es" } - }); - - expect(pretify(code)).toBe( - pretify(readFixture("expected-sources-metadata.js")) - ); - - expect(metadata).toEqual({ - decorators: [ - { - type: "api", - targets: [ - { type: "property", name: "publicProp" }, - { type: "method", name: "publicMethod" } - ] - }, - { - type: "wire", - targets: [ - { - type: "property", - adapter: { name: "getTodo", reference: "todo" }, - name: "wiredProp", - params: {}, - static: {} - }, - { - type: "method", - adapter: { - name: "getHello", - reference: "@schema/foo.bar" - }, - name: "wiredMethod", - params: { name: "publicProp" }, - static: { fields: ["one", "two"] } - } - ] - } - ], - importLocations: [] - }); - }); -}); diff --git a/packages/lwc-compiler/src/compiler/__tests__/stylesheet.spec.ts b/packages/lwc-compiler/src/compiler/__tests__/stylesheet.spec.ts deleted file mode 100644 index d8505c8009..0000000000 --- a/packages/lwc-compiler/src/compiler/__tests__/stylesheet.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { readFile } from "fs"; -import { compile } from "../../index"; -import { fixturePath, readFixture, pretify } from "../../__tests__/utils"; - -describe("stylesheet", () => { - it("should import the associated stylesheet by default", async () => { - const { result: { code } } = await compile({ - name: "styled", - namespace: "x", - files: { - "styled.js": readFixture("namespaced_folder/styled/styled.js"), - "styled.html": readFixture( - "namespaced_folder/styled/styled.html" - ), - "styled.css": readFixture("namespaced_folder/styled/styled.css") - }, - outputConfig: { format: "es" } - }); - expect(pretify(code)).toBe(pretify(readFixture("expected-styled.js"))); - }); - - it("should import compress css in prod mode", async () => { - const { result: { code } } = await compile({ - name: "styled", - namespace: "x", - files: { - "styled.js": readFixture("namespaced_folder/styled/styled.js"), - "styled.html": readFixture( - "namespaced_folder/styled/styled.html" - ), - "styled.css": readFixture("namespaced_folder/styled/styled.css") - }, - outputConfig: { format: "es", minify: true } - }); - expect(pretify(code)).toBe( - pretify(readFixture("expected-styled-prod.js")) - ); - }); -}); diff --git a/packages/lwc-compiler/src/compiler/compiler.ts b/packages/lwc-compiler/src/compiler/compiler.ts deleted file mode 100755 index 33fb145aec..0000000000 --- a/packages/lwc-compiler/src/compiler/compiler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { bundle } from "../bundler/bundler"; -import { BundleMetadata } from "../bundler/meta-collector"; -import { Diagnostic, DiagnosticLevel } from "../diagnostics/diagnostic"; -import { CompilerOptions, validateOptions, normalizeOptions, NormalizedOutputConfig } from "./options"; -import { version } from '../index'; - -export { default as templateCompiler } from "lwc-template-compiler"; - -export interface CompilerOutput { - success: boolean; - diagnostics: Diagnostic[]; - result?: BundleResult; - version: string; -} - -export interface BundleResult { - code: string; - map: null; - metadata: BundleMetadata; - outputConfig: NormalizedOutputConfig; -} - -export async function compile( - options: CompilerOptions -): Promise { - validateOptions(options); - const normalizedOptions = normalizeOptions(options); - - let result: BundleResult | undefined; - const diagnostics: Diagnostic[] = []; - - const { - diagnostics: bundleDiagnostics, - code, - metadata, - } = await bundle(normalizedOptions); - - diagnostics.push(...bundleDiagnostics); - - if (!hasError(diagnostics)) { - result = { - code, - map: null, - metadata, - outputConfig: normalizedOptions.outputConfig, - }; - } - return { - version, - success: !hasError(diagnostics), - diagnostics, - result - }; -} - -function hasError(diagnostics: Diagnostic[]) { - return diagnostics.some(d => { - return d.level <= DiagnosticLevel.Error; - }); -} diff --git a/packages/lwc-compiler/src/compiler/options.ts b/packages/lwc-compiler/src/compiler/options.ts deleted file mode 100755 index e63fbd77c2..0000000000 --- a/packages/lwc-compiler/src/compiler/options.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { isBoolean, isString, isUndefined } from "../utils"; - -const DEFAULT_OUTPUT_CONFIG = { - env: { - NODE_ENV: "development" - }, - minify: false, - compat: false -}; - -export type OutputProxyCompatConfig = - | { global: string } - | { module: string } - | { independent: string }; - -export interface OutputConfig { - env?: { [name: string]: string }; - compat?: boolean; - minify?: boolean; - resolveProxyCompat?: OutputProxyCompatConfig; -} - -export interface BundleFiles { - [filename: string]: string; -} - -export interface CompilerOptions { - name: string; - namespace: string; - files: BundleFiles; - outputConfig?: OutputConfig; -} - -export interface NormalizedCompilerOptions extends CompilerOptions { - outputConfig: NormalizedOutputConfig; -} - -export interface NormalizedOutputConfig extends OutputConfig { - compat: boolean; - minify: boolean; - env: { - [name: string]: string; - }; -} - -export function validateNormalizedOptions(options: NormalizedCompilerOptions) { - validateOptions(options); - validateOutputConfig(options.outputConfig); -} - -export function validateOptions(options: CompilerOptions) { - if (isUndefined(options)) { - throw new TypeError(`Expected options object, received "${options}".`); - } - - if (!isString(options.name)) { - throw new TypeError( - `Expected a string for name, received "${options.name}".` - ); - } - - if (!isString(options.namespace)) { - throw new TypeError( - `Expected a string for namespace, received "${options.namespace}".` - ); - } - - if (isUndefined(options.files) || !Object.keys(options.files).length) { - throw new TypeError(`Expected an object with files to be compiled.`); - } - - for (const key of Object.keys(options.files)) { - const value = options.files[key]; - if (isUndefined(value) || !isString(value)) { - throw new TypeError( - `Unexpected file content for "${key}". Expected a string, received "${value}".` - ); - } - } - - if (!isUndefined(options.outputConfig)) { - validateOutputConfig(options.outputConfig); - } -} - -function validateOutputConfig(config: OutputConfig) { - if (!isUndefined(config.minify) && !isBoolean(config.minify)) { - throw new TypeError( - `Expected a boolean for outputConfig.minify, received "${ - config.minify - }".` - ); - } - - if (!isUndefined(config.compat) && !isBoolean(config.compat)) { - throw new TypeError( - `Expected a boolean for outputConfig.compat, received "${ - config.compat - }".` - ); - } -} - -export function normalizeOptions( - options: CompilerOptions -): NormalizedCompilerOptions { - const outputConfig: NormalizedOutputConfig = { - ...DEFAULT_OUTPUT_CONFIG, - ...options.outputConfig - }; - - return { - ...options, - outputConfig - }; -} diff --git a/packages/lwc-compiler/src/diagnostics/diagnostic-collector.ts b/packages/lwc-compiler/src/diagnostics/diagnostic-collector.ts new file mode 100644 index 0000000000..ee8074bbba --- /dev/null +++ b/packages/lwc-compiler/src/diagnostics/diagnostic-collector.ts @@ -0,0 +1,33 @@ +import { Diagnostic, DiagnosticLevel } from './diagnostic'; + +export class DiagnosticCollector { + private diagnostics: Diagnostic[] = []; + + constructor(diagnostics?: Diagnostic[]) { + if (diagnostics) { + this.addAll(diagnostics); + } + } + public getAll() { + return this.diagnostics; + } + + public addAll(items: Diagnostic[]) { + if (!Array.isArray(items)) { + return; + } + for (const item of items) { + this.add(item); + } + } + + public add(item: Diagnostic) { + this.diagnostics.push(item); + } + + public hasError() { + return this.diagnostics.some(d => { + return d.level <= DiagnosticLevel.Error; + }); + } +} diff --git a/packages/lwc-compiler/src/diagnostics/diagnostic.ts b/packages/lwc-compiler/src/diagnostics/diagnostic.ts old mode 100755 new mode 100644 index 3a03bbf246..3fdc09169d --- a/packages/lwc-compiler/src/diagnostics/diagnostic.ts +++ b/packages/lwc-compiler/src/diagnostics/diagnostic.ts @@ -1,15 +1,13 @@ -import { Location } from '../common-interfaces/location'; - export interface Diagnostic { /** Level of the diagnostic */ level: DiagnosticLevel; + /** Relative path location of the file in the bundle. */ + filename: string; + /** Error messages that should be outputed */ message: string; - /** Relative path location of the file in the bundle. */ - filename?: string; - /** * Location in the code affected by the diagnostic. * This field is optional, for example when the compiler throws an uncaught exception. @@ -27,3 +25,11 @@ export enum DiagnosticLevel { /** Logging messages */ Log = 3, } + +export interface Location { + /** 0-base character index in the file */ + start: number; + + /** Number of character after the start index */ + length: number; +} diff --git a/packages/lwc-compiler/src/index.js b/packages/lwc-compiler/src/index.js new file mode 100644 index 0000000000..1c0dc4cfbc --- /dev/null +++ b/packages/lwc-compiler/src/index.js @@ -0,0 +1,159 @@ +import * as path from 'path'; + +import bundle from './bundle'; +import transformFile from './transform'; + +import { MODES, ALL_MODES, isProd } from './modes'; +import { zipObject, isUndefined, isString } from './utils'; + +import * as replacePlugin from "rollup-plugin-replace"; +import fsModuleResolver from './module-resolvers/fs'; +import inMemoryModuleResolver from './module-resolvers/in-memory'; +import minifyPlugin from "./rollup-plugins/minify"; + +export { default as templateCompiler } from 'lwc-template-compiler'; + +const DEFAULT_NAMESPACE = 'x'; +const DEFAULT_TRANSFORM_OPTIONS = { mode: MODES.DEV }; + +const DEFAULT_COMPILE_OPTIONS = { + format: 'es', + mode: MODES.DEV, + mapNamespaceFromPath: false, + env: {}, + resolveProxyCompat: { + independent: 'proxy-compat', + }, +}; + +export function compile(entry, options = {}) { + if (isUndefined(entry) || !isString(entry)) { + throw new Error( + `Expected a string for entry. Received instead ${entry}`, + ); + } + + entry = normalizeEntryPath(entry); + options = Object.assign({ entry }, DEFAULT_COMPILE_OPTIONS, options); + + const acceptedModes = Object.keys(MODES).map(mode => MODES[mode]); + if (!acceptedModes.includes(options.mode)) { + throw new Error( + `Expected a mode in ${acceptedModes.join( + ', ', + )}. Received instead ${options.mode}`, + ); + } + + // Extract component namespace and name and add it to option + // TODO: rename the componentName and componentNamespace APIs, to moduleName and moduleNamespace, + // not all the modules are components. + const { name, namespace, normalizedName } = getNormalizedName(entry, options); + options.moduleName = name; + options.moduleNamespace = namespace; + options.normalizedModuleName = normalizedName; + + // Add extra properties needed for the compilation + // TODO: provide proper abstraction instead of stuffing the options object + options.$metadata = {}; + options.moduleResolver = isUndefined(options.sources) + ? fsModuleResolver() + : inMemoryModuleResolver(options); + + if (options.mode !== MODES.ALL) { + return bundle(entry, options); + } else { + // Compile the bundle in all modes at the same time + + // TODO: should this be exposed at the compiler level or should the caller call the compiler in all the modes ? + // Because the caller has probalby a better understanding of the current architecture, it can leverage multiple + // processor at the same time! + return Promise.all( + ALL_MODES.map(mode => + compile(entry, Object.assign({}, options, { mode })), + ), + ).then(results => { + return zipObject(ALL_MODES, results); + }); + } +} + +export function transform(src, id, options) { + if (!isString(src)) { + throw new Error(`Expect a string for source. Received ${src}`); + } + + if (!isString(id)) { + throw new Error(`Expect a string for id. Received ${id}`); + } + + options = Object.assign({}, DEFAULT_TRANSFORM_OPTIONS, options); + return transformFile(src, id, options); +} + +export function transformBundle(src, options) { + const mode = options.mode; + + if (isProd(mode)) { + const rollupReplace = replacePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }); + const rollupMinify = minifyPlugin(options); + const resultReplace = rollupReplace.transform(src, '$__tmpBundleSrc'); + const result = rollupMinify.transformBundle(resultReplace ? resultReplace.code : src); + + src = result.code; + } + + return src; +} + +/** + * Takes a module location and returns it's entry point: + * '/foo/bar' => '/foo/bar/bar' + * '/foo/bar/bar.js' => '/foo/bar/bar.js' + * + * @param {string} fileName + */ +function normalizeEntryPath(fileName) { + fileName = path.normalize(fileName.replace(/\/$/, '')); + const ext = path.extname(fileName); + return ext + ? fileName + : path.join(fileName, fileName.split(path.sep).pop() + ext); +} + +/** + * Names and namespace mapping: + * 'foo.js' => ns: default, name: foo + * '.../foo/foo.js' => ns: default, name: foo + * '.../myns/foo/foo.js' => ns: myns, name: foo + */ +function getNormalizedName(fileName,{ componentName, componentNamespace, mapNamespaceFromPath }) { + const ext = path.extname(fileName); + const parts = fileName.split(path.sep); + + const name = componentName || path.basename(parts.pop(), ext); + + let namespace; + if (!isUndefined(componentNamespace)) { + namespace = componentNamespace; + } else { + namespace = name.indexOf('-') === -1 ? DEFAULT_NAMESPACE : null; + + let tmpNs = parts.pop(); + if (tmpNs === name) { + tmpNs = parts.pop(); + } + // If mapping folder structure override namespace + if (tmpNs && mapNamespaceFromPath) { + namespace = tmpNs; + } + } + + return { + name, + namespace, + normalizedName: [namespace, name].join('-'), + }; +} + +export const version = '__VERSION__'; diff --git a/packages/lwc-compiler/src/index.ts b/packages/lwc-compiler/src/index.ts deleted file mode 100755 index 2890fe7902..0000000000 --- a/packages/lwc-compiler/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { compile } from './compiler/compiler'; -export { transform } from './transformers/transformer'; -export const version = '__VERSION__'; diff --git a/packages/lwc-compiler/src/lwc-bundle.ts b/packages/lwc-compiler/src/lwc-bundle.ts new file mode 100644 index 0000000000..35ffce15d3 --- /dev/null +++ b/packages/lwc-compiler/src/lwc-bundle.ts @@ -0,0 +1,4 @@ +export interface LwcBundle { + entry: string; + sources: [{ filename: string }]; +} diff --git a/packages/lwc-compiler/src/modes.js b/packages/lwc-compiler/src/modes.js new file mode 100644 index 0000000000..74eff5d864 --- /dev/null +++ b/packages/lwc-compiler/src/modes.js @@ -0,0 +1,32 @@ +/** Available compilation modes */ +export const MODES = { + DEV: 'dev', + PROD: 'prod', + COMPAT: 'compat', + PROD_COMPAT: 'prod_compat', + ALL: 'all', +}; + +/** List of all the modes */ +export const ALL_MODES = [ + MODES.DEV, + MODES.PROD, + MODES.COMPAT, + MODES.PROD_COMPAT, +]; + +/** + * Returns true if the passed mode is a PROD mode + * @param {string} mode + */ +export function isProd(mode) { + return mode === MODES.PROD || mode === MODES.PROD_COMPAT; +} + +/** + * Returns true if the passed mode is a COMPAT mode + * @param {string} mode + */ +export function isCompat(mode) { + return mode === MODES.COMPAT || mode === MODES.PROD_COMPAT; +} diff --git a/packages/lwc-compiler/src/module-resolvers/fs.js b/packages/lwc-compiler/src/module-resolvers/fs.js new file mode 100644 index 0000000000..67a30d5fe5 --- /dev/null +++ b/packages/lwc-compiler/src/module-resolvers/fs.js @@ -0,0 +1,23 @@ +import * as fs from 'fs'; + +const DEFAULT_ENCODING = 'utf-8'; + +export default function() { + return { + fileExists(fileName) { + return new Promise(resolve => + fs.access(fileName, fs.constants.F_OK, err => { + return err ? resolve(false) : resolve(true); + }) + ); + }, + + readFile(fileName) { + return new Promise((resolve, reject) => + fs.readFile(fileName, DEFAULT_ENCODING, (err, data) => { + return err ? reject(err) : resolve(data); + }) + ); + }, + }; +} diff --git a/packages/lwc-compiler/src/module-resolvers/in-memory.js b/packages/lwc-compiler/src/module-resolvers/in-memory.js new file mode 100644 index 0000000000..d137a552c5 --- /dev/null +++ b/packages/lwc-compiler/src/module-resolvers/in-memory.js @@ -0,0 +1,25 @@ +export default function({ sources } = {}) { + for (let key of Object.keys(sources)) { + if (sources[key] == undefined || typeof sources[key] !== 'string') { + throw new Error( + `in-memory module resolution expects values to be string. Received ${sources[ + key + ]} for key ${key}` + ); + } + } + + const fileExists = fileName => sources.hasOwnProperty(fileName); + + return { + fileExists(fileName) { + return Promise.resolve(fileExists(fileName)); + }, + + readFile(fileName) { + return fileExists(fileName) + ? Promise.resolve(sources[fileName]) + : Promise.reject(new Error(`No such file ${fileName}`)); + }, + }; +} diff --git a/packages/lwc-compiler/src/references/__tests__/css.spec.ts b/packages/lwc-compiler/src/references/__tests__/css.spec.ts new file mode 100644 index 0000000000..ff903f4ed5 --- /dev/null +++ b/packages/lwc-compiler/src/references/__tests__/css.spec.ts @@ -0,0 +1,342 @@ +import { getReferences } from '../../references/css'; + +test('single selector', () => { + expect(getReferences(`x-foo { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + ]); +}); + +test('single selector with attributes', () => { + expect( + getReferences(`x-foo[title="Hello"] { color: red; }`, 'test.js'), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + ]); +}); + +test('selector chain', () => { + expect(getReferences(`header x-foo { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + ]); +}); + +test('selector list', () => { + expect( + getReferences(`header x-foo, div, x-bar { color: red; }`, 'test.js'), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 19, + length: 5, + }, + ], + }, + ]); +}); + +test('multiline selector list', () => { + expect(getReferences(`x-foo,\nx-bar { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + ]); +}); + +test('ugly selector list', () => { + expect( + getReferences( + ` x-foo, p .foo, \nx-bar {\n color : red ;\n }`, + 'test.js', + ), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 6, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 28, + length: 5, + }, + ], + }, + ]); +}); + +test('multiple rules', () => { + expect( + getReferences( + `x-foo {\ncolor: red;\n }\n \nx-bar {\ncolor: red;\n}`, + 'test.js', + ), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 25, + length: 5, + }, + ], + }, + ]); +}); + + +test('single selector', () => { + expect(getReferences(`x-foo { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + ]); +}); + +test('single selector with attributes', () => { + expect( + getReferences(`x-foo[title="Hello"] { color: red; }`, 'test.js'), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + ]); +}); + +test('selector chain', () => { + expect(getReferences(`header x-foo { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + ]); +}); + +test('selector list', () => { + expect( + getReferences(`header x-foo, div, x-bar { color: red; }`, 'test.js'), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 19, + length: 5, + }, + ], + }, + ]); +}); + +test('multiline selector list', () => { + expect(getReferences(`x-foo,\nx-bar { color: red; }`, 'test.js')).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 7, + length: 5, + }, + ], + }, + ]); +}); + +test('ugly selector list', () => { + expect( + getReferences( + ` x-foo, p .foo, \nx-bar {\n color : red ;\n }`, + 'test.js', + ), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 6, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 28, + length: 5, + }, + ], + }, + ]); +}); + +test('multiple rules', () => { + expect( + getReferences( + `x-foo {\ncolor: red;\n }\n \nx-bar {\ncolor: red;\n}`, + 'test.js', + ), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 0, + length: 5, + }, + ], + }, + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 25, + length: 5, + }, + ], + }, + ]); +}); diff --git a/packages/lwc-compiler/src/references/__tests__/html.spec.ts b/packages/lwc-compiler/src/references/__tests__/html.spec.ts new file mode 100644 index 0000000000..516cedf084 --- /dev/null +++ b/packages/lwc-compiler/src/references/__tests__/html.spec.ts @@ -0,0 +1,78 @@ +import { getReferences } from '../../references/html'; + +test('simple component', () => { + expect( + getReferences(``, 'test.js'), + ).toEqual([ + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 11, + length: 5, + }, + { + start: 19, + length: 5, + }, + ], + }, + ]); +}); + +test('nested components', () => { + expect( + getReferences( + ``, + 'test.js', + ), + ).toEqual([ + { + id: 'x-bar', + file: 'test.js', + type: 'component', + locations: [ + { + start: 18, + length: 5, + }, + { + start: 26, + length: 5, + }, + ], + }, + { + id: 'x-foo', + file: 'test.js', + type: 'component', + locations: [ + { + start: 11, + length: 5, + }, + { + start: 34, + length: 5, + }, + ], + }, + { + id: 'x-baz', + file: 'test.js', + type: 'component', + locations: [ + { + start: 41, + length: 5, + }, + { + start: 49, + length: 5, + }, + ], + }, + ]); +}); diff --git a/packages/lwc-compiler/src/references/__tests__/javascript.spec.ts b/packages/lwc-compiler/src/references/__tests__/javascript.spec.ts new file mode 100644 index 0000000000..622b541077 --- /dev/null +++ b/packages/lwc-compiler/src/references/__tests__/javascript.spec.ts @@ -0,0 +1,115 @@ +import { getReferences } from '../../references/javascript'; + +describe('resource-url', () => { + test('gather metadata', () => { + expect( + getReferences( + `import resource from '@resource-url/foo';`, + 'test.js', + ), + ).toEqual([ + { + id: 'foo', + file: 'test.js', + type: 'resourceUrl', + locations: [ + { + start: 36, + length: 3, + }, + ], + }, + ]); + }); + + test('errors when using namespaced import', () => { + expect(() => + getReferences( + `import * as resource from '@resource-url/foo';`, + 'test.js', + ), + ).toThrow('@resource-url modules only support default imports.'); + }); + + test('errors when using a named import', () => { + expect(() => + getReferences( + `import { resource } from '@resource-url/foo';`, + 'test.js', + ), + ).toThrow('@resource-url modules only support default imports.'); + }); +}); + +describe('label', () => { + test('gather metadata', () => { + expect( + getReferences(`import label from '@label/foo';`, 'test.js'), + ).toEqual([ + { + id: 'foo', + file: 'test.js', + type: 'label', + locations: [ + { + start: 26, + length: 3, + }, + ], + }, + ]); + }); + + test('errors when using namespaced import', () => { + expect(() => + getReferences(`import * as label from '@label/foo';`, 'test.js'), + ).toThrow('@label modules only support default imports.'); + }); + + test('errors when using a named import', () => { + expect(() => + getReferences(`import { label } from '@label/foo';`, 'test.js'), + ).toThrow('@label modules only support default imports.'); + }); +}); + +describe('apex', () => { + test('gather metadata', () => { + expect( + getReferences( + `import methodA from '@apex/MyClass.methodA';`, + 'test.js', + ), + ).toEqual([ + { + id: 'MyClass.methodA', + file: 'test.js', + type: 'apexMethod', + locations: [ + { + start: 27, + length: 15, + }, + ], + }, + ]); + }); + + test('errors when using namespaced import', () => { + expect(() => + getReferences( + `import * as MyClass from '@apex/MyClass';`, + 'test.js', + ), + ).toThrow('@apex modules only support default imports.'); + }); + + test('errors when using a default import', () => { + expect(() => + getReferences( + `import { methodA } from '@apex/MyClass';`, + 'test.js', + ), + ).toThrow('@apex modules only support default imports.'); + }); +}); diff --git a/packages/lwc-compiler/src/references/css.ts b/packages/lwc-compiler/src/references/css.ts new file mode 100644 index 0000000000..2e51862b11 --- /dev/null +++ b/packages/lwc-compiler/src/references/css.ts @@ -0,0 +1,71 @@ +import { Reference } from './types'; + +import * as postcss from 'postcss'; +import { Rule } from 'postcss'; + +import * as postcssSelector from 'postcss-selector-parser'; +import { isTag } from 'postcss-selector-parser'; + +function isCustomElementSelector(tag: string) { + return tag.includes('-'); +} + +function getSelectorOffset(rule: Rule, source: string) { + // line and column are both 1 based. + const { line, column } = rule.source.start!; + + const lines = source.split('\n'); + const lineOffset = lines + .slice(0, line - 1) + .reduce((offset, l) => offset + l.length + 1, 0); + + return lineOffset + column - 1; +} + +function getSelectorReferences( + selector: string, + filename: string, + offset: number, +): Reference[] { + const references: Reference[] = []; + + const processor = postcssSelector(); + const root = processor.astSync(selector, { lossless: true }); + + root.walk(node => { + if (!isTag(node) || !isCustomElementSelector(node.value)) { + return; + } + + references.push({ + id: node.value, + type: 'component', + file: filename, + locations: [ + { + start: offset + node.sourceIndex, + length: node.value.length, + }, + ], + }); + }); + + return references; +} + +export function getReferences(source: string, filename: string): Reference[] { + const references: Reference[] = []; + + const root = postcss.parse(source, { from: filename }); + root.walkRules(rule => { + const selectorOffset = getSelectorOffset(rule, source); + const ruleReferences = getSelectorReferences( + rule.selector, + filename, + selectorOffset, + ); + references.push(...ruleReferences); + }); + + return references; +} diff --git a/packages/lwc-compiler/src/references/html.ts b/packages/lwc-compiler/src/references/html.ts new file mode 100644 index 0000000000..013a48f8b9 --- /dev/null +++ b/packages/lwc-compiler/src/references/html.ts @@ -0,0 +1,58 @@ +import { Reference } from './types'; +import { SAXParser } from 'parse5'; + +function isCustomElement(name: string) { + return name.includes('-'); +} + +export function getReferences(source: string, filename: string): Reference[] { + const parser = new SAXParser({ locationInfo: true }); + + const references: Reference[] = []; + const stack: Reference[] = []; + + parser.on('startTag', (name, attrs, selfClosing, location) => { + if (!isCustomElement(name)) { + return; + } + + const startTagRef: Reference = { + id: name, + type: 'component', + file: filename, + locations: [ + { + // Location offset given by the parser includes the preceding "<" + start: location!.startOffset + 1, + length: name.length, + }, + ], + }; + + stack.push(startTagRef); + }); + + parser.on('endTag', (name, location) => { + if (!isCustomElement(name)) { + return; + } + + const tagRef = stack.pop(); + + if (!tagRef) { + throw new Error(`Missing matching tack for <${name}>`); + } + + tagRef.locations.push({ + // Location offset given by the parser includes the preceding " source.startsWith(`${prefix}/`); +} + +const isApexSource = isGvpSource(APEX_PREFIX); +const isLabelSource = isGvpSource(LABEL_PREFIX); +const isResourceUrlSource = isGvpSource(RESOURCE_URL_PREFIX); + +function getGvpId(path: NodePath) { + const { value } = path.node.source; + const res = /^@[\w-]+\/(.+)$/.exec(value); + + if (!res) { + throw new Error(`Unexpected GVP source for ${value}`); + } + + return res[1]; +} + +function assertOnlyDefaultImport( + path: NodePath, + error: string, +) { + const hasNamedImport = path.node.specifiers.some( + node => !t.isImportDefaultSpecifier(node), + ); + + if (hasNamedImport) { + // TODO: convert into diagnostic + throw new Error(error); + } +} + +function getResourceReferences( + path: NodePath, + filename: string, +): Reference[] { + assertOnlyDefaultImport( + path, + `${RESOURCE_URL_PREFIX} modules only support default imports.`, + ); + + const id = getGvpId(path); + const { source } = path.node; + + return [ + { + id, + type: 'resourceUrl', + file: filename, + locations: [ + { + start: source.start + RESOURCE_URL_PREFIX.length + 2, + length: id.length, + }, + ], + }, + ]; +} + +function getLabelReferences( + path: NodePath, + filename: string, +): Reference[] { + assertOnlyDefaultImport( + path, + `${LABEL_PREFIX} modules only support default imports.`, + ); + + const id = getGvpId(path); + const { source } = path.node; + + return [ + { + id, + type: 'label', + file: filename, + locations: [ + { + start: source.start + LABEL_PREFIX.length + 2, + length: id.length, + }, + ], + }, + ]; +} + +function getApexReferences( + path: NodePath, + filename: string, +): Reference[] { + assertOnlyDefaultImport( + path, + `${APEX_PREFIX} modules only support default imports.`, + ); + + const id = getGvpId(path); + const { source } = path.node; + + return [ + { + id, + type: 'apexMethod', + file: filename, + locations: [ + { + start: source.start + APEX_PREFIX.length + 2, + length: id.length, + }, + ], + }, + ]; +} + +function sfdcReferencesVisitor(references: Reference[], filename: string) { + return { + ImportDeclaration(path: NodePath) { + const { value } = path.node.source; + + let importReferences: Reference[] = []; + + if (isResourceUrlSource(value)) { + importReferences = getResourceReferences(path, filename); + } else if (isLabelSource(value)) { + importReferences = getLabelReferences(path, filename); + } else if (isApexSource(value)) { + importReferences = getApexReferences(path, filename); + } + + references.push(...importReferences); + }, + }; +} + +export function getReferences(source: string, filename: string): Reference[] { + const references: Reference[] = []; + + const ast = parse(source, { + sourceFilename: filename, + sourceType: 'module', + plugins: ['classProperties', 'decorators', 'objectRestSpread'], + }); + + traverse(ast, sfdcReferencesVisitor(references, filename)); + + return references; +} diff --git a/packages/lwc-compiler/src/references/references.ts b/packages/lwc-compiler/src/references/references.ts new file mode 100644 index 0000000000..a150030748 --- /dev/null +++ b/packages/lwc-compiler/src/references/references.ts @@ -0,0 +1,41 @@ +import * as path from 'path'; + +import { getReferences as getCssReferences } from './css'; +import { getReferences as getHtmlReferences } from './html'; +import { getReferences as getJsReferences } from './javascript'; +import { Reference } from './types'; + +import { LwcBundle } from '../lwc-bundle'; + +export function getBundleReferences(bundle: LwcBundle): Reference[] { + if (!bundle || !bundle.sources) { + return []; + } + // TODO: ts is complaining if [filename, source] is used instead of entry + return Object.entries(bundle.sources).reduce( + (refs: Reference[], entry: any) => { + const filename = entry[0]; + const source = entry[1]; + return [...refs, ...getFileReferences(source, filename)]; + }, + [], + ); +} + +export function getFileReferences( + source: string, + filename: string, +): Reference[] { + const ext = path.extname(filename); + + switch (ext) { + case '.html': + return getHtmlReferences(source, filename); + case '.js': + return getJsReferences(source, filename); + case '.css': + return getCssReferences(source, filename); + default: + return []; + } +} diff --git a/packages/lwc-compiler/src/references/types.ts b/packages/lwc-compiler/src/references/types.ts new file mode 100644 index 0000000000..2fb84c08b9 --- /dev/null +++ b/packages/lwc-compiler/src/references/types.ts @@ -0,0 +1,11 @@ +export declare type ReferenceType = 'resourceUrl' | 'label' | 'apexClass' | 'apexMethod' | 'sobjectClass' | 'sobjectField' | 'component'; +export interface ReferenceLocation { + start: number; + length: number; +} +export interface Reference { + type: ReferenceType; + id: string; + file: string; + locations: ReferenceLocation[]; +} diff --git a/packages/lwc-compiler/src/rollup-plugins/compat.js b/packages/lwc-compiler/src/rollup-plugins/compat.js new file mode 100644 index 0000000000..0c2b1f84de --- /dev/null +++ b/packages/lwc-compiler/src/rollup-plugins/compat.js @@ -0,0 +1,24 @@ +import { transform } from '@babel/core'; +import presetCompat from 'babel-preset-compat'; +import { BABEL_CONFIG_BASE } from '../babel-plugins'; + +/** + * Rollup plugin transforms for compat mode: + * - Proxy compat + * - ES2015+ transpilation to ES5 + */ +export default function({ resolveProxyCompat }) { + const config = Object.assign({}, BABEL_CONFIG_BASE, { + presets: [ + [presetCompat, { proxy: true, resolveProxyCompat }], + ], + }); + + return { + name: 'compat', + transform(src) { + const { code, map } = transform(src, config); + return { code, map }; + }, + }; +} diff --git a/packages/lwc-compiler/src/rollup-plugins/compat.ts b/packages/lwc-compiler/src/rollup-plugins/compat.ts deleted file mode 100755 index b41bd244c0..0000000000 --- a/packages/lwc-compiler/src/rollup-plugins/compat.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as babel from '@babel/core'; -import * as presetCompat from 'babel-preset-compat'; -import { BABEL_CONFIG_BASE } from '../babel-plugins'; -import { OutputProxyCompatConfig } from "../compiler/options"; - -export default function(proxyCompatOption: OutputProxyCompatConfig | undefined) { - const config = Object.assign({}, BABEL_CONFIG_BASE, { - presets: [ - [presetCompat, { proxy: true, resolveProxyCompat: proxyCompatOption || { independent: "proxy-compat" } }], - ], - }); - - return { - name: "lwc-compat", - - transform(src: string) { - const { code, map } = babel.transform(src, config); - return { code, map }; - } - }; -} diff --git a/packages/lwc-compiler/src/rollup-plugins/minify.ts b/packages/lwc-compiler/src/rollup-plugins/minify.js old mode 100755 new mode 100644 similarity index 52% rename from packages/lwc-compiler/src/rollup-plugins/minify.ts rename to packages/lwc-compiler/src/rollup-plugins/minify.js index 53fb1817dc..b13419a695 --- a/packages/lwc-compiler/src/rollup-plugins/minify.ts +++ b/packages/lwc-compiler/src/rollup-plugins/minify.js @@ -1,21 +1,21 @@ -import * as babel from '@babel/core'; +import { transform } from '@babel/core'; import * as minify from 'babel-preset-minify'; import { BABEL_CONFIG_BASE } from '../babel-plugins'; -export const MINIFY_CONFIG: any = Object.assign({}, BABEL_CONFIG_BASE, { +export const MINIFY_CONFIG = Object.assign({}, BABEL_CONFIG_BASE, { presets: [[minify, { guards: false, evaluate: false }]] }); /** * Rollup plugin applying minification to the generated bundle. */ -export default function() { +export default function(options) { return { - name: 'lwc-minify', + name: 'minify', - transformBundle(src: string) { - const { code, map } = babel.transform(src, MINIFY_CONFIG); + transformBundle(src) { + const { code, map } = transform(src, MINIFY_CONFIG); return { code, map }; }, }; diff --git a/packages/lwc-compiler/src/rollup-plugins/module-resolver.js b/packages/lwc-compiler/src/rollup-plugins/module-resolver.js new file mode 100644 index 0000000000..5be84e026e --- /dev/null +++ b/packages/lwc-compiler/src/rollup-plugins/module-resolver.js @@ -0,0 +1,62 @@ +import * as path from 'path'; + +const EMPTY_CSS_CONTENT = ``; + +function isRelativeImport(id) { + return id.startsWith('.'); +} + +function shouldRecordDependency(id) { + return !id.startsWith('babel-compat/') && !id.startsWith('proxy-compat/'); +} + +function isTemplateCss(id, importee) { + return path.extname(id) === '.css' + && path.extname(importee) === '.html' + && path.basename(id, '.css') === path.basename(importee, '.html'); +} + +/** + * Resolve files in the context of LWC modules and store external + * dependencies + */ +export default function({ moduleResolver, $metadata }) { + $metadata.rollupDependencies = []; + + return { + name: 'module-resolver', + + resolveId: function(id, importee) { + if (!isRelativeImport(id) && importee) { + if (shouldRecordDependency(id)) { + $metadata.rollupDependencies.push(id); + } + } else { + const relPath = importee ? path.dirname(importee) : ''; + let absPath = path.join(relPath, id); + + if (!path.extname(id)) { + absPath += '.js'; + } + + return moduleResolver.fileExists(absPath).then(exists => { + if (!exists && !isTemplateCss(id, importee)) { + throw new Error( + `Could not resolve '${id}' from '${importee}'` + ); + } + + return absPath; + }); + } + }, + + load(id) { + return moduleResolver.fileExists(id).then(exists => { + return !exists && path.extname(id) === '.css' + ? EMPTY_CSS_CONTENT + : moduleResolver.readFile(id); + }) + }, + }; +} diff --git a/packages/lwc-compiler/src/rollup-plugins/module-resolver.ts b/packages/lwc-compiler/src/rollup-plugins/module-resolver.ts deleted file mode 100755 index f1c64d257f..0000000000 --- a/packages/lwc-compiler/src/rollup-plugins/module-resolver.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as path from "path"; -import { MetadataCollector } from "../bundler/meta-collector"; -import { NormalizedCompilerOptions } from "../compiler/options"; - -const EMPTY_CSS_CONTENT = ``; - -function isRelativeImport(id: string) { - return id.startsWith("."); -} - -function isTemplateCss(id: string, importee: string) { - return ( - path.extname(id) === ".css" && - path.extname(importee) === ".html" && - path.basename(id, ".css") === path.basename(importee, ".html") - ); -} - -function fileExists( - fileName: string, - { files }: NormalizedCompilerOptions -): boolean { - return files.hasOwnProperty(fileName); -} - -function readFile( - fileName: string, - options: NormalizedCompilerOptions -): string { - const { files } = options; - - if (fileExists(fileName, options)) { - return files[fileName]; - } else { - throw new Error(`No such file ${fileName}`); - } -} - -export default function({ - metadataCollector, - options -}: { - metadataCollector: MetadataCollector; - options: NormalizedCompilerOptions; -}) { - return { - name: "lwc-module-resolver", - - resolveId(id: string, importee: string) { - if (!isRelativeImport(id) && importee) { - return; - } - - const relPath = importee ? path.dirname(importee) : ""; - let absPath = path.join(relPath, id); - - if (!path.extname(id)) { - absPath += ".js"; - } - - if ( - !fileExists(absPath, options) && - !isTemplateCss(id, importee) - ) { - throw new Error( - `Could not resolve '${id}' from '${importee}'` - ); - } - return absPath; - }, - - load(id: string) { - return !fileExists(id, options) && path.extname(id) === ".css" - ? EMPTY_CSS_CONTENT - : readFile(id, options); - } - }; -} diff --git a/packages/lwc-compiler/src/rollup-plugins/transform.js b/packages/lwc-compiler/src/rollup-plugins/transform.js new file mode 100644 index 0000000000..1f5a98f0af --- /dev/null +++ b/packages/lwc-compiler/src/rollup-plugins/transform.js @@ -0,0 +1,50 @@ +import * as path from 'path'; + +import styleTransform from '../transformers/style'; +import templateTransformer from '../transformers/template'; +import javascriptTransformer from '../transformers/javascript'; + +/** + * Returns the associted transformer for a specific file + * @param {string} fileName + */ +function getTransformer(fileName) { + switch (path.extname(fileName)) { + case '.html': + return templateTransformer; + + case '.css': + return styleTransform; + + case '.js': + case '.ts': + return javascriptTransformer; + + default: + throw new Error(`No available transformer for ${fileName}`); + } +} + +/** + * Transfrom each file individually + */ +export default function(options) { + const { $metadata } = options; + + return { + name: 'transform', + + async transform(src, id) { + const transfom = getTransformer(id); + const result = await transfom( + src, + Object.assign({}, options, { + filename: id, + }) + ); + + $metadata[id] = result.metadata; + return result; + }, + }; +} diff --git a/packages/lwc-compiler/src/rollup-plugins/transform.ts b/packages/lwc-compiler/src/rollup-plugins/transform.ts deleted file mode 100755 index 39a05ad20e..0000000000 --- a/packages/lwc-compiler/src/rollup-plugins/transform.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getTransformer, FileTransformerResult } from '../transformers/transformer'; - -import { NormalizedCompilerOptions } from "../compiler/options"; -import { MetadataCollector } from '../bundler/meta-collector'; - -export default function({ - options, - metadataCollector, -}: { - options: NormalizedCompilerOptions; - metadataCollector?: MetadataCollector -}) { - return { - name: "lwc-file-transform", - async transform(src: string, id: string): Promise { - const transform = getTransformer(id); - return await transform( - src, - id, - options, - metadataCollector, - ); - } - }; -} diff --git a/packages/lwc-compiler/src/transform.js b/packages/lwc-compiler/src/transform.js new file mode 100644 index 0000000000..4de0c462ed --- /dev/null +++ b/packages/lwc-compiler/src/transform.js @@ -0,0 +1,45 @@ +import * as path from 'path'; + +import styleTransform from './transformers/style'; +import templateTransformer from './transformers/template'; +import javascriptTransformer from './transformers/javascript'; +import compatPluginFactory from "./rollup-plugins/compat"; +import { isCompat } from './modes'; + +/** + * Returns the associted transformer for a specific file + * @param {string} fileName + */ +function getTransformer(fileName) { + switch (path.extname(fileName)) { + case '.html': + return templateTransformer; + + case '.css': + return styleTransform; + + case '.js': + case '.ts': + return javascriptTransformer; + + default: + throw new Error(`No available transformer for ${fileName}`); + + } +} + +/** + * Run approriate transformation for the passed source + */ +export default async function transform(src, id, options) { + const transfomer = getTransformer(id); + const mergedOptions = Object.assign({}, options, { filename: id }); + const result = await Promise.resolve(transfomer( src, mergedOptions)); + + if (isCompat(options.mode)) { + const { transform } = compatPluginFactory(mergedOptions); + return transform(result.code); + } + + return result; +} diff --git a/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts b/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts deleted file mode 100755 index 2326e9674c..0000000000 --- a/packages/lwc-compiler/src/transformers/__tests__/transform.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { transform } from "../../transformers/transformer"; -import { pretify } from "../../__tests__/utils"; - -const VALID_TEST_JS = ` -import label from '@label/mylabel'; -function isTrue() { - return label; -} -isTrue(); -`.trim(); - -describe("transform", () => { - it("should validate presence of src", () => { - expect(() => transform()).toThrow( - /Expect a string for source. Received undefined/ - ); - }); - - it("should validate presence of id", () => { - expect(() => transform(`console.log('Hello')`)).toThrow( - /Expect a string for id. Received undefined/ - ); - }); - - it("should throw if invalid resolveProxyCompat value is specified in compat mode", async () => { - expect.assertions(1); - try { - const result = await transform(`debugger`, "foo.js", { - namespace: "x", - name: "foo", - outputConfig: { - compat: true, - resolveProxyCompat: { - badkey: "hello" - } - } - }); - } catch (error) { - expect(error.message).toBe( - 'Unexpected resolveProxyCompat option, expected property "module", "global" or "independent"' - ); - } - }); - - it("should throw when invalid javascript file is specified", async () => { - expect.assertions(1); - try { - await transform(`const`, "foo.js", { - namespace: "x", - name: "foo" - }); - } catch (error) { - // TODO: Figure out how to disable error message code snippet for failing token. - expect( - error.message.indexOf("foo.js: Unexpected token (1:5)") - ).toBe(0); - } - }); - it("should apply transformation for valid javascript file", async () => { - const actual = ` - import { Element } from 'engine'; - export default class Foo extends Element {} - `; - - const expected = ` - import _tmpl from "./foo.html"; - import { Element } from 'engine'; - export default class Foo extends Element { - render() { - return _tmpl; - } - } - Foo.style = _tmpl.style; - `; - - const { code } = await transform(actual, "foo.js", { - namespace: "x", - name: "foo" - }); - expect(pretify(code)).toBe(pretify(expected)); - }); - - it("should throw when invalid template file is specified", async () => { - expect.assertions(1); - try { - await transform(` { - const actual = ` - - `; - - const expected = ` - import style from './foo.css' - - export default function tmpl($api, $cmp, $slotset, $ctx) { - const { - t: api_text, - h: api_element - } = $api; - - return [api_element("div", { - key: 1 - }, [api_text("Hello")])]; - } - - if (style) { - const tagName = 'x-foo'; - const token = 'x-foo_foo'; - tmpl.token = token; - tmpl.style = style(tagName, token); - } - `; - - const { code } = await transform(actual, "foo.html", { - namespace: "x", - name: "foo" - }); - - expect(pretify(code)).toBe(pretify(expected)); - }); - - it("should throw when invalid tempalte file is specified", async () => { - expect.assertions(1); - try { - await transform(`<`, "foo.css", { - namespace: "x", - name: "foo" - }); - } catch (error) { - expect(error.message).toBe(":1:1: Unknown word"); - } - }); - - it("should apply transformation for stylesheet file", async () => { - const actual = ` - div { - background-color: red; - } - `; - - const expected = ` - function style(tagName, token) { - return \` - div[\${token}] { - background-color: red; - } - \`; - } - export default style; - `; - - const { code } = await transform(actual, "foo.css", { - namespace: "x", - name: "foo" - }); - - expect(pretify(code)).toBe(pretify(expected)); - }); - - it("javascript metadata", async () => { - const content = ` - import { Element, api } from 'engine'; - /** Foo doc */ - export default class Foo extends Element { - _privateTodo; - @api get todo () { - return this._privateTodo; - } - @api set todo (val) { - return this._privateTodo = val; - } - @api - index; - } - `; - - const result = await transform(content, "foo.js", { - namespace: "x", - name: "foo" - }); - - const metadata = result.metadata; - - expect(metadata.decorators).toEqual([ - { - type: "api", - targets: [ - { type: "property", name: "todo" }, - { type: "property", name: "index" } - ] - } - ]); - expect(metadata.doc).toBe("Foo doc"); - expect(metadata.declarationLoc).toEqual({ - start: { line: 4, column: 12 }, - end: { line: 14, column: 13 } - }); - }); -}); diff --git a/packages/lwc-compiler/src/transformers/javascript.js b/packages/lwc-compiler/src/transformers/javascript.js new file mode 100644 index 0000000000..37aba9224a --- /dev/null +++ b/packages/lwc-compiler/src/transformers/javascript.js @@ -0,0 +1,24 @@ +import { transform } from '@babel/core'; +import * as lwcClassTransformPlugin from 'babel-plugin-transform-lwc-class'; + +import { BABEL_CONFIG_BASE, BABEL_PLUGINS_BASE } from '../babel-plugins'; + +export default function(code, options) { + const { filename } = options; + + const config = Object.assign({}, BABEL_CONFIG_BASE, { + plugins: [ + lwcClassTransformPlugin, + ...BABEL_PLUGINS_BASE, + ], + filename, + }); + + const transformed = transform(code, config); + + return { + code: transformed.code, + map: transformed.map, + metadata: transformed.metadata, + }; +} diff --git a/packages/lwc-compiler/src/transformers/javascript.ts b/packages/lwc-compiler/src/transformers/javascript.ts deleted file mode 100755 index 3509f3fd77..0000000000 --- a/packages/lwc-compiler/src/transformers/javascript.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as babel from "@babel/core"; -import * as lwcClassTransformPlugin from "babel-plugin-transform-lwc-class"; - -import { BABEL_CONFIG_BASE, BABEL_PLUGINS_BASE } from "../babel-plugins"; -import { NormalizedCompilerOptions } from "../compiler/options"; -import { FileTransformerResult } from "./transformer"; -import { MetadataCollector } from "../bundler/meta-collector"; - -export default function( - code: string, - filename: string, - options: NormalizedCompilerOptions, - metadataCollector?: MetadataCollector -): FileTransformerResult { - const config = Object.assign({}, BABEL_CONFIG_BASE, { - plugins: [lwcClassTransformPlugin, ...BABEL_PLUGINS_BASE], - filename, - }); - const result = babel.transform(code, config); - - const metadata: lwcClassTransformPlugin.Metadata = (result as any) - .metadata; - - if (metadataCollector) { - metadata.decorators.forEach(d => metadataCollector.collectDecorator(d)); - } - - return { - code: result.code, - map: null, - metadata, - }; -} diff --git a/packages/lwc-compiler/src/transformers/style.js b/packages/lwc-compiler/src/transformers/style.js new file mode 100644 index 0000000000..33a77a1526 --- /dev/null +++ b/packages/lwc-compiler/src/transformers/style.js @@ -0,0 +1,72 @@ +import * as postcss from 'postcss'; +import * as cssnano from 'cssnano'; +import postcssPluginRaptor from 'postcss-plugin-lwc'; + +import { isProd } from '../modes'; + +const TOKEN_PLACEHOLDER = '__TOKEN__'; +const TAG_NAME_PLACEHOLDER = '__TAG_NAME__'; + +const EMPTY_CSS_OUTPUT = ` +const style = undefined; +export default style; +` + +function generateScopedStyle(src) { + src = src + .replace(new RegExp(TOKEN_PLACEHOLDER, 'g'), '${token}') + .replace(new RegExp(TAG_NAME_PLACEHOLDER, 'g'), '${tagName}'); + + return [ + 'function style(tagName, token) {', + ' return `' + src + '`;', + '}', + 'export default style;' + ].join('\n'); +} + +/** + * Transforms a css string into a module exporting a function producing a stylesheet. + * The produced function accepts 2 parameters, tagName and token to enforce style scoping. + * + * export default function style({ token, style }) { + * return `div[${token}] { background-color: red; }`; + * } + * + * In the case where the stylesheet the produced module exports undefined. + * + * export default undefined; + */ +export default function transformStyle(src, options) { + const { mode } = options; + + const plugins = [ + postcssPluginRaptor({ + token: TOKEN_PLACEHOLDER, + tagName: TAG_NAME_PLACEHOLDER, + }), + ]; + + if (isProd(mode)) { + plugins.push( + cssnano({ + svgo: false, + preset: ['default'], + }), + ); + } + + return postcss(plugins) + .process(src, { from: undefined }) + .then(res => { + const code = + res.css && res.css.length + ? generateScopedStyle(res.css) + : EMPTY_CSS_OUTPUT; + + return { + code, + metadata: {}, + }; + }); +} diff --git a/packages/lwc-compiler/src/transformers/style.ts b/packages/lwc-compiler/src/transformers/style.ts deleted file mode 100755 index 33685a697b..0000000000 --- a/packages/lwc-compiler/src/transformers/style.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as postcss from "postcss"; -import * as cssnano from "cssnano"; -import postcssPluginRaptor from "postcss-plugin-lwc"; - -import { NormalizedCompilerOptions } from "../compiler/options"; -import { FileTransformerResult } from "./transformer"; - -const TOKEN_PLACEHOLDER = "__TOKEN__"; -const TAG_NAME_PLACEHOLDER = "__TAG_NAME__"; - -const EMPTY_CSS_OUTPUT = ` -const style = undefined; -export default style; -`; - -function generateScopedStyle(src: string) { - src = src - .replace(new RegExp(TOKEN_PLACEHOLDER, "g"), "${token}") - .replace(new RegExp(TAG_NAME_PLACEHOLDER, "g"), "${tagName}"); - - return [ - "function style(tagName, token) {", - " return `" + src + "`;", - "}", - "export default style;" - ].join("\n"); -} - -/** - * Transforms a css string into a module exporting a function producing a stylesheet. - * The produced function accepts 2 parameters, tagName and token to enforce style scoping. - * - * export default function style({ token, style }) { - * return `div[${token}] { background-color: red; }`; - * } - * - * In the case where the stylesheet the produced module exports undefined. - * - * export default undefined; - */ -export default function transformStyle( - src: string, - filename: string, - { outputConfig }: NormalizedCompilerOptions -): Promise { - const plugins = [ - postcssPluginRaptor({ - token: TOKEN_PLACEHOLDER, - tagName: TAG_NAME_PLACEHOLDER - }) - ]; - - if (outputConfig && outputConfig.minify) { - plugins.push( - cssnano({ - svgo: false, - preset: ["default"] - }) - ); - } - - return postcss(plugins) - .process(src, { from: undefined }) - .then(res => { - const code = - res.css && res.css.length - ? generateScopedStyle(res.css) - : EMPTY_CSS_OUTPUT; - - return { code, map: null }; - }); -} diff --git a/packages/lwc-compiler/src/transformers/template.js b/packages/lwc-compiler/src/transformers/template.js new file mode 100644 index 0000000000..285d55032e --- /dev/null +++ b/packages/lwc-compiler/src/transformers/template.js @@ -0,0 +1,42 @@ +import * as path from 'path'; +import compile from 'lwc-template-compiler'; + +export function getTemplateToken(filename, options) { + const templateId = path.basename(filename, path.extname(filename)); + return `${options.moduleNamespace}-${options.moduleName}_${templateId}`; +} + +/** + * Transforms a HTML template into module exporting a template function. + * The transform also add a style import for the default stylesheet associated with + * the template regardless if there is an actual style or not. + */ +export default function(src, options) { + const { filename, moduleName, moduleNamespace } = options; + const { code: template, metadata, warnings } = compile(src, {}); + + const fatalError = warnings.find(warning => warning.level === 'error'); + if (fatalError) { + throw new Error(fatalError.message); + } + + + const token = getTemplateToken(filename, options); + const cssName = path.basename(filename, path.extname(filename)) + '.css'; + + const code = [ + `import style from './${cssName}'`, + '', + template, + '', + `if (style) {`, + ` const tagName = '${moduleNamespace}-${moduleName}';`, + ` const token = '${token}';`, + ``, + ` tmpl.token = token;`, + ` tmpl.style = style(tagName, token);`, + `}`, + ].join('\n'); + + return { code, metadata }; +} diff --git a/packages/lwc-compiler/src/transformers/template.ts b/packages/lwc-compiler/src/transformers/template.ts deleted file mode 100755 index dfad30ff07..0000000000 --- a/packages/lwc-compiler/src/transformers/template.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as path from "path"; -import compile from "lwc-template-compiler"; -import { NormalizedCompilerOptions } from "../compiler/options"; -import { FileTransformer } from "./transformer"; - -// TODO: once we come up with a strategy to export all types from the module, -// below interface should be removed and resolved from template-compiler module. -export interface TemplateMetadata { - templateUsedIds: string[]; - definedSlots: string[]; - templateDependencies: string[]; -} - -export function getTemplateToken(name: string, namespace: string) { - const templateId = path.basename(name, path.extname(name)); - return `${namespace}-${name}_${templateId}`; -} - -/** - * Transforms a HTML template into module exporting a template function. - * The transform also add a style import for the default stylesheet associated with - * the template regardless if there is an actual style or not. - */ -const transform: FileTransformer = function( - src: string, - filename: string, - options: NormalizedCompilerOptions, -) { - const { name, namespace } = options; - const { code: template, warnings } = compile(src, {}); - - const fatalError = warnings.find(warning => warning.level === "error"); - if (fatalError) { - throw new Error(fatalError.message); - } - - const token = getTemplateToken(name, namespace); - const cssName = path.basename(name, path.extname(name)) + ".css"; - - const code = [ - `import style from './${cssName}'`, - "", - template, - "", - `if (style) {`, - ` const tagName = '${namespace}-${name}';`, - ` const token = '${token}';`, - ``, - ` tmpl.token = token;`, - ` tmpl.style = style(tagName, token);`, - `}` - ].join("\n"); - - return { code, map: null }; -}; - -export default transform; diff --git a/packages/lwc-compiler/src/transformers/transformer.ts b/packages/lwc-compiler/src/transformers/transformer.ts deleted file mode 100755 index c1ce338a9d..0000000000 --- a/packages/lwc-compiler/src/transformers/transformer.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as path from "path"; -import * as lwcClassTransformPlugin from "babel-plugin-transform-lwc-class"; - -import { - NormalizedCompilerOptions, - CompilerOptions, - normalizeOptions -} from "../compiler/options"; - -import styleTransform from "./style"; -import templateTransformer, { TemplateMetadata } from "./template"; -import javascriptTransformer from "./javascript"; -import compatPluginFactory from "../rollup-plugins/compat"; - -import { isString, isUndefined } from "../utils"; -import { MetadataCollector } from "../bundler/meta-collector"; - -// TODO: Improve on metadata type by providing consistent interface. Currently -// javascript transformer output differs from css and html in that later return a promise -export interface FileTransformerResult { - code: string; - metadata?: - | TemplateMetadata - | lwcClassTransformPlugin.Metadata; - map: null; -} - -export type FileTransformer = ( - source: string, - filename: string, - options: NormalizedCompilerOptions, - metadataCollector?: MetadataCollector -) => FileTransformerResult | Promise; - -export function transform(src: string, id: string, options: CompilerOptions) { - if (!isString(src)) { - throw new Error(`Expect a string for source. Received ${src}`); - } - - if (!isString(id)) { - throw new Error(`Expect a string for id. Received ${id}`); - } - - return transformFile(src, id, normalizeOptions(options)); -} - -export function getTransformer(fileName: string): FileTransformer { - switch (path.extname(fileName)) { - case ".html": - return templateTransformer; - - case ".css": - return styleTransform; - - case ".js": - return javascriptTransformer; - - default: - throw new TypeError(`No available transformer for "${fileName}"`); - } -} - -export async function transformFile( - src: string, - id: string, - options: NormalizedCompilerOptions, - metadataCollector?: MetadataCollector -): Promise { - const transformer = getTransformer(id); - const result = await transformer(src, id, options, metadataCollector); - - if (options.outputConfig.compat) { - const compatPlugin = compatPluginFactory( - options.outputConfig.resolveProxyCompat - ); - const compatResult = compatPlugin.transform(result.code); - if (isUndefined(compatResult) || isUndefined(compatResult.code)) { - throw new Error( - "babel transform failed to produce code in compat mode" - ); - } - return { code: compatResult.code, map: null }; - } - - return result; -} diff --git a/packages/lwc-compiler/src/typings/@babel-core.d.ts b/packages/lwc-compiler/src/typings/@babel-core.d.ts deleted file mode 100644 index 0d72592cc5..0000000000 --- a/packages/lwc-compiler/src/typings/@babel-core.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@babel/core' { - const core: any; - export = core; -} diff --git a/packages/lwc-compiler/src/typings/@babel-plugin-proposal-class-properties.d.ts b/packages/lwc-compiler/src/typings/@babel-plugin-proposal-class-properties.d.ts deleted file mode 100755 index 54efcfce5e..0000000000 --- a/packages/lwc-compiler/src/typings/@babel-plugin-proposal-class-properties.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@babel/plugin-proposal-class-properties' { - const props: any; - export = props; -} diff --git a/packages/lwc-compiler/src/typings/@babel-plugin-proposal-object-rest-spread.d.ts b/packages/lwc-compiler/src/typings/@babel-plugin-proposal-object-rest-spread.d.ts deleted file mode 100755 index e719245e67..0000000000 --- a/packages/lwc-compiler/src/typings/@babel-plugin-proposal-object-rest-spread.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@babel/plugin-proposal-object-rest-spread' { - const spread: any; - export = spread; -} diff --git a/packages/lwc-compiler/src/typings/babel-merge-copy.d.ts b/packages/lwc-compiler/src/typings/babel-merge-copy.d.ts deleted file mode 100755 index f05d1677cb..0000000000 --- a/packages/lwc-compiler/src/typings/babel-merge-copy.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -declare module 'babel-plugin-check-es2015-constants' { - const transform: any; - export = transform; -} - -declare module 'babel-plugin-transform-es2015-arrow-functions' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-block-scoped-functions' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-block-scoping' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-classes' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-computed-properties' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-destructuring' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-duplicate-keys' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-for-of' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-literals' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-object-super' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-parameters' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-shorthand-properties' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-spread' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-sticky-regex' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-template-literals' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-typeof-symbol' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-es2015-unicode-regex' { - const transform: any; - export = transform; -} - -declare module 'babel-plugin-transform-async-to-generator' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-regenerator' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-runtime' { - const transform: any; - export = transform; -} - -declare module 'babel-plugin-syntax-trailing-function-commas' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-exponentiation-operator' { - const transform: any; - export = transform; -} - -declare module 'babel-plugin-transform-object-rest-spread' { - const transform: any; - export = transform; -} -declare module 'babel-plugin-transform-class-properties' { - const transform: any; - export = transform; -} - -declare module 'babel-preset-minify' { - const transform: any; - export = transform; -} - -declare module 'babel-plugin-transform-proxy-compat' { - const transform: any; - export = transform; -} diff --git a/packages/lwc-compiler/src/typings/babel-plugins.d.ts b/packages/lwc-compiler/src/typings/babel-plugins.d.ts deleted file mode 100755 index 74725e223c..0000000000 --- a/packages/lwc-compiler/src/typings/babel-plugins.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'babel-plugins' { - const plugins: any; - export = plugins; -} diff --git a/packages/lwc-compiler/src/typings/babel-preset-compat.d.ts b/packages/lwc-compiler/src/typings/babel-preset-compat.d.ts deleted file mode 100755 index 59b7a017c3..0000000000 --- a/packages/lwc-compiler/src/typings/babel-preset-compat.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'babel-preset-compat' { - const compat: any; - export = compat; -} diff --git a/packages/lwc-compiler/src/typings/cssnano.d.ts b/packages/lwc-compiler/src/typings/cssnano.d.ts deleted file mode 100755 index 4353eba786..0000000000 --- a/packages/lwc-compiler/src/typings/cssnano.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'cssnano' { - const nano: any; - export = nano; -} diff --git a/packages/lwc-compiler/src/typings/rollup-plugin-replace.d.ts b/packages/lwc-compiler/src/typings/rollup-plugin-replace.d.ts deleted file mode 100755 index 3a8a237cb0..0000000000 --- a/packages/lwc-compiler/src/typings/rollup-plugin-replace.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'rollup-plugin-replace' { - const replace: any; - export = replace; -} diff --git a/packages/lwc-compiler/src/typings/rollup.d.ts b/packages/lwc-compiler/src/typings/rollup.d.ts deleted file mode 100755 index b794ab5132..0000000000 --- a/packages/lwc-compiler/src/typings/rollup.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'rollup' { - export const rollup: any; -} diff --git a/packages/lwc-compiler/src/utils.js b/packages/lwc-compiler/src/utils.js new file mode 100644 index 0000000000..10eaf2d812 --- /dev/null +++ b/packages/lwc-compiler/src/utils.js @@ -0,0 +1,27 @@ +/** + * Takes 2 arrays and returns an object formed by associating properties with values + * @param {string[]} props + * @param {any[]} values + */ +export function zipObject(props, values) { + return props.reduce((obj, prop, index) => { + obj[prop] = values[index]; + return obj; + }, {}); +} + +/** + * Returns true if the value is undefined + * @param {any} o + */ +export function isUndefined(o) { + return o === undefined; +} + +/** + * Returns true if the value is a string + * @param {any} o + */ +export function isString(o) { + return typeof o === 'string'; +} diff --git a/packages/lwc-compiler/src/utils.ts b/packages/lwc-compiler/src/utils.ts deleted file mode 100755 index 2fe2aec76b..0000000000 --- a/packages/lwc-compiler/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Returns true if the value is undefined - * @param {any} o - */ -export function isUndefined(o: any): o is undefined { - return o === undefined; -} - -/** - * Returns true if the value is a string - * @param {any} o - */ -export function isString(o: any): o is string { - return typeof o === 'string'; -} - -/** - * Returns true if the value is a boolean - * @param {any} o - */ -export function isBoolean(o: any): o is boolean { - return typeof o === 'boolean'; -} diff --git a/packages/lwc-compiler/tsconfig.json b/packages/lwc-compiler/tsconfig.json old mode 100755 new mode 100644 index 7d10d48545..86b07926b6 --- a/packages/lwc-compiler/tsconfig.json +++ b/packages/lwc-compiler/tsconfig.json @@ -1,29 +1,19 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "allowJs": false, - "checkJs": false, "target": "es2017", "module": "commonjs", - "noImplicitAny": true, - "sourceMap": true, - "declaration": true, - "rootDir": "src", "outDir": "dist/commonjs", - "declarationDir": "dist/types", - "moduleResolution": "node", - "strict": true, - "strictFunctionTypes": true, - "strictPropertyInitialization": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, + "checkJs": false, + "allowJs": true, + "declaration": false }, "include": [ - "src/**/*" + "src/**/*", ], "exclude": [ "**/node_modules", "**/dist", - "**/__tests__" + "**/__tests__", ] } diff --git a/packages/lwc-compiler/webpack.config.js b/packages/lwc-compiler/webpack.config.js old mode 100755 new mode 100644 diff --git a/packages/lwc-template-compiler/package.json b/packages/lwc-template-compiler/package.json index faae4d4725..20ec5479cb 100644 --- a/packages/lwc-template-compiler/package.json +++ b/packages/lwc-template-compiler/package.json @@ -3,7 +3,7 @@ "version": "0.20.0", "description": "Template compiler package", "main": "dist/commonjs/index.js", - "typings": "dist/types/", + "typings": "dist/types/index.d.ts", "license": "MIT", "scripts": { "clean": "rm -rf dist", diff --git a/packages/rollup-plugin-lwc-compiler/package.json b/packages/rollup-plugin-lwc-compiler/package.json index ee2b031468..35f1d1ecb2 100644 --- a/packages/rollup-plugin-lwc-compiler/package.json +++ b/packages/rollup-plugin-lwc-compiler/package.json @@ -18,7 +18,7 @@ "rollup-pluginutils": "^2.0.1" }, "peerDependencies": { - "lwc-compiler": "0.19.x", - "lwc-engine": "0.19.x" + "lwc-compiler": "0.18.x", + "lwc-engine": "0.18.x" } } diff --git a/packages/rollup-plugin-lwc-compiler/src/index.js b/packages/rollup-plugin-lwc-compiler/src/index.js index cec5e522dd..c1e1e52b31 100644 --- a/packages/rollup-plugin-lwc-compiler/src/index.js +++ b/packages/rollup-plugin-lwc-compiler/src/index.js @@ -1,26 +1,17 @@ -const fs = require("fs"); -const path = require("path"); -const babel = require("@babel/core"); -const minify = require("babel-preset-minify"); -const compiler = require("lwc-compiler"); -const pluginUtils = require("rollup-pluginutils"); -const replacePlugin = require("rollup-plugin-replace"); -const lwcResolver = require("lwc-module-resolver"); -const rollupCompatPlugin = require("rollup-plugin-compat").default; +const fs = require('fs'); +const path = require('path'); +const compiler = require('lwc-compiler'); +const pluginUtils = require('rollup-pluginutils'); +const lwcResolver = require('lwc-module-resolver'); +const rollupCompatPlugin = require('rollup-plugin-compat').default; - -const { DEFAULT_NS, DEFAULT_OPTIONS, DEFAULT_MODE } = require("./constants"); +const { DEFAULT_NS, DEFAULT_OPTIONS, DEFAULT_MODE } = require('./constants'); function getModuleQualifiedName(file, { mapNamespaceFromPath }) { - const registry = { - entry: file, - moduleSpecifier: null, - moduleName: null, - moduleNamespace: DEFAULT_NS - }; + const registry = { entry: file, moduleSpecifier: null, moduleName: null, moduleNamespace: DEFAULT_NS }; const fileName = path.basename(file, path.extname(file)); const rootParts = path.dirname(file).split(path.sep); - const nameParts = fileName.split("-"); + const nameParts = fileName.split('-'); const validModuleName = nameParts.length > 1; if (mapNamespaceFromPath) { @@ -28,7 +19,7 @@ function getModuleQualifiedName(file, { mapNamespaceFromPath }) { registry.moduleNamespace = rootParts.pop(); } else if (validModuleName) { registry.moduleNamespace = nameParts.shift(); - registry.moduleName = nameParts.join("-"); + registry.moduleName = nameParts.join('-'); } else { registry.moduleName = fileName; } @@ -37,7 +28,7 @@ function getModuleQualifiedName(file, { mapNamespaceFromPath }) { } function normalizeResult(result) { - return { code: result.code || result, map: result.map || { mappings: "" } }; + return { code: result.code || result, map: result.map || { mappings: '' } }; } /* @@ -53,14 +44,9 @@ function normalizeResult(result) { module.exports = function rollupLwcCompiler(pluginOptions = {}) { const { include, exclude, mapNamespaceFromPath } = pluginOptions; const filter = pluginUtils.createFilter(include, exclude); - const mergedPluginOptions = Object.assign( - {}, - DEFAULT_OPTIONS, - pluginOptions, - { - mapNamespaceFromPath: Boolean(mapNamespaceFromPath) - } - ); + const mergedPluginOptions = Object.assign({}, DEFAULT_OPTIONS, pluginOptions, { + mapNamespaceFromPath: Boolean(mapNamespaceFromPath), + }); const { mode, compat } = mergedPluginOptions; // We will compose compat plugin on top of this one @@ -70,17 +56,13 @@ module.exports = function rollupLwcCompiler(pluginOptions = {}) { const modulePaths = {}; return { - name: "rollup-plugin-lwc-compiler", + name: 'rollup-plugin-lwc-compiler', options(rollupOptions) { const entry = rollupOptions.input || rollupOptions.entry; const entryDir = mergedPluginOptions.rootDir || path.dirname(entry); - const externalPaths = mergedPluginOptions.resolveFromPackages - ? lwcResolver.resolveLwcNpmModules(mergedPluginOptions) - : {}; - const sourcePaths = mergedPluginOptions.resolveFromSource - ? lwcResolver.resolveModulesInDir(entryDir, mergedPluginOptions) - : {}; + const externalPaths = mergedPluginOptions.resolveFromPackages ? lwcResolver.resolveLwcNpmModules(mergedPluginOptions) : {}; + const sourcePaths = mergedPluginOptions.resolveFromSource ? lwcResolver.resolveModulesInDir(entryDir, mergedPluginOptions): {}; Object.assign(modulePaths, externalPaths, sourcePaths); }, @@ -91,11 +73,8 @@ module.exports = function rollupLwcCompiler(pluginOptions = {}) { } // Normalize relative import to absolute import - if (importee.startsWith(".") && importer) { - const normalizedPath = path.resolve( - path.dirname(importer), - importee - ); + if (importee.startsWith('.') && importer) { + const normalizedPath = path.resolve(path.dirname(importer), importee); return pluginUtils.addExtension(normalizedPath); } @@ -105,10 +84,10 @@ module.exports = function rollupLwcCompiler(pluginOptions = {}) { load(id) { const exists = fs.existsSync(id); - const isCSS = path.extname(id) === ".css"; + const isCSS = path.extname(id) === '.css'; if (!exists && isCSS) { - return ""; + return ''; } // Check if compat knows how to load this file @@ -121,63 +100,36 @@ module.exports = function rollupLwcCompiler(pluginOptions = {}) { } // If we don't find the moduleId, just resolve the module name/namespace - const moduleEntry = Object.values(modulePaths).find( - r => id === r.entry - ); - const moduleRegistry = - moduleEntry || getModuleQualifiedName(id, mergedPluginOptions); + const moduleEntry = Object.values(modulePaths).find(r => id === r.entry); + const moduleRegistry = moduleEntry || getModuleQualifiedName(id, mergedPluginOptions); let result = code; if (!rollupCompatInstance.knownCompatModule(id)) { result = await compiler.transform(code, id, { mode: DEFAULT_MODE, // Use always default mode since any other (prod or compat) will be resolved later - name: moduleRegistry.moduleName, - namespace: moduleRegistry.moduleNamespace, + moduleName: moduleRegistry.moduleName, + moduleNamespace: moduleRegistry.moduleNamespace, moduleSpecifier: moduleRegistry.moduleSpecifier }); } result = normalizeResult(result); - if (mode === "compat" || mode === "prod_compat") { - result = normalizeResult( - rollupCompatInstance.transform(result.code, id) - ); + if (mode === 'compat' || mode === 'prod_compat') { + result = normalizeResult(rollupCompatInstance.transform(result.code, id)); } return { code: result.code, map: result.map }; + }, transformBundle(code) { - if (mode === "compat" || mode === "prod_compat") { + if (mode === 'compat' || mode === 'prod_compat') { code = rollupCompatInstance.transformBundle(code); } - let result = undefined; - if (mode === "prod" || mode === "prod_compat") { - const rollupReplace = replacePlugin({ - "process.env.NODE_ENV": JSON.stringify("production") - }); - const resultReplace = rollupReplace.transform( - code, - "$__tmpBundleSrc" - ); - - const transformConfig = { - babelrc: false, - sourceMaps: true, - parserOpts: { plugins: ["*"] }, - presets: [[minify, { guards: false, evaluate: false }]], - }; - - const output = babel.transform( - resultReplace ? resultReplace.code : code, - transformConfig - ); - - result = output.code; - } - return result || code; + + return compiler.transformBundle(code, { mode }); } }; }; diff --git a/yarn.lock b/yarn.lock index 26aeffb28c..3a428bef82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -502,30 +502,20 @@ dependencies: "@types/estree" "*" -"@types/babel-core@^6.25.3": - version "6.25.3" - resolved "https://registry.npmjs.org/@types/babel-core/-/babel-core-6.25.3.tgz#c7fda09007375ae89942307917ed21ed309a1c2a" - dependencies: - "@types/babel-generator" "*" - "@types/babel-template" "*" - "@types/babel-traverse" "*" - "@types/babel-types" "*" - "@types/babylon" "*" - -"@types/babel-generator@*", "@types/babel-generator@^6.25.1": +"@types/babel-generator@^6.25.1": version "6.25.1" resolved "http://npm.lwcjs.org/@types/babel-generator/-/babel-generator-6.25.1/f86ab3cf132b04597fe6c431d3083aaf1b76b530.tgz#f86ab3cf132b04597fe6c431d3083aaf1b76b530" dependencies: "@types/babel-types" "*" -"@types/babel-template@*", "@types/babel-template@^6.25.0": +"@types/babel-template@^6.25.0": version "6.25.0" resolved "http://npm.lwcjs.org/@types/babel-template/-/babel-template-6.25.0/2505d7b55b88f821d98048b4fdf07b3b22563d30.tgz#2505d7b55b88f821d98048b4fdf07b3b22563d30" dependencies: "@types/babel-types" "*" "@types/babylon" "*" -"@types/babel-traverse@*", "@types/babel-traverse@^6.25.3": +"@types/babel-traverse@^6.25.3": version "6.25.3" resolved "http://npm.lwcjs.org/@types/babel-traverse/-/babel-traverse-6.25.3/34dd69d1e469aee75e20ba00fbc52c7bc717b1c8.tgz#34dd69d1e469aee75e20ba00fbc52c7bc717b1c8" dependencies: @@ -541,21 +531,10 @@ dependencies: "@types/babel-types" "*" -"@types/chokidar@^1.7.5": - version "1.7.5" - resolved "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz#1fa78c8803e035bed6d98e6949e514b133b0c9b6" - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/estree@*": version "0.0.38" resolved "http://npm.lwcjs.org/@types/estree/-/estree-0.0.38/c1be40aa933723c608820a99a373a16d215a1ca2.tgz#c1be40aa933723c608820a99a373a16d215a1ca2" -"@types/events@*": - version "1.2.0" - resolved "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" - "@types/he@^0.5.29": version "0.5.29" resolved "http://npm.lwcjs.org/@types/he/-/he-0.5.29/67f9392287b05558013d1cabc96801a5185adbea.tgz#67f9392287b05558013d1cabc96801a5185adbea" @@ -7586,10 +7565,6 @@ rollup@~0.56.2: version "0.56.2" resolved "http://npm.lwcjs.org/rollup/-/rollup-0.56.2/1788cf56d4350b6d8ecf76b5d654c59c7bf9c24a.tgz#1788cf56d4350b6d8ecf76b5d654c59c7bf9c24a" -rollup@~0.56.5: - version "0.56.5" - resolved "http://npm.lwcjs.org/rollup/-/rollup-0.56.5/40fe3cf0cd1659d469baad11f4d5b6336c14ce84.tgz#40fe3cf0cd1659d469baad11f4d5b6336c14ce84" - run-async@^2.2.0: version "2.3.0" resolved "http://npm.lwcjs.org/run-async/-/run-async-2.3.0/0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"