Merge pull request #13255 from Budibase/chore/stringtemplates-to-esm

String-templates to typescript
This commit is contained in:
Adria Navarro 2024-03-18 10:18:02 +01:00 committed by GitHub
commit 9ebec131b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 652 additions and 672 deletions

View File

@ -7,11 +7,12 @@ module.exports = {
if ( if (
/^@budibase\/[^/]+\/.*$/.test(importPath) && /^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests" importPath !== "@budibase/backend-core/tests" &&
importPath !== "@budibase/string-templates/test/utils"
) { ) {
context.report({ context.report({
node, node,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`, message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`,
}) })
} }
}, },

View File

@ -12,8 +12,6 @@ COPY .yarnrc .
COPY packages/server/package.json packages/server/package.json COPY packages/server/package.json packages/server/package.json
COPY packages/worker/package.json packages/worker/package.json COPY packages/worker/package.json packages/worker/package.json
# string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY packages/string-templates/package.json packages/string-templates/package.json
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
@ -26,7 +24,7 @@ RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
RUN echo '' > scripts/syncProPackage.js RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production --frozen-lockfile
# copy the actual code # copy the actual code
COPY packages/server/dist packages/server/dist COPY packages/server/dist packages/server/dist
@ -35,7 +33,6 @@ COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb:v3.3.3 as runner FROM budibase/couchdb:v3.3.3 as runner
@ -100,9 +97,6 @@ COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
EXPOSE 80 EXPOSE 80

@ -1 +1 @@
Subproject commit 0c050591c21d3b67dc0c9225d60cc9e2324c8dac Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f

View File

@ -15,7 +15,8 @@
"@budibase/types": ["../types/src"], "@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"], "@budibase/backend-core": ["../backend-core/src"],
"@budibase/backend-core/*": ["../backend-core/*"], "@budibase/backend-core/*": ["../backend-core/*"],
"@budibase/shared-core": ["../shared-core/src"] "@budibase/shared-core": ["../shared-core/src"],
"@budibase/string-templates": ["../string-templates/src"]
} }
}, },
"include": ["src/**/*"], "include": ["src/**/*"],

View File

@ -1,16 +1,8 @@
{ {
"extends": "./tsconfig.build.json", "extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"resolveJsonModule": true
},
"ts-node": { "ts-node": {
"require": ["tsconfig-paths/register"], "require": ["tsconfig-paths/register"],
"swc": true "swc": true
}, },
"include": ["src/**/*", "package.json"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

@ -1 +1 @@
Subproject commit c4c98ae70f2e936009250893898ecf11f4ddf2c3 Subproject commit 65ac3fc8a20a5244fbe47629cf79678db2d9ae8a

View File

@ -41,17 +41,9 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
COPY packages/string-templates .
WORKDIR /app WORKDIR /app
COPY packages/server/package.json . COPY packages/server/package.json .
COPY packages/server/dist/yarn.lock . COPY packages/server/dist/yarn.lock .
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh

View File

@ -30,6 +30,8 @@ const baseConfig: Config.InitialProjectOptions = {
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/shared-core": "<rootDir>/../shared-core/src", "@budibase/shared-core": "<rootDir>/../shared-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
"@budibase/string-templates/(.*)": ["<rootDir>/../string-templates/$1"],
"@budibase/string-templates": ["<rootDir>/../string-templates/src"],
}, },
} }

View File

@ -1,7 +1,7 @@
import { validate as isValidUUID } from "uuid" import { validate as isValidUUID } from "uuid"
import { processStringSync, encodeJSBinding } from "@budibase/string-templates" import { processStringSync, encodeJSBinding } from "@budibase/string-templates"
const { runJsHelpersTests } = require("@budibase/string-templates/test/utils") import { runJsHelpersTests } from "@budibase/string-templates/test/utils"
import tk from "timekeeper" import tk from "timekeeper"
import { init } from ".." import { init } from ".."

View File

@ -4,6 +4,7 @@
*/ */
module.exports = { module.exports = {
preset: "ts-jest",
// All imported modules in your tests should be mocked automatically // All imported modules in your tests should be mocked automatically
// automock: false, // automock: false,

View File

@ -2,29 +2,28 @@
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.0.0", "version": "0.0.0",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.js", "main": "dist/bundle.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",
"types": "src/index.ts",
"license": "MPL-2.0", "license": "MPL-2.0",
"types": "dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"require": "./src/index.js", "require": "./dist/bundle.cjs",
"import": "./dist/bundle.mjs" "import": "./dist/bundle.mjs"
}, },
"./package.json": "./package.json", "./package.json": "./package.json",
"./test/utils": "./test/utils.js",
"./iife": "./src/iife.js" "./iife": "./src/iife.js"
}, },
"files": [ "files": [
"dist", "dist",
"src", "src"
"manifest.json"
], ],
"scripts": { "scripts": {
"build": "tsc && rollup -c", "build": "tsc --emitDeclarationOnly && rollup -c",
"dev": "concurrently \"tsc --watch\" \"rollup -cw\"", "dev": "rollup -cw",
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
"test": "jest", "test": "jest",
"manifest": "node ./scripts/gen-collection-info.js" "manifest": "ts-node ./scripts/gen-collection-info.ts"
}, },
"dependencies": { "dependencies": {
"@budibase/handlebars-helpers": "^0.13.1", "@budibase/handlebars-helpers": "^0.13.1",
@ -34,8 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-typescript": "8.3.0",
"concurrently": "^8.2.2",
"doctrine": "^3.0.0", "doctrine": "^3.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"marked": "^4.0.10", "marked": "^4.0.10",

View File

@ -4,19 +4,20 @@ import json from "@rollup/plugin-json"
import { terser } from "rollup-plugin-terser" import { terser } from "rollup-plugin-terser"
import builtins from "rollup-plugin-node-builtins" import builtins from "rollup-plugin-node-builtins"
import globals from "rollup-plugin-node-globals" import globals from "rollup-plugin-node-globals"
import typescript from "@rollup/plugin-typescript"
import injectProcessEnv from "rollup-plugin-inject-process-env" import injectProcessEnv from "rollup-plugin-inject-process-env"
const production = !process.env.ROLLUP_WATCH const production = !process.env.ROLLUP_WATCH
export default [ const config = (format, outputFile) => ({
{ input: "src/index.ts",
input: "src/index.mjs",
output: { output: {
sourcemap: !production, sourcemap: !production,
format: "esm", format,
file: "./dist/bundle.mjs", file: outputFile,
}, },
plugins: [ plugins: [
typescript(),
resolve({ resolve({
preferBuiltins: true, preferBuiltins: true,
browser: true, browser: true,
@ -30,5 +31,9 @@ export default [
}), }),
production && terser(), production && terser(),
], ],
}, })
export default [
config("cjs", "./dist/bundle.cjs"),
config("esm", "./dist/bundle.mjs"),
] ]

View File

@ -22,7 +22,7 @@ const COLLECTIONS = [
"object", "object",
"uuid", "uuid",
] ]
const FILENAME = join(__dirname, "..", "manifest.json") const FILENAME = join(__dirname, "..", "src", "manifest.json")
const outputJSON = {} const outputJSON = {}
const ADDED_HELPERS = { const ADDED_HELPERS = {
date: { date: {
@ -126,7 +126,7 @@ const excludeFunctions = { string: ["raw"] }
* This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them. * This script is very specific to purpose, parsing the handlebars-helpers files to attempt to get information about them.
*/ */
function run() { function run() {
const foundNames = [] const foundNames: string[] = []
for (let collection of COLLECTIONS) { for (let collection of COLLECTIONS) {
const collectionFile = fs.readFileSync( const collectionFile = fs.readFileSync(
`${path.dirname(require.resolve(HELPER_LIBRARY))}/lib/${collection}.js`, `${path.dirname(require.resolve(HELPER_LIBRARY))}/lib/${collection}.js`,
@ -147,7 +147,7 @@ function run() {
} }
foundNames.push(name) foundNames.push(name)
// this is ridiculous, but it parse the function header // this is ridiculous, but it parse the function header
const fnc = entry[1].toString() const fnc = entry[1]!.toString()
const jsDocInfo = getCommentInfo(collectionFile, fnc) const jsDocInfo = getCommentInfo(collectionFile, fnc)
let args = jsDocInfo.tags let args = jsDocInfo.tags
.filter(tag => tag.title === "param") .filter(tag => tag.title === "param")
@ -176,8 +176,8 @@ function run() {
} }
// convert all markdown to HTML // convert all markdown to HTML
for (let collection of Object.values(outputJSON)) { for (let collection of Object.values<any>(outputJSON)) {
for (let helper of Object.values(collection)) { for (let helper of Object.values<any>(collection)) {
helper.description = marked.parse(helper.description) helper.description = marked.parse(helper.description)
} }
} }

View File

@ -1,11 +1,11 @@
const { getJsHelperList } = require("../helpers") import { getJsHelperList } from "../helpers"
function getLayers(fullBlock) { function getLayers(fullBlock: string): string[] {
let layers = [] let layers = []
while (fullBlock.length) { while (fullBlock.length) {
const start = fullBlock.lastIndexOf("("), const start = fullBlock.lastIndexOf("("),
end = fullBlock.indexOf(")") end = fullBlock.indexOf(")")
let layer let layer: string
if (start === -1 || end === -1) { if (start === -1 || end === -1) {
layer = fullBlock.trim() layer = fullBlock.trim()
fullBlock = "" fullBlock = ""
@ -21,7 +21,7 @@ function getLayers(fullBlock) {
return layers return layers
} }
function getVariable(variableName) { function getVariable(variableName: string) {
if (!variableName || typeof variableName !== "string") { if (!variableName || typeof variableName !== "string") {
return variableName return variableName
} }
@ -47,10 +47,12 @@ function getVariable(variableName) {
return `$("${variableName}")` return `$("${variableName}")`
} }
function buildList(parts, value) { function buildList(parts: string[], value: any) {
function build() { function build() {
return parts return parts
.map(part => (part.startsWith("helper") ? part : getVariable(part))) .map((part: string) =>
part.startsWith("helper") ? part : getVariable(part)
)
.join(", ") .join(", ")
} }
if (!value) { if (!value) {
@ -60,12 +62,12 @@ function buildList(parts, value) {
} }
} }
function splitBySpace(layer) { function splitBySpace(layer: string) {
const parts = [] const parts: string[] = []
let started = null, let started = null,
endChar = null, endChar = null,
last = 0 last = 0
function add(str) { function add(str: string) {
const startsWith = ["]"] const startsWith = ["]"]
while (startsWith.indexOf(str.substring(0, 1)) !== -1) { while (startsWith.indexOf(str.substring(0, 1)) !== -1) {
str = str.substring(1, str.length) str = str.substring(1, str.length)
@ -103,7 +105,7 @@ function splitBySpace(layer) {
return parts return parts
} }
module.exports.convertHBSBlock = (block, blockNumber) => { export function convertHBSBlock(block: string, blockNumber: number) {
const braceLength = block[2] === "{" ? 3 : 2 const braceLength = block[2] === "{" ? 3 : 2
block = block.substring(braceLength, block.length - braceLength).trim() block = block.substring(braceLength, block.length - braceLength).trim()
const layers = getLayers(block) const layers = getLayers(block)
@ -114,7 +116,7 @@ module.exports.convertHBSBlock = (block, blockNumber) => {
const parts = splitBySpace(layer) const parts = splitBySpace(layer)
if (value || parts.length > 1 || list[parts[0]]) { if (value || parts.length > 1 || list[parts[0]]) {
// first of layer should always be the helper // first of layer should always be the helper
const helper = parts.splice(0, 1) const [helper] = parts.splice(0, 1)
if (list[helper]) { if (list[helper]) {
value = `helpers.${helper}(${buildList(parts, value)})` value = `helpers.${helper}(${buildList(parts, value)})`
} }

View File

@ -1,11 +0,0 @@
class JsErrorTimeout extends Error {
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
constructor() {
super()
}
}
module.exports = {
JsErrorTimeout,
}

View File

@ -0,0 +1,3 @@
export class JsErrorTimeout extends Error {
code = "ERR_SCRIPT_EXECUTION_TIMEOUT"
}

View File

@ -1,29 +0,0 @@
class Helper {
constructor(name, fn, useValueFallback = true) {
this.name = name
this.fn = fn
this.useValueFallback = useValueFallback
}
register(handlebars) {
// wrap the function so that no helper can cause handlebars to break
handlebars.registerHelper(this.name, (value, info) => {
let context = {}
if (info && info.data && info.data.root) {
context = info.data.root
}
const result = this.fn(value, context)
if (result == null) {
return this.useValueFallback ? value : null
} else {
return result
}
})
}
unregister(handlebars) {
handlebars.unregisterHelper(this.name)
}
}
module.exports = Helper

View File

@ -0,0 +1,34 @@
export default class Helper {
private name: any
private fn: any
private useValueFallback: boolean
constructor(name: string, fn: any, useValueFallback = true) {
this.name = name
this.fn = fn
this.useValueFallback = useValueFallback
}
register(handlebars: typeof Handlebars) {
// wrap the function so that no helper can cause handlebars to break
handlebars.registerHelper(
this.name,
(value: any, info: { data: { root: {} } }) => {
let context = {}
if (info && info.data && info.data.root) {
context = info.data.root
}
const result = this.fn(value, context)
if (result == null) {
return this.useValueFallback ? value : null
} else {
return result
}
}
)
}
unregister(handlebars: { unregisterHelper: any }) {
handlebars.unregisterHelper(this.name)
}
}

View File

@ -1,4 +1,4 @@
module.exports.HelperFunctionBuiltin = [ export const HelperFunctionBuiltin = [
"#if", "#if",
"#unless", "#unless",
"#each", "#each",
@ -15,11 +15,11 @@ module.exports.HelperFunctionBuiltin = [
"with", "with",
] ]
module.exports.HelperFunctionNames = { export const HelperFunctionNames = {
OBJECT: "object", OBJECT: "object",
ALL: "all", ALL: "all",
LITERAL: "literal", LITERAL: "literal",
JS: "js", JS: "js",
} }
module.exports.LITERAL_MARKER = "%LITERAL%" export const LITERAL_MARKER = "%LITERAL%"

View File

@ -1,12 +1,22 @@
const dayjs = require("dayjs") import dayjs from "dayjs"
dayjs.extend(require("dayjs/plugin/duration"))
dayjs.extend(require("dayjs/plugin/advancedFormat")) import dayjsDurationPlugin from "dayjs/plugin/duration"
dayjs.extend(require("dayjs/plugin/isoWeek")) import dayjsAdvancedFormatPlugin from "dayjs/plugin/advancedFormat"
dayjs.extend(require("dayjs/plugin/weekYear")) import dayjsIsoWeekPlugin from "dayjs/plugin/isoWeek"
dayjs.extend(require("dayjs/plugin/weekOfYear")) import dayjsWeekYearPlugin from "dayjs/plugin/weekYear"
dayjs.extend(require("dayjs/plugin/relativeTime")) import dayjsWeekOfYearPlugin from "dayjs/plugin/weekOfYear"
dayjs.extend(require("dayjs/plugin/utc")) import dayjsRelativeTimePlugin from "dayjs/plugin/relativeTime"
dayjs.extend(require("dayjs/plugin/timezone")) import dayjsUtcPlugin from "dayjs/plugin/utc"
import dayjsTimezonePlugin from "dayjs/plugin/timezone"
dayjs.extend(dayjsDurationPlugin)
dayjs.extend(dayjsAdvancedFormatPlugin)
dayjs.extend(dayjsIsoWeekPlugin)
dayjs.extend(dayjsWeekYearPlugin)
dayjs.extend(dayjsWeekOfYearPlugin)
dayjs.extend(dayjsRelativeTimePlugin)
dayjs.extend(dayjsUtcPlugin)
dayjs.extend(dayjsTimezonePlugin)
/** /**
* This file was largely taken from the helper-date package - we did this for two reasons: * This file was largely taken from the helper-date package - we did this for two reasons:
@ -17,11 +27,11 @@ dayjs.extend(require("dayjs/plugin/timezone"))
* https://github.com/helpers/helper-date * https://github.com/helpers/helper-date
*/ */
function isOptions(val) { function isOptions(val: any) {
return typeof val === "object" && typeof val.hash === "object" return typeof val === "object" && typeof val.hash === "object"
} }
function isApp(thisArg) { function isApp(thisArg: any) {
return ( return (
typeof thisArg === "object" && typeof thisArg === "object" &&
typeof thisArg.options === "object" && typeof thisArg.options === "object" &&
@ -29,7 +39,7 @@ function isApp(thisArg) {
) )
} }
function getContext(thisArg, locals, options) { function getContext(thisArg: any, locals: any, options: any) {
if (isOptions(thisArg)) { if (isOptions(thisArg)) {
return getContext({}, locals, thisArg) return getContext({}, locals, thisArg)
} }
@ -58,7 +68,7 @@ function getContext(thisArg, locals, options) {
return context return context
} }
function initialConfig(str, pattern, options) { function initialConfig(str: any, pattern: any, options?: any) {
if (isOptions(pattern)) { if (isOptions(pattern)) {
options = pattern options = pattern
pattern = null pattern = null
@ -72,7 +82,7 @@ function initialConfig(str, pattern, options) {
return { str, pattern, options } return { str, pattern, options }
} }
function setLocale(str, pattern, options) { function setLocale(this: any, str: any, pattern: any, options?: any) {
// if options is null then it'll get updated here // if options is null then it'll get updated here
const config = initialConfig(str, pattern, options) const config = initialConfig(str, pattern, options)
const defaults = { lang: "en", date: new Date(config.str) } const defaults = { lang: "en", date: new Date(config.str) }
@ -83,7 +93,7 @@ function setLocale(str, pattern, options) {
dayjs.locale(opts.lang || opts.language) dayjs.locale(opts.lang || opts.language)
} }
module.exports.date = (str, pattern, options) => { export const date = (str: any, pattern: any, options: any) => {
const config = initialConfig(str, pattern, options) const config = initialConfig(str, pattern, options)
// if no args are passed, return a formatted date // if no args are passed, return a formatted date
@ -109,7 +119,7 @@ module.exports.date = (str, pattern, options) => {
return date.format(config.pattern) return date.format(config.pattern)
} }
module.exports.duration = (str, pattern, format) => { export const duration = (str: any, pattern: any, format: any) => {
const config = initialConfig(str, pattern) const config = initialConfig(str, pattern)
setLocale(config.str, config.pattern) setLocale(config.str, config.pattern)

View File

@ -1,6 +1,8 @@
const helpers = require("@budibase/handlebars-helpers") // @ts-ignore we don't have types for it
const { date, duration } = require("./date") import helpers from "@budibase/handlebars-helpers"
const { HelperFunctionBuiltin } = require("./constants")
import { date, duration } from "./date"
import { HelperFunctionBuiltin } from "./constants"
/** /**
* full list of supported helpers can be found here: * full list of supported helpers can be found here:
@ -24,10 +26,10 @@ const ADDED_HELPERS = {
duration: duration, duration: duration,
} }
exports.externalCollections = EXTERNAL_FUNCTION_COLLECTIONS export const externalCollections = EXTERNAL_FUNCTION_COLLECTIONS
exports.addedHelpers = ADDED_HELPERS export const addedHelpers = ADDED_HELPERS
exports.registerAll = handlebars => { export function registerAll(handlebars: typeof Handlebars) {
for (let [name, helper] of Object.entries(ADDED_HELPERS)) { for (let [name, helper] of Object.entries(ADDED_HELPERS)) {
handlebars.registerHelper(name, helper) handlebars.registerHelper(name, helper)
} }
@ -52,17 +54,17 @@ exports.registerAll = handlebars => {
}) })
} }
// add date external functionality // add date external functionality
exports.externalHelperNames = externalNames.concat(Object.keys(ADDED_HELPERS)) externalHelperNames = externalNames.concat(Object.keys(ADDED_HELPERS))
} }
exports.unregisterAll = handlebars => { export function unregisterAll(handlebars: typeof Handlebars) {
for (let name of Object.keys(ADDED_HELPERS)) { for (let name of Object.keys(ADDED_HELPERS)) {
handlebars.unregisterHelper(name) handlebars.unregisterHelper(name)
} }
for (let name of module.exports.externalHelperNames) { for (let name of externalHelperNames) {
handlebars.unregisterHelper(name) handlebars.unregisterHelper(name)
} }
exports.externalHelperNames = [] externalHelperNames = []
} }
exports.externalHelperNames = [] export let externalHelperNames: any[] = []

View File

@ -1,100 +0,0 @@
const Helper = require("./Helper")
const { SafeString } = require("handlebars")
const externalHandlebars = require("./external")
const { processJS } = require("./javascript")
const {
HelperFunctionNames,
HelperFunctionBuiltin,
LITERAL_MARKER,
} = require("./constants")
const { getJsHelperList } = require("./list")
const HTML_SWAPS = {
"<": "&lt;",
">": "&gt;",
}
function isObject(value) {
if (value == null || typeof value !== "object") {
return false
}
return (
value.toString() === "[object Object]" ||
(value.length > 0 && typeof value[0] === "object")
)
}
const HELPERS = [
// external helpers
new Helper(HelperFunctionNames.OBJECT, value => {
return new SafeString(JSON.stringify(value))
}),
// javascript helper
new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements
new Helper(HelperFunctionNames.ALL, (value, inputs) => {
const { __opts } = inputs
if (isObject(value)) {
return new SafeString(JSON.stringify(value))
}
// null/undefined values produce bad results
if (__opts && __opts.onlyFound && value == null) {
return __opts.input
}
if (value == null || typeof value !== "string") {
return value == null ? "" : value
}
if (value && value.string) {
value = value.string
}
let text = value
if (__opts && __opts.escapeNewlines) {
text = value.replace(/\n/g, "\\n")
}
text = new SafeString(text.replace(/&amp;/g, "&"))
if (text == null || typeof text !== "string") {
return text
}
return text.replace(/[<>]/g, tag => {
return HTML_SWAPS[tag] || tag
})
}),
// adds a note for post-processor
new Helper(HelperFunctionNames.LITERAL, value => {
if (value === undefined) {
return ""
}
const type = typeof value
const outputVal = type === "object" ? JSON.stringify(value) : value
return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`
}),
]
module.exports.HelperNames = () => {
return Object.values(HelperFunctionNames).concat(
HelperFunctionBuiltin,
externalHandlebars.externalHelperNames
)
}
module.exports.registerMinimum = handlebars => {
for (let helper of HELPERS) {
helper.register(handlebars)
}
}
module.exports.registerAll = handlebars => {
module.exports.registerMinimum(handlebars)
// register imported helpers
externalHandlebars.registerAll(handlebars)
}
module.exports.unregisterAll = handlebars => {
for (let helper of HELPERS) {
helper.unregister(handlebars)
}
// unregister all imported helpers
externalHandlebars.unregisterAll(handlebars)
}
module.exports.getJsHelperList = getJsHelperList

View File

@ -0,0 +1,103 @@
import Helper from "./Helper"
import { SafeString } from "handlebars"
import * as externalHandlebars from "./external"
import { processJS } from "./javascript"
import {
HelperFunctionNames,
HelperFunctionBuiltin,
LITERAL_MARKER,
} from "./constants"
export { getJsHelperList } from "./list"
const HTML_SWAPS = {
"<": "&lt;",
">": "&gt;",
}
function isObject(value: string | any[]) {
if (value == null || typeof value !== "object") {
return false
}
return (
value.toString() === "[object Object]" ||
(value.length > 0 && typeof value[0] === "object")
)
}
const HELPERS = [
// external helpers
new Helper(HelperFunctionNames.OBJECT, (value: any) => {
return new SafeString(JSON.stringify(value))
}),
// javascript helper
new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements
new Helper(
HelperFunctionNames.ALL,
(value: string, inputs: { __opts: any }) => {
const { __opts } = inputs
if (isObject(value)) {
return new SafeString(JSON.stringify(value))
}
// null/undefined values produce bad results
if (__opts && __opts.onlyFound && value == null) {
return __opts.input
}
if (value == null || typeof value !== "string") {
return value == null ? "" : value
}
// TODO: check, this should always be false
if (value && (value as any).string) {
value = (value as any).string
}
let text: any = value
if (__opts && __opts.escapeNewlines) {
text = value.replace(/\n/g, "\\n")
}
text = new SafeString(text.replace(/&amp;/g, "&"))
if (text == null || typeof text !== "string") {
return text
}
return text.replace(/[<>]/g, (tag: string) => {
return HTML_SWAPS[tag as keyof typeof HTML_SWAPS] || tag
})
}
),
// adds a note for post-processor
new Helper(HelperFunctionNames.LITERAL, (value: any) => {
if (value === undefined) {
return ""
}
const type = typeof value
const outputVal = type === "object" ? JSON.stringify(value) : value
return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`
}),
]
export function HelperNames() {
return Object.values(HelperFunctionNames).concat(
HelperFunctionBuiltin,
externalHandlebars.externalHelperNames
)
}
export function registerMinimum(handlebars: typeof Handlebars) {
for (let helper of HELPERS) {
helper.register(handlebars)
}
}
export function registerAll(handlebars: typeof Handlebars) {
registerMinimum(handlebars)
// register imported helpers
externalHandlebars.registerAll(handlebars)
}
export function unregisterAll(handlebars: any) {
for (let helper of HELPERS) {
helper.unregister(handlebars)
}
// unregister all imported helpers
externalHandlebars.unregisterAll(handlebars)
}

View File

@ -1,22 +1,24 @@
const { atob, isBackendService, isJSAllowed } = require("../utilities") import { atob, isJSAllowed } from "../utilities"
const cloneDeep = require("lodash.clonedeep") import cloneDeep from "lodash/fp/cloneDeep"
const { LITERAL_MARKER } = require("../helpers/constants") import { LITERAL_MARKER } from "../helpers/constants"
const { getJsHelperList } = require("./list") import { getJsHelperList } from "./list"
const { iifeWrapper } = require("../iife") import { iifeWrapper } from "../iife"
// The method of executing JS scripts depends on the bundle being built. // The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.js or index.mjs). // This setter is used in the entrypoint (either index.js or index.mjs).
let runJS let runJS: ((js: string, context: any) => any) | undefined = undefined
module.exports.setJSRunner = runner => (runJS = runner) export const setJSRunner = (runner: typeof runJS) => (runJS = runner)
module.exports.removeJSRunner = () => {
export const removeJSRunner = () => {
runJS = undefined runJS = undefined
} }
let onErrorLog let onErrorLog: (message: Error) => void
module.exports.setOnErrorLog = delegate => (onErrorLog = delegate) export const setOnErrorLog = (delegate: typeof onErrorLog) =>
(onErrorLog = delegate)
// Helper utility to strip square brackets from a value // Helper utility to strip square brackets from a value
const removeSquareBrackets = value => { const removeSquareBrackets = (value: string) => {
if (!value || typeof value !== "string") { if (!value || typeof value !== "string") {
return value return value
} }
@ -30,7 +32,7 @@ const removeSquareBrackets = value => {
// Our context getter function provided to JS code as $. // Our context getter function provided to JS code as $.
// Extracts a value from context. // Extracts a value from context.
const getContextValue = (path, context) => { const getContextValue = (path: string, context: any) => {
let data = context let data = context
path.split(".").forEach(key => { path.split(".").forEach(key => {
if (data == null || typeof data !== "object") { if (data == null || typeof data !== "object") {
@ -42,8 +44,8 @@ const getContextValue = (path, context) => {
} }
// Evaluates JS code against a certain context // Evaluates JS code against a certain context
module.exports.processJS = (handlebars, context) => { export function processJS(handlebars: string, context: any) {
if (!isJSAllowed() || (isBackendService() && !runJS)) { if (!isJSAllowed() || !runJS) {
throw new Error("JS disabled in environment.") throw new Error("JS disabled in environment.")
} }
try { try {
@ -53,8 +55,8 @@ module.exports.processJS = (handlebars, context) => {
// Transform snippets into an object for faster access, and cache previously // Transform snippets into an object for faster access, and cache previously
// evaluated snippets // evaluated snippets
let snippetMap = {} let snippetMap: any = {}
let snippetCache = {} let snippetCache: any = {}
for (let snippet of context.snippets || []) { for (let snippet of context.snippets || []) {
snippetMap[snippet.name] = snippet.code snippetMap[snippet.name] = snippet.code
} }
@ -64,7 +66,7 @@ module.exports.processJS = (handlebars, context) => {
// app context. // app context.
const clonedContext = cloneDeep({ ...context, snippets: null }) const clonedContext = cloneDeep({ ...context, snippets: null })
const sandboxContext = { const sandboxContext = {
$: path => getContextValue(path, clonedContext), $: (path: string) => getContextValue(path, clonedContext),
helpers: getJsHelperList(), helpers: getJsHelperList(),
// Proxy to evaluate snippets when running in the browser // Proxy to evaluate snippets when running in the browser
@ -84,7 +86,7 @@ module.exports.processJS = (handlebars, context) => {
// Create a sandbox with our context and run the JS // Create a sandbox with our context and run the JS
const res = { data: runJS(js, sandboxContext) } const res = { data: runJS(js, sandboxContext) }
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}` return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
} catch (error) { } catch (error: any) {
onErrorLog && onErrorLog(error) onErrorLog && onErrorLog(error)
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {

View File

@ -1,7 +1,7 @@
const { date, duration } = require("./date") import { date, duration } from "./date"
// https://github.com/evanw/esbuild/issues/56 // https://github.com/evanw/esbuild/issues/56
const externalCollections = { const getExternalCollections = (): Record<string, () => any> => ({
math: require("@budibase/handlebars-helpers/lib/math"), math: require("@budibase/handlebars-helpers/lib/math"),
array: require("@budibase/handlebars-helpers/lib/array"), array: require("@budibase/handlebars-helpers/lib/array"),
number: require("@budibase/handlebars-helpers/lib/number"), number: require("@budibase/handlebars-helpers/lib/number"),
@ -11,32 +11,32 @@ const externalCollections = {
object: require("@budibase/handlebars-helpers/lib/object"), object: require("@budibase/handlebars-helpers/lib/object"),
regex: require("@budibase/handlebars-helpers/lib/regex"), regex: require("@budibase/handlebars-helpers/lib/regex"),
uuid: require("@budibase/handlebars-helpers/lib/uuid"), uuid: require("@budibase/handlebars-helpers/lib/uuid"),
} })
const helpersToRemoveForJs = ["sortBy"] export const helpersToRemoveForJs = ["sortBy"]
module.exports.helpersToRemoveForJs = helpersToRemoveForJs
const addedHelpers = { const addedHelpers = {
date: date, date: date,
duration: duration, duration: duration,
} }
let helpers = undefined let helpers: Record<string, any>
module.exports.getJsHelperList = () => { export function getJsHelperList() {
if (helpers) { if (helpers) {
return helpers return helpers
} }
helpers = {} helpers = {}
for (let collection of Object.values(externalCollections)) { for (let collection of Object.values(getExternalCollections())) {
for (let [key, func] of Object.entries(collection)) { for (let [key, func] of Object.entries(collection)) {
// Handlebars injects the hbs options to the helpers by default. We are adding an empty {} as a last parameter to simulate it // Handlebars injects the hbs options to the helpers by default. We are adding an empty {} as a last parameter to simulate it
helpers[key] = (...props) => func(...props, {}) helpers[key] = (...props: any) => func(...props, {})
} }
} }
for (let key of Object.keys(addedHelpers)) { helpers = {
helpers[key] = addedHelpers[key] ...helpers,
addedHelpers,
} }
for (const toRemove of helpersToRemoveForJs) { for (const toRemove of helpersToRemoveForJs) {

View File

@ -1,3 +0,0 @@
module.exports.iifeWrapper = script => {
return `(function(){\n${script}\n})();`
}

View File

@ -0,0 +1,3 @@
export const iifeWrapper = (script: string) => {
return `(function(){\n${script}\n})();`
}

View File

@ -1,26 +0,0 @@
import templates from "./index.js"
/**
* ES6 entrypoint for rollup
*/
export const isValid = templates.isValid
export const makePropSafe = templates.makePropSafe
export const getManifest = templates.getManifest
export const isJSBinding = templates.isJSBinding
export const encodeJSBinding = templates.encodeJSBinding
export const decodeJSBinding = templates.decodeJSBinding
export const processStringSync = templates.processStringSync
export const processObjectSync = templates.processObjectSync
export const processString = templates.processString
export const processObject = templates.processObject
export const doesContainStrings = templates.doesContainStrings
export const doesContainString = templates.doesContainString
export const disableEscaping = templates.disableEscaping
export const findHBSBlocks = templates.findHBSBlocks
export const convertToJS = templates.convertToJS
export const setJSRunner = templates.setJSRunner
export const setOnErrorLog = templates.setOnErrorLog
export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
export const helpersToRemoveForJs = templates.helpersToRemoveForJs
export * from "./errors.js"

View File

@ -1,24 +1,30 @@
const vm = require("vm") import { Context, createContext, runInNewContext } from "vm"
const handlebars = require("handlebars") import { create } from "handlebars"
const { registerAll, registerMinimum } = require("./helpers/index") import { registerAll, registerMinimum } from "./helpers/index"
const processors = require("./processors") import { preprocess, postprocess } from "./processors"
const { atob, btoa, isBackendService } = require("./utilities") import {
const { iifeWrapper } = require("./iife") atob,
const manifest = require("../manifest.json") btoa,
const { isBackendService,
FIND_HBS_REGEX, FIND_HBS_REGEX,
FIND_ANY_HBS_REGEX, FIND_ANY_HBS_REGEX,
findDoubleHbsInstances, findDoubleHbsInstances,
} = require("./utilities") } from "./utilities"
const { convertHBSBlock } = require("./conversion") import { convertHBSBlock } from "./conversion"
const javascript = require("./helpers/javascript") import { setJSRunner, removeJSRunner } from "./helpers/javascript"
const { helpersToRemoveForJs } = require("./helpers/list") import { helpersToRemoveForJs } from "./helpers/list"
const hbsInstance = handlebars.create() import manifest from "./manifest.json"
import { ProcessOptions } from "./types"
export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
export { iifeWrapper } from "./iife"
const hbsInstance = create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = create()
registerMinimum(hbsInstanceNoHelpers) registerMinimum(hbsInstanceNoHelpers)
const defaultOpts = { const defaultOpts: ProcessOptions = {
noHelpers: false, noHelpers: false,
cacheTemplates: false, cacheTemplates: false,
noEscaping: false, noEscaping: false,
@ -29,7 +35,7 @@ const defaultOpts = {
/** /**
* Utility function to check if the object is valid. * Utility function to check if the object is valid.
*/ */
function testObject(object) { function testObject(object: any) {
// JSON stringify will fail if there are any cycles, stops infinite recursion // JSON stringify will fail if there are any cycles, stops infinite recursion
try { try {
JSON.stringify(object) JSON.stringify(object)
@ -41,8 +47,8 @@ function testObject(object) {
/** /**
* Creates a HBS template function for a given string, and optionally caches it. * Creates a HBS template function for a given string, and optionally caches it.
*/ */
let templateCache = {} const templateCache: Record<string, HandlebarsTemplateDelegate<any>> = {}
function createTemplate(string, opts) { function createTemplate(string: string, opts?: ProcessOptions) {
opts = { ...defaultOpts, ...opts } opts = { ...defaultOpts, ...opts }
// Finalising adds a helper, can't do this with no helpers // Finalising adds a helper, can't do this with no helpers
@ -53,11 +59,11 @@ function createTemplate(string, opts) {
return templateCache[key] return templateCache[key]
} }
string = processors.preprocess(string, opts) string = preprocess(string, opts)
// Optionally disable built in HBS escaping // Optionally disable built in HBS escaping
if (opts.noEscaping) { if (opts.noEscaping) {
string = exports.disableEscaping(string) string = disableEscaping(string)
} }
// This does not throw an error when template can't be fulfilled, // This does not throw an error when template can't be fulfilled,
@ -78,24 +84,25 @@ function createTemplate(string, opts) {
* @param {object|undefined} [opts] optional - specify some options for processing. * @param {object|undefined} [opts] optional - specify some options for processing.
* @returns {Promise<object|array>} The structure input, as fully updated as possible. * @returns {Promise<object|array>} The structure input, as fully updated as possible.
*/ */
module.exports.processObject = async (object, context, opts) => { export async function processObject<T extends Record<string, any>>(
object: T,
context: object,
opts?: { noHelpers?: boolean; escapeNewlines?: boolean; onlyFound?: boolean }
): Promise<T> {
testObject(object) testObject(object)
for (let key of Object.keys(object || {})) {
for (const key of Object.keys(object || {})) {
if (object[key] != null) { if (object[key] != null) {
let val = object[key] const val = object[key]
let parsedValue
if (typeof val === "string") { if (typeof val === "string") {
object[key] = await module.exports.processString( parsedValue = await processString(object[key], context, opts)
object[key],
context,
opts
)
} else if (typeof val === "object") { } else if (typeof val === "object") {
object[key] = await module.exports.processObject( parsedValue = await processObject(object[key], context, opts)
object[key],
context,
opts
)
} }
// @ts-ignore
object[key] = parsedValue
} }
} }
return object return object
@ -109,9 +116,13 @@ module.exports.processObject = async (object, context, opts) => {
* @param {object|undefined} [opts] optional - specify some options for processing. * @param {object|undefined} [opts] optional - specify some options for processing.
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be. * @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
*/ */
module.exports.processString = async (string, context, opts) => { export async function processString(
string: string,
context: object,
opts?: ProcessOptions
): Promise<string> {
// TODO: carry out any async calls before carrying out async call // TODO: carry out any async calls before carrying out async call
return module.exports.processStringSync(string, context, opts) return processStringSync(string, context, opts)
} }
/** /**
@ -123,14 +134,18 @@ module.exports.processString = async (string, context, opts) => {
* @param {object|undefined} [opts] optional - specify some options for processing. * @param {object|undefined} [opts] optional - specify some options for processing.
* @returns {object|array} The structure input, as fully updated as possible. * @returns {object|array} The structure input, as fully updated as possible.
*/ */
module.exports.processObjectSync = (object, context, opts) => { export function processObjectSync(
object: { [x: string]: any },
context: any,
opts: any
): object | Array<any> {
testObject(object) testObject(object)
for (let key of Object.keys(object || {})) { for (let key of Object.keys(object || {})) {
let val = object[key] let val = object[key]
if (typeof val === "string") { if (typeof val === "string") {
object[key] = module.exports.processStringSync(object[key], context, opts) object[key] = processStringSync(object[key], context, opts)
} else if (typeof val === "object") { } else if (typeof val === "object") {
object[key] = module.exports.processObjectSync(object[key], context, opts) object[key] = processObjectSync(object[key], context, opts)
} }
} }
return object return object
@ -144,17 +159,20 @@ module.exports.processObjectSync = (object, context, opts) => {
* @param {object|undefined} [opts] optional - specify some options for processing. * @param {object|undefined} [opts] optional - specify some options for processing.
* @returns {string} The enriched string, all templates should have been replaced if they can be. * @returns {string} The enriched string, all templates should have been replaced if they can be.
*/ */
module.exports.processStringSync = (string, context, opts) => { export function processStringSync(
string: string,
context?: object,
opts?: ProcessOptions
): string {
// Take a copy of input in case of error // Take a copy of input in case of error
const input = string const input = string
if (typeof string !== "string") { if (typeof string !== "string") {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
function process(stringPart) { function process(stringPart: string) {
const template = createTemplate(stringPart, opts) const template = createTemplate(stringPart, opts)
const now = Math.floor(Date.now() / 1000) * 1000 const now = Math.floor(Date.now() / 1000) * 1000
return processors.postprocess( const processedString = template({
template({
now: new Date(now).toISOString(), now: new Date(now).toISOString(),
__opts: { __opts: {
...opts, ...opts,
@ -162,11 +180,11 @@ module.exports.processStringSync = (string, context, opts) => {
}, },
...context, ...context,
}) })
) return postprocess(processedString)
} }
try { try {
if (opts && opts.onlyFound) { if (opts && opts.onlyFound) {
const blocks = exports.findHBSBlocks(string) const blocks = findHBSBlocks(string)
for (let block of blocks) { for (let block of blocks) {
const outcome = process(block) const outcome = process(block)
string = string.replace(block, outcome) string = string.replace(block, outcome)
@ -186,7 +204,7 @@ module.exports.processStringSync = (string, context, opts) => {
* this function will find any double braces and switch to triple. * this function will find any double braces and switch to triple.
* @param string the string to have double HBS statements converted to triple. * @param string the string to have double HBS statements converted to triple.
*/ */
module.exports.disableEscaping = string => { export function disableEscaping(string: string) {
const matches = findDoubleHbsInstances(string) const matches = findDoubleHbsInstances(string)
if (matches == null) { if (matches == null) {
return string return string
@ -207,7 +225,7 @@ module.exports.disableEscaping = string => {
* @param {string} property The property which is to be wrapped. * @param {string} property The property which is to be wrapped.
* @returns {string} The wrapped property ready to be added to a templating string. * @returns {string} The wrapped property ready to be added to a templating string.
*/ */
module.exports.makePropSafe = property => { export function makePropSafe(property: any): string {
return `[${property}]`.replace("[[", "[").replace("]]", "]") return `[${property}]`.replace("[[", "[").replace("]]", "]")
} }
@ -217,7 +235,7 @@ module.exports.makePropSafe = property => {
* @param [opts] optional - specify some options for processing. * @param [opts] optional - specify some options for processing.
* @returns {boolean} Whether or not the input string is valid. * @returns {boolean} Whether or not the input string is valid.
*/ */
module.exports.isValid = (string, opts) => { export function isValid(string: any, opts?: any): boolean {
const validCases = [ const validCases = [
"string", "string",
"number", "number",
@ -238,7 +256,7 @@ module.exports.isValid = (string, opts) => {
}) })
template(context) template(context)
return true return true
} catch (err) { } catch (err: any) {
const msg = err && err.message ? err.message : err const msg = err && err.message ? err.message : err
if (!msg) { if (!msg) {
return false return false
@ -259,7 +277,7 @@ module.exports.isValid = (string, opts) => {
* This manifest provides information about each of the helpers and how it can be used. * This manifest provides information about each of the helpers and how it can be used.
* @returns The manifest JSON which has been generated from the helpers. * @returns The manifest JSON which has been generated from the helpers.
*/ */
module.exports.getManifest = () => { export function getManifest() {
return manifest return manifest
} }
@ -268,8 +286,8 @@ module.exports.getManifest = () => {
* @param handlebars the HBS expression to check * @param handlebars the HBS expression to check
* @returns {boolean} whether the expression is JS or not * @returns {boolean} whether the expression is JS or not
*/ */
module.exports.isJSBinding = handlebars => { export function isJSBinding(handlebars: any): boolean {
return module.exports.decodeJSBinding(handlebars) != null return decodeJSBinding(handlebars) != null
} }
/** /**
@ -277,7 +295,7 @@ module.exports.isJSBinding = handlebars => {
* @param javascript the JS code to encode * @param javascript the JS code to encode
* @returns {string} the JS HBS expression * @returns {string} the JS HBS expression
*/ */
module.exports.encodeJSBinding = javascript => { export function encodeJSBinding(javascript: string): string {
return `{{ js "${btoa(javascript)}" }}` return `{{ js "${btoa(javascript)}" }}`
} }
@ -286,7 +304,7 @@ module.exports.encodeJSBinding = javascript => {
* @param handlebars the JS HBS expression * @param handlebars the JS HBS expression
* @returns {string|null} the raw JS code * @returns {string|null} the raw JS code
*/ */
module.exports.decodeJSBinding = handlebars => { export function decodeJSBinding(handlebars: string): string | null {
if (!handlebars || typeof handlebars !== "string") { if (!handlebars || typeof handlebars !== "string") {
return null return null
} }
@ -311,7 +329,7 @@ module.exports.decodeJSBinding = handlebars => {
* @param {string[]} strings The strings to look for. * @param {string[]} strings The strings to look for.
* @returns {boolean} Will return true if all strings found in HBS statement. * @returns {boolean} Will return true if all strings found in HBS statement.
*/ */
module.exports.doesContainStrings = (template, strings) => { export function doesContainStrings(template: string, strings: any[]): boolean {
let regexp = new RegExp(FIND_HBS_REGEX) let regexp = new RegExp(FIND_HBS_REGEX)
let matches = template.match(regexp) let matches = template.match(regexp)
if (matches == null) { if (matches == null) {
@ -319,8 +337,8 @@ module.exports.doesContainStrings = (template, strings) => {
} }
for (let match of matches) { for (let match of matches) {
let hbs = match let hbs = match
if (exports.isJSBinding(match)) { if (isJSBinding(match)) {
hbs = exports.decodeJSBinding(match) hbs = decodeJSBinding(match)!
} }
let allFound = true let allFound = true
for (let string of strings) { for (let string of strings) {
@ -341,7 +359,7 @@ module.exports.doesContainStrings = (template, strings) => {
* @param {string} string The string to search within. * @param {string} string The string to search within.
* @return {string[]} The found HBS blocks. * @return {string[]} The found HBS blocks.
*/ */
module.exports.findHBSBlocks = string => { export function findHBSBlocks(string: string): string[] {
if (!string || typeof string !== "string") { if (!string || typeof string !== "string") {
return [] return []
} }
@ -362,18 +380,15 @@ module.exports.findHBSBlocks = string => {
* @param {string} string The word or sentence to search for. * @param {string} string The word or sentence to search for.
* @returns {boolean} The this return true if the string is found, false if not. * @returns {boolean} The this return true if the string is found, false if not.
*/ */
module.exports.doesContainString = (template, string) => { export function doesContainString(template: any, string: any): boolean {
return exports.doesContainStrings(template, [string]) return doesContainStrings(template, [string])
} }
module.exports.setJSRunner = javascript.setJSRunner export function convertToJS(hbs: string) {
module.exports.setOnErrorLog = javascript.setOnErrorLog const blocks = findHBSBlocks(hbs)
module.exports.convertToJS = hbs => {
const blocks = exports.findHBSBlocks(hbs)
let js = "return `", let js = "return `",
prevBlock = null prevBlock: string | null = null
const variables = {} const variables: Record<string, any> = {}
if (blocks.length === 0) { if (blocks.length === 0) {
js += hbs js += hbs
} }
@ -387,7 +402,7 @@ module.exports.convertToJS = hbs => {
prevBlock = block prevBlock = block
const { variable, value } = convertHBSBlock(block, count++) const { variable, value } = convertHBSBlock(block, count++)
variables[variable] = value variables[variable] = value
js += `${stringPart.split()}\${${variable}}` js += `${[stringPart]}\${${variable}}`
} }
let varBlock = "" let varBlock = ""
for (let [variable, value] of Object.entries(variables)) { for (let [variable, value] of Object.entries(variables)) {
@ -397,34 +412,34 @@ module.exports.convertToJS = hbs => {
return `${varBlock}${js}` return `${varBlock}${js}`
} }
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX const _FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
export { _FIND_ANY_HBS_REGEX as FIND_ANY_HBS_REGEX }
const errors = require("./errors") export { JsErrorTimeout } from "./errors"
// We cannot use dynamic exports, otherwise the typescript file will not be generating it
module.exports.JsErrorTimeout = errors.JsErrorTimeout
module.exports.helpersToRemoveForJs = helpersToRemoveForJs const _helpersToRemoveForJs = helpersToRemoveForJs
export { _helpersToRemoveForJs as helpersToRemoveForJs }
function defaultJSSetup() { function defaultJSSetup() {
if (!isBackendService()) { if (!isBackendService()) {
/** /**
* Use polyfilled vm to run JS scripts in a browser Env * Use polyfilled vm to run JS scripts in a browser Env
*/ */
javascript.setJSRunner((js, context) => { setJSRunner((js: string, context: Context) => {
context = { context = {
...context, ...context,
alert: undefined, alert: undefined,
setInterval: undefined, setInterval: undefined,
setTimeout: undefined, setTimeout: undefined,
} }
vm.createContext(context) createContext(context)
return vm.runInNewContext(js, context, { timeout: 1000 }) return runInNewContext(js, context, { timeout: 1000 })
}) })
} else { } else {
javascript.removeJSRunner() removeJSRunner()
} }
} }
defaultJSSetup() defaultJSSetup()
module.exports.defaultJSSetup = defaultJSSetup const _defaultJSSetup = defaultJSSetup
module.exports.iifeWrapper = iifeWrapper export { _defaultJSSetup as defaultJSSetup }

View File

@ -1,8 +1,9 @@
const { FIND_HBS_REGEX } = require("../utilities") import { FIND_HBS_REGEX } from "../utilities"
const preprocessor = require("./preprocessor") import * as preprocessor from "./preprocessor"
const postprocessor = require("./postprocessor") import * as postprocessor from "./postprocessor"
import { ProcessOptions } from "../types"
function process(output, processors, opts) { function process(output: string, processors: any[], opts?: ProcessOptions) {
for (let processor of processors) { for (let processor of processors) {
// if a literal statement has occurred stop // if a literal statement has occurred stop
if (typeof output !== "string") { if (typeof output !== "string") {
@ -21,7 +22,7 @@ function process(output, processors, opts) {
return output return output
} }
module.exports.preprocess = (string, opts) => { export function preprocess(string: string, opts: ProcessOptions) {
let processors = preprocessor.processors let processors = preprocessor.processors
if (opts.noFinalise) { if (opts.noFinalise) {
processors = processors.filter( processors = processors.filter(
@ -30,7 +31,7 @@ module.exports.preprocess = (string, opts) => {
} }
return process(string, processors, opts) return process(string, processors, opts)
} }
module.exports.postprocess = string => { export function postprocess(string: string) {
let processors = postprocessor.processors let processors = postprocessor.processors
return process(string, processors) return process(string, processors)
} }

View File

@ -1,49 +0,0 @@
const { LITERAL_MARKER } = require("../helpers/constants")
const PostProcessorNames = {
CONVERT_LITERALS: "convert-literals",
}
/* eslint-disable no-unused-vars */
class Postprocessor {
constructor(name, fn) {
this.name = name
this.fn = fn
}
process(statement) {
return this.fn(statement)
}
}
module.exports.PostProcessorNames = PostProcessorNames
module.exports.processors = [
new Postprocessor(PostProcessorNames.CONVERT_LITERALS, statement => {
if (typeof statement !== "string" || !statement.includes(LITERAL_MARKER)) {
return statement
}
const splitMarkerIndex = statement.indexOf("-")
const type = statement.substring(12, splitMarkerIndex)
const value = statement.substring(
splitMarkerIndex + 1,
statement.length - 2
)
switch (type) {
case "string":
return value
case "number":
return parseFloat(value)
case "boolean":
return value === "true"
case "object":
return JSON.parse(value)
case "js_result":
// We use the literal helper to process the result of JS expressions
// as we want to be able to return any types.
// We wrap the value in an abject to be able to use undefined properly.
return JSON.parse(value).data
}
return value
}),
]

View File

@ -0,0 +1,56 @@
import { LITERAL_MARKER } from "../helpers/constants"
export const PostProcessorNames = {
CONVERT_LITERALS: "convert-literals",
}
/* eslint-disable no-unused-vars */
class Postprocessor {
name: string
private fn: any
constructor(name: string, fn: any) {
this.name = name
this.fn = fn
}
process(statement: any) {
return this.fn(statement)
}
}
export const processors = [
new Postprocessor(
PostProcessorNames.CONVERT_LITERALS,
(statement: string) => {
if (
typeof statement !== "string" ||
!statement.includes(LITERAL_MARKER)
) {
return statement
}
const splitMarkerIndex = statement.indexOf("-")
const type = statement.substring(12, splitMarkerIndex)
const value = statement.substring(
splitMarkerIndex + 1,
statement.length - 2
)
switch (type) {
case "string":
return value
case "number":
return parseFloat(value)
case "boolean":
return value === "true"
case "object":
return JSON.parse(value)
case "js_result":
// We use the literal helper to process the result of JS expressions
// as we want to be able to return any types.
// We wrap the value in an abject to be able to use undefined properly.
return JSON.parse(value).data
}
return value
}
),
]

View File

@ -1,78 +0,0 @@
const { HelperNames } = require("../helpers")
const { swapStrings, isAlphaNumeric } = require("../utilities")
const FUNCTION_CASES = ["#", "else", "/"]
const PreprocessorNames = {
SWAP_TO_DOT: "swap-to-dot-notation",
FIX_FUNCTIONS: "fix-functions",
FINALISE: "finalise",
}
/* eslint-disable no-unused-vars */
class Preprocessor {
constructor(name, fn) {
this.name = name
this.fn = fn
}
process(fullString, statement, opts) {
const output = this.fn(statement, opts)
const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output)
}
}
module.exports.processors = [
new Preprocessor(PreprocessorNames.SWAP_TO_DOT, statement => {
let startBraceIdx = statement.indexOf("[")
let lastIdx = 0
while (startBraceIdx !== -1) {
// if the character previous to the literal specifier is alphanumeric this should happen
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
}
lastIdx = startBraceIdx + 1
const nextBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
startBraceIdx = nextBraceIdx > 0 ? lastIdx + 1 + nextBraceIdx : -1
}
return statement
}),
new Preprocessor(PreprocessorNames.FIX_FUNCTIONS, statement => {
for (let specialCase of FUNCTION_CASES) {
const toFind = `{ ${specialCase}`,
replacement = `{${specialCase}`
statement = statement.replace(new RegExp(toFind, "g"), replacement)
}
return statement
}),
new Preprocessor(PreprocessorNames.FINALISE, (statement, opts) => {
const noHelpers = opts && opts.noHelpers
let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1)
}
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
}
const possibleHelper = insideStatement.split(" ")[0]
// function helpers can't be wrapped
for (let specialCase of FUNCTION_CASES) {
if (possibleHelper.includes(specialCase)) {
return statement
}
}
const testHelper = possibleHelper.trim().toLowerCase()
if (
!noHelpers &&
HelperNames().some(option => testHelper === option.toLowerCase())
) {
insideStatement = `(${insideStatement})`
}
return `{{ all ${insideStatement} }}`
}),
]
module.exports.PreprocessorNames = PreprocessorNames

View File

@ -0,0 +1,82 @@
import { HelperNames } from "../helpers"
import { swapStrings, isAlphaNumeric } from "../utilities"
const FUNCTION_CASES = ["#", "else", "/"]
export const PreprocessorNames = {
SWAP_TO_DOT: "swap-to-dot-notation",
FIX_FUNCTIONS: "fix-functions",
FINALISE: "finalise",
}
/* eslint-disable no-unused-vars */
class Preprocessor {
name: string
private fn: any
constructor(name: string, fn: any) {
this.name = name
this.fn = fn
}
process(fullString: string, statement: string, opts: Object) {
const output = this.fn(statement, opts)
const idx = fullString.indexOf(statement)
return swapStrings(fullString, idx, statement.length, output)
}
}
export const processors = [
new Preprocessor(PreprocessorNames.SWAP_TO_DOT, (statement: string) => {
let startBraceIdx = statement.indexOf("[")
let lastIdx = 0
while (startBraceIdx !== -1) {
// if the character previous to the literal specifier is alphanumeric this should happen
if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) {
statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[")
}
lastIdx = startBraceIdx + 1
const nextBraceIdx = statement.substring(lastIdx + 1).indexOf("[")
startBraceIdx = nextBraceIdx > 0 ? lastIdx + 1 + nextBraceIdx : -1
}
return statement
}),
new Preprocessor(PreprocessorNames.FIX_FUNCTIONS, (statement: string) => {
for (let specialCase of FUNCTION_CASES) {
const toFind = `{ ${specialCase}`,
replacement = `{${specialCase}`
statement = statement.replace(new RegExp(toFind, "g"), replacement)
}
return statement
}),
new Preprocessor(
PreprocessorNames.FINALISE,
(statement: string, opts: { noHelpers: any }) => {
const noHelpers = opts && opts.noHelpers
let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1)
}
if (insideStatement.charAt(insideStatement.length - 1) === " ") {
insideStatement = insideStatement.slice(0, insideStatement.length - 1)
}
const possibleHelper = insideStatement.split(" ")[0]
// function helpers can't be wrapped
for (let specialCase of FUNCTION_CASES) {
if (possibleHelper.includes(specialCase)) {
return statement
}
}
const testHelper = possibleHelper.trim().toLowerCase()
if (
!noHelpers &&
HelperNames().some(option => testHelper === option.toLowerCase())
) {
insideStatement = `(${insideStatement})`
}
return `{{ all ${insideStatement} }}`
}
),
]

View File

@ -0,0 +1,8 @@
export interface ProcessOptions {
cacheTemplates?: boolean
noEscaping?: boolean
noHelpers?: boolean
noFinalise?: boolean
escapeNewlines?: boolean
onlyFound?: boolean
}

View File

@ -1,28 +1,28 @@
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g export const FIND_HBS_REGEX = /{{([^{].*?)}}/g
module.exports.FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g export const FIND_ANY_HBS_REGEX = /{?{{([^{].*?)}}}?/g
module.exports.FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g export const FIND_TRIPLE_HBS_REGEX = /{{{([^{].*?)}}}/g
module.exports.isBackendService = () => { export const isBackendService = () => {
return typeof window === "undefined" return typeof window === "undefined"
} }
module.exports.isJSAllowed = () => { export const isJSAllowed = () => {
return process && !process.env.NO_JS return process && !process.env.NO_JS
} }
// originally this could be done with a single regex using look behinds // originally this could be done with a single regex using look behinds
// but safari does not support this feature // but safari does not support this feature
// original regex: /(?<!{){{[^{}]+}}(?!})/g // original regex: /(?<!{){{[^{}]+}}(?!})/g
module.exports.findDoubleHbsInstances = string => { export const findDoubleHbsInstances = (string: string): string[] => {
let copied = string let copied = string
const doubleRegex = new RegExp(exports.FIND_HBS_REGEX) const doubleRegex = new RegExp(FIND_HBS_REGEX)
const regex = new RegExp(exports.FIND_TRIPLE_HBS_REGEX) const regex = new RegExp(FIND_TRIPLE_HBS_REGEX)
const tripleMatches = copied.match(regex) const tripleMatches = copied.match(regex)
// remove triple braces // remove triple braces
if (tripleMatches) { if (tripleMatches) {
tripleMatches.forEach(match => { tripleMatches.forEach((match: string) => {
copied = copied.replace(match, "") copied = copied.replace(match, "")
}) })
} }
@ -30,34 +30,39 @@ module.exports.findDoubleHbsInstances = string => {
return doubleMatches ? doubleMatches : [] return doubleMatches ? doubleMatches : []
} }
module.exports.isAlphaNumeric = char => { export const isAlphaNumeric = (char: string) => {
return char.match(ALPHA_NUMERIC_REGEX) return char.match(ALPHA_NUMERIC_REGEX)
} }
module.exports.swapStrings = (string, start, length, swap) => { export const swapStrings = (
string: string,
start: number,
length: number,
swap: string
) => {
return string.slice(0, start) + swap + string.slice(start + length) return string.slice(0, start) + swap + string.slice(start + length)
} }
module.exports.removeHandlebarsStatements = ( export const removeHandlebarsStatements = (
string, string: string,
replacement = "Invalid binding" replacement = "Invalid binding"
) => { ) => {
let regexp = new RegExp(exports.FIND_HBS_REGEX) let regexp = new RegExp(FIND_HBS_REGEX)
let matches = string.match(regexp) let matches = string.match(regexp)
if (matches == null) { if (matches == null) {
return string return string
} }
for (let match of matches) { for (let match of matches) {
const idx = string.indexOf(match) const idx = string.indexOf(match)
string = exports.swapStrings(string, idx, match.length, replacement) string = swapStrings(string, idx, match.length, replacement)
} }
return string return string
} }
module.exports.btoa = plainText => { export const btoa = (plainText: string) => {
return Buffer.from(plainText, "utf-8").toString("base64") return Buffer.from(plainText, "utf-8").toString("base64")
} }
module.exports.atob = base64 => { export const atob = (base64: string) => {
return Buffer.from(base64, "base64").toString("utf-8") return Buffer.from(base64, "base64").toString("utf-8")
} }

View File

@ -1,4 +1,4 @@
const { import {
processObject, processObject,
processString, processString,
isValid, isValid,
@ -8,7 +8,7 @@ const {
doesContainString, doesContainString,
disableEscaping, disableEscaping,
findHBSBlocks, findHBSBlocks,
} = require("../src/index.js") } from "../src/index"
describe("Test that the string processing works correctly", () => { describe("Test that the string processing works correctly", () => {
it("should process a basic template string", async () => { it("should process a basic template string", async () => {
@ -28,7 +28,7 @@ describe("Test that the string processing works correctly", () => {
it("should fail gracefully when wrong type passed in", async () => { it("should fail gracefully when wrong type passed in", async () => {
let error = null let error = null
try { try {
await processString(null, null) await processString(null as any, null as any)
} catch (err) { } catch (err) {
error = err error = err
} }
@ -76,7 +76,7 @@ describe("Test that the object processing works correctly", () => {
it("should fail gracefully when object passed in has cycles", async () => { it("should fail gracefully when object passed in has cycles", async () => {
let error = null let error = null
try { try {
const innerObj = { a: "thing {{ a }}" } const innerObj: any = { a: "thing {{ a }}" }
innerObj.b = innerObj innerObj.b = innerObj
await processObject(innerObj, { a: 1 }) await processObject(innerObj, { a: 1 })
} catch (err) { } catch (err) {
@ -98,7 +98,7 @@ describe("Test that the object processing works correctly", () => {
it("should be able to handle null objects", async () => { it("should be able to handle null objects", async () => {
let error = null let error = null
try { try {
await processObject(null, null) await processObject(null as any, null as any)
} catch (err) { } catch (err) {
error = err error = err
} }

View File

@ -1,2 +1,2 @@
module.exports.UUID_REGEX = export const UUID_REGEX =
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i

View File

@ -1,4 +1,4 @@
const { processString } = require("../src/index.js") import { processString } from "../src/index"
describe("Handling context properties with spaces in their name", () => { describe("Handling context properties with spaces in their name", () => {
it("should allow through literal specifiers", async () => { it("should allow through literal specifiers", async () => {

View File

@ -1,6 +1,6 @@
const { convertToJS } = require("../src/index.js") import { convertToJS } from "../src/index"
function checkLines(response, lines) { function checkLines(response: string, lines: string[]) {
const toCheck = response.split("\n") const toCheck = response.split("\n")
let count = 0 let count = 0
for (let line of lines) { for (let line of lines) {

View File

@ -1,7 +1,7 @@
const { processString, processObject, isValid } = require("../src/index.js") import { processString, processObject, isValid } from "../src/index"
const tableJson = require("./examples/table.json") import tableJson from "./examples/table.json"
const dayjs = require("dayjs") import dayjs from "dayjs"
const { UUID_REGEX } = require("./constants") import { UUID_REGEX } from "./constants"
describe("test the custom helpers we have applied", () => { describe("test the custom helpers we have applied", () => {
it("should be able to use the object helper", async () => { it("should be able to use the object helper", async () => {
@ -188,9 +188,7 @@ describe("test the date helpers", () => {
time: date.toUTCString(), time: date.toUTCString(),
} }
) )
const formatted = new dayjs(date) const formatted = dayjs(date).tz("America/New_York").format("HH-mm-ss Z")
.tz("America/New_York")
.format("HH-mm-ss Z")
expect(output).toBe(formatted) expect(output).toBe(formatted)
}) })
@ -200,7 +198,7 @@ describe("test the date helpers", () => {
time: date.toUTCString(), time: date.toUTCString(),
}) })
const timezone = dayjs.tz.guess() const timezone = dayjs.tz.guess()
const offset = new dayjs(date).tz(timezone).format("Z") const offset = dayjs(date).tz(timezone).format("Z")
expect(output).toBe(offset) expect(output).toBe(offset)
}) })
}) })
@ -273,7 +271,7 @@ describe("test the string helpers", () => {
}) })
describe("test the comparison helpers", () => { describe("test the comparison helpers", () => {
async function compare(func, a, b) { async function compare(func: string, a: any, b: any) {
const output = await processString( const output = await processString(
`{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`, `{{ #${func} a b }}Success{{ else }}Fail{{ /${func} }}`,
{ {
@ -344,14 +342,14 @@ describe("Test the literal helper", () => {
}) })
it("should allow use of the literal specifier for an object", async () => { it("should allow use of the literal specifier for an object", async () => {
const output = await processString(`{{literal a}}`, { const output: any = await processString(`{{literal a}}`, {
a: { b: 1 }, a: { b: 1 },
}) })
expect(output.b).toBe(1) expect(output.b).toBe(1)
}) })
it("should allow use of the literal specifier for an object with dashes", async () => { it("should allow use of the literal specifier for an object with dashes", async () => {
const output = await processString(`{{literal a}}`, { const output: any = await processString(`{{literal a}}`, {
a: { b: "i-have-dashes" }, a: { b: "i-have-dashes" },
}) })
expect(output.b).toBe("i-have-dashes") expect(output.b).toBe("i-have-dashes")

View File

@ -1,13 +1,9 @@
const vm = require("vm") import vm from "vm"
const { import { processStringSync, encodeJSBinding, setJSRunner } from "../src/index"
processStringSync, import { UUID_REGEX } from "./constants"
encodeJSBinding,
setJSRunner,
} = require("../src/index.js")
const { UUID_REGEX } = require("./constants")
const processJS = (js, context) => { const processJS = (js: string, context?: object): any => {
return processStringSync(encodeJSBinding(js), context) return processStringSync(encodeJSBinding(js), context)
} }

View File

@ -1,4 +1,4 @@
const vm = require("vm") import vm from "vm"
jest.mock("@budibase/handlebars-helpers/lib/math", () => { jest.mock("@budibase/handlebars-helpers/lib/math", () => {
const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math") const actual = jest.requireActual("@budibase/handlebars-helpers/lib/math")
@ -17,14 +17,14 @@ jest.mock("@budibase/handlebars-helpers/lib/uuid", () => {
} }
}) })
const { processString, setJSRunner } = require("../src/index.js") import { processString, setJSRunner } from "../src/index"
const tk = require("timekeeper") import tk from "timekeeper"
const { getParsedManifest, runJsHelpersTests } = require("./utils") import { getParsedManifest, runJsHelpersTests } from "./utils"
tk.freeze("2021-01-21T12:00:00") tk.freeze("2021-01-21T12:00:00")
function escapeRegExp(string) { function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
} }
@ -40,9 +40,9 @@ describe("manifest", () => {
describe("examples are valid", () => { describe("examples are valid", () => {
describe.each(Object.keys(manifest))("%s", collection => { describe.each(Object.keys(manifest))("%s", collection => {
it.each(manifest[collection])("%s", async (_, { hbs, js }) => { it.each(manifest[collection])("%s", async (_, { hbs, js }) => {
const context = { const context: any = {
double: i => i * 2, double: (i: number) => i * 2,
isString: x => typeof x === "string", isString: (x: any) => typeof x === "string",
} }
const arrays = hbs.match(/\[[^/\]]+\]/) const arrays = hbs.match(/\[[^/\]]+\]/)

View File

@ -1,4 +1,4 @@
const { processString } = require("../src/index.js") import { processString } from "../src/index"
describe("specific test case for whether or not full app template can still be rendered", () => { describe("specific test case for whether or not full app template can still be rendered", () => {
it("should be able to render the app template", async () => { it("should be able to render the app template", async () => {

View File

@ -1,13 +1,9 @@
const { getManifest } = require("../src") import { getManifest } from "../src"
const { getJsHelperList } = require("../src/helpers") import { getJsHelperList } from "../src/helpers"
const { import { convertToJS, processStringSync, encodeJSBinding } from "../src/index"
convertToJS,
processStringSync,
encodeJSBinding,
} = require("../src/index.js")
function tryParseJson(str) { function tryParseJson(str: string) {
if (typeof str !== "string") { if (typeof str !== "string") {
return return
} }
@ -19,23 +15,35 @@ function tryParseJson(str) {
} }
} }
const getParsedManifest = () => { type ExampleType = [
const manifest = getManifest() string,
{
hbs: string
js: string
requiresHbsBody: boolean
}
]
export const getParsedManifest = () => {
const manifest: any = getManifest()
const collections = Object.keys(manifest) const collections = Object.keys(manifest)
const examples = collections.reduce((acc, collection) => { const examples = collections.reduce((acc, collection) => {
const functions = Object.entries(manifest[collection]) const functions = Object.entries<{
.filter(([_, details]) => details.example) example: string
.map(([name, details]) => { requiresBlock: boolean
}>(manifest[collection])
.filter(
([_, details]) =>
details.example?.split("->").map(x => x.trim()).length > 1
)
.map(([name, details]): ExampleType => {
const example = details.example const example = details.example
let [hbs, js] = example.split("->").map(x => x.trim()) let [hbs, js] = example.split("->").map(x => x.trim())
if (!js) {
// The function has no return value
return
}
// Trim 's // Trim 's
js = js.replace(/^'|'$/g, "") js = js.replace(/^'|'$/g, "")
let parsedExpected let parsedExpected: string
if ((parsedExpected = tryParseJson(js))) { if ((parsedExpected = tryParseJson(js))) {
if (Array.isArray(parsedExpected)) { if (Array.isArray(parsedExpected)) {
if (typeof parsedExpected[0] === "object") { if (typeof parsedExpected[0] === "object") {
@ -48,36 +56,40 @@ const getParsedManifest = () => {
const requiresHbsBody = details.requiresBlock const requiresHbsBody = details.requiresBlock
return [name, { hbs, js, requiresHbsBody }] return [name, { hbs, js, requiresHbsBody }]
}) })
.filter(x => !!x)
if (Object.keys(functions).length) { if (functions.length) {
acc[collection] = functions acc[collection] = functions
} }
return acc return acc
}, {}) }, {} as Record<string, ExampleType[]>)
return examples return examples
} }
module.exports.getParsedManifest = getParsedManifest
module.exports.runJsHelpersTests = ({ funcWrap, testsToSkip } = {}) => { export const runJsHelpersTests = ({
funcWrap = funcWrap || (delegate => delegate()) funcWrap,
testsToSkip,
}: {
funcWrap?: any
testsToSkip?: any
} = {}) => {
funcWrap = funcWrap || ((delegate: () => any) => delegate())
const manifest = getParsedManifest() const manifest = getParsedManifest()
const processJS = (js, context) => { const processJS = (js: string, context: object | undefined) => {
return funcWrap(() => processStringSync(encodeJSBinding(js), context)) return funcWrap(() => processStringSync(encodeJSBinding(js), context))
} }
function escapeRegExp(string) { function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string
} }
describe("can be parsed and run as js", () => { describe("can be parsed and run as js", () => {
const jsHelpers = getJsHelperList() const jsHelpers = getJsHelperList()!
const jsExamples = Object.keys(manifest).reduce((acc, v) => { const jsExamples = Object.keys(manifest).reduce((acc, v) => {
acc[v] = manifest[v].filter(([key]) => jsHelpers[key]) acc[v] = manifest[v].filter(([key]) => jsHelpers[key])
return acc return acc
}, {}) }, {} as typeof manifest)
describe.each(Object.keys(jsExamples))("%s", collection => { describe.each(Object.keys(jsExamples))("%s", collection => {
const examplesToRun = jsExamples[collection] const examplesToRun = jsExamples[collection]
@ -86,9 +98,9 @@ module.exports.runJsHelpersTests = ({ funcWrap, testsToSkip } = {}) => {
examplesToRun.length && examplesToRun.length &&
it.each(examplesToRun)("%s", async (_, { hbs, js }) => { it.each(examplesToRun)("%s", async (_, { hbs, js }) => {
const context = { const context: any = {
double: i => i * 2, double: (i: number) => i * 2,
isString: x => typeof x === "string", isString: (x: any) => typeof x === "string",
} }
const arrays = hbs.match(/\[[^/\]]+\]/) const arrays = hbs.match(/\[[^/\]]+\]/)

View File

@ -5,8 +5,10 @@ jest.mock("../src/utilities", () => {
isBackendService: jest.fn().mockReturnValue(true), isBackendService: jest.fn().mockReturnValue(true),
} }
}) })
const { defaultJSSetup, processStringSync, encodeJSBinding } = require("../src")
const { isBackendService } = require("../src/utilities") import { defaultJSSetup, processStringSync, encodeJSBinding } from "../src"
import { isBackendService } from "../src/utilities"
const mockedBackendService = jest.mocked(isBackendService) const mockedBackendService = jest.mocked(isBackendService)
const binding = encodeJSBinding("return 1") const binding = encodeJSBinding("return 1")

View File

@ -1,11 +1,15 @@
{ {
"include": ["src/**/*"], "include": ["src/**/*"],
"compilerOptions": { "compilerOptions": {
"allowJs": true,
"declaration": true, "declaration": true,
"emitDeclarationOnly": true, "target": "es6",
"moduleResolution": "node",
"noImplicitAny": true,
"incremental": true,
"lib": ["dom"],
"outDir": "dist", "outDir": "dist",
"esModuleInterop": true, "esModuleInterop": true,
"types": ["node", "jest"] "types": ["node", "jest"],
"resolveJsonModule": true
} }
} }

View File

@ -16,17 +16,11 @@ COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000
COPY packages/string-templates .
WORKDIR /app WORKDIR /app
COPY packages/worker/package.json . COPY packages/worker/package.json .
COPY packages/worker/dist/yarn.lock . COPY packages/worker/dist/yarn.lock .
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
RUN ../scripts/removeWorkspaceDependencies.sh package.json RUN ../scripts/removeWorkspaceDependencies.sh package.json

View File

@ -15,6 +15,7 @@ const config: Config.InitialOptions = {
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",
"@budibase/shared-core": ["<rootDir>/../shared-core/src"], "@budibase/shared-core": ["<rootDir>/../shared-core/src"],
"@budibase/string-templates": ["<rootDir>/../string-templates/src"],
}, },
} }

View File

@ -16,7 +16,8 @@
"@budibase/backend-core": ["../backend-core/src"], "@budibase/backend-core": ["../backend-core/src"],
"@budibase/backend-core/*": ["../backend-core/*"], "@budibase/backend-core/*": ["../backend-core/*"],
"@budibase/shared-core": ["../shared-core/src"], "@budibase/shared-core": ["../shared-core/src"],
"@budibase/pro": ["../pro/src"] "@budibase/pro": ["../pro/src"],
"@budibase/string-templates": ["../string-templates/src"]
} }
}, },
"include": ["src/**/*"], "include": ["src/**/*"],

View File

@ -1988,7 +1988,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.21.0": "@babel/runtime@^7.10.5", "@babel/runtime@^7.13.10":
version "7.23.9" version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
@ -8467,21 +8467,6 @@ concat-with-sourcemaps@^1.1.0:
dependencies: dependencies:
source-map "^0.6.1" source-map "^0.6.1"
concurrently@^8.2.2:
version "8.2.2"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784"
integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==
dependencies:
chalk "^4.1.2"
date-fns "^2.30.0"
lodash "^4.17.21"
rxjs "^7.8.1"
shell-quote "^1.8.1"
spawn-command "0.0.2"
supports-color "^8.1.1"
tree-kill "^1.2.2"
yargs "^17.7.2"
condense-newlines@^0.2.1: condense-newlines@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f" resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f"
@ -9049,13 +9034,6 @@ data-urls@^4.0.0:
whatwg-mimetype "^3.0.0" whatwg-mimetype "^3.0.0"
whatwg-url "^12.0.0" whatwg-url "^12.0.0"
date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dateformat@^3.0.3: dateformat@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
@ -19400,13 +19378,6 @@ rxjs@^7.5.5:
dependencies: dependencies:
tslib "^2.1.0" tslib "^2.1.0"
rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
safe-array-concat@^1.0.1: safe-array-concat@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
@ -19682,11 +19653,6 @@ shell-exec@1.0.2:
resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756" resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756"
integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg== integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==
shell-quote@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
shortid@2.2.15: shortid@2.2.15:
version "2.2.15" version "2.2.15"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
@ -20005,11 +19971,6 @@ sparse-bitfield@^3.0.3:
dependencies: dependencies:
memory-pager "^1.0.2" memory-pager "^1.0.2"
spawn-command@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
spdx-correct@^3.0.0: spdx-correct@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@ -20582,7 +20543,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
supports-color@^8.0.0, supports-color@^8.1.1: supports-color@^8.0.0:
version "8.1.1" version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
@ -21209,11 +21170,6 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-newlines@^3.0.0: trim-newlines@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"