@aaqu/fromcubes-react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +405 -2
- package/THIRD-PARTY-NOTICES.md +164 -0
- package/examples/D3 Poland Map.json +90 -0
- package/examples/Live Chart.json +101 -0
- package/examples/PixiJS Sprites.json +114 -0
- package/examples/Sensor Portal.json +93 -0
- package/examples/Shared Components.json +68 -0
- package/examples/Three.js Scene.json +118 -0
- package/examples/Utility Hooks.json +94 -0
- package/examples/WebGPU Shader.json +110 -0
- package/nodes/editor/ark-pixel.LICENSE.txt +96 -0
- package/nodes/editor/ark-pixel.woff2 +0 -0
- package/nodes/editor/file-tree.js +403 -0
- package/nodes/editor/format.js +182 -0
- package/nodes/editor/icons.svg +46 -0
- package/nodes/editor/mark.svg +5 -0
- package/nodes/editor/provider-settings.js +192 -0
- package/nodes/editor/workspace.css +653 -0
- package/nodes/editor/workspace.js +1565 -0
- package/nodes/icons/fromcubes-component.svg +27 -0
- package/nodes/icons/fromcubes-react.svg +25 -0
- package/nodes/icons/fromcubes-utility.svg +21 -0
- package/nodes/lib/admin-api.js +238 -0
- package/nodes/lib/admin-security.js +161 -0
- package/nodes/lib/api.js +29 -0
- package/nodes/lib/helpers.js +990 -0
- package/nodes/lib/hooks.js +145 -0
- package/nodes/lib/page-builder.js +499 -0
- package/nodes/lib/portal-page-route.js +131 -0
- package/nodes/lib/providers.js +536 -0
- package/nodes/lib/registry-nodes.js +331 -0
- package/nodes/lib/router.js +103 -0
- package/nodes/lib/ws-heartbeat.js +58 -0
- package/nodes/portal-react.html +2539 -0
- package/nodes/portal-react.js +1652 -0
- package/package.json +76 -4
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Code Workspace — pure file-tree layer.
|
|
3
|
+
*
|
|
4
|
+
* Maps Node-RED editor node objects (`portal-react`, `fc-portal-component`,
|
|
5
|
+
* `fc-portal-utility`) onto a virtual file listing, then folds that flat
|
|
6
|
+
* listing into a nested tree. No DOM, no jQuery, no `RED` access — the whole
|
|
7
|
+
* module is a pair of pure functions so it can be unit-tested under vitest and
|
|
8
|
+
* still be loaded as a plain browser script by the Node-RED editor.
|
|
9
|
+
*
|
|
10
|
+
* Loaded in the browser via `<script src="fromcubes-react/editor/file-tree.js">`
|
|
11
|
+
* (exposes `window.__fcFileTree`) and in tests via `require()`.
|
|
12
|
+
*/
|
|
13
|
+
(function (root, factory) {
|
|
14
|
+
const api = factory();
|
|
15
|
+
if (typeof module === "object" && module.exports) {
|
|
16
|
+
module.exports = api;
|
|
17
|
+
} else {
|
|
18
|
+
root.__fcFileTree = api;
|
|
19
|
+
}
|
|
20
|
+
})(typeof self !== "undefined" ? self : this, function () {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
/** Matches lib/registry-nodes.js — oversized code is not scanned. */
|
|
24
|
+
const MAX_UTIL_CODE_BYTES = 1000000;
|
|
25
|
+
|
|
26
|
+
const GROUP_PORTALS = "Portals";
|
|
27
|
+
const GROUP_COMPONENTS = "Components";
|
|
28
|
+
const GROUP_UTILITIES = "Utilities";
|
|
29
|
+
|
|
30
|
+
/** Fixed group ordering — mirrors bundle order (utilities → components → portal). */
|
|
31
|
+
const GROUP_ORDER = [GROUP_PORTALS, GROUP_COMPONENTS, GROUP_UTILITIES];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Per-node-type description of which config fields are editable "files".
|
|
35
|
+
*
|
|
36
|
+
* `file` is a fixed name; without it the file is named after the node's
|
|
37
|
+
* registered name plus `ext`. `optional: true` fields are hidden while empty
|
|
38
|
+
* unless `showHead` is set. `under: true` nests the file beneath the node's
|
|
39
|
+
* primary file instead of beside it.
|
|
40
|
+
*/
|
|
41
|
+
const FIELD_MAP = {
|
|
42
|
+
"portal-react": {
|
|
43
|
+
group: GROUP_PORTALS,
|
|
44
|
+
nameField: "subPath",
|
|
45
|
+
files: [
|
|
46
|
+
// The portal reads as one file named after its sub-path; its custom
|
|
47
|
+
// <head> hangs off that file rather than forcing a folder around a
|
|
48
|
+
// pair, so a portal costs one row until you ask for the second.
|
|
49
|
+
{ field: "componentCode", ext: "jsx", lang: "javascript" },
|
|
50
|
+
{
|
|
51
|
+
field: "customHead",
|
|
52
|
+
file: "head.html",
|
|
53
|
+
lang: "html",
|
|
54
|
+
optional: true,
|
|
55
|
+
under: true,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
"fc-portal-component": {
|
|
60
|
+
group: GROUP_COMPONENTS,
|
|
61
|
+
nameField: "compName",
|
|
62
|
+
files: [{ field: "compCode", ext: "jsx", lang: "javascript" }],
|
|
63
|
+
},
|
|
64
|
+
"fc-portal-utility": {
|
|
65
|
+
group: GROUP_UTILITIES,
|
|
66
|
+
nameField: "utilName",
|
|
67
|
+
files: [{ field: "utilCode", ext: "js", lang: "javascript" }],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* True when the node type owns portal-react source code.
|
|
73
|
+
* @param {string} type
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function isPortalNodeType(type) {
|
|
77
|
+
return Object.prototype.hasOwnProperty.call(FIELD_MAP, type);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Display name for a node: its registered name (subPath / compName /
|
|
82
|
+
* utilName), falling back to the canvas label and finally the node id, so a
|
|
83
|
+
* half-configured node still shows up in the tree instead of vanishing.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} node
|
|
86
|
+
* @param {string} nameField
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
function displayName(node, nameField) {
|
|
90
|
+
const registered = (node[nameField] || "").trim();
|
|
91
|
+
if (registered) return registered;
|
|
92
|
+
const label = (node.name || "").trim();
|
|
93
|
+
if (label) return label;
|
|
94
|
+
return node.id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Flatten editor nodes into virtual files.
|
|
99
|
+
*
|
|
100
|
+
* Two layouts, both keeping the flow level only where it disambiguates:
|
|
101
|
+
* - `groupBy: "kind"` → `Utilities/<flow>/<name>.js`. Kind first, so all
|
|
102
|
+
* utilities sit in one folder however many flow tabs there are.
|
|
103
|
+
* - `groupBy: "flow"` → `<flow>/<name>.js`, mirroring the canvas: one flat
|
|
104
|
+
* alphabetical list per tab. No kind level — inside a flow the extension
|
|
105
|
+
* already says what a file is, and a folder per kind holding one file is
|
|
106
|
+
* noise. Portals keep their own folder, since a portal owns two files.
|
|
107
|
+
*
|
|
108
|
+
* @param {Array<Object>} nodes Plain node objects from `RED.nodes.eachNode`.
|
|
109
|
+
* @param {Object} [opts]
|
|
110
|
+
* @param {(flowId: string) => string} [opts.flowLabel] Resolves a flow tab id
|
|
111
|
+
* to its label. Only consulted when a level actually spans several flows.
|
|
112
|
+
* @param {boolean} [opts.showHead] Include empty `head.html` files.
|
|
113
|
+
* @param {"kind"|"flow"} [opts.groupBy] Layout, default `"kind"`.
|
|
114
|
+
* @param {(flowId: string) => boolean} [opts.flowDisabled] Is that flow tab
|
|
115
|
+
* disabled? Files inherit it, since a disabled tab takes its nodes with it.
|
|
116
|
+
* @returns {Array<Object>} File descriptors, sorted by group then path.
|
|
117
|
+
*/
|
|
118
|
+
function collectPortalFiles(nodes, opts) {
|
|
119
|
+
const options = opts || {};
|
|
120
|
+
const byFlow = options.groupBy === "flow";
|
|
121
|
+
const list = [];
|
|
122
|
+
const allFlows = new Set();
|
|
123
|
+
const flowsPerGroup = {};
|
|
124
|
+
|
|
125
|
+
(nodes || []).forEach(function (node) {
|
|
126
|
+
if (!node || !isPortalNodeType(node.type)) return;
|
|
127
|
+
const flowId = node.z || "";
|
|
128
|
+
allFlows.add(flowId);
|
|
129
|
+
const group = FIELD_MAP[node.type].group;
|
|
130
|
+
(flowsPerGroup[group] = flowsPerGroup[group] || new Set()).add(flowId);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const labelOf =
|
|
134
|
+
options.flowLabel ||
|
|
135
|
+
function (id) {
|
|
136
|
+
return id || "Flow";
|
|
137
|
+
};
|
|
138
|
+
const flowOff = options.flowDisabled || function () {
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
(nodes || []).forEach(function (node) {
|
|
143
|
+
if (!node || !isPortalNodeType(node.type)) return;
|
|
144
|
+
const spec = FIELD_MAP[node.type];
|
|
145
|
+
const name = displayName(node, spec.nameField);
|
|
146
|
+
// Only pay for a flow level when it separates something: the whole tree
|
|
147
|
+
// for flow-first, this one group for kind-first.
|
|
148
|
+
const scope = byFlow ? allFlows : flowsPerGroup[spec.group];
|
|
149
|
+
const flowLevel = scope.size > 1 ? labelOf(node.z || "") || "Flow" : null;
|
|
150
|
+
// A node can be disabled on its own, or ride a disabled flow tab down
|
|
151
|
+
// with it. Both mean "this code is not running"; the tree says which.
|
|
152
|
+
const flowIsOff = flowOff(node.z || "");
|
|
153
|
+
const disabled = node.d ? "node" : flowIsOff ? "flow" : "";
|
|
154
|
+
|
|
155
|
+
const primaryName = name + "." + (spec.files[0].ext || "jsx");
|
|
156
|
+
|
|
157
|
+
spec.files.forEach(function (f) {
|
|
158
|
+
const value = node[f.field];
|
|
159
|
+
if (f.optional && !options.showHead && !(value || "").trim()) return;
|
|
160
|
+
|
|
161
|
+
const fileName = f.file || name + "." + f.ext;
|
|
162
|
+
const segments = [];
|
|
163
|
+
if (byFlow) {
|
|
164
|
+
if (flowLevel) segments.push(flowLevel);
|
|
165
|
+
} else {
|
|
166
|
+
segments.push(spec.group);
|
|
167
|
+
if (flowLevel) segments.push(flowLevel);
|
|
168
|
+
}
|
|
169
|
+
if (f.under) segments.push(primaryName);
|
|
170
|
+
segments.push(fileName);
|
|
171
|
+
|
|
172
|
+
list.push({
|
|
173
|
+
key: node.id + ":" + f.field,
|
|
174
|
+
nodeId: node.id,
|
|
175
|
+
type: node.type,
|
|
176
|
+
field: f.field,
|
|
177
|
+
lang: f.lang,
|
|
178
|
+
name: fileName,
|
|
179
|
+
registryName: node.type === "portal-react" ? null : name,
|
|
180
|
+
group: spec.group,
|
|
181
|
+
groupIndex: GROUP_ORDER.indexOf(spec.group),
|
|
182
|
+
flowId: node.z || "",
|
|
183
|
+
flowDisabled: flowIsOff,
|
|
184
|
+
disabled: disabled,
|
|
185
|
+
// Which segment is the flow tab, so the tree can badge that folder
|
|
186
|
+
// as a flow rather than a plain directory.
|
|
187
|
+
flowSegment: flowLevel ? (byFlow ? 0 : 1) : -1,
|
|
188
|
+
segments: segments,
|
|
189
|
+
path: segments.join("/"),
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
list.sort(function (a, b) {
|
|
195
|
+
// Kind leads — portals, then components, then utilities: how the bundle
|
|
196
|
+
// is assembled, and how you read a portal (the page first, then what it
|
|
197
|
+
// is built from). Which level the kind sits at differs per layout, so the
|
|
198
|
+
// comparison follows the segment order the layout actually produced.
|
|
199
|
+
const dirOf = (f) => f.segments.slice(0, -1).join("/");
|
|
200
|
+
const byName = () => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
201
|
+
const byDir = () => dirOf(a).localeCompare(dirOf(b), undefined, { sensitivity: "base" });
|
|
202
|
+
if (byFlow) {
|
|
203
|
+
if (dirOf(a) !== dirOf(b)) return byDir();
|
|
204
|
+
if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
|
|
205
|
+
return byName();
|
|
206
|
+
}
|
|
207
|
+
if (a.groupIndex !== b.groupIndex) return a.groupIndex - b.groupIndex;
|
|
208
|
+
if (dirOf(a) !== dirOf(b)) return byDir();
|
|
209
|
+
return byName();
|
|
210
|
+
});
|
|
211
|
+
return list;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fold a flat file listing into a nested directory tree.
|
|
216
|
+
*
|
|
217
|
+
* @param {Array<Object>} files Output of {@link collectPortalFiles}.
|
|
218
|
+
* @returns {{name: string, path: string, type: string, children: Array}}
|
|
219
|
+
* Synthetic root node; directories sort before files, both alphabetically.
|
|
220
|
+
*/
|
|
221
|
+
function buildTree(files) {
|
|
222
|
+
const root = { name: "", path: "", type: "dir", children: [], index: {} };
|
|
223
|
+
|
|
224
|
+
(files || []).forEach(function (file) {
|
|
225
|
+
let cursor = root;
|
|
226
|
+
const dirs = file.segments.slice(0, -1);
|
|
227
|
+
dirs.forEach(function (seg, i) {
|
|
228
|
+
let child = cursor.index[seg];
|
|
229
|
+
if (!child) {
|
|
230
|
+
const isFlowDir = i === file.flowSegment;
|
|
231
|
+
child = {
|
|
232
|
+
name: seg,
|
|
233
|
+
// Group folders sort by kind like their files do; flow folders
|
|
234
|
+
// have no kind of their own and fall back to their name.
|
|
235
|
+
order: isFlowDir ? null : GROUP_ORDER.indexOf(seg),
|
|
236
|
+
path: file.segments.slice(0, i + 1).join("/"),
|
|
237
|
+
type: "dir",
|
|
238
|
+
kind: isFlowDir ? "flow" : "group",
|
|
239
|
+
flowId: isFlowDir ? file.flowId : null,
|
|
240
|
+
disabled: isFlowDir && file.flowDisabled ? "flow" : "",
|
|
241
|
+
children: [],
|
|
242
|
+
index: {},
|
|
243
|
+
};
|
|
244
|
+
cursor.index[seg] = child;
|
|
245
|
+
cursor.children.push(child);
|
|
246
|
+
}
|
|
247
|
+
cursor = child;
|
|
248
|
+
});
|
|
249
|
+
// A path can be both a file and a parent — a portal's own `.jsx` holds
|
|
250
|
+
// its `head.html`. Whichever arrives second merges into the first.
|
|
251
|
+
const existing = cursor.index[file.name];
|
|
252
|
+
if (existing) {
|
|
253
|
+
existing.file = file;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const leaf = {
|
|
257
|
+
name: file.name,
|
|
258
|
+
path: file.path,
|
|
259
|
+
order: file.groupIndex,
|
|
260
|
+
type: "file",
|
|
261
|
+
file: file,
|
|
262
|
+
children: [],
|
|
263
|
+
index: {},
|
|
264
|
+
};
|
|
265
|
+
cursor.index[file.name] = leaf;
|
|
266
|
+
cursor.children.push(leaf);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
sortTree(root);
|
|
270
|
+
return root;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* In-place recursive sort: directories first, then case-insensitive by name.
|
|
275
|
+
* Also drops the `index` lookup maps used while building.
|
|
276
|
+
*
|
|
277
|
+
* @param {Object} dir
|
|
278
|
+
* @returns {void}
|
|
279
|
+
*/
|
|
280
|
+
function sortTree(dir) {
|
|
281
|
+
delete dir.index;
|
|
282
|
+
if (!dir.children) return;
|
|
283
|
+
dir.children.sort(function (a, b) {
|
|
284
|
+
// Kind order first where both siblings have one — that is what puts
|
|
285
|
+
// portals above components above utilities inside a flow.
|
|
286
|
+
const ka = typeof a.order === "number" && a.order >= 0 ? a.order : null;
|
|
287
|
+
const kb = typeof b.order === "number" && b.order >= 0 ? b.order : null;
|
|
288
|
+
if (ka !== null && kb !== null && ka !== kb) return ka - kb;
|
|
289
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
290
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
291
|
+
});
|
|
292
|
+
dir.children.forEach(sortTree);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A file that also holds files — a portal's `.jsx` owning its `head.html`.
|
|
297
|
+
* @param {Object} node A tree node from {@link buildTree}.
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
function isParentFile(node) {
|
|
301
|
+
return !!(node && node.file && node.children && node.children.length);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Does this node start closed?
|
|
306
|
+
*
|
|
307
|
+
* Folders open by default, because that is what a tree is for. Two things
|
|
308
|
+
* earn the opposite: a file holding files (you asked for the portal, not its
|
|
309
|
+
* `head.html`) and a flow Node-RED is not running (it is context, not work).
|
|
310
|
+
*
|
|
311
|
+
* @param {Object} node A tree node from {@link buildTree}.
|
|
312
|
+
* @returns {boolean}
|
|
313
|
+
*/
|
|
314
|
+
function startsCollapsed(node) {
|
|
315
|
+
if (!node) return false;
|
|
316
|
+
return isParentFile(node) || node.disabled === "flow";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Which kind of thing a file is, for icon and colour purposes.
|
|
321
|
+
*
|
|
322
|
+
* Decided by node type and field, never by extension: a portal and a
|
|
323
|
+
* component are both `.jsx`, and they are exactly the two the tree most
|
|
324
|
+
* needs to tell apart.
|
|
325
|
+
*
|
|
326
|
+
* @param {Object} file A descriptor from {@link collectPortalFiles}.
|
|
327
|
+
* @returns {"portal"|"component"|"utility"|"head"|"file"}
|
|
328
|
+
*/
|
|
329
|
+
function iconKindFor(file) {
|
|
330
|
+
if (!file) return "file";
|
|
331
|
+
if (file.type === "portal-react") {
|
|
332
|
+
return file.field === "customHead" ? "head" : "portal";
|
|
333
|
+
}
|
|
334
|
+
if (file.type === "fc-portal-component") return "component";
|
|
335
|
+
if (file.type === "fc-portal-utility") return "utility";
|
|
336
|
+
return "file";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Top-level `function`/`const`/`let`/`var`/`class` names in utility code.
|
|
341
|
+
*
|
|
342
|
+
* Client mirror of `extractUtilitySymbols` in lib/registry-nodes.js — same
|
|
343
|
+
* anchored regex, so what the editor treats as a utility's exported name
|
|
344
|
+
* matches what the bundler injects. Kept here (pure) so it is testable, and
|
|
345
|
+
* kept as a scan so it still answers for half-written code. Multi-declarator
|
|
346
|
+
* statements are not followed; the caller falls back to a plain reference
|
|
347
|
+
* search, which costs at worst a jump to the wrong line of the right file.
|
|
348
|
+
*
|
|
349
|
+
* @param {string} code
|
|
350
|
+
* @returns {Set<string>}
|
|
351
|
+
*/
|
|
352
|
+
function topLevelSymbols(code) {
|
|
353
|
+
const names = new Set();
|
|
354
|
+
if (!code || code.length > MAX_UTIL_CODE_BYTES) return names;
|
|
355
|
+
const re = /^(?:export\s+)?(?:async\s+)?(?:function\s*\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
356
|
+
let m;
|
|
357
|
+
while ((m = re.exec(code))) names.add(m[1]);
|
|
358
|
+
return names;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Does this utility code declare `name` at its top level?
|
|
363
|
+
* @param {string} code
|
|
364
|
+
* @param {string} name
|
|
365
|
+
* @returns {boolean}
|
|
366
|
+
*/
|
|
367
|
+
function declaresSymbol(code, name) {
|
|
368
|
+
return topLevelSymbols(code).has(name);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Case-insensitive substring filter over full paths.
|
|
373
|
+
*
|
|
374
|
+
* @param {Array<Object>} files
|
|
375
|
+
* @param {string} query
|
|
376
|
+
* @returns {Array<Object>}
|
|
377
|
+
*/
|
|
378
|
+
function filterFiles(files, query) {
|
|
379
|
+
const q = (query || "").trim().toLowerCase();
|
|
380
|
+
if (!q) return files || [];
|
|
381
|
+
return (files || []).filter(function (f) {
|
|
382
|
+
return f.path.toLowerCase().indexOf(q) !== -1;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
GROUP_PORTALS: GROUP_PORTALS,
|
|
388
|
+
GROUP_COMPONENTS: GROUP_COMPONENTS,
|
|
389
|
+
GROUP_UTILITIES: GROUP_UTILITIES,
|
|
390
|
+
GROUP_ORDER: GROUP_ORDER,
|
|
391
|
+
FIELD_MAP: FIELD_MAP,
|
|
392
|
+
isPortalNodeType: isPortalNodeType,
|
|
393
|
+
displayName: displayName,
|
|
394
|
+
isParentFile: isParentFile,
|
|
395
|
+
startsCollapsed: startsCollapsed,
|
|
396
|
+
iconKindFor: iconKindFor,
|
|
397
|
+
topLevelSymbols: topLevelSymbols,
|
|
398
|
+
declaresSymbol: declaresSymbol,
|
|
399
|
+
collectPortalFiles: collectPortalFiles,
|
|
400
|
+
buildTree: buildTree,
|
|
401
|
+
filterFiles: filterFiles,
|
|
402
|
+
};
|
|
403
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting for the fromcubes editors — Prettier, loaded on first use.
|
|
3
|
+
*
|
|
4
|
+
* Registers Monaco formatting providers for `javascript` (portal JSX,
|
|
5
|
+
* components, utilities) and `html` (a portal's custom head). Providers are
|
|
6
|
+
* global per language, so this serves the React Editor overlay and the four
|
|
7
|
+
* Monaco instances inside node dialogs alike.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is fetched until someone actually formats: Prettier's browser builds
|
|
10
|
+
* are ~772 KB, and most sessions never ask for them.
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const PREFIX = "[FC-Format]";
|
|
16
|
+
const BASE = "fromcubes-react/prettier/";
|
|
17
|
+
// UMD builds: each sets window.prettier / window.prettierPlugins.
|
|
18
|
+
const SCRIPTS = [
|
|
19
|
+
"standalone.js",
|
|
20
|
+
"plugins/babel.js",
|
|
21
|
+
"plugins/estree.js",
|
|
22
|
+
"plugins/html.js",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** @type {Promise<boolean>|null} in-flight or settled load */
|
|
26
|
+
let loading = null;
|
|
27
|
+
let registered = false;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load one script tag.
|
|
31
|
+
* @param {string} src
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
function loadScript(src) {
|
|
35
|
+
return new Promise(function (resolve, reject) {
|
|
36
|
+
const s = document.createElement("script");
|
|
37
|
+
s.src = src;
|
|
38
|
+
s.onload = function () {
|
|
39
|
+
resolve();
|
|
40
|
+
};
|
|
41
|
+
s.onerror = function () {
|
|
42
|
+
reject(new Error("failed to load " + src));
|
|
43
|
+
};
|
|
44
|
+
document.head.appendChild(s);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch Prettier once. Resolves false when it could not be loaded, so the
|
|
50
|
+
* caller can fall back rather than fail.
|
|
51
|
+
* @returns {Promise<boolean>}
|
|
52
|
+
*/
|
|
53
|
+
function ensurePrettier() {
|
|
54
|
+
if (loading) return loading;
|
|
55
|
+
loading = SCRIPTS.reduce(function (chain, name) {
|
|
56
|
+
return chain.then(function () {
|
|
57
|
+
return loadScript(BASE + name);
|
|
58
|
+
});
|
|
59
|
+
}, Promise.resolve())
|
|
60
|
+
.then(function () {
|
|
61
|
+
if (!window.prettier || !window.prettierPlugins) {
|
|
62
|
+
throw new Error("prettier globals missing after load");
|
|
63
|
+
}
|
|
64
|
+
// Monaco already ships a TypeScript formatter on `javascript`. With two
|
|
65
|
+
// providers registered it picks one, and a silent loss would read as
|
|
66
|
+
// "formatting is broken" — so TS steps aside, but only once Prettier is
|
|
67
|
+
// definitely here. Until then its formatter stays as the fallback.
|
|
68
|
+
try {
|
|
69
|
+
const ts = monaco.languages.typescript.javascriptDefaults;
|
|
70
|
+
ts.setModeConfiguration(
|
|
71
|
+
Object.assign({}, ts.modeConfiguration, {
|
|
72
|
+
documentFormattingEdits: false,
|
|
73
|
+
documentRangeFormattingEdits: false,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.warn(PREFIX, "could not stand down the TS formatter", err);
|
|
78
|
+
}
|
|
79
|
+
console.log(PREFIX, "prettier", window.prettier.version, "ready");
|
|
80
|
+
return true;
|
|
81
|
+
})
|
|
82
|
+
.catch(function (err) {
|
|
83
|
+
console.warn(PREFIX, "prettier unavailable — Monaco's own formatter stays in charge", err);
|
|
84
|
+
return false;
|
|
85
|
+
});
|
|
86
|
+
return loading;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run Prettier over a whole model.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} model Monaco text model
|
|
93
|
+
* @param {string} parser Prettier parser name
|
|
94
|
+
* @returns {Promise<Array>} Monaco text edits — empty when nothing can be done
|
|
95
|
+
*/
|
|
96
|
+
function formatModel(model, parser) {
|
|
97
|
+
return ensurePrettier().then(function (ok) {
|
|
98
|
+
if (!ok) return [];
|
|
99
|
+
const text = model.getValue();
|
|
100
|
+
return window.prettier
|
|
101
|
+
.format(text, {
|
|
102
|
+
parser: parser,
|
|
103
|
+
plugins: [
|
|
104
|
+
window.prettierPlugins.babel,
|
|
105
|
+
window.prettierPlugins.estree,
|
|
106
|
+
window.prettierPlugins.html,
|
|
107
|
+
],
|
|
108
|
+
// Follow the editor rather than impose a second opinion on indent.
|
|
109
|
+
tabWidth: model.getOptions().tabSize || 2,
|
|
110
|
+
})
|
|
111
|
+
.then(function (out) {
|
|
112
|
+
if (out === text) return [];
|
|
113
|
+
return [{ range: model.getFullModelRange(), text: out }];
|
|
114
|
+
})
|
|
115
|
+
.catch(function (err) {
|
|
116
|
+
// Prettier throws on code that does not parse — which is normal for
|
|
117
|
+
// a file still being written. Say so instead of doing nothing.
|
|
118
|
+
reportParseError(err);
|
|
119
|
+
return [];
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Surface a parse failure where the user is looking.
|
|
126
|
+
* @param {Error} err
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
function reportParseError(err) {
|
|
130
|
+
const loc = err && err.loc && err.loc.start;
|
|
131
|
+
const first = String((err && err.message) || err).split("\n")[0];
|
|
132
|
+
// Prettier usually puts "(line:column)" in the message already; only add
|
|
133
|
+
// one when it did not, so the notification says it once.
|
|
134
|
+
const where = loc && !/\(\d+:\d+\)/.test(first)
|
|
135
|
+
? " (line " + loc.line + ", column " + loc.column + ")"
|
|
136
|
+
: "";
|
|
137
|
+
console.warn(PREFIX, "not formatted:", err);
|
|
138
|
+
if (typeof RED !== "undefined" && RED.notify) {
|
|
139
|
+
RED.notify("Cannot format — " + first + where, { type: "warning", timeout: 6000 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register the providers once Monaco exists.
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
function register() {
|
|
148
|
+
if (registered) return;
|
|
149
|
+
if (typeof monaco === "undefined" || !monaco.languages) {
|
|
150
|
+
setTimeout(register, 300);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
registered = true;
|
|
154
|
+
|
|
155
|
+
[
|
|
156
|
+
["javascript", "babel"],
|
|
157
|
+
["html", "html"],
|
|
158
|
+
].forEach(function (pair) {
|
|
159
|
+
const language = pair[0];
|
|
160
|
+
const parser = pair[1];
|
|
161
|
+
monaco.languages.registerDocumentFormattingEditProvider(language, {
|
|
162
|
+
displayName: "Prettier (fromcubes)",
|
|
163
|
+
provideDocumentFormattingEdits: function (model) {
|
|
164
|
+
return formatModel(model, parser);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
// Format Selection falls out of the same call: Prettier has no partial
|
|
168
|
+
// mode, so the whole document is formatted and Monaco applies the edit.
|
|
169
|
+
monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
|
|
170
|
+
displayName: "Prettier (fromcubes)",
|
|
171
|
+
provideDocumentRangeFormattingEdits: function (model) {
|
|
172
|
+
return formatModel(model, parser);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
window.__fcFormat = { ensure: ensurePrettier, format: formatModel };
|
|
178
|
+
console.log(PREFIX, "formatting providers registered");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
register();
|
|
182
|
+
})();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="display:none" aria-hidden="true">
|
|
2
|
+
<!-- Generated by tools/build-sprite.mjs — do not edit.
|
|
3
|
+
Icons from Lucide (https://lucide.dev), ISC — see THIRD-PARTY-NOTICES.md.
|
|
4
|
+
Stroke is currentColor and stroke-width is a presentation attribute, so
|
|
5
|
+
the page can recolour and thicken each use from CSS. -->
|
|
6
|
+
<symbol id="fc-icon-atom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
7
|
+
<circle cx="12" cy="12" r="1"/>
|
|
8
|
+
<path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/>
|
|
9
|
+
<path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>
|
|
10
|
+
</symbol>
|
|
11
|
+
<symbol id="fc-icon-component" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
12
|
+
<path d="M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
13
|
+
<path d="M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"/>
|
|
14
|
+
<path d="M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"/>
|
|
15
|
+
<path d="M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="fc-icon-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
18
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>
|
|
19
|
+
</symbol>
|
|
20
|
+
<symbol id="fc-icon-code-xml" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<path d="m18 16 4-4-4-4"/>
|
|
22
|
+
<path d="m6 8-4 4 4 4"/>
|
|
23
|
+
<path d="m14.5 4-5 16"/>
|
|
24
|
+
</symbol>
|
|
25
|
+
<symbol id="fc-icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
26
|
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>
|
|
27
|
+
</symbol>
|
|
28
|
+
<symbol id="fc-icon-folder-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
29
|
+
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>
|
|
30
|
+
</symbol>
|
|
31
|
+
<symbol id="fc-icon-workflow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
32
|
+
<rect width="8" height="8" x="3" y="3" rx="2"/>
|
|
33
|
+
<path d="M7 11v4a2 2 0 0 0 2 2h4"/>
|
|
34
|
+
<rect width="8" height="8" x="13" y="13" rx="2"/>
|
|
35
|
+
</symbol>
|
|
36
|
+
<symbol id="fc-icon-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
37
|
+
<circle cx="12" cy="12" r="10"/>
|
|
38
|
+
<path d="M4.929 4.929 19.07 19.071"/>
|
|
39
|
+
</symbol>
|
|
40
|
+
<symbol id="fc-icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
41
|
+
<path d="m9 18 6-6-6-6"/>
|
|
42
|
+
</symbol>
|
|
43
|
+
<symbol id="fc-icon-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
44
|
+
<path d="m6 9 6 6 6-6"/>
|
|
45
|
+
</symbol>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<!-- The fromcubes mark, traced from the project artwork by tools/build-icon.mjs.
|
|
3
|
+
currentColor so the editor chrome can recolour it per surface. -->
|
|
4
|
+
<path fill="currentColor" fill-rule="evenodd" d="M31.03 1.00L32.97 1.00L34.91 1.97L49.11 10.69L57.83 15.21L59.13 16.50L59.13 47.50L57.83 48.79L49.11 53.31L34.91 62.03L32.97 63.00L30.39 62.68L17.15 54.60L8.43 50.08L4.88 47.50L4.88 16.50L12.95 11.33L31.03 1.32ZM31.68 3.91L9.07 16.82L9.40 17.47L10.69 17.47L15.21 14.56L20.70 13.27L23.28 13.27L33.61 19.08L37.17 21.34L37.49 23.93L36.20 25.22L35.23 27.48L31.35 29.42L31.68 30.39L54.93 17.15L53.64 16.50L48.79 19.73L41.04 21.02L30.71 15.21L28.77 13.59L26.83 12.95L26.51 10.36L29.09 6.49L32.97 4.55L32.00 3.91ZM35.55 6.17L31.35 8.43L29.74 11.33L41.69 18.11L47.18 17.15L51.05 14.56L35.88 6.17ZM21.02 16.18L16.18 17.15L12.95 19.41L22.96 24.90L24.90 26.51L27.48 27.48L27.80 28.13L33.29 25.22L34.26 22.96L25.22 17.47L22.64 16.18L21.34 16.18ZM7.78 19.73L7.46 21.02L12.63 23.60L17.79 29.42L18.11 46.21L16.82 47.50L11.98 47.50L8.43 45.24L7.46 45.24L7.46 46.21L13.27 49.11L30.39 59.45L30.39 57.51L25.54 55.25L20.70 50.08L20.05 32.65L21.34 31.35L26.51 31.35L30.06 33.61L30.71 33.61L30.71 32.65L8.10 19.73ZM56.22 19.73L54.93 20.38L54.93 27.16L52.67 33.61L39.43 41.69L37.81 42.01L36.20 41.36L34.26 37.81L33.94 32.65L33.29 32.97L33.29 59.13L34.58 58.80L34.26 52.99L36.52 45.89L50.73 37.17L53.31 38.14L55.25 41.69L55.25 46.53L56.54 46.21L56.54 19.73ZM52.02 21.99L36.84 30.71L37.17 37.17L37.81 38.46L38.78 38.78L41.69 36.52L50.41 31.68L52.02 26.83L52.02 22.31ZM7.78 24.25L7.46 41.69L11.98 43.95L12.30 44.59L15.21 44.59L14.89 30.06L11.98 27.48L11.33 26.19L7.78 24.57ZM23.28 34.26L23.28 48.47L27.48 52.99L30.39 54.60L30.71 37.17L25.86 34.26L23.60 34.26ZM50.73 40.72L38.78 47.82L37.17 52.99L37.17 57.19L52.34 48.47L52.34 42.33L51.05 40.72Z"/>
|
|
5
|
+
</svg>
|