Repository: teleporthq/teleport-code-generators (ts)

Issue:
Navlinks with state reference
Navlinks have an attribute `transitionTo` which is mapped at the moment to the `href` attribute or to the `to` attribute depending on the specific project flavor.

However, there's no transformation there for the content, so you have to specify the url as the attribute, ex: `/about-us`. It would be nice to be able to set only the state in which you wish to arrive (urls are just a meta info for each state value of the router).

So ideally, we should be able to set `about` as the attribute and the resolver will check the route stateDefinition and will extract the meta information (which url needs to go as a href/to for the navlink)

Pull request description:
feat(navlinks): transitionTo can now refer to the state key, not the url
this is solved at resolve time, based on the state definitions of the route key, which are
accompanied by a meta.path field

closes #189

Patch:
diff --git a/examples/uidl-samples/project.json b/examples/uidl-samples/project.json
index e7e587e4..c1904350 100644
--- a/examples/uidl-samples/project.json
+++ b/examples/uidl-samples/project.json
@@ -199,7 +199,7 @@
               "content": {
                 "elementType": "navlink",
                 "attrs": {
-                  "transitionTo": "/"
+                  "transitionTo": "index"
                 },
                 "children": [
                   {
@@ -214,7 +214,7 @@
               "content": {
                 "elementType": "navlink",
                 "attrs": {
-                  "transitionTo": "/about"
+                  "transitionTo": "about"
                 },
                 "children": [
                   {
@@ -229,7 +229,7 @@
               "content": {
                 "elementType": "navlink",
                 "attrs": {
-                  "transitionTo": "/here-we-are"
+                  "transitionTo": "contact-us"
                 },
                 "children": [
                   {
diff --git a/packages/teleport-generator-core/src/resolver/index.ts b/packages/teleport-generator-core/src/resolver/index.ts
index da065744..c669755a 100644
--- a/packages/teleport-generator-core/src/resolver/index.ts
+++ b/packages/teleport-generator-core/src/resolver/index.ts
@@ -38,6 +38,11 @@ export default class Resolver {
     }
 
     const node = cloneObject(uidl.node)
+
+    if (options.projectRouteDefinition) {
+      utils.resolveNavlinks(node, options.projectRouteDefinition)
+    }
+
     utils.resolveNode(node, newOptions)
 
     const nodesLookup = {}
diff --git a/packages/teleport-generator-core/src/resolver/utils.ts b/packages/teleport-generator-core/src/resolver/utils.ts
index 23763de3..2dd49548 100644
--- a/packages/teleport-generator-core/src/resolver/utils.ts
+++ b/packages/teleport-generator-core/src/resolver/utils.ts
@@ -14,6 +14,7 @@ import {
   UIDLRepeatContent,
   UIDLAttributeValue,
   Mapping,
+  UIDLStateDefinition,
 } from '@teleporthq/teleport-generator-shared/lib/typings/uidl'
 import { GeneratorOptions } from '@teleporthq/teleport-generator-shared/lib/typings/generators'
 import { camelCaseToDashCase } from '@teleporthq/teleport-generator-shared/lib/utils/string-utils'
@@ -33,6 +34,36 @@ export const mergeMappings = (oldMapping: Mapping, newMapping?: Mapping) => {
   }
 }
 
+// Finds all the navlink elements and converts the content of the transitionTo attribute
+// to the actual route value that is defined in the project UIDL, in the routing definition
+export const resolveNavlinks = (uidlNode: UIDLNode, routesDefinition: UIDLStateDefinition) => {
+  traverseElements(uidlNode, (element) => {
+    if (element.elementType === 'navlink') {
+      const transitionAttribute = element.attrs.transitionTo
+      if (transitionAttribute.type !== 'static') {
+        throw new Error(
+          `Navlink does not support dynamic 'transitionTo' attributes\n ${JSON.stringify(
+            transitionAttribute,
+            null,
+            2
+          )}`
+        )
+      }
+
+      const transitionState = transitionAttribute.content.toString()
+      const transitionRoute = routesDefinition.values.find(
+        (route) => route.value === transitionState
+      )
+
+      if (transitionRoute && transitionRoute.meta && transitionRoute.meta.path) {
+        transitionAttribute.content = transitionRoute.meta.path
+      } else {
+        console.warn(`No path was defined for router state: '${transitionState}'.`)
+      }
+    }
+  })
+}
+
 export const resolveNode = (uidlNode: UIDLNode, options: GeneratorOptions) => {
   traverseNodes(uidlNode, (node, parentNode) => {
     if (node.type === 'element') {
diff --git a/packages/teleport-generator-shared/src/typings/generators.ts b/packages/teleport-generator-shared/src/typings/generators.ts
index c9412f6d..41420cd0 100644
--- a/packages/teleport-generator-shared/src/typings/generators.ts
+++ b/packages/teleport-generator-shared/src/typings/generators.ts
@@ -6,6 +6,7 @@ import {
   UIDLElement,
   UIDLNode,
   UIDLAttributeValue,
+  UIDLStateDefinition,
 } from './uidl'
 
 export type ChunkContent = string | any | any[]
@@ -64,6 +65,7 @@ export interface GeneratorOptions {
   assetsPrefix?: string
   mapping?: Mapping
   skipValidation?: boolean
+  projectRouteDefinition?: UIDLStateDefinition
 }
 
 export type CodeGeneratorFunction<T> = (content: T) => string
@@ -114,11 +116,7 @@ export interface GeneratedFile {
 export interface ComponentFactoryParams {
   componentGenerator: ComponentGenerator
   componentUIDL: ComponentUIDL
-  componentOptions: {
-    assetsPrefix: string
-    localDependenciesPrefix?: string
-    skipValidation?: boolean
-  }
+  generatorOptions: GeneratorOptions
   metadataOptions?: {
     usePathAsFileName?: boolean
     convertDefaultToIndex?: boolean
diff --git a/packages/teleport-generator-shared/src/utils/project-utils.ts b/packages/teleport-generator-shared/src/utils/project-utils.ts
index a71d1cf9..3c2bdd95 100644
--- a/packages/teleport-generator-shared/src/utils/project-utils.ts
+++ b/packages/teleport-generator-shared/src/utils/project-utils.ts
@@ -205,14 +205,14 @@ export const createPageOutputs = async (
 export const createComponentOutputs = async (
   params: ComponentFactoryParams
 ): Promise<ComponentGeneratorOutput> => {
-  const { componentGenerator, componentUIDL, componentOptions } = params
+  const { componentGenerator, componentUIDL, generatorOptions } = params
 
   let files: GeneratedFile[] = []
   let dependencies: Record<string, string> = {}
 
   try {
     const compiledComponent = await componentGenerator.generateComponent(componentUIDL, {
-      ...componentOptions,
+      ...generatorOptions,
       skipValidation: true,
     })
 
diff --git a/packages/teleport-project-generator-react-basic/src/index.ts b/packages/teleport-project-generator-react-basic/src/index.ts
index 7dead90d..174db678 100644
--- a/packages/teleport-project-generator-react-basic/src/index.ts
+++ b/packages/teleport-project-generator-react-basic/src/index.ts
@@ -72,9 +72,10 @@ const createReactBasicGenerator = (generatorOptions: ProjectGeneratorOptions = {
       const pageParams: ComponentFactoryParams = {
         componentGenerator: reactGenerator,
         componentUIDL,
-        componentOptions: {
+        generatorOptions: {
           assetsPrefix: ASSETS_PREFIX,
           localDependenciesPrefix: LOCAL_DEPENDENCIES_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
         },
       }
       return createPageOutputs(pageParams)
@@ -86,7 +87,10 @@ const createReactBasicGenerator = (generatorOptions: ProjectGeneratorOptions = {
       const componentParams: ComponentFactoryParams = {
         componentGenerator: reactGenerator,
         componentUIDL,
-        componentOptions: { assetsPrefix: ASSETS_PREFIX },
+        generatorOptions: {
+          assetsPrefix: ASSETS_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
+        },
       }
       return createComponentOutputs(componentParams)
     })
diff --git a/packages/teleport-project-generator-react-next/src/index.ts b/packages/teleport-project-generator-react-next/src/index.ts
index e6088bf9..68bdd220 100644
--- a/packages/teleport-project-generator-react-next/src/index.ts
+++ b/packages/teleport-project-generator-react-next/src/index.ts
@@ -71,9 +71,10 @@ const createReactNextGenerator = (generatorOptions: ProjectGeneratorOptions = {}
       const pageParams: ComponentFactoryParams = {
         componentGenerator: reactGenerator,
         componentUIDL,
-        componentOptions: {
+        generatorOptions: {
           assetsPrefix: ASSETS_PREFIX,
           localDependenciesPrefix: LOCAL_DEPENDENCIES_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
         },
         metadataOptions: {
           usePathAsFileName: true,
@@ -89,7 +90,10 @@ const createReactNextGenerator = (generatorOptions: ProjectGeneratorOptions = {}
       const componentParams: ComponentFactoryParams = {
         componentUIDL,
         componentGenerator: reactGenerator,
-        componentOptions: { assetsPrefix: ASSETS_PREFIX },
+        generatorOptions: {
+          assetsPrefix: ASSETS_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
+        },
       }
       return createComponentOutputs(componentParams)
     })
diff --git a/packages/teleport-project-generator-vue-basic/src/index.ts b/packages/teleport-project-generator-vue-basic/src/index.ts
index 2b2631d1..ffff60d0 100644
--- a/packages/teleport-project-generator-vue-basic/src/index.ts
+++ b/packages/teleport-project-generator-vue-basic/src/index.ts
@@ -66,9 +66,10 @@ const createVueBasicGenerator = (generatorOptions: ProjectGeneratorOptions = {})
       const pageParams: ComponentFactoryParams = {
         componentGenerator: vueGenerator,
         componentUIDL,
-        componentOptions: {
+        generatorOptions: {
           assetsPrefix: ASSETS_PREFIX,
           localDependenciesPrefix: LOCAL_DEPENDENCIES_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
         },
       }
       return createPageOutputs(pageParams)
@@ -80,7 +81,10 @@ const createVueBasicGenerator = (generatorOptions: ProjectGeneratorOptions = {})
       const componentParams: ComponentFactoryParams = {
         componentUIDL,
         componentGenerator: vueGenerator,
-        componentOptions: { assetsPrefix: ASSETS_PREFIX },
+        generatorOptions: {
+          assetsPrefix: ASSETS_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
+        },
       }
 
       return createComponentOutputs(componentParams)
diff --git a/packages/teleport-project-generator-vue-nuxt/src/index.ts b/packages/teleport-project-generator-vue-nuxt/src/index.ts
index 0f7278ad..aeb7f3b2 100644
--- a/packages/teleport-project-generator-vue-nuxt/src/index.ts
+++ b/packages/teleport-project-generator-vue-nuxt/src/index.ts
@@ -67,9 +67,10 @@ const createVueNuxtGenerator = (generatorOptions: ProjectGeneratorOptions = {})
       const pageParams: ComponentFactoryParams = {
         componentGenerator: vueGenerator,
         componentUIDL,
-        componentOptions: {
+        generatorOptions: {
           assetsPrefix: ASSETS_PREFIX,
           localDependenciesPrefix: LOCAL_DEPENDENCIES_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
         },
         metadataOptions: {
           usePathAsFileName: true,
@@ -86,7 +87,10 @@ const createVueNuxtGenerator = (generatorOptions: ProjectGeneratorOptions = {})
       const componentParams: ComponentFactoryParams = {
         componentUIDL,
         componentGenerator: vueGenerator,
-        componentOptions: { assetsPrefix: ASSETS_PREFIX },
+        generatorOptions: {
+          assetsPrefix: ASSETS_PREFIX,
+          projectRouteDefinition: root.stateDefinitions.route,
+        },
       }
       return createComponentOutputs(componentParams)
     })


Test patch:
diff --git a/packages/teleport-generator-core/__tests__/resolver/utils.ts b/packages/teleport-generator-core/__tests__/resolver/utils.ts
index 5e5d0303..bdf7964d 100644
--- a/packages/teleport-generator-core/__tests__/resolver/utils.ts
+++ b/packages/teleport-generator-core/__tests__/resolver/utils.ts
@@ -3,9 +3,19 @@ import {
   repeatNode,
   elementNode,
   dynamicNode,
+  definition,
 } from '@teleporthq/teleport-generator-shared/lib/builders/uidl-builders'
-import { generateUniqueKeys, createNodesLookup, resolveChildren } from '../../src/resolver/utils'
-import { UIDLElement, UIDLNode } from '@teleporthq/teleport-generator-shared/lib/typings/uidl'
+import {
+  generateUniqueKeys,
+  createNodesLookup,
+  resolveChildren,
+  resolveNavlinks,
+} from '../../src/resolver/utils'
+import {
+  UIDLElement,
+  UIDLNode,
+  UIDLStateDefinition,
+} from '@teleporthq/teleport-generator-shared/lib/typings/uidl'
 
 describe('generateUniqueKeys', () => {
   it('adds name and key to node', async () => {
@@ -181,3 +191,52 @@ describe('resolveChildren', () => {
     expect(innerChildren[4].content).toBe('other-original-text')
   })
 })
+
+describe('resolveNavlinks', () => {
+  const routeDef: UIDLStateDefinition = {
+    type: 'string',
+    defaultValue: 'home-link',
+    values: [
+      {
+        value: 'home-link',
+        meta: {
+          path: '/home',
+        },
+      },
+    ],
+  }
+
+  it('replaces the transitionTo attribute content', () => {
+    const navlink = elementNode('navlink', {
+      transitionTo: staticNode('home-link'),
+    })
+
+    const uidlNode = elementNode('container', {}, [elementNode('div', {}, [navlink])])
+
+    resolveNavlinks(uidlNode, routeDef)
+    expect(navlink.content.attrs.transitionTo.content).toBe('/home')
+  })
+
+  it('throws an error for dynamic attributes', () => {
+    const navlink = elementNode('navlink', {
+      transitionTo: dynamicNode('prop', 'path'),
+    })
+
+    expect(() => resolveNavlinks(navlink, routeDef)).toThrow(
+      "Navlink does not support dynamic 'transitionTo' attributes"
+    )
+  })
+
+  it('does not change the attribute if no route is present', () => {
+    const navlink = elementNode('navlink', {
+      transitionTo: staticNode('non-existing-state'),
+    })
+
+    const warn = jest.spyOn(global.console, 'warn')
+
+    resolveNavlinks(navlink, routeDef)
+
+    expect(warn).toHaveBeenCalledWith("No path was defined for router state: 'non-existing-state'.")
+    expect(navlink.content.attrs.transitionTo.content).toBe('non-existing-state')
+  })
+})