diff --git a/.baserc.json b/.baserc.json index 6a01347..665ccae 100644 --- a/.baserc.json +++ b/.baserc.json @@ -7,5 +7,5 @@ { "repository": "nuxt-content-git", "description": "Additional module for @nuxt/content that replaces or adds createdAt and updatedAt dates based on the git history." }, { "repository": "nuxt-babel-runtime", "description": "Nuxt CLI that supports babel. Inspired by @nuxt/typescript-runtime." } ], - "supportedNodeVersions": [14, 16] + "supportedNodeVersions": [16, 18] } \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4670b38..ef025b7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,6 +2,6 @@ "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {} }, - "image": "mcr.microsoft.com/devcontainers/javascript-node:0-16", + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-20", "updateContentCommand": "yarn --frozen-lockfile" } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0e3265..e9d63ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: github.event.pull_request.head.ref || '' }} - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - run: git config --global user.email "actions@github.com" - run: git config --global user.name "GitHub Actions" - run: yarn --frozen-lockfile @@ -54,20 +54,20 @@ jobs: with: name: Image Snapshot Diffs path: "**/__image_snapshots__/__diff_output__" - - if: matrix.os == 'ubuntu-latest' && matrix.node == 16 + - if: matrix.os == 'ubuntu-latest' && matrix.node == 20 uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} strategy: matrix: include: - - node: 14 - os: ubuntu-latest - node: 16 os: ubuntu-latest - - node: 16 + - node: 18 + os: ubuntu-latest + - node: 20 os: macos-latest - - node: 16 + - node: 20 os: windows-latest name: build on: diff --git a/.github/workflows/deprecated-dependencies.yml b/.github/workflows/deprecated-dependencies.yml index a01b6bf..4611ebd 100644 --- a/.github/workflows/deprecated-dependencies.yml +++ b/.github/workflows/deprecated-dependencies.yml @@ -20,7 +20,7 @@ jobs: update_existing: true - if: ${{ !steps.check-deprecated-js-deps.outputs.deprecated && steps.create-deprecation-issue.outputs.number }} - uses: peter-evans/close-issue@v2 + uses: peter-evans/close-issue@v3 with: comment: Auto-closing the issue issue-number: ${{ steps.create-deprecation-issue.outputs.number }} diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index cb54d0d..6c71dee 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -6,7 +6,7 @@ RUN sudo apt-get install git-lfs RUN git lfs install # https://www.gitpod.io/docs/languages/javascript -RUN bash -c 'VERSION="16" && source $HOME/.nvm/nvm.sh && nvm install $VERSION && nvm use $VERSION && nvm alias default $VERSION' +RUN bash -c 'VERSION="20" && source $HOME/.nvm/nvm.sh && nvm install $VERSION && nvm use $VERSION && nvm alias default $VERSION' RUN echo "\nexport PATH=$(yarn global bin):\$PATH" >> /home/gitpod/.bashrc diff --git a/.gitpod.yml b/.gitpod.yml index 1c92dfb..73ae874 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -17,3 +17,5 @@ vscode: - https://sebastianlandwehr.com/vscode-extensions/karlito40.fix-irregular-whitespace-0.1.1.vsix - https://sebastianlandwehr.com/vscode-extensions/adrianwilczynski.toggle-hidden-1.0.2.vsix - octref.vetur@0.33.1 + - Tobermory.es6-string-html + - zjcompt.es6-string-javascript diff --git a/README.md b/README.md index f819f32..93b1384 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ Nuxt pages have a `meta` property that allows to define meta data. These can be ℹ️ **Note that this module can only extract static data from the pages at build time. It will not work with dynamic data depending on `this`. In case you have an idea how to improve that, feel free to open up an issue or pull request.** +## Compatibility + +| nuxt-route-meta | Nuxt | +|-----------------|------| +| <= 5 | 2 | +| >= 6 | 3 | + ## Install @@ -102,126 +109,40 @@ export default { That's it! Now you can access the meta data in `route.meta` from anywhere as you know it from [vue-router](https://www.npmjs.com/package/vue-router). The module takes all properties that all properties that are not functions, and the meta property itself is merged into the result. So `route.meta` from the example above is `{ auth: true, theme: 'water' }`. -Here is an example to use it inside `this.extendRoutes` in a module: +Here is an example to use it inside `nuxt.hook('pages:extend')` in a module: ```js -export default function () { - this.extendRoutes(routes => - routes.forEach(route => { +export default defineNuxtModule((options, nuxt) => + nuxt.hook('pages:extend', routes => + for (const route of routes) { if (route.meta.auth) { // do something with auth routes } - }) + } ) -} +) ``` -## TypeScript - -The module has built-in support for TypeScript. Requirement is that the TypeScript module is installed as described in [the Nuxt TypeScript docs](https://typescript.nuxtjs.org/guide/setup). - -```js - -``` - -## Supported APIs - -### Plain object +## Composition API ```js - -``` - -This API does not make much sense with TypeScript because you do not have type information in the components. - -### Options API - -```js - -``` - -As of writing this, Nuxt with TypeScript does not seem to support the options API with Nuxt-specific properties like `asyncData`, `fetch`, `meta`, etc. In case this changes, open up an issue. - -### Class API - -```js - ``` -### Property decorator - -Plain JavaScript: Install [nuxt-property-decorator](https://github.com/nuxt-community/nuxt-property-decorator). - -```js - -``` +## TypeScript -TypeScript: +The module has built-in support for TypeScript. ```js -``` - -## Composition API - -This package ships with support for the Vue composition API. When setting up your nuxt project, make sure to follow the [`@nuxtjs/composition-api` guide](https://composition-api.nuxtjs.org/getting-started/setup) closely. - -```js - diff --git a/package.json b/package.json index 76b2351..9959039 100644 --- a/package.json +++ b/package.json @@ -47,27 +47,32 @@ "dependencies": { "@babel/core": "^7.11.1", "@babel/traverse": "^7.13.13", - "@dword-design/functions": "^4.0.0", + "@vue/compiler-sfc": "^3.3.4", "ast-to-literal": "^0.0.5", + "deepmerge": "^4.3.1", "fs-extra": "^11.1.0", - "ts-ast-to-literal": "^3.0.10", - "typescript": "~4.2", - "vue-template-compiler": "^2.6.11" + "lodash-es": "^4.17.21", + "ts-ast-to-literal": "^3.0.10" }, "devDependencies": { - "@dword-design/base": "^9.1.7", - "@nuxt/typescript-build": "^2.1.0", - "@nuxtjs/composition-api": "^0.27.0", - "depcheck-package-name": "^3.0.0", - "nuxt": "^2.15.3", - "nuxt-property-decorator": "^2.9.1", + "@babel/plugin-proposal-pipeline-operator": "^7.22.5", + "@dword-design/base": "^10.1.2", + "@dword-design/functions": "^5.0.22", + "@dword-design/tester": "^2.0.19", + "@dword-design/tester-plugin-tmp-dir": "^2.1.26", + "depcheck-package-name": "^3.0.1", + "execa": "^7.1.1", + "expect": "^29.5.0", + "nuxt": "^3.6.1", + "nuxt-babel-runtime": "^4.0.0", "output-files": "^2.0.0", - "vue-class-component": "^7.2.6", - "vue-property-decorator": "^9.1.2", - "with-local-tmp-dir": "^5.0.0" + "typescript": "^5.1.6" + }, + "peerDependencies": { + "typescript": "*" }, "engines": { - "node": ">=14" + "node": ">=16" }, "publishConfig": { "access": "public" diff --git a/src/index.js b/src/index.js index c2ab77e..cef3613 100644 --- a/src/index.js +++ b/src/index.js @@ -1,147 +1,44 @@ -import * as babel from '@babel/core' -import traverse from '@babel/traverse' -import { - filter, - fromPairs, - keys, - map, - omit, - pick, - some, -} from '@dword-design/functions' -import astToLiteral from 'ast-to-literal' +import { parse as parseVue } from '@vue/compiler-sfc' +import deepmerge from 'deepmerge' import fs from 'fs-extra' import P from 'path' -import tsAstToLiteral from 'ts-ast-to-literal' -import ts from 'typescript' -import { parseComponent } from 'vue-template-compiler' -const predefinedProperties = { - components: true, - computed: true, - data: true, - methods: true, - mixins: true, - render: true, - watch: true, -} +import parseBabel from './parse-babel.js' +import parseTypescript from './parse-typescript.js' -export default function () { +export default (options, nuxt) => { const extractMeta = filename => { const fileContent = fs.readFileSync(filename, 'utf8') - let data = {} const Component = P.extname(filename) === '.vue' - ? parseComponent(fileContent) - : { script: { content: fileContent, lang: 'js' } } + ? parseVue(fileContent) + : { descriptor: { script: { content: fileContent, lang: 'js' } } } - const scriptContent = Component.script?.content - if (scriptContent) { - if (Component.script.lang === 'ts') { - const rootNode = ts.createSourceFile( - 'x.ts', - scriptContent, - ts.ScriptTarget.Latest, - ) - ts.forEachChild(rootNode, node => { - switch (node.kind) { - case ts.SyntaxKind.ExportAssignment: { - const object = - node.expression.kind === ts.SyntaxKind.CallExpression && - ((node.expression.expression.kind === - ts.SyntaxKind.PropertyAccessExpression && - node.expression.expression.expression.escapedText === 'Vue' && - node.expression.expression.name.escapedText === 'extend' && - node.expression.arguments.length === 1) || - (node.expression.expression.kind === - ts.SyntaxKind.Identifier && - node.expression.expression.escapedText === - 'defineComponent')) - ? node.expression.arguments[0] - : node.expression - data = object |> tsAstToLiteral - break - } - case ts.SyntaxKind.ClassDeclaration: { - if ( - (node.modifiers || [] - |> some( - modifier => modifier.kind === ts.SyntaxKind.ExportKeyword, - )) && - (node.modifiers || [] - |> some( - modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword, - )) && - (node.heritageClauses || [] - |> some( - clause => - clause.types - |> some(type => type.expression.escapedText === 'Vue'), - )) - ) { - data = - node.members - |> filter(member => member.initializer !== undefined) - |> map(member => [ - member.name.escapedText, - member.initializer |> tsAstToLiteral, - ]) - |> fromPairs - } - break - } - default: - } - }) - } else { - const ast = babel.parseSync(scriptContent, { + const data = ['script', 'scriptSetup'] + .filter(name => Component.descriptor[name]) + .map(name => + (Component.descriptor[name].lang === 'ts' + ? parseTypescript + : parseBabel)(Component.descriptor[name], { filename, - ...(this.options.build.babel |> pick(['configFile', 'babelrc'])), - ...(!this.options.build.babel.configFile && - !this.options.build.babel.babelrc && { - extends: '@nuxt/babel-preset-app', - }), - }) - traverse.default(ast, { - ClassDeclaration: path => { - if (path.node.superClass.name === 'Vue') { - data = - path.node.body.body - |> map(property => [ - property.key.name, - property.value |> astToLiteral, - ]) - |> fromPairs - } - }, - ExportDefaultDeclaration: path => { - const object = - path.node.declaration.type === 'CallExpression' && - (path.node.declaration.callee.name === 'defineComponent' || - (path.node.declaration.callee.object?.name === 'Vue' && - path.node.declaration.callee.property?.name === 'extend')) - ? path.node.declaration.arguments[0] - : path.node.declaration - data = object |> astToLiteral - }, - }) - } - } + nuxt, + }), + ) - return { - ...(data |> omit(['meta', ...(predefinedProperties |> keys)])), - ...data?.meta, - } + return deepmerge.all(data) } const parseRoutes = routes => { for (const route of routes) { - route.meta = route.component |> extractMeta - if (route.children !== undefined) { + route.meta = { + ...route.meta, + ...extractMeta(route.file), + } + if (route.children.length > 0) { parseRoutes(route.children) } } } - this.extendRoutes(parseRoutes) + nuxt.hook('pages:extend', parseRoutes) } diff --git a/src/index.spec.js b/src/index.spec.js index c71d135..b22b881 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -1,399 +1,202 @@ -import { endent, mapValues } from '@dword-design/functions' +import { endent } from '@dword-design/functions' +import tester from '@dword-design/tester' +import testerPluginTmpDir from '@dword-design/tester-plugin-tmp-dir' import packageName from 'depcheck-package-name' -import { Builder, Nuxt } from 'nuxt' +import { execaCommand } from 'execa' import outputFiles from 'output-files' -import withLocalTmpDir from 'with-local-tmp-dir' -import self from './index.js' +export default tester( + { + 'additional properties': async () => { + await outputFiles({ + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' -const tsconfig = { - compilerOptions: { - allowJs: true, - baseUrl: '.', - esModuleInterop: true, - experimentalDecorators: true, - lib: ['ESNext', 'ESNext.AsyncIterable', 'DOM'], - module: 'ESNext', - moduleResolution: 'Node', - noEmit: true, - paths: { - '@/*': ['./*'], - '~/*': ['./*'], + export default { + modules: [ + '../src/index.js', + (options, nuxt) => nuxt.hook('pages:extend', routes => expect(routes[0].meta.foo).toEqual(true)), + ], + } + `, + 'pages/index.vue': endent` + + + + `, + }) + await execaCommand('nuxt build') }, - sourceMap: true, - strict: true, - target: 'ES2018', - types: ['@types/node', '@nuxt/types'], - }, - exclude: ['node_modules'], -} + array: async () => { + await outputFiles({ + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' -const runTest = config => () => - withLocalTmpDir(async () => { - await outputFiles(config.files) + export default { + modules: [ + '../src/index.js', + (options, nuxt) => nuxt.hook('pages:extend', routes => expect(routes[0].meta).toEqual({ foo: [1, 2] })) + ], + } + `, + 'pages/index.vue': endent` + - const nuxt = new Nuxt({ - dev: false, - ...config.config, - }) - if (config.error) { - await expect(new Builder(nuxt).build()).rejects.toThrow(config.error) - } else { - await new Builder(nuxt).build() - } - }) + -export default { - 'additional properties': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual(true) - ) - } - `, - 'pages/index.vue': endent` - + `, + }) + await execaCommand('nuxt build') + }, + babel: async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`@babel/plugin-proposal-pipeline-operator`, + { proposal: 'fsharp' }, + ], + ], + }), + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' - `, - }, - }, - array: { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual([1, 2]) - ) - } - `, - 'pages/index.vue': endent` - + export default { + modules: [ + '../src/index.js', + (options, nuxt) => nuxt.hook('pages:extend', routes => expect(routes[0].meta).toEqual({ foo: true })), + ], + } + `, + 'pages/index.vue': endent` + - `, - }, - }, - 'babel syntax with config': { - config: { - build: { - babel: { - babelrc: true, - }, - }, - modules: [self], - }, - files: { - '.babelrc.json': JSON.stringify({ - extends: '@dword-design/babel-config', - }), - 'pages/index.vue': endent` - - `, - }, - }, - 'babel syntax without config': { - config: { - modules: [self], - }, - error: - "Support for the experimental syntax 'pipelineOperator' isn't currently enabled", - files: { - 'pages/index.vue': endent` - - `, - }, - }, - false: { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual(false) - ) - } - `, - 'pages/index.vue': endent` - - `, - }, - }, - 'js composition api': { - config: { - buildModules: [packageName`@nuxtjs/composition-api/module`], - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual(true) - ) - } - `, - 'pages/index.vue': endent` - - `, - }, - }, - 'js file': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual(true) - ) - } - `, - 'pages/index.js': endent` - export default { - foo: true, - render: () =>
- } - `, - }, - }, - meta: { - config: { - modules: [self, '~/modules/module'], + }) + + `, + }) + await execaCommand('nuxt-babel build') }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta.foo).toEqual(true) - ) - } - `, - 'pages/index.vue': endent` - - `, - }, - }, - 'multiple routes': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => { - expect(routes[0].path).toEqual('/bar') - expect(routes[0].meta).toEqual({ bar: true }) - expect(routes[1].path).toEqual('/foo') - expect(routes[1].meta).toEqual({ foo: true }) }) - } - `, - pages: { - 'bar.vue': endent` + + `, + }) + await execaCommand('nuxt build') + }, + false: async () => { + await outputFiles({ + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' + + export default { + modules: [ + '../src/index.js', + (options, nuxt) => nuxt.hook('pages.extend', routes => expect(routes[0].meta).toEqual({ foo: false })), + ], + } + `, + 'pages/index.vue': endent` + + + `, + }) + await execaCommand('nuxt build') + }, + meta: async () => { + await outputFiles({ + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' + export default { + modules: [ + '../src/index.js', + (options, nuxt) => nuxt.hook('pages:extend', routes => expect(routes[0].meta).toEqual({ foo: true, bar: true })), + ], + } `, - 'foo.vue': endent` + 'pages/index.vue': endent` + + `, - }, + }) + await execaCommand('nuxt build') }, - }, - 'options api': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => { - expect(routes[0].meta.foo).toEqual(true) - expect(routes[0].meta.bar).toEqual(true) - }) - } - `, - 'pages/index.vue': endent` - + 'multiple routes': async () => { + await outputFiles({ + 'nuxt.config.js': endent` + import expect from '${packageName`expect`}' - - - `, - 'tsconfig.json': JSON.stringify(tsconfig), - }, - }, - 'predefined properties': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => - expect(routes[0].meta).toEqual({}) - ) - } - `, - 'pages/index.vue': endent` - - `, - }, - }, - 'property decorator': { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => { - expect(routes[0].meta.foo).toEqual(true) - expect(routes[0].meta.bar).toEqual(true) - }) - } - `, - 'pages/index.vue': endent` - - `, - }, - }, - 'spread operator': { - config: { - modules: [self], - }, - files: { - 'pages/index.vue': endent` - - `, - }, - }, - subroutes: { - config: { - modules: [self, '~/modules/module'], - }, - files: { - 'modules/module.js': endent` - export default function () { - this.extendRoutes(routes => { - expect(routes[0].path).toEqual('/foo') - expect(routes[0].meta).toEqual({ foo: true }) - expect(routes[0].children[0].path).toEqual('') - expect(routes[0].children[0].meta).toEqual({ bar: true }) - expect(routes[0].children[1].path).toEqual('bar') - expect(routes[0].children[1].meta).toEqual({ baz: true }) - expect(routes[0].children[1].children[0].path).toEqual('') - expect(routes[0].children[1].children[0].meta).toEqual({ test: true }) - }) - } - `, - pages: { - foo: { + `, + pages: { 'bar.vue': endent`