Skip to content

Commit

Permalink
[8.6] [@kbn/handlebars] Split index.ts into several files (elastic#15…
Browse files Browse the repository at this point in the history
…0230) (elastic#150422)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[@kbn/handlebars] Split index.ts into several files
(elastic#150230)](elastic#150230)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Thomas
Watson","email":"[email protected]"},"sourceCommit":{"committedDate":"2023-02-07T09:39:10Z","message":"[@kbn/handlebars]
Split index.ts into several files (elastic#150230)\n\nAn attempt to make the
code a little bit easier to
parse","sha":"eafa5e7f0536715e26d71c3e104525ddd7551a14","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","v8.7.0"],"number":150230,"url":"https://github.com/elastic/kibana/pull/150230","mergeCommit":{"message":"[@kbn/handlebars]
Split index.ts into several files (elastic#150230)\n\nAn attempt to make the
code a little bit easier to
parse","sha":"eafa5e7f0536715e26d71c3e104525ddd7551a14"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/150230","number":150230,"mergeCommit":{"message":"[@kbn/handlebars]
Split index.ts into several files (elastic#150230)\n\nAn attempt to make the
code a little bit easier to
parse","sha":"eafa5e7f0536715e26d71c3e104525ddd7551a14"}}]}] BACKPORT-->
  • Loading branch information
Thomas Watson authored Feb 7, 2023
1 parent 3f3846c commit b8d278c
Show file tree
Hide file tree
Showing 8 changed files with 1,081 additions and 1,011 deletions.
1,017 changes: 13 additions & 1,004 deletions packages/kbn-handlebars/index.ts

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions packages/kbn-handlebars/src/__jest__/test_bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
* See `packages/kbn-handlebars/LICENSE` for more information.
*/

import Handlebars, {
type DecoratorFunction,
type DecoratorsHash,
type ExtendedCompileOptions,
type ExtendedRuntimeOptions,
} from '../..';
import Handlebars from '../..';
import type {
DecoratorFunction,
DecoratorsHash,
ExtendedCompileOptions,
ExtendedRuntimeOptions,
} from '../types';

type CompileFns = 'compile' | 'compileAST';
const compileFns: CompileFns[] = ['compile', 'compileAST'];
Expand Down
56 changes: 56 additions & 0 deletions packages/kbn-handlebars/src/handlebars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Elasticsearch B.V licenses this file to you under the MIT License.
* See `packages/kbn-handlebars/LICENSE` for more information.
*/

// The handlebars module uses `export =`, so we should technically use `import Handlebars = require('handlebars')`, but Babel will not allow this:
// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require
import Handlebars from 'handlebars';

import type { DecoratorsHash, ExtendedCompileOptions, ExtendedRuntimeOptions } from './types';
import { ElasticHandlebarsVisitor } from './visitor';

const originalCreate = Handlebars.create;

export { Handlebars };

/**
* Creates an isolated Handlebars environment.
*
* Each environment has its own helpers.
* This is only necessary for use cases that demand distinct helpers.
* Most use cases can use the root Handlebars environment directly.
*
* @returns A sandboxed/scoped version of the @kbn/handlebars module
*/
Handlebars.create = function (): typeof Handlebars {
const SandboxedHandlebars = originalCreate.call(Handlebars) as typeof Handlebars;
// When creating new Handlebars environments, ensure the custom compileAST function is present in the new environment as well
SandboxedHandlebars.compileAST = Handlebars.compileAST;
return SandboxedHandlebars;
};

/**
* Compiles the given Handlbars template without the use of `eval`.
*
* @returns A render function with the same API as the return value from the regular Handlebars `compile` function.
*/
Handlebars.compileAST = function (
input: string | hbs.AST.Program,
options?: ExtendedCompileOptions
) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Handlebars.Exception(
`You must pass a string or Handlebars AST to Handlebars.compileAST. You passed ${input}`
);
}

// If `Handlebars.compileAST` is reassigned, `this` will be undefined.
const helpers = (this ?? Handlebars).helpers;
const partials = (this ?? Handlebars).partials;
const decorators = (this ?? Handlebars).decorators as DecoratorsHash;

const visitor = new ElasticHandlebarsVisitor(this, input, options, helpers, partials, decorators);
return (context: any, runtimeOptions?: ExtendedRuntimeOptions) =>
visitor.render(context, runtimeOptions);
};
8 changes: 8 additions & 0 deletions packages/kbn-handlebars/src/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Elasticsearch B.V licenses this file to you under the MIT License.
* See `packages/kbn-handlebars/LICENSE` for more information.
*/

export const kHelper = Symbol('helper');
export const kAmbiguous = Symbol('ambiguous');
export const kSimple = Symbol('simple');
136 changes: 136 additions & 0 deletions packages/kbn-handlebars/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Elasticsearch B.V licenses this file to you under the MIT License.
* See `packages/kbn-handlebars/LICENSE` for more information.
*/

import { kHelper, kAmbiguous, kSimple } from './symbols';

/**
* A custom version of the Handlesbars module with an extra `compileAST` function and fixed typings.
*/
declare module 'handlebars' {
export function compileAST(
input: string | hbs.AST.Program,
options?: ExtendedCompileOptions
): (context?: any, options?: ExtendedRuntimeOptions) => string;

// --------------------------------------------------------
// Override/Extend inherited types below that are incorrect
// --------------------------------------------------------

export interface TemplateDelegate<T = any> {
(context?: T, options?: RuntimeOptions): string; // Override to ensure `context` is optional
blockParams?: number; // TODO: Can this really be optional?
partials?: any; // TODO: Narrow type to something better than any?
}

export interface HelperOptions {
name: string;
loc: { start: hbs.AST.SourceLocation['start']; end: hbs.AST.SourceLocation['end'] };
lookupProperty: LookupProperty;
}

export interface HelperDelegate {
// eslint-disable-next-line @typescript-eslint/prefer-function-type
(...params: any[]): any;
}

export function registerPartial(spec: { [name: string]: Handlebars.Template }): void; // Ensure `spec` object values can be strings
}

export type NodeType = typeof kHelper | typeof kAmbiguous | typeof kSimple;

type LookupProperty = <T = any>(parent: { [name: string]: any }, propertyName: string) => T;

export type ProcessableStatementNode =
| hbs.AST.MustacheStatement
| hbs.AST.PartialStatement
| hbs.AST.SubExpression;
export type ProcessableBlockStatementNode = hbs.AST.BlockStatement | hbs.AST.PartialBlockStatement;
export type ProcessableNode = ProcessableStatementNode | ProcessableBlockStatementNode;
export type ProcessableNodeWithPathParts = ProcessableNode & { path: hbs.AST.PathExpression };
export type ProcessableNodeWithPathPartsOrLiteral = ProcessableNode & {
path: hbs.AST.PathExpression | hbs.AST.Literal;
};

export interface Helper {
fn?: Handlebars.HelperDelegate;
context: any[];
params: any[];
options: AmbiguousHelperOptions;
}

export type NonBlockHelperOptions = Omit<Handlebars.HelperOptions, 'fn' | 'inverse'>;
export type AmbiguousHelperOptions = Handlebars.HelperOptions | NonBlockHelperOptions;

export interface DecoratorOptions extends Omit<Handlebars.HelperOptions, 'lookupProperties'> {
args?: any[];
}

/**
* Supported Handlebars compile options.
*
* This is a subset of all the compile options supported by the upstream
* Handlebars module.
*/
export type ExtendedCompileOptions = Pick<
CompileOptions,
| 'data'
| 'knownHelpers'
| 'knownHelpersOnly'
| 'noEscape'
| 'strict'
| 'assumeObjects'
| 'preventIndent'
| 'explicitPartialContext'
>;

/**
* Supported Handlebars runtime options
*
* This is a subset of all the runtime options supported by the upstream
* Handlebars module.
*/
export type ExtendedRuntimeOptions = Pick<
RuntimeOptions,
'data' | 'helpers' | 'partials' | 'decorators' | 'blockParams'
>;

/**
* According to the [decorator docs]{@link https://github.com/handlebars-lang/handlebars.js/blob/4.x/docs/decorators-api.md},
* a decorator will be called with a different set of arugments than what's actually happening in the upstream code.
* So here I assume that the docs are wrong and that the upstream code is correct. In reality, `context` is the last 4
* documented arguments rolled into one object.
*/
export type DecoratorFunction = (
prog: Handlebars.TemplateDelegate,
props: Record<string, any>,
container: Container,
options: any
) => any;

export interface HelpersHash {
[name: string]: Handlebars.HelperDelegate;
}

export interface PartialsHash {
[name: string]: HandlebarsTemplateDelegate;
}

export interface DecoratorsHash {
[name: string]: DecoratorFunction;
}

export interface Container {
helpers: HelpersHash;
partials: PartialsHash;
decorators: DecoratorsHash;
strict: (obj: { [name: string]: any }, name: string, loc: hbs.AST.SourceLocation) => any;
lookupProperty: LookupProperty;
lambda: (current: any, context: any) => any;
data: (value: any, depth: number) => any;
hooks: {
helperMissing?: Handlebars.HelperDelegate;
blockHelperMissing?: Handlebars.HelperDelegate;
};
}
69 changes: 69 additions & 0 deletions packages/kbn-handlebars/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Elasticsearch B.V licenses this file to you under the MIT License.
* See `packages/kbn-handlebars/LICENSE` for more information.
*/

// @ts-expect-error: Could not find a declaration file for module
import { createFrame } from 'handlebars/dist/cjs/handlebars/utils';

import type { AmbiguousHelperOptions, DecoratorOptions } from './types';

export function isBlock(node: hbs.AST.Node): node is hbs.AST.BlockStatement {
return 'program' in node || 'inverse' in node;
}

export function isDecorator(
node: hbs.AST.Node
): node is hbs.AST.Decorator | hbs.AST.DecoratorBlock {
return node.type === 'Decorator' || node.type === 'DecoratorBlock';
}

export function toDecoratorOptions(options: AmbiguousHelperOptions) {
// There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context
delete (options as any).lookupProperty;

return options as DecoratorOptions;
}

export function noop() {
return '';
}

// liftet from handlebars lib/handlebars/runtime.js
export function initData(context: any, data: any) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
data.root = context;
}
return data;
}

// liftet from handlebars lib/handlebars/compiler/compiler.js
export function transformLiteralToPath(node: { path: hbs.AST.PathExpression | hbs.AST.Literal }) {
const pathIsLiteral = 'parts' in node.path === false;

if (pathIsLiteral) {
const literal = node.path;
// @ts-expect-error: Not all `hbs.AST.Literal` sub-types has an `original` property, but that's ok, in that case we just want `undefined`
const original = literal.original;
// Casting to string here to make false and 0 literal values play nicely with the rest
// of the system.
node.path = {
type: 'PathExpression',
data: false,
depth: 0,
parts: [original + ''],
original: original + '',
loc: literal.loc,
};
}
}

export function allowUnsafeEval() {
try {
new Function();
return true;
} catch (e) {
return false;
}
}
Loading

0 comments on commit b8d278c

Please sign in to comment.