From 936b3f0dce3651e04936894522607979e0a4d154 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 13 Aug 2023 11:57:43 +0530 Subject: [PATCH] feat: add supercharged plugin --- src/edge/main.ts | 12 ++++- src/loader.ts | 17 ++++--- src/plugins/supercharged.ts | 94 +++++++++++++++++++++++++++++++++++++ src/types.ts | 13 +++++ tests/edge.spec.ts | 36 ++++++++++++++ 5 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 src/plugins/supercharged.ts diff --git a/src/edge/main.ts b/src/edge/main.ts index f20e1c7..b07548b 100644 --- a/src/edge/main.ts +++ b/src/edge/main.ts @@ -11,6 +11,7 @@ import { Loader } from '../loader.js' import * as Tags from '../tags/main.js' import { Compiler } from '../compiler.js' import { Template } from '../template.js' +import { edgeGlobals } from './globals.js' import { Processor } from '../processor.js' import { EdgeRenderer } from './renderer.js' import type { @@ -20,6 +21,7 @@ import type { LoaderTemplate, LoaderContract, } from '../types.js' +import { pluginSuperCharged } from '../plugins/supercharged.js' /** * Exposes the API to render templates, register custom tags and globals @@ -77,7 +79,7 @@ export class Edge { /** * Globals are shared with all rendered templates */ - globals: { [key: string]: any } = {} + globals: { [key: string]: any } = edgeGlobals /** * List of registered tags. Adding new tags will only impact @@ -98,9 +100,17 @@ export class Edge { async: true, }) + /** + * Registering bundled set of tags + */ Object.keys(Tags).forEach((name) => { this.registerTag(Tags[name as keyof typeof Tags]) }) + + /** + * Registering super charged plugin + */ + this.use(pluginSuperCharged, { recurring: !options.cache }) } /** diff --git a/src/loader.ts b/src/loader.ts index 5203dbf..bbe0c05 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -10,8 +10,8 @@ import { fileURLToPath } from 'node:url' import string from '@poppinss/utils/string' import { join, isAbsolute } from 'node:path' -import { readFileSync, readdirSync } from 'node:fs' -import type { LoaderContract, LoaderTemplate } from './types.js' +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import type { ComponentsTree, LoaderContract, LoaderTemplate } from './types.js' /** * The job of a loader is to load the template from a given path. @@ -50,16 +50,21 @@ export class Loader implements LoaderContract { /** * Returns a list of components for a given disk */ - #getDiskComponents(diskName: string): { componentName: string; tagName: string }[] { + #getDiskComponents(diskName: string): ComponentsTree[0]['components'] { + const componentsDirName = 'components' const diskBasePath = this.#mountedDirs.get(diskName)! - const files = readdirSync(join(diskBasePath, 'components'), { + if (!existsSync(join(diskBasePath, componentsDirName))) { + return [] + } + + const files = readdirSync(join(diskBasePath, componentsDirName), { recursive: true, encoding: 'utf8', }).filter((file) => file.endsWith('.edge')) return files.map((file) => { const fileName = file.replace(/\.edge$/, '') - const componentPath = `components/${fileName}` + const componentPath = `${componentsDirName}/${fileName}` const tagName = fileName .split('/') .filter((segment, index) => { @@ -299,7 +304,7 @@ export class Loader implements LoaderContract { * The return path is same the path you will pass to the `@component` * tag. */ - listComponents(): { diskName: string; components: string[] }[] { + listComponents(): ComponentsTree { const diskNames = [...this.#mountedDirs.keys()] return diskNames.map((diskName) => { return { diff --git a/src/plugins/supercharged.ts b/src/plugins/supercharged.ts new file mode 100644 index 0000000..423348f --- /dev/null +++ b/src/plugins/supercharged.ts @@ -0,0 +1,94 @@ +/* + * edge.js + * + * (c) EdgeJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Edge } from '../edge/main.js' +import { PluginFn } from '../types.js' + +/** + * Hooks into the compiler phase of Edge and converts + * tags to components. + * + * The components are discovered from the components directory + * inside every registered disk. + */ +class SuperChargedComponents { + #edge: Edge + #components: Record = {} + + constructor(edge: Edge) { + this.#edge = edge + this.#claimTags() + this.#transformTags() + } + + /** + * Refreshes the list of components + */ + refreshComponents() { + this.#components = this.#edge.loader + .listComponents() + .reduce>((result, { components }) => { + components.forEach((component) => { + result[component.tagName] = component.componentName + }) + return result + }, {}) + } + + /** + * Registers hook to claim self processing of tags that + * are references to components + */ + #claimTags() { + this.#edge.compiler.claimTag((name) => { + if (this.#components[name]) { + return { seekable: true, block: true } + } + return null + }) + + this.#edge.asyncCompiler.claimTag((name) => { + if (this.#components[name]) { + return { seekable: true, block: true } + } + return null + }) + } + + /** + * Transforms tags to component calls + */ + #transformTags() { + this.#edge.processor.process('tag', ({ tag }) => { + const component = this.#components[tag.properties.name] + if (!component) { + return + } + + tag.properties.name = 'component' + if (tag.properties.jsArg.trim() === '') { + tag.properties.jsArg = `'${component}'` + } else { + tag.properties.jsArg = `'${component}',${tag.properties.jsArg}` + } + }) + } +} + +/** + * The superCharged plugin converts components stored within the + * components directory of all the disk to Edge tags. + */ +let superCharged: SuperChargedComponents +export const pluginSuperCharged: PluginFn<{ recurring: boolean }> = (edge, firstRun) => { + if (firstRun) { + superCharged = new SuperChargedComponents(edge) + } + superCharged.refreshComponents() +} diff --git a/src/types.ts b/src/types.ts index 519c653..59f7555 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,14 @@ export type LoaderTemplate = { template: string } +export type ComponentsTree = { + diskName: string + components: { + componentName: string + tagName: string + }[] +}[] + /** * Loader contract that every loader must adheres to. */ @@ -64,6 +72,11 @@ export interface LoaderContract { * Remove the pre-registered template */ remove(templatePath: string): void + + /** + * Returns a list of components for all the registered disks + */ + listComponents(): ComponentsTree } /** diff --git a/tests/edge.spec.ts b/tests/edge.spec.ts index 0590c87..81b9edc 100644 --- a/tests/edge.spec.ts +++ b/tests/edge.spec.ts @@ -388,6 +388,42 @@ test.group('Edge', () => { const output = await edge.render('hello::foo') assert.equal(output.trim(), 'Hello bar') }) + + test('render components as tags', async ({ assert, fs }) => { + const edge = new Edge() + await fs.create('foo.edge', '@!foo({ username })') + await fs.create('components/foo.edge', 'Hello {{ username }}') + + edge.mount(fs.basePath) + const output = await edge.render('foo', { username: 'virk' }) + assert.equal(output.trim(), 'Hello virk') + }) + + test('refresh components list on each render', async ({ assert, fs }) => { + const edge = new Edge() + await fs.create('foo.edge', '@!foo({ username })') + edge.mount(fs.basePath) + + const output = await edge.render('foo', { username: 'virk' }) + assert.equal(output.trim(), '@!foo({ username })') + + await fs.create('components/foo.edge', 'Hello {{ username }}') + const output1 = await edge.render('foo', { username: 'virk' }) + assert.equal(output1.trim(), 'Hello virk') + }) + + test('do not refresh list when cache mode is enabled', async ({ assert, fs }) => { + const edge = new Edge({ cache: true }) + await fs.create('foo.edge', '@!foo({ username })') + edge.mount(fs.basePath) + + const output = await edge.render('foo', { username: 'virk' }) + assert.equal(output.trim(), '@!foo({ username })') + + await fs.create('components/foo.edge', 'Hello {{ username }}') + const output1 = await edge.render('foo', { username: 'virk' }) + assert.equal(output1.trim(), '@!foo({ username })') + }) }) test.group('Edge | regression', () => {