apostrophe 4.30.1-beta.1 → 4.31.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/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +22 -2
- package/claude-tools/detect-handles.js +46 -0
- package/claude-tools/minimal-hang-test.js +28 -0
- package/claude-tools/mongo-close-test.js +11 -0
- package/claude-tools/stdin-ref-test.js +14 -0
- package/eslint.config.js +3 -1
- package/modules/@apostrophecms/area/index.js +94 -2
- package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -40
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +0 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +0 -1
- package/modules/@apostrophecms/attachment/index.js +4 -1
- package/modules/@apostrophecms/db/index.js +68 -27
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +5 -3
- package/modules/@apostrophecms/express/index.js +2 -0
- package/modules/@apostrophecms/file/index.js +9 -8
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +2 -2
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +3 -0
- package/modules/@apostrophecms/job/index.js +9 -7
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +0 -1
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -1
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +10 -2
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -3
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +52 -23
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +6 -1
- package/modules/@apostrophecms/oembed/index.js +2 -1
- package/modules/@apostrophecms/piece-type/index.js +2 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +7 -2
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +7 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +10 -0
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +1 -0
- package/modules/@apostrophecms/template/index.js +117 -11
- package/modules/@apostrophecms/template/lib/jsxLoader.js +128 -0
- package/modules/@apostrophecms/template/lib/jsxRender.js +490 -0
- package/modules/@apostrophecms/template/lib/jsxRuntime.js +276 -0
- package/modules/@apostrophecms/template/lib/nunjucksLoader.js +11 -36
- package/modules/@apostrophecms/template/lib/viewWatcher.js +113 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellLastEdited.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +10 -4
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +6 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/modules/@apostrophecms/uploadfs/index.js +3 -0
- package/modules/@apostrophecms/util/index.js +20 -3
- package/package.json +14 -10
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/add-missing-schema-fields-project/test.js +22 -3
- package/test/assets.js +110 -67
- package/test/db-tools.js +365 -0
- package/test/db.js +24 -15
- package/test/default-adapter.js +256 -0
- package/test/external-front.js +419 -1
- package/test/files.js +28 -0
- package/test/job.js +1 -1
- package/test/modules/jsx-area-test/index.js +23 -0
- package/test/modules/jsx-area-test/views/bad-area.jsx +7 -0
- package/test/modules/jsx-area-test/views/with-area-ctx.jsx +13 -0
- package/test/modules/jsx-area-test/views/with-area.jsx +7 -0
- package/test/modules/jsx-area-test/views/with-widget-ctx.jsx +12 -0
- package/test/modules/jsx-area-test/views/with-widget.jsx +7 -0
- package/test/modules/jsx-async-widget/index.js +6 -0
- package/test/modules/jsx-async-widget/views/widget.jsx +11 -0
- package/test/modules/jsx-bridge-test/index.js +1 -0
- package/test/modules/jsx-bridge-test/views/cross-module.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/disambig-name-only.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/disambig-target.jsx +8 -0
- package/test/modules/jsx-bridge-test/views/disambig-with-template-name.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/include-html.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/include-target.html +4 -0
- package/test/modules/jsx-bridge-test/views/jsx-extends-via-extend.jsx +9 -0
- package/test/modules/jsx-bridge-test/views/jsx-extends.jsx +9 -0
- package/test/modules/jsx-bridge-test/views/jsx-layout.jsx +14 -0
- package/test/modules/jsx-bridge-test/views/njk-extends.jsx +14 -0
- package/test/modules/jsx-bridge-test/views/njk-layout.html +9 -0
- package/test/modules/jsx-bridge-test/views/short-form.jsx +7 -0
- package/test/modules/jsx-bridge-test/views/short-target.jsx +3 -0
- package/test/modules/jsx-component-test/index.js +15 -0
- package/test/modules/jsx-component-test/views/greet.html +1 -0
- package/test/modules/jsx-component-test/views/uses-component.jsx +8 -0
- package/test/modules/jsx-ctx-widget/index.js +6 -0
- package/test/modules/jsx-ctx-widget/views/widget.jsx +4 -0
- package/test/modules/jsx-mixed-test/index.js +9 -0
- package/test/modules/jsx-mixed-test/views/apos-full.jsx +21 -0
- package/test/modules/jsx-mixed-test/views/async-list.jsx +12 -0
- package/test/modules/jsx-mixed-test/views/lib/format.js +3 -0
- package/test/modules/jsx-mixed-test/views/localized.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/partial.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/safe-helper.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/syntax-error.jsx +3 -0
- package/test/modules/jsx-mixed-test/views/throws.jsx +5 -0
- package/test/modules/jsx-mixed-test/views/uses-import.jsx +5 -0
- package/test/modules/jsx-mixed-test/views/uses-require.jsx +5 -0
- package/test/modules/jsx-watcher-cross-test/index.js +5 -0
- package/test/modules/jsx-watcher-cross-test/views/cross-template.jsx +3 -0
- package/test/modules/jsx-watcher-test/index.js +5 -0
- package/test/modules/jsx-watcher-test/views/watcher-test.jsx +3 -0
- package/test/modules/template-jsx-options-test/index.js +12 -0
- package/test/modules/template-jsx-options-test/views/options-test.jsx +9 -0
- package/test/modules/template-jsx-subclass-test/index.js +3 -0
- package/test/modules/template-jsx-subclass-test/views/override-test.jsx +3 -0
- package/test/modules/template-jsx-test/index.js +9 -0
- package/test/modules/template-jsx-test/views/boolean-attrs.jsx +11 -0
- package/test/modules/template-jsx-test/views/class-and-for.jsx +7 -0
- package/test/modules/template-jsx-test/views/dangerously-set.jsx +3 -0
- package/test/modules/template-jsx-test/views/escape-attr.jsx +3 -0
- package/test/modules/template-jsx-test/views/escape-body.jsx +3 -0
- package/test/modules/template-jsx-test/views/inherit-test.jsx +3 -0
- package/test/modules/template-jsx-test/views/list.jsx +7 -0
- package/test/modules/template-jsx-test/views/override-test.jsx +3 -0
- package/test/modules/template-jsx-test/views/svg-attrs.jsx +27 -0
- package/test/modules/template-jsx-test/views/test.jsx +3 -0
- package/test/modules/template-jsx-test/views/void-elements.jsx +9 -0
- package/test/templates-jsx-watcher.js +135 -0
- package/test/templates-jsx.js +537 -0
- package/test/utils.js +103 -0
- package/test-lib/util.js +50 -14
- package/lib/mongodb-connect.js +0 -62
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// JSX runtime used by Apostrophe `.jsx` templates. The classic Babel
|
|
2
|
+
// transform (`@babel/plugin-transform-react-jsx`) compiles `<Tag a={x}>...</Tag>`
|
|
3
|
+
// into `h(Tag, { a: x }, ...children)` and `<>...</>` into
|
|
4
|
+
// `h(Fragment, null, ...children)`. The `h` function and `Fragment` symbol
|
|
5
|
+
// are injected into every compiled module by `jsxLoader.js`.
|
|
6
|
+
//
|
|
7
|
+
// Output model: `h` returns a nested array of strings, `Raw` markers,
|
|
8
|
+
// promises, and arrays. The array is flattened by `flatten()` which awaits
|
|
9
|
+
// every promise, escapes plain string values, and joins everything into a
|
|
10
|
+
// final HTML string. This array+promise model is what allows JSX templates
|
|
11
|
+
// to call asynchronous helpers (`Area`, `Component`, `Template`, `Extend`)
|
|
12
|
+
// directly inside markup without wrapping them in `await`.
|
|
13
|
+
|
|
14
|
+
const voidElements = require('void-elements');
|
|
15
|
+
|
|
16
|
+
const Fragment = Symbol('AposJsxFragment');
|
|
17
|
+
|
|
18
|
+
// Marker class for already-rendered raw HTML. Anything wrapped in a `Raw`
|
|
19
|
+
// is emitted into the final output without any additional escaping.
|
|
20
|
+
class Raw {
|
|
21
|
+
constructor(html) {
|
|
22
|
+
this.html = (html == null) ? '' : String(html);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Compatibility hook: Nunjucks `SafeString` instances (returned by Apostrophe
|
|
27
|
+
// helpers like `apos.area.html()` and the `safe` filter) need to flow through
|
|
28
|
+
// JSX templates without being escaped a second time. `template/index.js`
|
|
29
|
+
// registers Nunjucks's `SafeString` class via `registerSafeClass()`; any
|
|
30
|
+
// instance of a registered class is treated as raw HTML.
|
|
31
|
+
const safeClasses = [];
|
|
32
|
+
|
|
33
|
+
function registerSafeClass(cls) {
|
|
34
|
+
if (cls && !safeClasses.includes(cls)) {
|
|
35
|
+
safeClasses.push(cls);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSafeInstance(value) {
|
|
40
|
+
if (value == null || typeof value !== 'object') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (const cls of safeClasses) {
|
|
44
|
+
if (value instanceof cls) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function escapeHtml(value) {
|
|
52
|
+
return String(value)
|
|
53
|
+
.replace(/&/g, '&')
|
|
54
|
+
.replace(/</g, '<')
|
|
55
|
+
.replace(/>/g, '>');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function escapeAttr(value) {
|
|
59
|
+
return String(value)
|
|
60
|
+
.replace(/&/g, '&')
|
|
61
|
+
.replace(/"/g, '"')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build the JSX node for an element, function component, or fragment.
|
|
67
|
+
// For function types (including the runtime helpers `Area`, `Component`,
|
|
68
|
+
// `Template`, `Extend`, `Widget`) we invoke the function with a single
|
|
69
|
+
// `props` object that includes `children`, matching React conventions.
|
|
70
|
+
function h(type, props, ...children) {
|
|
71
|
+
props = props || {};
|
|
72
|
+
if (type === Fragment) {
|
|
73
|
+
return children;
|
|
74
|
+
}
|
|
75
|
+
if (typeof type === 'function') {
|
|
76
|
+
const finalProps = { ...props };
|
|
77
|
+
if (children.length > 0) {
|
|
78
|
+
finalProps.children = (children.length === 1) ? children[0] : children;
|
|
79
|
+
}
|
|
80
|
+
return type(finalProps);
|
|
81
|
+
}
|
|
82
|
+
if (typeof type !== 'string') {
|
|
83
|
+
throw new Error(`Invalid JSX element type: ${typeof type}`);
|
|
84
|
+
}
|
|
85
|
+
return buildElement(type, props, children);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildElement(tag, props, children) {
|
|
89
|
+
let attrs = '';
|
|
90
|
+
let dangerous = null;
|
|
91
|
+
for (const key of Object.keys(props)) {
|
|
92
|
+
if (key === 'children' || key === 'key' || key === 'ref') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (key === 'dangerouslySetInnerHTML') {
|
|
96
|
+
const v = props[key];
|
|
97
|
+
if (v && typeof v.__html === 'string') {
|
|
98
|
+
dangerous = v.__html;
|
|
99
|
+
} else if (v && v.__html != null) {
|
|
100
|
+
dangerous = String(v.__html);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const value = props[key];
|
|
105
|
+
if (value == null || value === false) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const attrName = jsxAttrName(key);
|
|
109
|
+
if (value === true) {
|
|
110
|
+
attrs += ` ${attrName}`;
|
|
111
|
+
} else {
|
|
112
|
+
attrs += ` ${attrName}="${escapeAttr(value)}"`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (dangerous != null) {
|
|
117
|
+
return [
|
|
118
|
+
new Raw(`<${tag}${attrs}>`),
|
|
119
|
+
new Raw(dangerous),
|
|
120
|
+
new Raw(`</${tag}>`)
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (voidElements[tag] && children.length === 0) {
|
|
125
|
+
return [ new Raw(`<${tag}${attrs} />`) ];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
new Raw(`<${tag}${attrs}>`),
|
|
130
|
+
...children,
|
|
131
|
+
new Raw(`</${tag}>`)
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Match React's friendly attribute names to standard HTML/SVG attribute
|
|
136
|
+
// names. `data-*` and `aria-*` props pass through verbatim; SVG presentation
|
|
137
|
+
// attributes (`strokeWidth`, `fillRule`, etc.) are converted to the
|
|
138
|
+
// kebab-case form actually understood by browsers when the document is
|
|
139
|
+
// parsed as text/html. The handful of SVG attributes that are *natively*
|
|
140
|
+
// camelCase (e.g. `viewBox`, `preserveAspectRatio`) stay as-is.
|
|
141
|
+
const svgAttrMap = {
|
|
142
|
+
// Stroke
|
|
143
|
+
strokeWidth: 'stroke-width',
|
|
144
|
+
strokeLinecap: 'stroke-linecap',
|
|
145
|
+
strokeLinejoin: 'stroke-linejoin',
|
|
146
|
+
strokeDasharray: 'stroke-dasharray',
|
|
147
|
+
strokeDashoffset: 'stroke-dashoffset',
|
|
148
|
+
strokeMiterlimit: 'stroke-miterlimit',
|
|
149
|
+
strokeOpacity: 'stroke-opacity',
|
|
150
|
+
// Fill
|
|
151
|
+
fillOpacity: 'fill-opacity',
|
|
152
|
+
fillRule: 'fill-rule',
|
|
153
|
+
// Clip / mask
|
|
154
|
+
clipPath: 'clip-path',
|
|
155
|
+
clipRule: 'clip-rule',
|
|
156
|
+
// Text
|
|
157
|
+
textAnchor: 'text-anchor',
|
|
158
|
+
textDecoration: 'text-decoration',
|
|
159
|
+
alignmentBaseline: 'alignment-baseline',
|
|
160
|
+
baselineShift: 'baseline-shift',
|
|
161
|
+
dominantBaseline: 'dominant-baseline',
|
|
162
|
+
fontFamily: 'font-family',
|
|
163
|
+
fontSize: 'font-size',
|
|
164
|
+
fontStyle: 'font-style',
|
|
165
|
+
fontVariant: 'font-variant',
|
|
166
|
+
fontWeight: 'font-weight',
|
|
167
|
+
letterSpacing: 'letter-spacing',
|
|
168
|
+
wordSpacing: 'word-spacing',
|
|
169
|
+
// Generic
|
|
170
|
+
colorInterpolation: 'color-interpolation',
|
|
171
|
+
colorInterpolationFilters: 'color-interpolation-filters',
|
|
172
|
+
colorProfile: 'color-profile',
|
|
173
|
+
colorRendering: 'color-rendering',
|
|
174
|
+
fillRendering: 'fill-rendering',
|
|
175
|
+
imageRendering: 'image-rendering',
|
|
176
|
+
shapeRendering: 'shape-rendering',
|
|
177
|
+
textRendering: 'text-rendering',
|
|
178
|
+
pointerEvents: 'pointer-events',
|
|
179
|
+
unicodeBidi: 'unicode-bidi',
|
|
180
|
+
vectorEffect: 'vector-effect',
|
|
181
|
+
writingMode: 'writing-mode',
|
|
182
|
+
enableBackground: 'enable-background',
|
|
183
|
+
floodColor: 'flood-color',
|
|
184
|
+
floodOpacity: 'flood-opacity',
|
|
185
|
+
glyphOrientationHorizontal: 'glyph-orientation-horizontal',
|
|
186
|
+
glyphOrientationVertical: 'glyph-orientation-vertical',
|
|
187
|
+
lightingColor: 'lighting-color',
|
|
188
|
+
markerEnd: 'marker-end',
|
|
189
|
+
markerMid: 'marker-mid',
|
|
190
|
+
markerStart: 'marker-start',
|
|
191
|
+
overflowWrap: 'overflow-wrap',
|
|
192
|
+
paintOrder: 'paint-order',
|
|
193
|
+
stopColor: 'stop-color',
|
|
194
|
+
stopOpacity: 'stop-opacity',
|
|
195
|
+
// Linked references
|
|
196
|
+
xlinkHref: 'xlink:href',
|
|
197
|
+
xlinkRole: 'xlink:role',
|
|
198
|
+
xlinkTitle: 'xlink:title',
|
|
199
|
+
xlinkType: 'xlink:type',
|
|
200
|
+
xlinkArcrole: 'xlink:arcrole',
|
|
201
|
+
xlinkActuate: 'xlink:actuate',
|
|
202
|
+
xlinkShow: 'xlink:show',
|
|
203
|
+
xmlBase: 'xml:base',
|
|
204
|
+
xmlLang: 'xml:lang',
|
|
205
|
+
xmlSpace: 'xml:space',
|
|
206
|
+
xmlnsXlink: 'xmlns:xlink'
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function jsxAttrName(name) {
|
|
210
|
+
if (name === 'className') {
|
|
211
|
+
return 'class';
|
|
212
|
+
}
|
|
213
|
+
if (name === 'htmlFor') {
|
|
214
|
+
return 'for';
|
|
215
|
+
}
|
|
216
|
+
const svg = svgAttrMap[name];
|
|
217
|
+
if (svg) {
|
|
218
|
+
return svg;
|
|
219
|
+
}
|
|
220
|
+
return name;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Walk the tree of strings, Raw markers, promises, and arrays, awaiting
|
|
224
|
+
// every promise and producing a final HTML string. Plain strings/numbers
|
|
225
|
+
// are escaped; Raw and Nunjucks SafeString values are not.
|
|
226
|
+
async function flatten(node) {
|
|
227
|
+
const out = [];
|
|
228
|
+
await walk(node, out);
|
|
229
|
+
return out.join('');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function walk(node, out) {
|
|
233
|
+
if (node == null || node === false || node === true) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(node)) {
|
|
237
|
+
for (const item of node) {
|
|
238
|
+
await walk(item, out);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (node instanceof Raw) {
|
|
243
|
+
out.push(node.html);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (isSafeInstance(node)) {
|
|
247
|
+
out.push(node.toString());
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (typeof node === 'object' && typeof node.then === 'function') {
|
|
251
|
+
const resolved = await node;
|
|
252
|
+
await walk(resolved, out);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (typeof node === 'string') {
|
|
256
|
+
out.push(escapeHtml(node));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (typeof node === 'number' || typeof node === 'bigint') {
|
|
260
|
+
out.push(escapeHtml(String(node)));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Anything else (objects without a known handler) is coerced to string
|
|
264
|
+
// and escaped, mirroring how React stringifies unexpected children.
|
|
265
|
+
out.push(escapeHtml(String(node)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
h,
|
|
270
|
+
Fragment,
|
|
271
|
+
Raw,
|
|
272
|
+
flatten,
|
|
273
|
+
escapeHtml,
|
|
274
|
+
escapeAttr,
|
|
275
|
+
registerSafeClass
|
|
276
|
+
};
|
|
@@ -7,23 +7,26 @@
|
|
|
7
7
|
// Note that if @apostrophecms/template has a project-level override
|
|
8
8
|
// of outerLayout.html, that will be loaded instead. This is
|
|
9
9
|
// intentional.
|
|
10
|
+
//
|
|
11
|
+
// File watching and cache invalidation are deliberately NOT handled here:
|
|
12
|
+
// the template module wires up `viewWatcher.js` once for both Nunjucks and
|
|
13
|
+
// JSX, and clears every loader's `cache` (read by Nunjucks itself) when a
|
|
14
|
+
// view file changes.
|
|
10
15
|
|
|
11
16
|
const fs = require('fs');
|
|
12
17
|
const path = require('path');
|
|
13
18
|
const _ = require('lodash');
|
|
14
19
|
const { stripIndent } = require('common-tags');
|
|
15
|
-
const chokidar = require('chokidar');
|
|
16
20
|
|
|
17
21
|
module.exports = function(moduleName, searchPaths, noWatch, templates, options) {
|
|
18
22
|
|
|
19
23
|
const self = this;
|
|
20
|
-
self.watches = [];
|
|
21
24
|
options = options || {};
|
|
22
25
|
const extensions = options.extensions || [ 'njk', 'html' ];
|
|
23
26
|
self.moduleName = moduleName;
|
|
24
27
|
self.templates = templates;
|
|
25
28
|
|
|
26
|
-
self.init = function(searchPaths
|
|
29
|
+
self.init = function(searchPaths) {
|
|
27
30
|
self.pathsToNames = {};
|
|
28
31
|
if (searchPaths) {
|
|
29
32
|
searchPaths = Array.isArray(searchPaths) ? searchPaths : [ searchPaths ];
|
|
@@ -32,34 +35,6 @@ module.exports = function(moduleName, searchPaths, noWatch, templates, options)
|
|
|
32
35
|
} else {
|
|
33
36
|
self.searchPaths = [];
|
|
34
37
|
}
|
|
35
|
-
// Unless and until chokidar declares this a supported config,
|
|
36
|
-
// no watching in WSL (it doesn't work without chokidar either)
|
|
37
|
-
if ((!noWatch) && (!require('is-wsl'))) {
|
|
38
|
-
_.each(self.searchPaths, function(p) {
|
|
39
|
-
if (fs.existsSync(p)) {
|
|
40
|
-
try {
|
|
41
|
-
const watcher = chokidar.watch(p);
|
|
42
|
-
watcher.on('change', (path, stats) => {
|
|
43
|
-
// Just blow the whole cache if anything is modified. Much
|
|
44
|
-
// simpler, avoids several false negatives, and works well for a
|
|
45
|
-
// CMS in dev. -Tom
|
|
46
|
-
self.cache = {};
|
|
47
|
-
});
|
|
48
|
-
self.watches.push(watcher);
|
|
49
|
-
} catch (e) {
|
|
50
|
-
if (!self.firstWatchFailure) {
|
|
51
|
-
// Don't crash in broken environments (not sure if any are left
|
|
52
|
-
// thanks to chokidar, but still a useful warning to have if it
|
|
53
|
-
// comes up)
|
|
54
|
-
self.firstWatchFailure = true;
|
|
55
|
-
self.templates.apos.util.warn('WARNING: fs.watch does not work on this system. That is OK but you\n' +
|
|
56
|
-
'will have to restart to see any template changes take effect.');
|
|
57
|
-
}
|
|
58
|
-
self.templates.apos.util.error(e);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
38
|
};
|
|
64
39
|
|
|
65
40
|
self.isRelative = function(filename) {
|
|
@@ -183,11 +158,11 @@ module.exports = function(moduleName, searchPaths, noWatch, templates, options)
|
|
|
183
158
|
}
|
|
184
159
|
};
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
161
|
+
// Retained for backwards compatibility — file watching now lives in
|
|
162
|
+
// `viewWatcher.js`, owned by the template module itself, so there is
|
|
163
|
+
// nothing for the loader to dispose of. Callers (Apostrophe internals
|
|
164
|
+
// and tests) may still invoke `destroy()` and that must keep working.
|
|
165
|
+
self.destroy = async () => {};
|
|
191
166
|
|
|
192
167
|
self.init(searchPaths, noWatch);
|
|
193
168
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// File-watching for view directories shared by Nunjucks and JSX.
|
|
2
|
+
//
|
|
3
|
+
// Owns one chokidar watcher per directory and dispatches change events to
|
|
4
|
+
// any number of registered handlers. The template module wires this up at
|
|
5
|
+
// init time with two handlers: clear every Nunjucks loader's cache, and
|
|
6
|
+
// invalidate compiled `.jsx` modules so the next render reloads them.
|
|
7
|
+
//
|
|
8
|
+
// WSL safety: chokidar's underlying file events are unreliable on WSL, so
|
|
9
|
+
// we deliberately skip watching there — same behavior as the previous
|
|
10
|
+
// in-loader code. Developers running on WSL will need to restart to pick
|
|
11
|
+
// up template edits, but the rest of Apostrophe still functions.
|
|
12
|
+
//
|
|
13
|
+
// Returns a methods mixin compatible with `@apostrophecms/module`'s
|
|
14
|
+
// `methods` factory pattern, the same way `bundlesLoader` and `jsxRender`
|
|
15
|
+
// are mixed in.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const chokidar = require('chokidar');
|
|
19
|
+
const isWsl = require('is-wsl');
|
|
20
|
+
|
|
21
|
+
module.exports = (self) => {
|
|
22
|
+
// Lazily-allocated state. Lives on `self` so it survives across method
|
|
23
|
+
// calls and can be inspected by tests if needed.
|
|
24
|
+
if (!self._viewWatchers) {
|
|
25
|
+
self._viewWatchers = [];
|
|
26
|
+
}
|
|
27
|
+
if (!self._viewWatchedDirs) {
|
|
28
|
+
self._viewWatchedDirs = new Set();
|
|
29
|
+
}
|
|
30
|
+
if (!self._viewChangeHandlers) {
|
|
31
|
+
self._viewChangeHandlers = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
// Begin watching the supplied directories for change events. Idempotent
|
|
36
|
+
// per absolute path: subsequent calls with the same dir do not create
|
|
37
|
+
// duplicate watchers. Honors the WSL skip flag and the loader's
|
|
38
|
+
// `noWatch` option (templates module exposes this via
|
|
39
|
+
// `options.loader.noWatch`).
|
|
40
|
+
watchViewFolders(dirs) {
|
|
41
|
+
if (self.options.loader && self.options.loader.noWatch) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// In production the file tree is static and watching only burns
|
|
45
|
+
// file descriptors. Honor NODE_ENV unconditionally.
|
|
46
|
+
if (process.env.NODE_ENV === 'production') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// chokidar's recursive watching is not reliable on WSL; preserve the
|
|
50
|
+
// historical behavior of skipping watch setup entirely there.
|
|
51
|
+
if (isWsl) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
if (self._viewWatchedDirs.has(dir)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!fs.existsSync(dir)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
self._viewWatchedDirs.add(dir);
|
|
62
|
+
try {
|
|
63
|
+
const watcher = chokidar.watch(dir);
|
|
64
|
+
watcher.on('change', (filePath) => {
|
|
65
|
+
for (const handler of self._viewChangeHandlers) {
|
|
66
|
+
try {
|
|
67
|
+
handler(filePath);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
self.apos.util.error(e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
self._viewWatchers.push(watcher);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Don't crash in broken environments. Warn at most once so the
|
|
76
|
+
// logs don't get spammed for every dir we fail to watch.
|
|
77
|
+
if (!self._viewFirstWatchFailure) {
|
|
78
|
+
self._viewFirstWatchFailure = true;
|
|
79
|
+
self.apos.util.warn(
|
|
80
|
+
'WARNING: fs.watch does not work on this system. That is OK but you\n' +
|
|
81
|
+
'will have to restart to see any template changes take effect.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
self.apos.util.error(e);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Register a callback invoked with the absolute path of the changed
|
|
90
|
+
// file every time chokidar reports a `change` event. Handlers run in
|
|
91
|
+
// registration order. Errors thrown by a handler are logged but never
|
|
92
|
+
// stop other handlers from running.
|
|
93
|
+
onViewChange(handler) {
|
|
94
|
+
self._viewChangeHandlers.push(handler);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Tear down every chokidar watcher created via `watchViewFolders`.
|
|
98
|
+
// Called from the `apostrophe:destroy` handler so a process that
|
|
99
|
+
// builds and tears down multiple Apostrophe instances (tests, the
|
|
100
|
+
// multisite harness) doesn't leak file descriptors.
|
|
101
|
+
async closeViewWatchers() {
|
|
102
|
+
const watchers = self._viewWatchers.splice(0, self._viewWatchers.length);
|
|
103
|
+
self._viewWatchedDirs.clear();
|
|
104
|
+
for (const watcher of watchers) {
|
|
105
|
+
try {
|
|
106
|
+
await watcher.close();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
self.apos.util.error(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
'apos-is-only-child': slatCount === 1,
|
|
9
9
|
'apos-is-selected': selected,
|
|
10
10
|
'apos-is-disabled': disabled,
|
|
11
|
+
'apos-is-not-draggable': !draggable,
|
|
11
12
|
}"
|
|
12
|
-
:aria-
|
|
13
|
+
:aria-current="engaged"
|
|
13
14
|
role="listitem"
|
|
14
15
|
:aria-labelledby="parent"
|
|
15
16
|
@keydown.space="toggleEngage"
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
>
|
|
23
24
|
<div class="apos-slat__main">
|
|
24
25
|
<drag-icon
|
|
25
|
-
v-if="slatCount > 1"
|
|
26
|
+
v-if="slatCount > 1 && draggable"
|
|
26
27
|
class="apos-slat__control apos-slat__control--drag"
|
|
27
28
|
:size="13"
|
|
28
29
|
/>
|
|
@@ -158,6 +159,10 @@ export default {
|
|
|
158
159
|
editorIcon: {
|
|
159
160
|
type: String,
|
|
160
161
|
default: null
|
|
162
|
+
},
|
|
163
|
+
draggable: {
|
|
164
|
+
type: Boolean,
|
|
165
|
+
default: true
|
|
161
166
|
}
|
|
162
167
|
},
|
|
163
168
|
emits: [ 'engage', 'disengage', 'move', 'remove', 'item-clicked', 'select' ],
|
|
@@ -209,7 +214,7 @@ export default {
|
|
|
209
214
|
return e.target.click();
|
|
210
215
|
},
|
|
211
216
|
toggleEngage() {
|
|
212
|
-
if (this.slatCount > 1) {
|
|
217
|
+
if (this.draggable && this.slatCount > 1) {
|
|
213
218
|
if (this.engaged) {
|
|
214
219
|
this.disengage();
|
|
215
220
|
} else {
|
|
@@ -278,7 +283,8 @@ export default {
|
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
&.apos-slat-list__item--disabled,
|
|
281
|
-
&.apos-is-only-child
|
|
286
|
+
&.apos-is-only-child,
|
|
287
|
+
&.apos-is-not-draggable {
|
|
282
288
|
&:hover,
|
|
283
289
|
&:active {
|
|
284
290
|
cursor: default;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
'apos-input--error': duplicate
|
|
27
27
|
}"
|
|
28
28
|
:disabled="disabled"
|
|
29
|
+
:draggable="draggable"
|
|
29
30
|
:engaged="engaged === item._id"
|
|
30
31
|
:parent="listId"
|
|
31
32
|
:slat-count="next.length"
|
|
@@ -87,6 +88,10 @@ export default {
|
|
|
87
88
|
duplicate: {
|
|
88
89
|
type: String,
|
|
89
90
|
default: null
|
|
91
|
+
},
|
|
92
|
+
draggable: {
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default: true
|
|
90
95
|
}
|
|
91
96
|
},
|
|
92
97
|
emits: [ 'item-clicked', 'select', 'update:modelValue' ],
|
|
@@ -104,7 +109,7 @@ export default {
|
|
|
104
109
|
dragOptions() {
|
|
105
110
|
return {
|
|
106
111
|
animation: 0,
|
|
107
|
-
disabled: this.disabled || this.next.length <= 1,
|
|
112
|
+
disabled: !this.draggable || this.disabled || this.next.length <= 1,
|
|
108
113
|
ghostClass: 'apos-is-dragging'
|
|
109
114
|
};
|
|
110
115
|
}
|
|
@@ -41,6 +41,9 @@ module.exports = {
|
|
|
41
41
|
const uploadfsSettings = {};
|
|
42
42
|
_.merge(uploadfsSettings, uploadfsDefaultSettings);
|
|
43
43
|
_.merge(uploadfsSettings, options);
|
|
44
|
+
if (process.env.APOS_UPLOADFS_DISABLED_FILE_KEY) {
|
|
45
|
+
uploadfsSettings.disabledFileKey = process.env.APOS_UPLOADFS_DISABLED_FILE_KEY;
|
|
46
|
+
}
|
|
44
47
|
if (process.env.APOS_S3_BUCKET) {
|
|
45
48
|
_.merge(uploadfsSettings, {
|
|
46
49
|
backend: 's3',
|
|
@@ -35,6 +35,13 @@ const util = require('util');
|
|
|
35
35
|
const { stripIndent } = require('common-tags');
|
|
36
36
|
const glob = require('../../../lib/glob.js');
|
|
37
37
|
|
|
38
|
+
// Dot-path segments that must never be traversed when walking a
|
|
39
|
+
// user-supplied path in `apos.util.get` and `apos.util.set`. Following any
|
|
40
|
+
// of these reaches the prototype chain and enables server-side prototype
|
|
41
|
+
// pollution (CWE-1321), e.g. a PATCH `$pullAll` key of
|
|
42
|
+
// `__proto__.publicApiProjection`.
|
|
43
|
+
const unsafePathSegments = new Set([ '__proto__', 'constructor', 'prototype' ]);
|
|
44
|
+
|
|
38
45
|
module.exports = {
|
|
39
46
|
options: {
|
|
40
47
|
alias: 'util',
|
|
@@ -724,7 +731,7 @@ module.exports = {
|
|
|
724
731
|
},
|
|
725
732
|
// Given a widget or doc, return the appropriate manager module. If the manager
|
|
726
733
|
// cannot be determined for any reason, undefined is returned.
|
|
727
|
-
getManagerOf(object) {
|
|
734
|
+
getManagerOf(object, { log = true } = {}) {
|
|
728
735
|
if (object.metaType === 'doc') {
|
|
729
736
|
return self.apos.doc.getManager(object.type);
|
|
730
737
|
} else if (object.metaType === 'widget') {
|
|
@@ -733,10 +740,10 @@ module.exports = {
|
|
|
733
740
|
return self.apos.schema.getArrayManager(object.scopedArrayName);
|
|
734
741
|
} else if (object.metaType === 'object') {
|
|
735
742
|
return self.apos.schema.getObjectManager(object.scopedObjectName);
|
|
736
|
-
} else {
|
|
743
|
+
} else if (log) {
|
|
737
744
|
self.apos.util.error(`Unsupported metaType in getManagerOf: ${object.metaType}`);
|
|
738
|
-
return undefined;
|
|
739
745
|
}
|
|
746
|
+
return undefined;
|
|
740
747
|
},
|
|
741
748
|
// fetch the value at the given path from the object or
|
|
742
749
|
// array `o`. `path` supports dot notation like MongoDB, and
|
|
@@ -755,6 +762,10 @@ module.exports = {
|
|
|
755
762
|
if (o == null) {
|
|
756
763
|
return undefined;
|
|
757
764
|
}
|
|
765
|
+
if (unsafePathSegments.has(p)) {
|
|
766
|
+
// Never read through the prototype chain (CWE-1321)
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
758
769
|
o = o[p];
|
|
759
770
|
}
|
|
760
771
|
}
|
|
@@ -819,6 +830,12 @@ module.exports = {
|
|
|
819
830
|
}
|
|
820
831
|
}
|
|
821
832
|
path = path.split('.');
|
|
833
|
+
for (p of path) {
|
|
834
|
+
if (unsafePathSegments.has(p)) {
|
|
835
|
+
// Refuse to write through the prototype chain (CWE-1321)
|
|
836
|
+
throw self.apos.error('invalid', `Unsafe property name "${p}" in dot path`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
822
839
|
for (i = 0; (i < (path.length - 1)); i++) {
|
|
823
840
|
p = path[i];
|
|
824
841
|
o = o[p];
|