diff --git a/.eslintrc.js b/.eslintrc.js index ef6c85f147..ed08d4468a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,10 @@ module.exports = { root: true, parserOptions: { parser: 'babel-eslint', - sourceType: 'module' + sourceType: 'module', + ecmaFeatures: { + legacyDecorators: true + } }, extends: [ '@nuxtjs' diff --git a/package.json b/package.json index aff7d145fd..4e2026923b 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "rollup-plugin-replace": "^2.1.0", "sort-package-json": "^1.17.0", "typescript": "^3.2.2", - "vue-jest": "^3.0.2" + "vue-jest": "^3.0.2", + "vue-property-decorator": "^7.2.0" }, "repository": { "type": "git", diff --git a/packages/babel-preset-app/package.json b/packages/babel-preset-app/package.json index dc83114255..a10148332e 100644 --- a/packages/babel-preset-app/package.json +++ b/packages/babel-preset-app/package.json @@ -17,6 +17,7 @@ "@babel/plugin-syntax-jsx": "^7.2.0", "@babel/plugin-transform-runtime": "^7.2.0", "@babel/preset-env": "^7.2.0", + "@babel/preset-typescript": "^7.1.0", "@babel/runtime": "^7.2.0", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-plugin-transform-vue-jsx": "^4.0.1" diff --git a/packages/babel-preset-app/src/index.js b/packages/babel-preset-app/src/index.js index 79add0f622..84888046b7 100644 --- a/packages/babel-preset-app/src/index.js +++ b/packages/babel-preset-app/src/index.js @@ -92,6 +92,11 @@ module.exports = (context, options = {}) => { } ]) + // TypeScript preset + if (options.typescript) { + presets.push(require('@babel/preset-typescript')) + } + plugins.push( require('@babel/plugin-syntax-dynamic-import'), [require('@babel/plugin-proposal-decorators'), { diff --git a/packages/builder/src/builder.js b/packages/builder/src/builder.js index 81742d127c..f654dede2e 100644 --- a/packages/builder/src/builder.js +++ b/packages/builder/src/builder.js @@ -44,6 +44,11 @@ export default class Builder { restart: null } + this.supportedExtensions = ['vue', 'js'] + if (this.options.build.typescript) { + this.supportedExtensions.push('ts', 'tsx') + } + // Helper to resolve build paths this.relativeToBuild = (...args) => relativeTo(this.options.buildDir, ...args) @@ -265,14 +270,14 @@ export default class Builder { // -- Layouts -- if (fsExtra.existsSync(path.resolve(this.options.srcDir, this.options.dir.layouts))) { - const layoutsFiles = await glob(`${this.options.dir.layouts}/**/*.{vue,js}`, { + const layoutsFiles = await glob(`${this.options.dir.layouts}/**/*.{${this.supportedExtensions.join(',')}}`, { cwd: this.options.srcDir, ignore: this.options.ignore }) layoutsFiles.forEach((file) => { const name = file .replace(new RegExp(`^${this.options.dir.layouts}/`), '') - .replace(/\.(vue|js)$/, '') + .replace(new RegExp(`\\.(${this.supportedExtensions.join('|')})$`), '') if (name === 'error') { if (!templateVars.components.ErrorPage) { templateVars.components.ErrorPage = this.relativeToBuild( @@ -308,11 +313,11 @@ export default class Builder { } else if (this._nuxtPages) { // Use nuxt.js createRoutes bases on pages/ const files = {} - ; (await glob(`${this.options.dir.pages}/**/*.{vue,js}`, { + ; (await glob(`${this.options.dir.pages}/**/*.{${this.supportedExtensions.join(',')}}`, { cwd: this.options.srcDir, ignore: this.options.ignore })).forEach((f) => { - const key = f.replace(/\.(js|vue)$/, '') + const key = f.replace(new RegExp(`\\.(${this.supportedExtensions.join('|')})$`), '') if (/\.vue$/.test(f) || !files[key]) { files[key] = f.replace(/(['"])/g, '\\$1') } @@ -320,7 +325,8 @@ export default class Builder { templateVars.router.routes = createRoutes( Object.values(files), this.options.srcDir, - this.options.dir.pages + this.options.dir.pages, + this.supportedExtensions ) } else { // If user defined a custom method to create routes templateVars.router.routes = this.options.build.createRoutes( @@ -502,20 +508,19 @@ export default class Builder { watchClient() { const src = this.options.srcDir + const rGlob = dir => ['*', '**/*'].map(glob => r(src, `${dir}/${glob}.{${this.supportedExtensions.join(',')}}`)) let patterns = [ r(src, this.options.dir.layouts), r(src, this.options.dir.store), r(src, this.options.dir.middleware), - r(src, `${this.options.dir.layouts}/*.{vue,js}`), - r(src, `${this.options.dir.layouts}/**/*.{vue,js}`) + ...rGlob(this.options.dir.layouts) ] if (this._nuxtPages) { patterns.push( r(src, this.options.dir.pages), - r(src, `${this.options.dir.pages}/*.{vue,js}`), - r(src, `${this.options.dir.pages}/**/*.{vue,js}`) + ...rGlob(this.options.dir.pages) ) } diff --git a/packages/common/src/utils.js b/packages/common/src/utils.js index 5f890e84d5..327a1b0285 100644 --- a/packages/common/src/utils.js +++ b/packages/common/src/utils.js @@ -315,12 +315,12 @@ const sortRoutes = function sortRoutes(routes) { return routes } -export const createRoutes = function createRoutes(files, srcDir, pagesDir) { +export const createRoutes = function createRoutes(files, srcDir, pagesDir, supportedExtensions = ['vue', 'js']) { const routes = [] files.forEach((file) => { const keys = file - .replace(RegExp(`^${pagesDir}`), '') - .replace(/\.(vue|js)$/, '') + .replace(new RegExp(`^${pagesDir}`), '') + .replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '') .replace(/\/{2,}/g, '/') .split('/') .slice(1) @@ -334,7 +334,7 @@ export const createRoutes = function createRoutes(files, srcDir, pagesDir) { ? route.name + '-' + sanitizedKey : sanitizedKey route.name += key === '_' ? 'all' : '' - route.chunkName = file.replace(/\.(vue|js)$/, '') + route.chunkName = file.replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '') const child = parent.find(parentRoute => parentRoute.name === route.name) if (child) { diff --git a/packages/config/src/config/build.js b/packages/config/src/config/build.js index 12381d7079..bf1b57891a 100644 --- a/packages/config/src/config/build.js +++ b/packages/config/src/config/build.js @@ -5,6 +5,7 @@ export default () => ({ analyze: false, profile: process.argv.includes('--profile'), extractCSS: false, + typescript: false, crossorigin: undefined, cssSourceMap: undefined, ssr: undefined, diff --git a/packages/config/src/options.js b/packages/config/src/options.js index 178fdc3227..05b8ca1274 100644 --- a/packages/config/src/options.js +++ b/packages/config/src/options.js @@ -119,6 +119,9 @@ export function getNuxtConfig(_options) { ) const mandatoryExtensions = ['js', 'mjs'] + if (options.build.typescript) { + mandatoryExtensions.push('ts') + } options.extensions = mandatoryExtensions .filter(ext => !options.extensions.includes(ext)) diff --git a/packages/webpack/src/config/base.js b/packages/webpack/src/config/base.js index 3ef9db51cb..3a4d6f3b46 100644 --- a/packages/webpack/src/config/base.js +++ b/packages/webpack/src/config/base.js @@ -75,7 +75,8 @@ export default class WebpackBaseConfig { [ require.resolve('@nuxt/babel-preset-app'), { - buildTarget: this.isServer ? 'server' : 'client' + buildTarget: this.isServer ? 'server' : 'client', + typescript: this.options.build.typescript } ] ] @@ -215,7 +216,7 @@ export default class WebpackBaseConfig { ] }, { - test: /\.jsx?$/, + test: this.options.build.typescript ? /\.(j|t)sx?$/ : /\.jsx?$/, exclude: (file) => { // not exclude files outside node_modules if (!/node_modules/.test(file)) { @@ -376,6 +377,10 @@ export default class WebpackBaseConfig { config() { // Prioritize nested node_modules in webpack search path (#2558) const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir) + let extensionsToResolve = ['.wasm', '.mjs', '.js', '.json', '.vue', '.jsx'] + if (this.options.build.typescript) { + extensionsToResolve = extensionsToResolve.concat(['.ts', '.tsx']) + } const config = { name: this.name, @@ -388,7 +393,7 @@ export default class WebpackBaseConfig { hints: this.options.dev ? false : 'warning' }, resolve: { - extensions: ['.wasm', '.mjs', '.js', '.json', '.vue', '.jsx'], + extensions: extensionsToResolve, alias: this.alias(), modules: webpackModulesDir }, diff --git a/test/fixtures/typescript/@types/process.d.ts b/test/fixtures/typescript/@types/process.d.ts new file mode 100644 index 0000000000..648fe143b2 --- /dev/null +++ b/test/fixtures/typescript/@types/process.d.ts @@ -0,0 +1,8 @@ +declare namespace NodeJS { + interface Process { + browser: boolean + client: boolean + server: boolean + static: boolean + } +} diff --git a/test/fixtures/typescript/@types/shims-tsx.d.ts b/test/fixtures/typescript/@types/shims-tsx.d.ts new file mode 100644 index 0000000000..22d8ef98a9 --- /dev/null +++ b/test/fixtures/typescript/@types/shims-tsx.d.ts @@ -0,0 +1,11 @@ +import Vue, { VNode } from 'vue' + +declare global { + namespace JSX { + interface Element extends VNode {} + interface ElementClass extends Vue {} + interface IntrinsicElements { + [elemName: string]: any + } + } +} diff --git a/test/fixtures/typescript/layouts/default.vue b/test/fixtures/typescript/layouts/default.vue new file mode 100644 index 0000000000..0526cdec9f --- /dev/null +++ b/test/fixtures/typescript/layouts/default.vue @@ -0,0 +1,13 @@ + + + diff --git a/test/fixtures/typescript/middleware/middleware.ts b/test/fixtures/typescript/middleware/middleware.ts new file mode 100644 index 0000000000..ead516c976 --- /dev/null +++ b/test/fixtures/typescript/middleware/middleware.ts @@ -0,0 +1 @@ +export default () => {} diff --git a/test/fixtures/typescript/nuxt.config.js b/test/fixtures/typescript/nuxt.config.js new file mode 100644 index 0000000000..ed451654a6 --- /dev/null +++ b/test/fixtures/typescript/nuxt.config.js @@ -0,0 +1,8 @@ +export default { + plugins: [ + '~/plugins/plugin.ts' + ], + build: { + typescript: true + } +} diff --git a/test/fixtures/typescript/pages/about.ts b/test/fixtures/typescript/pages/about.ts new file mode 100644 index 0000000000..1218ba9b58 --- /dev/null +++ b/test/fixtures/typescript/pages/about.ts @@ -0,0 +1,8 @@ +import Vue from 'vue' + +export default Vue.extend({ + name: 'About', + render (h) { + return h('div', 'About Page') + } +}) diff --git a/test/fixtures/typescript/pages/contact.tsx b/test/fixtures/typescript/pages/contact.tsx new file mode 100644 index 0000000000..b6d3cab018 --- /dev/null +++ b/test/fixtures/typescript/pages/contact.tsx @@ -0,0 +1,13 @@ +import Vue from 'vue' + +export default Vue.extend({ + name: 'Contact', + data () { + return { + message: 'Contact Page' + } + }, + render () { + return
{this.message}
+ } +}) diff --git a/test/fixtures/typescript/pages/index.vue b/test/fixtures/typescript/pages/index.vue new file mode 100644 index 0000000000..f4cd80a770 --- /dev/null +++ b/test/fixtures/typescript/pages/index.vue @@ -0,0 +1,14 @@ + + + diff --git a/test/fixtures/typescript/plugins/plugin.ts b/test/fixtures/typescript/plugins/plugin.ts new file mode 100644 index 0000000000..ead516c976 --- /dev/null +++ b/test/fixtures/typescript/plugins/plugin.ts @@ -0,0 +1 @@ +export default () => {} diff --git a/test/fixtures/typescript/tsconfig.json b/test/fixtures/typescript/tsconfig.json new file mode 100644 index 0000000000..31bd6e2208 --- /dev/null +++ b/test/fixtures/typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "lib": ["esnext", "esnext.asynciterable", "dom"], + "esModuleInterop": true, + "experimentalDecorators": true, + "allowJs": true, + "jsx": "preserve", + "sourceMap": true, + "strict": true, + "noImplicitAny": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + } +} diff --git a/test/fixtures/typescript/typescript.test.js b/test/fixtures/typescript/typescript.test.js new file mode 100644 index 0000000000..0d352b49dd --- /dev/null +++ b/test/fixtures/typescript/typescript.test.js @@ -0,0 +1,3 @@ +import { buildFixture } from '../../utils/build' + +buildFixture('typescript') diff --git a/test/unit/typescript.test.js b/test/unit/typescript.test.js new file mode 100644 index 0000000000..f86e20ca59 --- /dev/null +++ b/test/unit/typescript.test.js @@ -0,0 +1,32 @@ +import { loadFixture, getPort, Nuxt } from '../utils' + +let nuxt = null + +describe('typescript', () => { + beforeAll(async () => { + const options = await loadFixture('typescript') + nuxt = new Nuxt(options) + const port = await getPort() + await nuxt.server.listen(port, '0.0.0.0') + }) + + test('/', async () => { + const { html } = await nuxt.server.renderRoute('/') + expect(html).toContain('
Index Page
') + }) + + test('/about', async () => { + const { html } = await nuxt.server.renderRoute('/about') + expect(html).toContain('
About Page
') + }) + + test('/contact', async () => { + const { html } = await nuxt.server.renderRoute('/contact') + expect(html).toContain('
Contact Page
') + }) + + // Close server and ask nuxt to stop listening to file changes + afterAll(async () => { + await nuxt.close() + }) +}) diff --git a/yarn.lock b/yarn.lock index 5eceb9d26d..91dd30f0c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -354,6 +354,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.2.0": + version "7.2.0" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.2.0.tgz#55d240536bd314dcbbec70fd949c5cabaed1de29" + integrity sha512-WhKr6yu6yGpGcNMVgIBuI9MkredpVc7Y3YR4UzEZmDztHoL6wV56YBHLhWnjO1EvId1B32HrD3DRFc+zSoKI1g== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -570,6 +577,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.1.0": + version "7.2.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.2.0.tgz#bce7c06300434de6a860ae8acf6a442ef74a99d1" + integrity sha512-EnI7i2/gJ7ZNr2MuyvN2Hu+BHJENlxWte5XygPvfj/MbvtOkWor9zcnHpMMQL2YYaaCcqtIvJUyJ7QVfoGs7ew== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + "@babel/plugin-transform-unicode-regex@^7.2.0": version "7.2.0" resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" @@ -634,6 +649,14 @@ js-levenshtein "^1.1.3" semver "^5.3.0" +"@babel/preset-typescript@^7.1.0": + version "7.1.0" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.1.0.tgz#49ad6e2084ff0bfb5f1f7fb3b5e76c434d442c7f" + integrity sha512-LYveByuF9AOM8WrsNne5+N79k1YxjNB6gmpCQsnuSBAcV8QUeB+ZUxQzL7Rz7HksPbahymKkq2qBR+o36ggFZA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.1.0" + "@babel/register@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@babel/register/-/register-7.0.0.tgz#fa634bae1bfa429f60615b754fc1f1d745edd827" @@ -11037,6 +11060,11 @@ void-elements@^2.0.1: resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= +vue-class-component@^6.2.0: + version "6.3.2" + resolved "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.3.2.tgz#e6037e84d1df2af3bde4f455e50ca1b9eec02be6" + integrity sha512-cH208IoM+jgZyEf/g7mnFyofwPDJTM/QvBNhYMjqGB8fCsRyTf68rH2ISw/G20tJv+5mIThQ3upKwoL4jLTr1A== + vue-eslint-parser@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-4.0.2.tgz#7d10ec5b67d9b2ef240cac0f0e8b2f03773d810e" @@ -11095,6 +11123,13 @@ vue-no-ssr@^1.1.0: resolved "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.0.tgz#b323807112f676324d9d7cfde85d7831ced11dd9" integrity sha512-prJ9czuPrVu0GhUZKTS/epFfM15QjLuG6wt61g0nyixPXk0g6eY7wNF4RKIqJsxomOiuSYTv3Zo8A43Vi93xfw== +vue-property-decorator@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-7.2.0.tgz#8e6b0f4dcc630c357135f76366ba7bd91f1db015" + integrity sha512-sCI6NVM3tEDg+mpZrQlgkddtxd9LbFWetue8D+nqO3agfSLz0KoC/UIi2P1l5E0TDhcUeIXS9rasuP2HWg+L4w== + dependencies: + vue-class-component "^6.2.0" + vue-router@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"