From 761e2e347e5541dcc8dce213fb5434380d3e3226 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 18 Feb 2026 23:47:47 +0100 Subject: [PATCH] refactor: localize, remove helpers file when there's no reuse --- packages/transpiler/helpers.mjs | 159 ------------------- packages/transpiler/index.mjs | 7 +- packages/transpiler/plugin-kabelsalat.mjs | 141 +++++++++++++++- packages/transpiler/plugin-mini.mjs | 4 +- packages/transpiler/plugin-sample.mjs | 12 +- packages/transpiler/plugin-widgets.mjs | 5 + packages/transpiler/test/transpiler.test.mjs | 2 +- packages/transpiler/transpiler.mjs | 24 +-- 8 files changed, 176 insertions(+), 178 deletions(-) delete mode 100644 packages/transpiler/helpers.mjs diff --git a/packages/transpiler/helpers.mjs b/packages/transpiler/helpers.mjs deleted file mode 100644 index 7346a9afd..000000000 --- a/packages/transpiler/helpers.mjs +++ /dev/null @@ -1,159 +0,0 @@ -/* -helpers.mjs - -Copyright (C) 2022 Strudel contributors - see -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -*/ -import escodegen from 'escodegen'; -import { walk } from 'estree-walker'; - -let languages = new Map(); -// config = { getLocations: (code: string, offset?: number) => number[][] } -// see mondough.mjs for example use -// the language will kick in when the code contains a template literal of type -// example: mondo`...` will use language of type "mondo" -// TODO: refactor tidal.mjs to use this -export function registerLanguage(type, config) { - languages.set(type, config); -} -export function getLanguages() { - return languages; -} - -export function withAwait(node) { - return { - type: 'AwaitExpression', - argument: node, - }; -} - -export function genExprSource(expr) { - return escodegen.generate(expr, { format: { semicolons: false } }); -} - -export function extractPatternPlaceholders(expr) { - const templateExpr = cloneNode(expr); - const parents = new Map(); - const targets = []; - - walk(templateExpr, { - enter(node, parent, prop, index) { - parents.set(node, { parent, prop, index }); - const patternExpr = getStrudelPatternExpr(node); - if (patternExpr) { - targets.push({ node, patternExpr }); - this.skip(); - } - }, - }); - - if (!targets.length) { - return { template: genExprSource(templateExpr), patternExprs: [] }; - } - - targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node)); - - const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr)); - - let currentExpr = templateExpr; - targets.forEach(({ node }, index) => { - currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr); - }); - - const template = genExprSource(currentExpr); - return { template, patternExprs }; -} - -function getStrudelPatternExpr(node) { - if (isStrudelPatternWrap(node)) { - const arg = node.arguments?.[0]; - if (!arg) { - throw new Error('S(...) requires an argument'); - } - return arg; - } - if (isMiniCall(node)) { - return node; - } - return null; -} - -function isStrudelPatternWrap(node) { - if (node.type !== 'CallExpression') { - return false; - } - const callee = node.callee; - if (callee.type === 'Identifier') { - return callee.name === 'S'; - } - if (callee.type === 'MemberExpression' && !callee.computed) { - return callee.property?.name === 'S'; - } - return false; -} - -// If `start` is available, we use it. If it's already been transpiled -// to `m(...)`, use the provided offset -function getPatternNodeOrder(node) { - if (typeof node.start === 'number') { - return node.start; - } - if (isMiniCall(node)) { - const offsetArg = node.arguments?.[1]; - if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') { - return offsetArg.value; - } - } - return 0; -} - -function placeholderAst(index) { - return { - type: 'MemberExpression', - object: { type: 'Identifier', name: 'pat' }, - property: { type: 'Literal', value: index }, - computed: true, - optional: false, - }; -} - -// Used to identify transpiled `m(...)` calls for proper conversion -// to, say, kabelsalat placeholders -function isMiniCall(node) { - if (node.type !== 'CallExpression') { - return false; - } - const callee = node.callee; - if (callee.type !== 'Identifier') { - return false; - } - if (callee.name !== getMinilangName()) { - return false; - } - const firstArg = node.arguments?.[0]; - return firstArg?.type === 'Literal' && typeof firstArg.value === 'string'; -} - -function getMinilangName() { - const minilang = getLanguages().get('minilang'); - return minilang?.name || 'm'; -} - -function replaceNode(node, replacement, parents, currentRoot) { - const info = parents.get(node); - if (!info || !info.parent) { - return replacement; - } - - const { parent, prop, index } = info; - if (Array.isArray(parent[prop])) { - parent[prop][index] = replacement; - } else { - parent[prop] = replacement; - } - parents.set(replacement, { parent, prop, index }); - return currentRoot; -} - -function cloneNode(node) { - return JSON.parse(JSON.stringify(node)); -} diff --git a/packages/transpiler/index.mjs b/packages/transpiler/index.mjs index eba72f7c3..3baf1593a 100644 --- a/packages/transpiler/index.mjs +++ b/packages/transpiler/index.mjs @@ -7,6 +7,11 @@ import { evaluate as _evaluate } from '@strudel/core'; import { transpiler } from './transpiler.mjs'; export * from './transpiler.mjs'; -export * from './helpers.mjs'; + +import './plugin-kabelsalat.mjs'; +import './plugin-mini.mjs'; +import './plugin-sample.mjs'; +import './plugin-widgets.mjs'; + export { registerWidgetType, getWidgetID } from './plugin-widgets.mjs'; export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions); diff --git a/packages/transpiler/plugin-kabelsalat.mjs b/packages/transpiler/plugin-kabelsalat.mjs index cee96f973..69b8c453c 100644 --- a/packages/transpiler/plugin-kabelsalat.mjs +++ b/packages/transpiler/plugin-kabelsalat.mjs @@ -3,9 +3,144 @@ plugin-kabelsalat.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { genExprSource, extractPatternPlaceholders } from './helpers.mjs'; -export const transpilerPlugin = { +import { walk } from 'estree-walker'; +import escodegen from 'escodegen'; +import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs'; + +export function genExprSource(expr) { + return escodegen.generate(expr, { format: { semicolons: false } }); +} + +function isStrudelPatternWrap(node) { + if (node.type !== 'CallExpression') { + return false; + } + const callee = node.callee; + if (callee.type === 'Identifier') { + return callee.name === 'S'; + } + if (callee.type === 'MemberExpression' && !callee.computed) { + return callee.property?.name === 'S'; + } + return false; +} + +// Used to identify transpiled `m(...)` calls for proper conversion +// to, say, kabelsalat placeholders +function isMiniCall(node) { + if (node.type !== 'CallExpression') { + return false; + } + const callee = node.callee; + if (callee.type !== 'Identifier') { + return false; + } + if (callee.name !== getMinilangName()) { + return false; + } + const firstArg = node.arguments?.[0]; + return firstArg?.type === 'Literal' && typeof firstArg.value === 'string'; +} + +function getMinilangName() { + const minilang = getLanguages().get('minilang'); + return minilang?.name || 'm'; +} + +function replaceNode(node, replacement, parents, currentRoot) { + const info = parents.get(node); + if (!info || !info.parent) { + return replacement; + } + + const { parent, prop, index } = info; + if (Array.isArray(parent[prop])) { + parent[prop][index] = replacement; + } else { + parent[prop] = replacement; + } + parents.set(replacement, { parent, prop, index }); + return currentRoot; +} + +// If `start` is available, we use it. If it's already been transpiled +// to `m(...)`, use the provided offset +function getPatternNodeOrder(node) { + if (typeof node.start === 'number') { + return node.start; + } + if (isMiniCall(node)) { + const offsetArg = node.arguments?.[1]; + if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') { + return offsetArg.value; + } + } + return 0; +} + +function placeholderAst(index) { + return { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'pat' }, + property: { type: 'Literal', value: index }, + computed: true, + optional: false, + }; +} + +function getStrudelPatternExpr(node) { + if (isStrudelPatternWrap(node)) { + const arg = node.arguments?.[0]; + if (!arg) { + throw new Error('S(...) requires an argument'); + } + return arg; + } + if (isMiniCall(node)) { + return node; + } + return null; +} + +function cloneNode(node) { + return JSON.parse(JSON.stringify(node)); +} + +export function extractPatternPlaceholders(expr) { + const templateExpr = cloneNode(expr); + const parents = new Map(); + const targets = []; + + walk(templateExpr, { + enter(node, parent, prop, index) { + parents.set(node, { parent, prop, index }); + const patternExpr = getStrudelPatternExpr(node); + if (patternExpr) { + targets.push({ node, patternExpr }); + this.skip(); + } + }, + }); + + if (!targets.length) { + return { template: genExprSource(templateExpr), patternExprs: [] }; + } + + targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node)); + + const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr)); + + let currentExpr = templateExpr; + targets.forEach(({ node }, index) => { + currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr); + }); + + const template = genExprSource(currentExpr); + return { template, patternExprs }; +} + +const transpilerPlugin = { walk: (context) => ({ leave: function (node, parent, prop, index) { if (!isKabelCall(node)) return; @@ -69,6 +204,8 @@ export const transpilerPlugin = { }), }; +registerTranspilerPlugin(transpilerPlugin); + function isKabelCall(node) { if (node.type !== 'CallExpression') return false; let callee = node.callee; diff --git a/packages/transpiler/plugin-mini.mjs b/packages/transpiler/plugin-mini.mjs index ae17d9fed..0653ffef0 100644 --- a/packages/transpiler/plugin-mini.mjs +++ b/packages/transpiler/plugin-mini.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ import { getLeafLocations } from '@strudel/mini'; -import { getLanguages } from './helpers.mjs'; +import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs'; const languageLiteral = { walk: (context) => ({ @@ -197,4 +197,4 @@ function isStringWithDoubleQuotes(node, locations, code) { return node.raw[0] === '"'; } -export const miniTranspilerPlugins = [languageLiteral, tidal, backtick, doublequotes]; +registerTranspilerPlugin([languageLiteral, tidal, backtick, doublequotes]); diff --git a/packages/transpiler/plugin-sample.mjs b/packages/transpiler/plugin-sample.mjs index 388b98eae..0b129af05 100644 --- a/packages/transpiler/plugin-sample.mjs +++ b/packages/transpiler/plugin-sample.mjs @@ -3,7 +3,15 @@ plugin-sample.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { withAwait } from './helpers.mjs'; + +import { registerTranspilerPlugin } from './transpiler.mjs'; + +export function withAwait(node) { + return { + type: 'AwaitExpression', + argument: node, + }; +} const bareSample = { walk: (context) => ({ @@ -18,4 +26,4 @@ function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; } -export const sampleTranspilerPlugins = bareSample; +registerTranspilerPlugin(bareSample); diff --git a/packages/transpiler/plugin-widgets.mjs b/packages/transpiler/plugin-widgets.mjs index db6379ddd..a6a46eb3b 100644 --- a/packages/transpiler/plugin-widgets.mjs +++ b/packages/transpiler/plugin-widgets.mjs @@ -3,6 +3,9 @@ plugin-widgets.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ + +import { registerTranspilerPlugin } from './transpiler.mjs'; + let widgetMethods = []; export function registerWidgetType(type) { widgetMethods.push(type); @@ -121,3 +124,5 @@ function widgetWithLocation(node, widgetConfig) { }); return node; } + +registerTranspilerPlugin(widgetTranspilerPlugins); diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 986635beb..74bfaa911 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { describe, it, expect } from 'vitest'; -import { transpiler } from '../transpiler.mjs'; +import { transpiler } from '../index.mjs'; const simple = { wrapAsync: false, addReturn: false, simpleLocs: true }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index b471051d1..142f17c77 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -3,15 +3,22 @@ transpiler.mjs - Copyright (C) 2022 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { getLeafLocations } from '@strudel/mini'; import { parse } from 'acorn'; import escodegen from 'escodegen'; import { walk } from 'estree-walker'; -import { getLanguages } from './helpers.mjs'; -import { miniTranspilerPlugins } from './plugin-mini.mjs'; -import { widgetTranspilerPlugins } from './plugin-widgets.mjs'; -import { sampleTranspilerPlugins } from './plugin-sample.mjs'; -import { transpilerPlugin as kabelsalatTranspilerPlugins } from './plugin-kabelsalat.mjs'; + +let languages = new Map(); +// config = { getLocations: (code: string, offset?: number) => number[][] } +// see mondough.mjs for example use +// the language will kick in when the code contains a template literal of type +// example: mondo`...` will use language of type "mondo" +// TODO: refactor tidal.mjs to use this +export function registerLanguage(type, config) { + languages.set(type, config); +} +export function getLanguages() { + return languages; +} const plugins = []; @@ -22,11 +29,6 @@ export function getPlugins() { return plugins.flat(Infinity); } -registerTranspilerPlugin(miniTranspilerPlugins); -registerTranspilerPlugin(widgetTranspilerPlugins); -registerTranspilerPlugin(sampleTranspilerPlugins); -registerTranspilerPlugin(kabelsalatTranspilerPlugins); - export function transpiler(input, options = {}) { options = { wrapAsync: false,