Repository: teleporthq/teleport-code-generators
Pull request #853 (2023-09-12 17:23:06)

(feat): Add support for custom fonts and their initialisation
- [x] Add ability to define `local-font` that defined declarations in global style sheet.
- [x] Fix for fallback routes being resolved for all routes for `angular`, `react`, `vue` frameworks.
- [x] Fix `navLink` on `abilities` to use target routes.
- [x] Upgrade `react` and `craco` projects to use `node@18` by default.
- [x] Fix `typescript` version for `angular@14` projects.
- [x] Fix import for global stylesheets inside nested routes for html projects.
- [x] Fix using local fonts in nested routes for all frameworks.



Patch:
diff --git a/examples/uidl-samples/project.json b/examples/uidl-samples/project.json
index 2dc6a6e7..a1a1490d 100644
--- a/examples/uidl-samples/project.json
+++ b/examples/uidl-samples/project.json
@@ -24,6 +24,16 @@
               "path": "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap",
               "name": "Inter"
           },
+          {
+            "type": "local-font",
+            "path": "super-funky.ttf",
+            "properties": {
+              "font-family": {
+                "type": "static",
+                "content": "Super Funky"
+              }
+            }
+          },
           {
               "type": "script",
               "content": "https://unpkg.com/@teleporthq/teleport-custom-scripts"
@@ -6955,7 +6965,8 @@
                                               "maxWidth": {
                                                   "type": "static",
                                                   "content": "450px"
-                                              }
+                                              },
+                                              "fontFamily": "Super Funky"
                                           },
                                           "children": [
                                               {
diff --git a/examples/uidl-samples/react-project.json b/examples/uidl-samples/react-project.json
index 4530073c..1f6ca95d 100644
--- a/examples/uidl-samples/react-project.json
+++ b/examples/uidl-samples/react-project.json
@@ -24,6 +24,16 @@
                     "iconType": "image/png",
                     "iconSizes": "32x32"
                 }
+            },
+            {
+              "type": "local-font",
+              "path": "super-funky.ttf",
+              "properties": {
+                "font-family": {
+                  "type": "static",
+                  "content": "Super Funky"
+                }
+              }
             }
         ],
         "meta": [],
diff --git a/packages/teleport-plugin-angular-module/src/utils.ts b/packages/teleport-plugin-angular-module/src/utils.ts
index 20b2fb14..c7bde8f1 100644
--- a/packages/teleport-plugin-angular-module/src/utils.ts
+++ b/packages/teleport-plugin-angular-module/src/utils.ts
@@ -138,48 +138,58 @@ export const createRoutesAST = (
   stateDefinitions: { route: UIDLRouteDefinitions } & Record<string, UIDLStateDefinition>,
   t = types
 ) => {
-  const routesObject = routes.map((conditionalNode) => {
-    const { value: routeKey } = conditionalNode.content
-    const pageDefinition = stateDefinitions.route.values.find((route) => route.value === routeKey)
-    const { navLink, fileName } = pageDefinition.pageOptions
+  const routesObject = stateDefinitions.route.values
+    .sort((routeA) => {
+      if (routeA?.pageOptions?.fallback) {
+        return 1
+      }
+    })
+    .map((routeDefinition) => {
+      const page = routes.find(
+        (routeNode) => routeNode.content.value.toString() === routeDefinition.value
+      )
+      if (!page) {
+        throw new Error(`Failed to match route ${routeDefinition.value} with a page`)
+      }
 
-    return t.objectExpression([
-      t.objectProperty(t.identifier('path'), t.stringLiteral(navLink.replace('/', ''))),
-      t.objectProperty(
-        t.identifier('loadChildren'),
-        t.arrowFunctionExpression(
-          [],
-          t.callExpression(
-            t.memberExpression(
-              t.callExpression(t.identifier('import'), [
-                /*
-                  Now, navLink is being used to create a folder strucutre.
-                  So, it is important to append the same when generating the path
-                */
-                t.stringLiteral(
-                  `./pages/${join(
-                    ...navLink.split('/')?.slice(0, -1),
-                    fileName,
-                    fileName + '.module'
-                  )}`
-                ),
-              ]),
-              t.identifier('then')
-            ),
-            [
-              t.arrowFunctionExpression(
-                [t.identifier('m')],
-                t.memberExpression(
-                  t.identifier('m'),
-                  t.identifier(`${StringUtils.dashCaseToUpperCamelCase(fileName)}Module`)
-                )
+      const { navLink, fileName } = routeDefinition.pageOptions
+      return t.objectExpression([
+        t.objectProperty(t.identifier('path'), t.stringLiteral(navLink.replace('/', ''))),
+        t.objectProperty(
+          t.identifier('loadChildren'),
+          t.arrowFunctionExpression(
+            [],
+            t.callExpression(
+              t.memberExpression(
+                t.callExpression(t.identifier('import'), [
+                  /*
+                   Now, navLink is being used to create a folder strucutre.
+                   So, it is important to append the same when generating the path
+                 */
+                  t.stringLiteral(
+                    `./pages/${join(
+                      ...navLink.split('/')?.slice(0, -1),
+                      fileName,
+                      fileName + '.module'
+                    )}`
+                  ),
+                ]),
+                t.identifier('then')
               ),
-            ]
+              [
+                t.arrowFunctionExpression(
+                  [t.identifier('m')],
+                  t.memberExpression(
+                    t.identifier('m'),
+                    t.identifier(`${StringUtils.dashCaseToUpperCamelCase(fileName)}Module`)
+                  )
+                ),
+              ]
+            )
           )
-        )
-      ),
-    ])
-  })
+        ),
+      ])
+    })
 
   return t.variableDeclaration('const', [
     t.variableDeclarator(t.identifier('routes'), t.arrayExpression(routesObject)),
diff --git a/packages/teleport-plugin-common/src/builders/style-builders.ts b/packages/teleport-plugin-common/src/builders/style-builders.ts
index 42a9bacb..3918378a 100644
--- a/packages/teleport-plugin-common/src/builders/style-builders.ts
+++ b/packages/teleport-plugin-common/src/builders/style-builders.ts
@@ -47,6 +47,14 @@ export const createCSSClassWithSelector = (
     .toString()
 }
 
+export const createFontDecleration = (styleObject: Record<string, string | number>) => {
+  return jss
+    .createStyleSheet({
+      '@font-face': styleObject,
+    })
+    .toString()
+}
+
 export const createCSSClassWithMediaQuery = (
   mediaOffset: string,
   styleObject: Record<string, string | number>
diff --git a/packages/teleport-plugin-css-modules/src/style-sheet.ts b/packages/teleport-plugin-css-modules/src/style-sheet.ts
index c708b84b..7b0c8e05 100644
--- a/packages/teleport-plugin-css-modules/src/style-sheet.ts
+++ b/packages/teleport-plugin-css-modules/src/style-sheet.ts
@@ -6,39 +6,83 @@ import {
   FileType,
 } from '@teleporthq/teleport-types'
 import { StringUtils } from '@teleporthq/teleport-shared'
+import { relative, join } from 'path'
+
 interface StyleSheetPlugin {
   fileName?: string
   moduleExtension?: boolean
+  relativeFontPath?: boolean
 }
 
 const defaultConfig = {
   fileName: 'style',
   moduleExtension: false,
+  relativeFontPath: false,
 }
 
 export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> = (config) => {
-  const { fileName, moduleExtension } = {
+  const { fileName, moduleExtension, relativeFontPath } = {
     ...defaultConfig,
     ...config,
   }
 
   const styleSheetPlugin: ComponentPlugin = async (structure) => {
-    const { uidl, chunks } = structure
+    const {
+      uidl,
+      chunks,
+      options: { assets },
+    } = structure
     const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl
+    const {
+      localFonts = [],
+      fontsFolder,
+      identifier: assetIdentifier,
+    } = assets || { localFonts: [] }
+
+    const canSkipThePlugin =
+      localFonts?.length === 0 &&
+      Object.keys(styleSetDefinitions).length === 0 &&
+      Object.keys(tokens).length === 0
 
-    if (
-      (!styleSetDefinitions && !tokens) ||
-      (Object.keys(styleSetDefinitions).length === 0 && Object.keys(tokens).length === 0)
-    ) {
-      return
+    if (canSkipThePlugin) {
+      return structure
     }
 
+    uidl.outputOptions = uidl.outputOptions || {}
+    uidl.outputOptions.styleFileName = moduleExtension ? `${fileName}.module` : fileName
+
     const cssMap: string[] = []
     const mediaStylesMap: Record<
       string,
       Array<{ [x: string]: Record<string, string | number> }>
     > = {}
 
+    if (localFonts.length > 0) {
+      localFonts.forEach((font) => {
+        const properties = StyleUtils.getContentOfStyleObject(font.properties) as Record<
+          string,
+          string | number
+        >
+        const format = properties?.format ? `format('${properties.format}')` : ''
+        /* tslint:disable:no-string-literal */
+        delete properties['format']
+
+        const fontPath = relativeFontPath
+          ? join(
+              relative(join(...(uidl.outputOptions?.folderPath || [])), './'),
+              join(fontsFolder || '', font.path)
+            )
+          : join('/', assetIdentifier || '', 'fonts', font.path)
+
+        cssMap.push(
+          StyleBuilders.createFontDecleration({
+            ...properties,
+            src: `url("${fontPath}") ${format}`,
+          })
+        )
+      })
+    }
+
     if (Object.keys(tokens).length > 0) {
       cssMap.push(
         StyleBuilders.createCSSClassWithSelector(
@@ -65,9 +109,6 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
       return structure
     }
 
-    uidl.outputOptions = uidl.outputOptions || {}
-    uidl.outputOptions.styleFileName = moduleExtension ? `${fileName}.module` : fileName
-
     chunks.push({
       name: fileName,
       type: ChunkType.STRING,
diff --git a/packages/teleport-plugin-css/src/style-sheet.ts b/packages/teleport-plugin-css/src/style-sheet.ts
index d719ebd0..45fa7c9b 100644
--- a/packages/teleport-plugin-css/src/style-sheet.ts
+++ b/packages/teleport-plugin-css/src/style-sheet.ts
@@ -5,22 +5,42 @@ import {
   ChunkType,
   FileType,
 } from '@teleporthq/teleport-types'
+import { join, relative } from 'path'
 
 interface StyleSheetPlugin {
   fileName?: string
+  /*
+    Frameworks with dev servers don't need relative path mapping.
+  */
+  relativeFontPath?: boolean
 }
 
 export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> = (config) => {
-  const { fileName } = config || { fileName: 'style' }
+  const { fileName = 'style', relativeFontPath = false } = config || {
+    fileName: 'style',
+    relativeFontPath: false,
+  }
+
   const styleSheetPlugin: ComponentPlugin = async (structure) => {
-    const { uidl, chunks } = structure
+    const {
+      uidl,
+      chunks,
+      options: { assets },
+    } = structure
     const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl
+    const {
+      localFonts = [],
+      fontsFolder,
+      identifier: assetIdentifier,
+    } = assets || { localFonts: [] }
 
-    if (
-      (!styleSetDefinitions && !tokens) ||
-      (Object.keys(styleSetDefinitions).length === 0 && Object.keys(tokens).length === 0)
-    ) {
-      return
+    const canSkipThePlugin =
+      localFonts?.length === 0 &&
+      Object.keys(styleSetDefinitions).length === 0 &&
+      Object.keys(tokens).length === 0
+
+    if (canSkipThePlugin) {
+      return structure
     }
 
     const cssMap: string[] = []
@@ -29,6 +49,35 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
       Array<{ [x: string]: Record<string, string | number> }>
     > = {}
 
+    uidl.outputOptions = uidl.outputOptions || {}
+    uidl.outputOptions.styleFileName = fileName
+
+    if (localFonts.length > 0) {
+      localFonts.forEach((font) => {
+        const properties = StyleUtils.getContentOfStyleObject(font.properties) as Record<
+          string,
+          string | number
+        >
+        const format = properties?.format ? `format('${properties.format}')` : ''
+        /* tslint:disable:no-string-literal */
+        delete properties['format']
+
+        const fontPath = relativeFontPath
+          ? join(
+              relative(join(...(uidl.outputOptions?.folderPath || [])), './'),
+              join(fontsFolder || '', font.path)
+            )
+          : join('/', assetIdentifier || '', 'fonts', font.path)
+
+        cssMap.push(
+          StyleBuilders.createFontDecleration({
+            ...properties,
+            src: `url("${fontPath}") ${format}`,
+          })
+        )
+      })
+    }
+
     if (Object.keys(tokens).length > 0) {
       cssMap.push(
         StyleBuilders.createCSSClassWithSelector(
@@ -54,9 +103,6 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
       return structure
     }
 
-    uidl.outputOptions = uidl.outputOptions || {}
-    uidl.outputOptions.styleFileName = fileName
-
     chunks.push({
       name: fileName,
       type: ChunkType.STRING,
diff --git a/packages/teleport-plugin-html-base-component/src/node-handlers.ts b/packages/teleport-plugin-html-base-component/src/node-handlers.ts
index 999a2f14..0820236d 100644
--- a/packages/teleport-plugin-html-base-component/src/node-handlers.ts
+++ b/packages/teleport-plugin-html-base-component/src/node-handlers.ts
@@ -29,6 +29,16 @@ import { staticNode } from '@teleporthq/teleport-uidl-builders'
 import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
 import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
 
+const isValidURL = (url: string) => {
+  try {
+    /* tslint:disable:no-unused-expression */
+    new URL(url)
+    return true
+  } catch (error) {
+    return false
+  }
+}
+
 type NodeToHTML<NodeType, ReturnType> = (
   node: NodeType,
   templatesLookUp: Record<string, unknown>,
@@ -465,7 +475,7 @@ const handleAttributes = (
         elementType of image is always mapped to img.
         For reference, check `html-mapping` file.
       */
-      if (elementType === 'img' && attrKey === 'src') {
+      if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
         /*
           By default we just prefix all the asset paths with just the
           assetPrefix that is configured in the project. But for `html` generators
diff --git a/packages/teleport-plugin-react-app-routing/src/index.ts b/packages/teleport-plugin-react-app-routing/src/index.ts
index bb128e25..86782c16 100644
--- a/packages/teleport-plugin-react-app-routing/src/index.ts
+++ b/packages/teleport-plugin-react-app-routing/src/index.ts
@@ -37,45 +37,55 @@ export const createReactAppRoutingPlugin: ComponentPluginFactory<AppRoutingCompo
     registerReactRouterDeps(dependencies)
 
     const { stateDefinitions = {} } = uidl
+    const routeValues = (stateDefinitions.route as UIDLRouteDefinitions).values || []
 
     const routes = UIDLUtils.extractRoutes(uidl as UIDLRootComponent)
     const strategy = options.strategy
     const pageDependencyPrefix = options.localDependenciesPrefix || './'
 
-    const routeJSXDefinitions = routes.map((conditionalNode) => {
-      const { value: routeKey } = conditionalNode.content
-      const routeValues = (stateDefinitions.route as UIDLRouteDefinitions).values || []
-      const routeDefinition = routeValues.find((route) => route.value === routeKey)
-      const defaultOptions: UIDLPageOptions = {}
-      const { fileName, componentName, navLink, fallback } =
-        routeDefinition.pageOptions || defaultOptions
-
-      /* If pages are exported in their own folder and in custom file names.
+    const routeJSXDefinitions = routeValues
+      .sort((routeA) => {
+        if (routeA?.pageOptions?.fallback) {
+          return 1
+        }
+      })
+      .map((routeDefinition) => {
+        const page = routes.find(
+          (routeNode) => routeNode.content.value.toString() === routeDefinition.value
+        )
+        if (!page) {
+          throw new Error(`Failed to match route ${routeDefinition.value} with a page`)
+        }
+        const defaultOptions: UIDLPageOptions = {}
+        const { fileName, componentName, navLink, fallback } =
+          routeDefinition.pageOptions || defaultOptions
+
+        /* If pages are exported in their own folder and in custom file names.
          Import statements must then be:
 
          import Home from '../pages/home/component'
 
          so the `/component` suffix is computed below.
       */
-      const pageStrategyOptions = (strategy && strategy.pages.options) || {}
-      const pageComponentSuffix = pageStrategyOptions.createFolderForEachComponent ? '/index' : ''
+        const pageStrategyOptions = (strategy && strategy.pages.options) || {}
+        const pageComponentSuffix = pageStrategyOptions.createFolderForEachComponent ? '/index' : ''
 
-      /*
+        /*
         Now, navLink is being used to create a folder strucutre.
         So, it is important to append the same when generating the path
       */
 
-      dependencies[componentName] = {
-        type: 'local',
-        path: `${pageDependencyPrefix}${join(
-          ...navLink.split('/')?.slice(0, -1),
-          fileName,
-          pageComponentSuffix
-        )}`,
-      }
-
-      return constructRouteJSX(componentName, navLink, fallback)
-    })
+        dependencies[componentName] = {
+          type: 'local',
+          path: `${pageDependencyPrefix}${join(
+            ...navLink.split('/')?.slice(0, -1),
+            fileName,
+            pageComponentSuffix
+          )}`,
+        }
+
+        return constructRouteJSX(componentName, navLink, fallback)
+      })
 
     const rootRouterTag = createRouteRouterTag(routeJSXDefinitions)
 
diff --git a/packages/teleport-plugin-react-styled-components/src/style-sheet.ts b/packages/teleport-plugin-react-styled-components/src/style-sheet.ts
index 0b859bd8..65020327 100644
--- a/packages/teleport-plugin-react-styled-components/src/style-sheet.ts
+++ b/packages/teleport-plugin-react-styled-components/src/style-sheet.ts
@@ -19,9 +19,9 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
   const styleSheetPlugin: ComponentPlugin = async (structure) => {
     const { uidl, chunks, dependencies } = structure
     const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl
-    if (Object.keys(styleSetDefinitions).length === 0 && Object.keys(tokens).length === 0) {
-      return structure
-    }
+
+    uidl.outputOptions = uidl.outputOptions || {}
+    uidl.outputOptions.fileName = fileName
 
     const tokensMap: Record<string, string | number> = Object.keys(tokens || {}).reduce(
       (acc: Record<string, string | number>, key: string) => {
@@ -75,9 +75,6 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
       })
     }
 
-    uidl.outputOptions = uidl.outputOptions || {}
-    uidl.outputOptions.fileName = fileName
-
     return structure
   }
   return styleSheetPlugin
diff --git a/packages/teleport-plugin-vue-app-routing/src/index.ts b/packages/teleport-plugin-vue-app-routing/src/index.ts
index ffdc2380..0128c5a4 100644
--- a/packages/teleport-plugin-vue-app-routing/src/index.ts
+++ b/packages/teleport-plugin-vue-app-routing/src/index.ts
@@ -64,33 +64,51 @@ export const createVueAppRoutingPlugin: ComponentPluginFactory<VueRouterConfig>
     const pageStrategyOptions = (options.strategy && options.strategy.pages.options) || {}
     const pageComponentSuffix = pageStrategyOptions.createFolderForEachComponent ? '/index' : ''
 
-    const routesAST = routes.map((routeNode) => {
-      const pageKey = routeNode.content.value.toString()
-
-      const pageDefinition = routeValues.find((route) => route.value === pageKey)
-      const defaultOptions: UIDLPageOptions = {}
-      const { componentName, navLink, fileName } = pageDefinition.pageOptions || defaultOptions
-
-      /*
+    const routesAST = routeValues
+      .sort((routeA) => {
+        if (routeA?.pageOptions?.fallback) {
+          return 1
+        }
+      })
+      .map((route) => {
+        const page = routes.find((routeNode) => routeNode.content.value.toString() === route.value)
+        if (!page) {
+          throw new Error(`Failed to match route ${route.value} with a page`)
+        }
+
+        const pageKey = page.content.value.toString()
+
+        const defaultOptions: UIDLPageOptions = {}
+        const { componentName, navLink, fileName, fallback } = route.pageOptions || defaultOptions
+
+        /*
         Now, navLink is being used to create a folder strucutre.
         So, it is important to append the same when generating the path
       */
 
-      dependencies[componentName] = {
-        type: 'local',
-        path: `${pageDependencyPrefix}${join(
-          ...navLink.split('/')?.slice(0, -1),
-          fileName,
-          pageComponentSuffix
-        )}`,
-      }
-
-      return types.objectExpression([
-        types.objectProperty(types.identifier('name'), types.stringLiteral(pageKey)),
-        types.objectProperty(types.identifier('path'), types.stringLiteral(navLink)),
-        types.objectProperty(types.identifier('component'), types.identifier(componentName)),
-      ])
-    })
+        dependencies[componentName] = {
+          type: 'local',
+          path: `${pageDependencyPrefix}${join(
+            ...navLink.split('/')?.slice(0, -1),
+            fileName,
+            pageComponentSuffix
+          )}`,
+        }
+
+        const routeObject = types.objectExpression([
+          types.objectProperty(types.identifier('name'), types.stringLiteral(pageKey)),
+          types.objectProperty(types.identifier('path'), types.stringLiteral(navLink)),
+          types.objectProperty(types.identifier('component'), types.identifier(componentName)),
+        ])
+
+        if (fallback) {
+          routeObject.properties.push(
+            types.objectProperty(types.identifier('fallback'), types.booleanLiteral(true))
+          )
+        }
+
+        return routeObject
+      })
 
     const exportStatement = types.exportDefaultDeclaration(
       types.newExpression(types.identifier('Router'), [
diff --git a/packages/teleport-project-generator-angular/src/index.ts b/packages/teleport-project-generator-angular/src/index.ts
index ec514e99..ccc778e4 100644
--- a/packages/teleport-project-generator-angular/src/index.ts
+++ b/packages/teleport-project-generator-angular/src/index.ts
@@ -58,7 +58,7 @@ const createAngularProjectGenerator = () => {
     },
     projectStyleSheet: {
       generator: createComponentGenerator,
-      plugins: [createStyleSheetPlugin({ fileName: 'styles' })],
+      plugins: [createStyleSheetPlugin({ fileName: 'styles', relativeFontPath: true })],
       fileName: 'styles',
       path: ['src'],
     },
diff --git a/packages/teleport-project-generator-angular/src/project-template.ts b/packages/teleport-project-generator-angular/src/project-template.ts
index 131251ea..cc58375b 100644
--- a/packages/teleport-project-generator-angular/src/project-template.ts
+++ b/packages/teleport-project-generator-angular/src/project-template.ts
@@ -35,7 +35,7 @@ export const template: GeneratedFolder = {
     "@angular/cli": "^14.0.0",
     "@angular/compiler-cli": "^14.0.0",
     "@types/node": "^12.11.1",
-    "typescript": "^4.7.3"
+    "typescript": "~4.7.3"
   }
 }`,
       fileType: 'json',
diff --git a/packages/teleport-project-generator-html/src/index.ts b/packages/teleport-project-generator-html/src/index.ts
index 6f57445a..245fee92 100644
--- a/packages/teleport-project-generator-html/src/index.ts
+++ b/packages/teleport-project-generator-html/src/index.ts
@@ -30,7 +30,7 @@ const createHTMLProjectGenerator = (config?: { individualEntyFile: boolean }) =>
     },
     projectStyleSheet: {
       generator: createComponentGenerator,
-      plugins: [createStyleSheetPlugin()],
+      plugins: [createStyleSheetPlugin({ fileName: 'style', relativeFontPath: true })],
       fileName: 'style',
       path: [''],
       importFile: true,
diff --git a/packages/teleport-project-generator-html/src/plugin-clone-globals.ts b/packages/teleport-project-generator-html/src/plugin-clone-globals.ts
index 9d50f2c5..e7dd837f 100644
--- a/packages/teleport-project-generator-html/src/plugin-clone-globals.ts
+++ b/packages/teleport-project-generator-html/src/plugin-clone-globals.ts
@@ -6,6 +6,7 @@ import {
 } from '@teleporthq/teleport-types'
 import prettierHTML from '@teleporthq/teleport-postprocessor-prettier-html'
 import { load } from 'cheerio'
+import { relative, join } from 'path'
 
 class ProjectPluginCloneGlobals implements ProjectPlugin {
   async runBefore(structure: ProjectPluginStructure) {
@@ -31,10 +32,6 @@ class ProjectPluginCloneGlobals implements ProjectPlugin {
     parsedEntry('head').find('meta').remove()
     parsedEntry('head').find('title').remove()
 
-    if (Object.values(uidl.root?.styleSetDefinitions || {}).length > 0) {
-      parsedEntry('head').append(`<link rel="stylesheet" href="./style.css"></link>`)
-    }
-
     parsedEntry('head').append(scriptTagsFromRootHead.toString())
 
     const memoryFiles = Object.fromEntries(files)
@@ -51,6 +48,19 @@ class ProjectPluginCloneGlobals implements ProjectPlugin {
 
             const parsedIndividualFile = load(file.content)
 
+            if (Object.values(uidl.root?.styleSetDefinitions || {}).length > 0) {
+              const relativePath = join(
+                relative(
+                  join(...fileId.path.filter(Boolean)),
+                  join(...structure.strategy.projectStyleSheet?.path)
+                ),
+                '.'
+              )
+              parsedIndividualFile('head').append(
+                `<link rel="stylesheet" href="${relativePath}/${structure.strategy.projectStyleSheet.fileName}.css"></link>`
+              )
+            }
+
             const metaTags = parsedIndividualFile.root().find('meta')
             parsedEntry('head').prepend(metaTags.toString().concat(metaTagsFromRoot))
             metaTags.remove()
diff --git a/packages/teleport-project-generator-react/src/project-template.ts b/packages/teleport-project-generator-react/src/project-template.ts
index 8b2ad47d..0aa5061b 100644
--- a/packages/teleport-project-generator-react/src/project-template.ts
+++ b/packages/teleport-project-generator-react/src/project-template.ts
@@ -21,7 +21,7 @@ export default {
     "eject": "craco eject"
   },
   "engines": {
-    "node": "16.x"
+    "node": "18.x"
   },
   "browserslist": {
     "production": [
diff --git a/packages/teleport-project-generator/src/index.ts b/packages/teleport-project-generator/src/index.ts
index c9bd5404..1f96878a 100644
--- a/packages/teleport-project-generator/src/index.ts
+++ b/packages/teleport-project-generator/src/index.ts
@@ -15,6 +15,7 @@ import {
   GeneratorFactoryParams,
   HTMLComponentGenerator,
   ProjectGenerator as ProjectGeneratorType,
+  UIDLLocalFontAsset,
 } from '@teleporthq/teleport-types'
 import {
   injectFilesToPath,
@@ -212,7 +213,7 @@ export class ProjectGenerator implements ProjectGeneratorType {
     }
 
     const { components = {} } = uidl
-    const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl.root
+    const { styleSetDefinitions = {}, designLanguage: {} = {} } = uidl.root
 
     // Based on the routing roles, separate pages into distict UIDLs with their own file names and paths
     const pageUIDLs = createPageUIDLs(uidl, this.strategy)
@@ -236,22 +237,47 @@ export class ProjectGenerator implements ProjectGeneratorType {
         prefix: assetsPrefix,
         mappings: this.assetsAndPathMapping,
         identifier: this.assetIdentifier,
+        fontsFolder: join(
+          ...(this.strategy?.static?.path || []),
+          this.assetIdentifier ?? '',
+          'fonts'
+        ),
+        localFonts: uidl.globals?.assets.filter(
+          (asset) => asset.type === 'local-font'
+        ) as UIDLLocalFontAsset[],
       },
       projectRouteDefinition: uidl.root.stateDefinitions.route,
+      designLanguage: uidl.root?.designLanguage,
       mapping,
       skipValidation: true,
-      designLanguage: uidl.root?.designLanguage,
+      ...(this.strategy.projectStyleSheet?.generator &&
+        this.strategy.projectStyleSheet?.path && {
+          projectStyleSet: {
+            styleSetDefinitions,
+            fileName: this.strategy.projectStyleSheet?.fileName,
+            path: this.strategy.pages.options?.createFolderForEachComponent
+              ? join('..', ...this.strategy.projectStyleSheet.path)
+              : join(...this.strategy.projectStyleSheet?.path),
+            importFile: this.strategy.projectStyleSheet?.importFile || false,
+          },
+        }),
     }
 
     // Handling project style sheet
-    if (
-      this.strategy.projectStyleSheet?.generator &&
-      (Object.keys(styleSetDefinitions).length > 0 || Object.keys(tokens).length > 0)
-    ) {
-      const { files, dependencies } = await this.styleSheetGenerator.generateComponent(uidl.root, {
-        isRootComponent: true,
-        assets: options.assets,
-      })
+    if (this.strategy.projectStyleSheet?.generator) {
+      const { files, dependencies } = await this.styleSheetGenerator.generateComponent(
+        {
+          ...uidl.root,
+          outputOptions: {
+            folderPath: this.strategy.projectStyleSheet.path,
+          },
+        },
+        {
+          isRootComponent: true,
+          ...options,
+        }
+      )
+
       inMemoryFilesMap.set('projectStyleSheet', {
         path: this.strategy.projectStyleSheet.path,
         files,
@@ -267,26 +293,6 @@ export class ProjectGenerator implements ProjectGeneratorType {
         )
       }
 
-      let pageOptions = options
-      if (this.strategy.projectStyleSheet) {
-        const globalStyleSheetPath = generateLocalDependenciesPrefix(
-          this.strategy.pages.path,
-          this.strategy.projectStyleSheet.path
-        )
-        pageOptions = {
-          ...options,
-          projectStyleSet: {
-            styleSetDefinitions,
-            fileName: this.strategy.projectStyleSheet.fileName,
-            path: this.strategy.pages.options?.createFolderForEachComponent
-              ? join('..', globalStyleSheetPath)
-              : globalStyleSheetPath,
-            importFile: this.strategy.projectStyleSheet?.importFile || false,
-          },
-          designLanguage: uidl.root?.designLanguage,
-        }
-      }
-
       if ('addExternalComponents' in this.pageGenerator) {
         ;(this.pageGenerator as unknown as HTMLComponentGenerator).addExternalComponents({
           externals: components,
@@ -295,7 +301,7 @@ export class ProjectGenerator implements ProjectGeneratorType {
         })
       }
 
-      const { files, dependencies } = await createPage(pageUIDL, this.pageGenerator, pageOptions)
+      const { files, dependencies } = await createPage(pageUIDL, this.pageGenerator, options)
       // Pages might be generated inside subfolders in the main pages folder
       const relativePath = UIDLUtils.getComponentFolderPath(pageUIDL)
       const path = this.strategy.pages.path.concat(relativePath)
@@ -354,7 +360,7 @@ export class ProjectGenerator implements ProjectGeneratorType {
 
       let componentOptions = options
       if (this.strategy.projectStyleSheet) {
-        const globalStyleSheetPath = generateLocalDependenciesPrefix(
+        const globalStyleSheetPathForComponents = generateLocalDependenciesPrefix(
           this.strategy.components.path,
           this.strategy.projectStyleSheet.path
         )
@@ -364,8 +370,8 @@ export class ProjectGenerator implements ProjectGeneratorType {
             styleSetDefinitions,
             fileName: this.strategy.projectStyleSheet.fileName,
             path: this.strategy.components?.options?.createFolderForEachComponent
-              ? join('..', globalStyleSheetPath)
-              : globalStyleSheetPath,
+              ? join('..', globalStyleSheetPathForComponents)
+              : globalStyleSheetPathForComponents,
             importFile: this.strategy.projectStyleSheet?.importFile || false,
           },
           designLanguage: uidl.root?.designLanguage,
diff --git a/packages/teleport-types/src/generators.ts b/packages/teleport-types/src/generators.ts
index 20d2f984..abaa199b 100644
--- a/packages/teleport-types/src/generators.ts
+++ b/packages/teleport-types/src/generators.ts
@@ -8,6 +8,7 @@ import {
   UIDLDesignTokens,
   UIDLRootComponent,
   UIDLRouteDefinitions,
+  UIDLLocalFontAsset,
 } from './uidl'
 
 export enum FileType {
@@ -113,6 +114,8 @@ export interface GeneratorOptions {
     prefix?: string
     identifier?: string | null
     mappings?: Record<string, string>
+    fontsFolder?: string
+    localFonts?: UIDLLocalFontAsset[]
   }
   mapping?: Mapping
   skipValidation?: boolean
diff --git a/packages/teleport-types/src/uidl.ts b/packages/teleport-types/src/uidl.ts
index 8a1510e3..f85fc7a3 100755
--- a/packages/teleport-types/src/uidl.ts
+++ b/packages/teleport-types/src/uidl.ts
@@ -73,10 +73,18 @@ export interface UIDLFontAsset {
   path: string
   attrs?: Record<string, UIDLStaticValue>
 }
+
+export interface UIDLLocalFontAsset {
+  type: 'local-font'
+  path: string
+  properties: Record<string, UIDLStaticValue>
+}
+
 export interface UIDLCanonicalAsset {
   type: 'canonical'
   path: string
 }
+
 export interface UIDLIconAsset {
   type: 'icon'
   path: string
@@ -93,6 +101,7 @@ export type UIDLGlobalAsset =
   | UIDLFontAsset
   | UIDLCanonicalAsset
   | UIDLIconAsset
+  | UIDLLocalFontAsset
 
 export interface ComponentUIDL {
   name: string
diff --git a/packages/teleport-types/src/vuidl.ts b/packages/teleport-types/src/vuidl.ts
index f048cfe2..118cd489 100644
--- a/packages/teleport-types/src/vuidl.ts
+++ b/packages/teleport-types/src/vuidl.ts
@@ -39,6 +39,7 @@ import {
   UIDLStateValueDetails,
   UIDLRouteDefinitions,
   UIDLStateDefinition,
+  UIDLLocalFontAsset,
 } from './uidl'
 import { Modify } from './helper'
 
@@ -225,6 +226,7 @@ export type VUIDLGlobalAsset =
   | UIDLIconAsset
   | VUIDLStyleInlineAsset
   | VUIDLFontAsset
+  | UIDLLocalFontAsset
 
 export type VUIDLStyleInlineAsset = Modify<
   UIDLStyleInlineAsset,
diff --git a/packages/teleport-uidl-resolver/src/resolvers/abilities/utils.ts b/packages/teleport-uidl-resolver/src/resolvers/abilities/utils.ts
index 463ddd23..78cd68ef 100644
--- a/packages/teleport-uidl-resolver/src/resolvers/abilities/utils.ts
+++ b/packages/teleport-uidl-resolver/src/resolvers/abilities/utils.ts
@@ -5,7 +5,6 @@ import {
   UIDLElementNode,
   UIDLAttributeValue,
 } from '@teleporthq/teleport-types'
-import { join } from 'path'
 
 export const insertLinks = (
   node: UIDLElementNode,
@@ -196,12 +195,6 @@ const resolveNavlink = (routeName: string, options: GeneratorOptions) => {
     return transitionRoute.pageOptions.navLink
   }
 
-  /*
-    We are using `navLink` + fileName for the target location calculation.
-    See `teleport-project-generator` -> createPageUIDL for more info.
-  */
-
-  return transitionRoute?.pageOptions?.navLink && transitionRoute?.pageOptions?.fileName
-    ? join(transitionRoute.pageOptions.navLink, `../${transitionRoute.pageOptions.fileName}`)
-    : transitionRoute.pageOptions.navLink ?? `/${friendlyURL}`
+  const { pageOptions } = transitionRoute
+  return pageOptions.navLink ?? `/${friendlyURL}`
 }
diff --git a/packages/teleport-uidl-validator/src/decoders/utils.ts b/packages/teleport-uidl-validator/src/decoders/utils.ts
index 478ead42..53fd7196 100644
--- a/packages/teleport-uidl-validator/src/decoders/utils.ts
+++ b/packages/teleport-uidl-validator/src/decoders/utils.ts
@@ -73,6 +73,7 @@ import {
   UIDLComponentStyleReference,
   UIDLInjectValue,
   VUIDLStateValueDetails,
+  UIDLLocalFontAsset,
 } from '@teleporthq/teleport-types'
 import { CustomCombinators } from './custom-combinators'
 
@@ -191,7 +192,8 @@ export const globalAssetsDecoder: Decoder<VUIDLGlobalAsset> = union(
   lazy(() => externalStyleAssetDecoder),
   lazy(() => fontAssetDecoder),
   lazy(() => canonicalAssetDecoder),
-  lazy(() => iconAssetDecoder)
+  lazy(() => iconAssetDecoder),
+  lazy(() => localFontDecoder)
 )
 
 export const baseAssetDecoder: Decoder<UIDLAssetBase> = object({
@@ -237,6 +239,12 @@ export const fontAssetDecoder: Decoder<VUIDLFontAsset> = object({
   path: string(),
 })
 
+export const localFontDecoder: Decoder<UIDLLocalFontAsset> = object({
+  type: constant('local-font' as const),
+  path: string(),
+  properties: dict(staticValueDecoder),
+})
+
 export const canonicalAssetDecoder: Decoder<UIDLCanonicalAsset> = object({
   type: constant('canonical' as const),
   path: string(),


Test patch:
diff --git a/packages/teleport-plugin-css-modules/__tests__/style-sheet.ts b/packages/teleport-plugin-css-modules/__tests__/style-sheet.ts
index 06a88f7e..0ad32219 100644
--- a/packages/teleport-plugin-css-modules/__tests__/style-sheet.ts
+++ b/packages/teleport-plugin-css-modules/__tests__/style-sheet.ts
@@ -99,7 +99,8 @@ describe('plugin-css-modules-style-sheet', () => {
 
     const result = await plugin(structure)
 
-    expect(result).toBe(undefined)
+    expect(result.chunks.length).toBe(1)
+    expect(structure.chunks[0]).toEqual(result.chunks[0])
   })
 
   it('Generates styles from UIDLStyleSetDefinitions', () => {
diff --git a/packages/teleport-plugin-css/__tests__/style-sheet.ts b/packages/teleport-plugin-css/__tests__/style-sheet.ts
index 9ad8fa3b..9547d640 100644
--- a/packages/teleport-plugin-css/__tests__/style-sheet.ts
+++ b/packages/teleport-plugin-css/__tests__/style-sheet.ts
@@ -5,7 +5,7 @@ import {
 } from '@teleporthq/teleport-types'
 import { staticNode, dynamicNode } from '@teleporthq/teleport-uidl-builders'
 import { createStyleSheetPlugin } from '../src/style-sheet'
-import { setUpStructureWithHASTChunk } from './mocks'
+import { setUpHASTChunk, setUpStructureWithHASTChunk } from './mocks'
 
 describe('plugin-css-style-sheet', () => {
   it('should generate css when the styleSetDefinitions are presnet', async () => {
@@ -101,7 +101,7 @@ describe('plugin-css-style-sheet', () => {
     const structure = setUpStructureWithHASTChunk()
 
     const result = await plugin(structure)
-
-    expect(result).toBe(undefined)
+    expect(result.chunks.length).toBe(1)
+    expect(result.chunks[0]).toEqual(setUpHASTChunk())
   })
 })
diff --git a/packages/teleport-project-generator/__tests__/index.ts b/packages/teleport-project-generator/__tests__/index.ts
index 6ac2da71..201d8523 100644
--- a/packages/teleport-project-generator/__tests__/index.ts
+++ b/packages/teleport-project-generator/__tests__/index.ts
@@ -51,6 +51,8 @@ describe('Generic Project Generator', () => {
             identifier: null,
             mappings: {},
             prefix: '/test/static',
+            fontsFolder: 'test/static/fonts',
+            localFonts: [],
           },
           designLanguage: undefined,
           mapping: {},
@@ -114,6 +116,8 @@ describe('Generic Project Generator', () => {
             identifier: null,
             mappings: {},
             prefix: '/static',
+            fontsFolder: 'test/static/fonts',
+            localFonts: [],
           },
           designLanguage: undefined,
           projectRouteDefinition: uidl.root.stateDefinitions.route,
@@ -131,6 +135,8 @@ describe('Generic Project Generator', () => {
             identifier: null,
             mappings: {},
             prefix: '/static',
+            fontsFolder: 'test/static/fonts',
+            localFonts: [],
           },
           designLanguage: undefined,
           projectRouteDefinition: uidl.root.stateDefinitions.route,
@@ -183,6 +189,8 @@ describe('Generic Project Generator', () => {
             identifier: null,
             mappings: {},
             prefix: '/test/static',
+            fontsFolder: 'test/static/fonts',
+            localFonts: [],
           },
           designLanguage: undefined,
           projectRouteDefinition: uidl.root.stateDefinitions.route,
diff --git a/packages/teleport-test/src/standalone.ts b/packages/teleport-test/src/standalone.ts
index 9a2c3214..79759e57 100644
--- a/packages/teleport-test/src/standalone.ts
+++ b/packages/teleport-test/src/standalone.ts
@@ -1,4 +1,4 @@
-import { readFileSync, mkdirSync, rmdirSync } from 'fs'
+import { readFileSync, rmdirSync, mkdirSync } from 'fs'
 import { join } from 'path'
 import chalk from 'chalk'
 import { packProject } from '@teleporthq/teleport-code-generator'
@@ -50,6 +50,13 @@ const packerOptions: PackerOptions = {
       location: 'remote',
       path: ['one', 'two'],
     },
+    {
+      content:
+        'https://storage.googleapis.com/playground-bucket-v2.teleporthq.io/8db63146-c3cc-47b2-a38d-1f2b39418d4e/8f055b25-4689-4305-b41a-0655571542ca',
+      name: 'super-funky.ttf',
+      location: 'remote',
+      path: ['fonts'],
+    },
   ],
 }
 
@@ -111,8 +118,7 @@ const run = async () => {
       return `${ProjectType.NEXT} - Parse Embeds`
     })
 
-    // /* Frameworks using Css-Modules */
-
+    /* Frameworks using Css-Modules */
     await log(async () => {
       result = await packProject(projectUIDL, {
         ...packerOptions,