From 7b0f28252f52983b7da17b67901f153185d8199e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 11 Oct 2026 18:52:09 +0100 Subject: [PATCH 1/3] feat: script and style support in the ui-template code editor Builds on node-red/node-red#5994, which adds JavaScript and CSS support to ') } + } + + describe('findComponent', function () { + it('finds the object of a single export default, like Dashboard does', function () { + const body = '\n// a comment\nexport default {\n data () { return {} }\n}\n' + const c = script.findComponent(body) + body[c.open].should.equal('{') + body[c.close].should.equal('}') + body.slice(c.close + 1).should.equal('\n') + should(script.findComponent('/* x */ export default {};')).not.be.null() + }) + it('treats anything else as raw JavaScript', function () { + should(script.findComponent('const a = 1\nexport default {}')).be.null() + should(script.findComponent('this.send({ payload: 1 })')).be.null() + should(script.findComponent('export default { }\nconsole.log(1)')).be.null() + }) + }) + + describe('wrapScript', function () { + it('wraps a component in __uiTemplate( ... )', function () { + const text = '\n' + const wrapped = applyInsertions(text, script.wrapScript(text, scriptRegion(text), 0)) + wrapped.should.containEql('export default __uiTemplate({\n data () { return { a: 1 } }\n})\n') + }) + it('wraps raw JavaScript in a function whose this is the component', function () { + const text = '
\n' + const wrapped = applyInsertions(text, script.wrapScript(text, scriptRegion(text), 2)) + wrapped.should.containEql('') + }) + it('only inserts inside the script region', function () { + const text = '' + const region = scriptRegion(text) + for (const ins of script.wrapScript(text, region, 0)) { + ins.at.should.be.within(region.start, region.end) + } + }) + }) + + describe('UI_TEMPLATE_TYPES', function () { + it('describes what Dashboard adds to the component', function () { + for (const member of ['send(msg: any)', 'submit(event: Event)', 'readonly msg:', 'readonly id:', 'readonly props:', 'value: any', 'readonly $socket:', 'readonly $store:', 'readonly $router:', 'readonly $vuetify:']) { + script.UI_TEMPLATE_TYPES.should.containEql(member) + } + script.UI_TEMPLATE_TYPES.should.containEql('declare function __uiTemplate(') + }) + }) + + describe('activate', function () { + afterEach(function () { delete global.RED }) + + it('registers the wrapper, types and CSS with Node-RED', function () { + const calls = [] + global.RED = { editor: { codeEditor: { monaco: { registerEmbeddedLanguages: (id, options) => calls.push({ id, options }) } } } } + script.activate({}, { languageId: 'vue-sfc' }).should.be.true() + calls.should.have.length(1) + calls[0].id.should.equal('vue-sfc') + const { javascript, css } = calls[0].options + javascript.wrap.should.equal(script.wrapScript) + javascript.extraLibs[0].content.should.equal(script.UI_TEMPLATE_TYPES) + javascript.hiddenNames.test('__uiTemplateScript0').should.be.true() + javascript.demotedNames.test('$socket').should.be.true() + css.should.eql({}) + }) + it('does nothing on Node-RED versions without registerEmbeddedLanguages', function () { + global.RED = { editor: { codeEditor: { monaco: {} } } } + script.activate({}, { languageId: 'vue-sfc' }).should.be.false() + }) + }) +}) From 91f39b8826e5dfacc0d246d0800b909f4eec1593 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sat, 17 Oct 2026 09:53:30 +0100 Subject: [PATCH 2/3] fix: allow comments after a ui-template component script The editor only treated a script as a component when nothing but `}` or `;` followed the closing brace, so a trailing comment wrapped it as raw JavaScript and flagged `export default`. Dashboard ignores comments here, so the editor now skips // and /* */ comments after the component as well as before it. --- resources/vue-editor/vue-script.mjs | 7 +++++-- test/resources/vue-script.spec.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/resources/vue-editor/vue-script.mjs b/resources/vue-editor/vue-script.mjs index a38f8415c..0adad15ec 100644 --- a/resources/vue-editor/vue-script.mjs +++ b/resources/vue-editor/vue-script.mjs @@ -86,6 +86,9 @@ declare function __uiTemplate(component: { } & ThisType>): void ` +// whitespace and comments, which Dashboard allows around a component (they aren't statements) +const GAP = String.raw`(?:\s|\/\/[^\n]*(?:\n|$)|\/\*(?:[^*]|\*(?!\/))*\*\/)*` + /** * Where a ui-template script is a component: Dashboard treats it as one when its only statement is * `export default { ... }`. @@ -93,8 +96,8 @@ declare function __uiTemplate(component: { * @returns {{ open: number, close: number } | null} offsets (in body) of the object's `{` and `}` */ export function findComponent (body) { - const head = /^(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*export\s+default\s*(?=\{)/.exec(body) - const tail = /\}\s*;?\s*$/.exec(body) + const head = new RegExp(`^${GAP}export\\s+default\\s*(?=\\{)`).exec(body) + const tail = new RegExp(`\\}\\s*;?${GAP}$`).exec(body) return head && tail ? { open: head[0].length, close: tail.index } : null } diff --git a/test/resources/vue-script.spec.js b/test/resources/vue-script.spec.js index 3d9abd060..ae036ab23 100644 --- a/test/resources/vue-script.spec.js +++ b/test/resources/vue-script.spec.js @@ -31,6 +31,20 @@ describe('vue-editor