Skip to content

Commit

Permalink
feature: flexMapPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobrosenberg committed Nov 16, 2023
1 parent 9447efa commit e31dbdd
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 60 deletions.
158 changes: 158 additions & 0 deletions plugins/flexMap/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* @typedef {Object} FlexMapPluginOptions
* @property {Object<string, string[]>} [variationsMap] - an array of variations for each route dir.
*/

/**
* This plugin generates a route file for each specified variation in the `variationsMap` option.
*
* Example:
* If `variationsMap` is set to `{ 'default': ['en', 'de'] }`, the plugin will create two additional
* route files: `routes.default.en.js` and `routes.default.de.js`.
*
* Usage:
* - Components specific to a variation should be suffixed with the variation's key.
* For example, a German variation of the Home component should be named `Home.de.svelte`.
*
* Fallback Variations:
* - Specify fallback variations using a comma-separated list within the array.
* For example, `['en-us,en-uk']` sets up a priority order for route resolution:
* 1. First, Routify 3 will look for a component suffixed with `.en-us.svelte` (e.g., `Home.en-us.svelte`).
* 2. If the `.en-us` variant is not available, it will then try the `.en-uk` variant (e.g., `Home.en-uk.svelte`).
* 3. If neither variant is available, Routify 3 will default to the base component (e.g., `Home.svelte`).
*
* This setup ensures that Routify 3 searches for the most specific variation first, falling back to more general
* variations if the specific ones are not found, thereby providing a flexible and efficient way to manage route variations.
*
* This approach allows for flexible and organized management of route variations based on
* language, theme, or other factors, enhancing the adaptability of your Routify application.
*
* @example
* const routifyConfig = {
* plugins: [
* i18nPlugin({
* variationsMap: {
* default: ['en-us,en','en', 'de'],
* widget: ['en', 'de'],
* }
* })
* ],
* routesDir: {
* 'default': 'src/routes',
* 'widget': 'src/widget'
* },
* }
* @param {Partial<FlexMapPluginOptions>} options
* @returns {RoutifyBuildtimePlugin[]}
*/
export const flexMapsPlugin = options => [
flexMapsPluginInit(options),
flexMapsNormalize(options),
]

/**
* Creates a route dir for each variation in the `variationsMap` option.
* Eg. if `variationsMap` is `{ 'default': ['en', 'de'] }`, the plugin will create
* a `routes.default.en.js` and a `routes.default.de.js` file.
* @params {Partial<FlexMapPluginOptions>} options
* @returns {RoutifyBuildtimePlugin}
*/
function flexMapsPluginInit(options) {
return {
name: 'flexMapPlugin-createRouteDirs',
before: 'filemapper',
build: async ctx => {
let { routesDir } = ctx.instance.options
Object.entries(options.variationsMap).forEach(([routeDir, variations]) => {
variations.forEach(variation => {
const variationRouteDir = `${routeDir}_${variation.split(',')[0]}`
routesDir[variationRouteDir] = routesDir[routeDir]
})
})
},
}
}

const missingBaseComponentWarning = (node, newName, ctx) => {
const variant = node.file.path
const base = newName + node.file.ext
ctx.tools.log.warn(`Node "${variant}" does not have a corresponding "${base}" node.`)
}

/**
*
* @param {Partial<FlexMapPluginOptions>} options
* @returns {RoutifyBuildtimePlugin}
*/
function flexMapsNormalize(options) {
const allRootDirPreferences = createRootDirPreferences(options.variationsMap)

return {
name: 'flexMapPlugin-mergeVariantRoutes',
after: 'metaFromFile',
before: 'exporter',
build: async ctx => {
Object.entries(ctx.instance.rootNodes).forEach(([name, rootNode]) => {
const rootDirPreferences = allRootDirPreferences[name]

;[...(rootDirPreferences?.priorities || [])]
.reverse()
.forEach(priority => {
rootNode.descendants.forEach(node => {
// if node.name ends with the current rootNode language
// remove the language from the name and remove the corresponding node

if (node.name.endsWith(`.${priority}`)) {
const newName = node.name.replace(`.${priority}`, '')

// remove old node
try {
node.parent.traverse(`./${newName}`).remove()
} catch (e) {
if (e.message.match('could not travel to'))
missingBaseComponentWarning(node, newName, ctx)
}

node.name = newName
}
})
})
// else if node.name ends with one of the other variations
// remove the node
rootNode.descendants.forEach(node => {
if (
rootDirPreferences?.unwanted.some(variation =>
node.name.endsWith(`.${variation}`),
)
) {
node.parent.traverse(`./${node.name}`).remove()
}
})
})
},
}
}

/**
*
* @param {Object<string,string[]>} variationsMap
*/
function createRootDirPreferences(variationsMap) {
/** @type {Object<string, {priorities: string[], unwanted: string[]}>} */
const rootDirNameToPriorities = {}

Object.entries(variationsMap).forEach(([_routeDir, variations]) => {
const allVariations = variations.map(variation => variation.split(',')).flat()
variations.forEach(variation => {
const priorities = variation.split(',')
const unwanted = allVariations.filter(
variation => !priorities.includes(variation),
)
const routeDir = `${_routeDir}_${priorities[0]}`

rootDirNameToPriorities[routeDir] = { priorities, unwanted }
})
})

return rootDirNameToPriorities
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
120 changes: 120 additions & 0 deletions plugins/flexMap/spec/flexMapPlugin.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { readFile } from 'fs/promises'
import { flexMapsPlugin } from '../index.js'
import { filemapper } from '../../../lib/buildtime/plugins/filemapper/lib/index.js'
import { RoutifyBuildtime } from '../../../lib/buildtime/RoutifyBuildtime.js'

const __dirname = dirname(fileURLToPath(import.meta.url))

const nameAndPath = node => `${node.name} (${node.file.path})`

const options = {
routifyDir: `${__dirname}/temp`,
routesDir: {
default: `${__dirname}/example`,
basicOnly: `${__dirname}/example`,
withPriorities: `${__dirname}/example`,
},
}

describe('flexMap plugin', async () => {
const instance = new RoutifyBuildtime(options)

const [createRouteDirs, mergeVariantRoutes] = flexMapsPlugin({
variationsMap: {
basicOnly: ['en', 'en-us', 'de'],
withPriorities: ['en-us,en', 'en', 'de'],
},
})

describe('createRouteDirs', async () => {
await createRouteDirs.build({ instance, tools: null })
await filemapper({ instance })
test('basic', () => {
assert.ok(instance.options.routesDir['basicOnly_en'])
assert.ok(instance.options.routesDir['basicOnly_de'])
})
test('withPriorities', () => {
assert.ok(instance.options.routesDir['withPriorities_en-us'])
assert.ok(instance.options.routesDir['withPriorities_en'])
assert.ok(instance.options.routesDir['withPriorities_de'])
})
})

describe('mergeVariantRoutes', async () => {
await mergeVariantRoutes.build({ instance, tools: null })

test('rootNodes', () => {
assert.ok(instance.rootNodes.default)
assert.ok(instance.rootNodes.basicOnly_en)
assert.ok(instance.rootNodes.basicOnly_de)

assert.ok(instance.rootNodes.withPriorities_en)
assert.ok(instance.rootNodes['withPriorities_en-us'])
assert.ok(instance.rootNodes.withPriorities_de)
})

test('default', () => {
assert.deepEqual(instance.rootNodes.default.children.map(nameAndPath), [
'about.de (plugins/flexMap/spec/example/about.de.svelte)',
'about.en (plugins/flexMap/spec/example/about.en.svelte)',
'about (plugins/flexMap/spec/example/about.svelte)',
'index.de (plugins/flexMap/spec/example/index.de.svelte)',
'index.en-us (plugins/flexMap/spec/example/index.en-us.svelte)',
'index.en (plugins/flexMap/spec/example/index.en.svelte)',
'index (plugins/flexMap/spec/example/index.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
])
})

test('basic_en', () => {
assert.deepEqual(instance.rootNodes.basicOnly_en.children.map(nameAndPath), [
'about (plugins/flexMap/spec/example/about.en.svelte)',
'index (plugins/flexMap/spec/example/index.en.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
])
})

test('basic_de', () => {
assert.deepEqual(instance.rootNodes.basicOnly_de.children.map(nameAndPath), [
'about (plugins/flexMap/spec/example/about.de.svelte)',
'index (plugins/flexMap/spec/example/index.de.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
])
})

test('withPriorities_en', () => {
assert.deepEqual(
instance.rootNodes.withPriorities_en.children.map(nameAndPath),
[
'about (plugins/flexMap/spec/example/about.en.svelte)',
'index (plugins/flexMap/spec/example/index.en.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
],
)
})

test('withPriorities_en-us', () => {
assert.deepEqual(
instance.rootNodes['withPriorities_en-us'].children.map(nameAndPath),
[
'about (plugins/flexMap/spec/example/about.en.svelte)',
'index (plugins/flexMap/spec/example/index.en-us.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
],
)
})

test('withPriorities_de', () => {
assert.deepEqual(
instance.rootNodes.withPriorities_de.children.map(nameAndPath),
[
'about (plugins/flexMap/spec/example/about.de.svelte)',
'index (plugins/flexMap/spec/example/index.de.svelte)',
'[...404] (plugins/flexMap/spec/temp/components/[...404].svelte)',
],
)
})
})
})
58 changes: 0 additions & 58 deletions plugins/i18n/index.js

This file was deleted.

4 changes: 2 additions & 2 deletions plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { i18nPlugin } from './i18n/index.js'
import { flexMapsPlugin } from './flexMap/index.js'
import indexByNamePlugin from './indexByName.js'

export { i18nPlugin, indexByNamePlugin }
export { flexMapsPlugin, indexByNamePlugin }

0 comments on commit e31dbdd

Please sign in to comment.