@flourish/sdk 3.17.3 → 3.18.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/README.md~ +473 -0
- package/RELEASE_NOTES.md +3 -0
- package/RELEASE_NOTES.md~ +372 -0
- package/lib/sdk.js +34 -22
- package/lib/sdk.js~ +540 -0
- package/lib/validate_config.js +2 -1
- package/lib/validate_config.js~ +437 -0
- package/package-lock.json~ +2669 -0
- package/package.json +1 -1
- package/package.json~ +53 -0
- package/server/index.js~ +540 -0
- package/test/lib/validate_config.js +11 -2
- package/test/lib/validate_config.js~ +1006 -0
package/package.json
CHANGED
package/package.json~
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flourish/sdk",
|
|
3
|
+
<<<<<<< HEAD
|
|
4
|
+
"version": "3.13.1",
|
|
5
|
+
=======
|
|
6
|
+
"version": "3.14.0",
|
|
7
|
+
>>>>>>> sdk-watch-command
|
|
8
|
+
"description": "The Flourish SDK",
|
|
9
|
+
"module": "src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepare": "cd .. && make sdk_clean sdk",
|
|
12
|
+
"test": "mocha"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"flourish": "bin/flourish"
|
|
16
|
+
},
|
|
17
|
+
"author": "Kiln Enterprises Ltd",
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
19
|
+
"repository": "kiln/flourish-sdk",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@flourish/semver": "^1.0.1",
|
|
22
|
+
"archiver": "^3.0.0",
|
|
23
|
+
"chokidar": "^3.0.1",
|
|
24
|
+
"colors": "^1.3.3",
|
|
25
|
+
"cross-spawn": "^6.0.5",
|
|
26
|
+
"d3-dsv": "^1.1.1",
|
|
27
|
+
"express": "^4.17.1",
|
|
28
|
+
"handlebars": "^4.1.2",
|
|
29
|
+
"js-yaml": "^3.13.1",
|
|
30
|
+
"minimist": "^1.2.0",
|
|
31
|
+
"ncp": "^2.0.0",
|
|
32
|
+
"parse5": "^5.1.0",
|
|
33
|
+
"read": "^1.0.7",
|
|
34
|
+
"request": "^2.88.0",
|
|
35
|
+
"resolve": "^1.11.1",
|
|
36
|
+
"rewrite-links": "^1.1.0",
|
|
37
|
+
"shell-quote": "^1.6.1",
|
|
38
|
+
"tmp": "^0.1.0",
|
|
39
|
+
"ws": "^7.0.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"d3-request": "^1.0.6",
|
|
43
|
+
"mocha": "^6.1.4",
|
|
44
|
+
"rollup": "^1.16.2",
|
|
45
|
+
"rollup-plugin-node-resolve": "^5.1.0",
|
|
46
|
+
"rollup-plugin-uglify": "^6.0.2",
|
|
47
|
+
"sinon": "^8.1.1",
|
|
48
|
+
"tempy": "^0.3.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=8.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/server/index.js~
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Modules
|
|
4
|
+
const crypto = require("crypto"),
|
|
5
|
+
fs = require("fs"),
|
|
6
|
+
path = require("path"),
|
|
7
|
+
|
|
8
|
+
cross_spawn = require("cross-spawn"),
|
|
9
|
+
chokidar = require("chokidar"),
|
|
10
|
+
d3_dsv = require("d3-dsv"),
|
|
11
|
+
express = require("express"),
|
|
12
|
+
handlebars = require("handlebars"),
|
|
13
|
+
shell_quote = require("shell-quote"),
|
|
14
|
+
ws = require("ws"),
|
|
15
|
+
yaml = require("js-yaml"),
|
|
16
|
+
|
|
17
|
+
columns = require("./columns"),
|
|
18
|
+
comms_js = require("./comms_js"),
|
|
19
|
+
data_utils = require("./data"),
|
|
20
|
+
index_html = require("./index_html"),
|
|
21
|
+
json = require("./json"),
|
|
22
|
+
|
|
23
|
+
log = require("../lib/log"),
|
|
24
|
+
sdk = require("../lib/sdk");
|
|
25
|
+
|
|
26
|
+
const TA = require("parse5/lib/tree-adapters/default.js");
|
|
27
|
+
|
|
28
|
+
// Generate a static prefix randomly
|
|
29
|
+
//
|
|
30
|
+
// Use a different prefix for /preview, to catch the situation where the template
|
|
31
|
+
// developer mistakenly prepends a / to the static prefix.
|
|
32
|
+
const static_prefix = crypto.randomBytes(15).toString("base64").replace(/[+/]/g, (c) => ({ "/": "_", "+": "-" })[c]),
|
|
33
|
+
preview_static_prefix = crypto.randomBytes(15).toString("base64").replace(/[+/]/g, (c) => ({ "/": "_", "+": "-" })[c]);
|
|
34
|
+
|
|
35
|
+
function loadFile(path_parts, options) {
|
|
36
|
+
return new Promise(function(resolve, reject) {
|
|
37
|
+
const file_path = path.join(...path_parts),
|
|
38
|
+
filename = path_parts[path_parts.length - 1];
|
|
39
|
+
|
|
40
|
+
function fail(message, error) {
|
|
41
|
+
if ("default" in options) {
|
|
42
|
+
log.warn(message, `Proceeding without ${filename}...`);
|
|
43
|
+
if (typeof options.default === "function") {
|
|
44
|
+
return resolve(options.default());
|
|
45
|
+
}
|
|
46
|
+
return resolve(options.default);
|
|
47
|
+
}
|
|
48
|
+
log.problem(message, error.message);
|
|
49
|
+
reject(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function succeed(result) {
|
|
53
|
+
if (!options.silentSuccess) log.success(`Loaded ${filename}`);
|
|
54
|
+
resolve(result);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fs.readFile(file_path, "utf8", function(error, loaded_text) {
|
|
58
|
+
if (error) return fail(`Failed to load ${file_path}`, error);
|
|
59
|
+
switch (options.type) {
|
|
60
|
+
case "json":
|
|
61
|
+
try { return succeed(JSON.parse(loaded_text)); }
|
|
62
|
+
catch (error) {
|
|
63
|
+
return fail(`Uh-oh! There's a problem with your ${filename} file.`, error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case "yaml":
|
|
67
|
+
try { return succeed(yaml.safeLoad(loaded_text)); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
return fail(`Uh-oh! There's a problem with your ${filename} file.`, error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
default:
|
|
73
|
+
return succeed(loaded_text);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadSDKTemplate() {
|
|
80
|
+
return loadFile([__dirname, "views", "index.html"], { silentSuccess: true })
|
|
81
|
+
.then((template_text) => handlebars.compile(template_text));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadTemplateText(template_dir) {
|
|
85
|
+
return loadFile([template_dir, "index.html"], {
|
|
86
|
+
default: () => loadFile([__dirname, "views", "default_template_index.html"], {
|
|
87
|
+
silentSuccess: true
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadJavaScript(template_dir) {
|
|
93
|
+
return loadFile([template_dir, "template.js"], {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadSettings(template_dir) {
|
|
97
|
+
return sdk.readAndValidateConfig(template_dir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listDataTables(template_dir) {
|
|
101
|
+
return new Promise(function(resolve, reject) {
|
|
102
|
+
fs.readdir(path.join(template_dir, "data"), function(error, filenames) {
|
|
103
|
+
if (error) {
|
|
104
|
+
if (error.code === "ENOENT") return resolve([]);
|
|
105
|
+
return reject(error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data_files = [];
|
|
109
|
+
for (let filename of filenames) {
|
|
110
|
+
if (!filename.endsWith(".csv")) continue;
|
|
111
|
+
|
|
112
|
+
var name = filename.substr(0, filename.length - 4);
|
|
113
|
+
data_files.push(name);
|
|
114
|
+
}
|
|
115
|
+
resolve(data_files);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getData(template_dir, data_tables) {
|
|
121
|
+
return Promise.all(data_tables.map((data_table) => getDataTable(template_dir, data_table)))
|
|
122
|
+
.then((data_array) => {
|
|
123
|
+
const data_by_name = {};
|
|
124
|
+
const column_types_by_name = {};
|
|
125
|
+
for (var i = 0; i < data_tables.length; i++) {
|
|
126
|
+
data_by_name[data_tables[i]] = data_array[i];
|
|
127
|
+
}
|
|
128
|
+
for (const data_table in data_by_name) {
|
|
129
|
+
const data = data_by_name[data_table];
|
|
130
|
+
column_types_by_name[data_table] = data_utils.transposeNestedArray(data)
|
|
131
|
+
.map(function(d, i) { return { type_id: data_utils.interpretColumn(d)[0].id, index: i }; });
|
|
132
|
+
}
|
|
133
|
+
return { data: data_by_name, column_types_by_name };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getDataTable(template_dir, data_table) {
|
|
138
|
+
return new Promise(function(resolve, reject) {
|
|
139
|
+
fs.readFile(path.join(template_dir, "data", data_table + ".csv"), "utf8", function(error, csv_text) {
|
|
140
|
+
if (error) return reject(error);
|
|
141
|
+
if (csv_text.charAt(0) === "\uFEFF") csv_text = csv_text.substr(1);
|
|
142
|
+
resolve(d3_dsv.csvParseRows(csv_text));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseDataBindings(data_bindings, data_tables) {
|
|
148
|
+
if (!data_bindings) return {1: {}};
|
|
149
|
+
|
|
150
|
+
// Use the names as ids
|
|
151
|
+
const name_by_id = {};
|
|
152
|
+
for (let name of data_tables) name_by_id[name] = name;
|
|
153
|
+
|
|
154
|
+
// Collect parsed bindings by dataset
|
|
155
|
+
const data_bindings_by_dataset = {};
|
|
156
|
+
for (let binding of data_bindings) {
|
|
157
|
+
let dataset = binding.dataset;
|
|
158
|
+
if (!dataset) continue;
|
|
159
|
+
|
|
160
|
+
if (!data_bindings_by_dataset[dataset]) data_bindings_by_dataset[dataset] = {};
|
|
161
|
+
data_bindings_by_dataset[dataset][binding.key] = columns.parseDataBinding(binding, name_by_id);
|
|
162
|
+
}
|
|
163
|
+
return { 1: data_bindings_by_dataset };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function documentFragment(elements) {
|
|
167
|
+
const fragment = TA.createDocumentFragment();
|
|
168
|
+
for (const element of elements) {
|
|
169
|
+
TA.appendChild(fragment, element);
|
|
170
|
+
}
|
|
171
|
+
return fragment;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function scriptElementInline(code) {
|
|
175
|
+
const element = TA.createElement("script", "http://www.w3.org/1999/xhtml", []);
|
|
176
|
+
TA.insertText(element, code);
|
|
177
|
+
return element;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scriptElementExternal(url) {
|
|
181
|
+
return TA.createElement("script", "http://www.w3.org/1999/xhtml", [{ name: "src", value: url }]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
function loadTemplate(template_dir, sdk_template, build_failed) {
|
|
186
|
+
return Promise.all([
|
|
187
|
+
listDataTables(template_dir),
|
|
188
|
+
loadSettings(template_dir),
|
|
189
|
+
])
|
|
190
|
+
.then(([data_tables, settings]) => {
|
|
191
|
+
const data_bindings = parseDataBindings(settings.data, data_tables);
|
|
192
|
+
return Promise.all([
|
|
193
|
+
settings, data_bindings, data_tables,
|
|
194
|
+
previewInitJs(template_dir, data_bindings["1"], data_tables),
|
|
195
|
+
loadTemplateText(template_dir),
|
|
196
|
+
loadJavaScript(template_dir)
|
|
197
|
+
]);
|
|
198
|
+
})
|
|
199
|
+
.then(([
|
|
200
|
+
settings, data_bindings, data_tables,
|
|
201
|
+
preview_init_js, template_text, template_js
|
|
202
|
+
]) => {
|
|
203
|
+
const page_params = {
|
|
204
|
+
// Always use ID of 1 for SDK
|
|
205
|
+
visualisation: { id: 1, can_edit: true },
|
|
206
|
+
visualisation_js: "new Flourish.Visualisation('1', 0," + json.safeStringify({
|
|
207
|
+
data_bindings: data_bindings,
|
|
208
|
+
data_tables: data_tables,
|
|
209
|
+
}) + ")",
|
|
210
|
+
settings: json.safeStringify(settings.settings || []),
|
|
211
|
+
data_bindings: json.safeStringify(settings.data || []),
|
|
212
|
+
template_name: settings.name || "Untitled template",
|
|
213
|
+
template_version: settings.version,
|
|
214
|
+
template_author: settings.author || "",
|
|
215
|
+
build_failed: build_failed && build_failed.size > 0
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const script = documentFragment([
|
|
219
|
+
scriptElementInline("window.Flourish = " + json.safeStringify({
|
|
220
|
+
static_prefix, environment: "sdk"
|
|
221
|
+
}) + ";"),
|
|
222
|
+
scriptElementExternal("/template.js"),
|
|
223
|
+
scriptElementExternal("/comms.js"),
|
|
224
|
+
scriptElementExternal("/embedded.js"),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const preview_script = documentFragment([
|
|
228
|
+
scriptElementInline("window.Flourish = " + json.safeStringify({
|
|
229
|
+
static_prefix: preview_static_prefix, environment: "sdk"
|
|
230
|
+
}) + ";"),
|
|
231
|
+
scriptElementExternal("/template.js"),
|
|
232
|
+
scriptElementExternal("/comms.js"),
|
|
233
|
+
scriptElementExternal("/embedded.js"),
|
|
234
|
+
scriptElementExternal("/talk_to_server.js"),
|
|
235
|
+
scriptElementInline("_Flourish_talkToServer();"),
|
|
236
|
+
scriptElementInline(preview_init_js),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
return Promise.all([
|
|
240
|
+
sdk_template(page_params),
|
|
241
|
+
index_html.render(template_text, {
|
|
242
|
+
title: "Flourish SDK template preview blah",
|
|
243
|
+
static: static_prefix,
|
|
244
|
+
parsed_script: script
|
|
245
|
+
}),
|
|
246
|
+
index_html.render(template_text, {
|
|
247
|
+
title: "Flourish SDK template preview flerm",
|
|
248
|
+
static: preview_static_prefix,
|
|
249
|
+
parsed_script: preview_script,
|
|
250
|
+
}),
|
|
251
|
+
template_js,
|
|
252
|
+
sdk.buildRules(template_dir),
|
|
253
|
+
]);
|
|
254
|
+
})
|
|
255
|
+
.then(([sdk_rendered, template_rendered, preview_rendered, template_js, build_rules]) => ({
|
|
256
|
+
sdk_rendered, template_rendered, preview_rendered, template_js, build_rules
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function previewInitJs(template_dir, data_bindings, data_tables) {
|
|
261
|
+
return getData(template_dir, data_tables).then(({data, column_types_by_name}) => {
|
|
262
|
+
const prepared_data = {};
|
|
263
|
+
for (let dataset in data_bindings) {
|
|
264
|
+
prepared_data[dataset] = data_utils.extractData(data_bindings[dataset], data, column_types_by_name);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const column_names = {};
|
|
268
|
+
const type_information = {};
|
|
269
|
+
for (let dataset in prepared_data) {
|
|
270
|
+
column_names[dataset] = prepared_data[dataset].column_names;
|
|
271
|
+
type_information[dataset] = prepared_data[dataset].type_information;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return `
|
|
275
|
+
var _Flourish_data_column_names = ${json.safeStringify(column_names)},
|
|
276
|
+
_Flourish_data_type_information = ${json.safeStringify(type_information)},
|
|
277
|
+
_Flourish_data = ${json.javaScriptStringify(prepared_data)};
|
|
278
|
+
for (var _Flourish_dataset in _Flourish_data) {
|
|
279
|
+
window.template.data[_Flourish_dataset] = _Flourish_data[_Flourish_dataset];
|
|
280
|
+
window.template.data[_Flourish_dataset].column_names = _Flourish_data_column_names[_Flourish_dataset];
|
|
281
|
+
window.template.data[_Flourish_dataset].type_information = _Flourish_data_type_information[_Flourish_dataset];
|
|
282
|
+
}
|
|
283
|
+
window.template.draw();
|
|
284
|
+
`;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function tryToOpen(url) {
|
|
289
|
+
// If it’s available and works, use /usr/bin/open to open
|
|
290
|
+
// the URL. If not just prompt the user to open it.
|
|
291
|
+
try {
|
|
292
|
+
cross_spawn.spawn("/usr/bin/open", [url])
|
|
293
|
+
.on("exit", function(exit_code) {
|
|
294
|
+
if (exit_code != 0) {
|
|
295
|
+
log.success("Now open " + url + " in your web browser!");
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
log.success("Opened browser window to " + url);
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.on("error", function() {
|
|
302
|
+
log.success("Now open " + url + " in your web browser!");
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
log.success("Now open " + url + " in your web browser!");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isPrefix(a, b) {
|
|
311
|
+
if (a.length > b.length) return false;
|
|
312
|
+
for (let i = 0; i < a.length; i++) {
|
|
313
|
+
if (a[i] !== b[i]) return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function splitPath(p) {
|
|
319
|
+
return p.split(path.sep).filter(c => c != "");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
module.exports = function(template_dir, options) {
|
|
324
|
+
let app = express(),
|
|
325
|
+
reloadPreview,
|
|
326
|
+
|
|
327
|
+
template;
|
|
328
|
+
|
|
329
|
+
// Editor and settings/bindings
|
|
330
|
+
app.get("/", function (req, res) {
|
|
331
|
+
log.success("Loading main page in browser");
|
|
332
|
+
res.header("Content-Type", "text/html; charset=utf-8")
|
|
333
|
+
.send(template.sdk_rendered);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
app.get("/template.js", function (req, res) {
|
|
337
|
+
res.header("Content-Type", "application/javascript").send(template.template_js);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
app.get("/template.js.map", function (req, res) {
|
|
341
|
+
res.sendFile(path.resolve(template_dir, "template.js.map"));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
app.get("/comms.js", function (req, res) {
|
|
345
|
+
res.header("Content-Type", "application/javascript").send(comms_js.withoutOriginCheck + comms_js.validate);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
app.get("/thumbnail", function (req, res) {
|
|
349
|
+
const jpg_path = path.resolve(template_dir, "thumbnail.jpg"),
|
|
350
|
+
png_path = path.resolve(template_dir, "thumbnail.png");
|
|
351
|
+
if (fs.existsSync(jpg_path)) {
|
|
352
|
+
return res.header("Content-Type", "image/jpeg").sendFile(jpg_path);
|
|
353
|
+
}
|
|
354
|
+
if (fs.existsSync(png_path)) {
|
|
355
|
+
return res.header("Content-Type", "image/jpeg").sendFile(png_path);
|
|
356
|
+
}
|
|
357
|
+
return res.status(404).send("thumbnail not found");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
app.get("/template/1/embed/", function(req, res) {
|
|
361
|
+
res.header("Content-Type", "text/html; charset=utf-8")
|
|
362
|
+
.send(template.template_rendered);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// API for accessing data tables
|
|
366
|
+
app.get("/api/data_table/:id/csv", function(req, res) {
|
|
367
|
+
res.status(200).header("Content-Type", "text/csv")
|
|
368
|
+
.sendFile(path.resolve(template_dir, "data", req.params.id + ".csv"));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Preview not in an iframe
|
|
372
|
+
app.get("/preview", function(req, res) {
|
|
373
|
+
res.header("Content-Type", "text/html; charset=utf-8")
|
|
374
|
+
.send(template.preview_rendered);
|
|
375
|
+
});
|
|
376
|
+
app.use(`/${preview_static_prefix}/`, express.static(path.join(template_dir, "static")));
|
|
377
|
+
|
|
378
|
+
// Static files
|
|
379
|
+
app.use("/", express.static(path.join(__dirname, "..", "site")));
|
|
380
|
+
app.use(`/template/1/embed/${static_prefix}/`, express.static(path.join(template_dir, "static")));
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
function startServer(sdk_template, template_) {
|
|
384
|
+
template = template_;
|
|
385
|
+
|
|
386
|
+
// Run the server
|
|
387
|
+
const listen_hostname = options.listen || "localhost";
|
|
388
|
+
const server = app.listen(options.port, listen_hostname, function() {
|
|
389
|
+
const url = "http://" + listen_hostname + ":" + options.port + "/";
|
|
390
|
+
log.info(`Running server at ${url}`);
|
|
391
|
+
|
|
392
|
+
// Set up the WebSocket server and the reloadPreview() function
|
|
393
|
+
const sockets = new Set();
|
|
394
|
+
const websocket_server = new ws.Server({ server });
|
|
395
|
+
websocket_server.on("connection", function(socket) {
|
|
396
|
+
sockets.add(socket);
|
|
397
|
+
socket.on("close", function() { sockets.delete(socket); });
|
|
398
|
+
});
|
|
399
|
+
reloadPreview = function() {
|
|
400
|
+
for (let socket of sockets) socket.close();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
watchForChanges(sdk_template);
|
|
404
|
+
if (options.open) tryToOpen(url);
|
|
405
|
+
})
|
|
406
|
+
.on("error", function(error) {
|
|
407
|
+
if (error.code === "EADDRINUSE") {
|
|
408
|
+
log.die("Another process is already listening on port " + options.port,
|
|
409
|
+
"Perhaps you’re already running flourish in another terminal?",
|
|
410
|
+
"You can use the --port option to listen on a different port");
|
|
411
|
+
}
|
|
412
|
+
log.die("Failed to start server", error.message);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let build_failed = new Set(),
|
|
417
|
+
rebuilding = new Set();
|
|
418
|
+
function watchForChanges(sdk_template) {
|
|
419
|
+
// Watch for file changes. If something changes, tell the page to reload itself
|
|
420
|
+
// If the source code has changed, rebuild it.
|
|
421
|
+
|
|
422
|
+
let reload_timer = null;
|
|
423
|
+
function reloadTemplate() {
|
|
424
|
+
if (rebuilding.size > 0) {
|
|
425
|
+
log.info("Not reloading template while rebuild is in progress.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (reload_timer) {
|
|
429
|
+
clearTimeout(reload_timer);
|
|
430
|
+
reload_timer = null;
|
|
431
|
+
}
|
|
432
|
+
reload_timer = setTimeout(_reloadTemplate, 50);
|
|
433
|
+
}
|
|
434
|
+
function _reloadTemplate() {
|
|
435
|
+
reload_timer = null;
|
|
436
|
+
log.info("Reloading...");
|
|
437
|
+
loadTemplate(template_dir, sdk_template, build_failed)
|
|
438
|
+
.then((template_) => {
|
|
439
|
+
template = template_;
|
|
440
|
+
log.info("Template reloaded. Trying to reload preview.");
|
|
441
|
+
reloadPreview();
|
|
442
|
+
})
|
|
443
|
+
.catch((error) => {
|
|
444
|
+
log.problem("Failed to reload template", error.message);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Run any custom watchers
|
|
449
|
+
if (template.build_rules) {
|
|
450
|
+
for (const build_rule of template.build_rules) {
|
|
451
|
+
if ("watch" in build_rule) {
|
|
452
|
+
const command_parts = shell_quote.parse(build_rule.watch),
|
|
453
|
+
prog = command_parts[0],
|
|
454
|
+
args = command_parts.slice(1);
|
|
455
|
+
|
|
456
|
+
const env = process.env;
|
|
457
|
+
env.NODE_ENV = "development";
|
|
458
|
+
|
|
459
|
+
log.info(`Running watcher command: ${build_rule.watch}`);
|
|
460
|
+
cross_spawn.spawn(prog, args, { cwd: template_dir, stdio: "inherit", env });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const chokidar_opts = { ignoreInitial: true, disableGlobbing: true, cwd: template_dir };
|
|
466
|
+
chokidar.watch(".", chokidar_opts).on("all", function(event_type, filename) {
|
|
467
|
+
const path_parts = filename.split(path.sep);
|
|
468
|
+
|
|
469
|
+
let should_reload = false;
|
|
470
|
+
if (sdk.TEMPLATE_SPECIAL.has(path_parts[0])) {
|
|
471
|
+
if (rebuilding.size > 0) return log.warn(`Rebuild in progress, ignoring change to ${filename}`);
|
|
472
|
+
log.info("Detected change to file: " + filename);
|
|
473
|
+
should_reload = true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const build_commands = new Map();
|
|
477
|
+
if (template.build_rules) {
|
|
478
|
+
for (const build_rule of template.build_rules) {
|
|
479
|
+
if ((build_rule.directory && isPrefix(splitPath(build_rule.directory), path_parts))
|
|
480
|
+
|| (build_rule.files && build_rule.files.indexOf(filename) != -1))
|
|
481
|
+
{
|
|
482
|
+
build_commands.set(build_rule.key, build_rule.script);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (build_commands.size > 0) {
|
|
488
|
+
const build_commands_to_run = [];
|
|
489
|
+
for (const [key, command] of build_commands) {
|
|
490
|
+
if (rebuilding.has(key)) continue;
|
|
491
|
+
rebuilding.add(key);
|
|
492
|
+
if (reload_timer) {
|
|
493
|
+
clearTimeout(reload_timer);
|
|
494
|
+
reload_timer = null;
|
|
495
|
+
}
|
|
496
|
+
log.info("Detected change to file: " + filename, "Running build for " + key);
|
|
497
|
+
build_commands_to_run.push(
|
|
498
|
+
sdk.runBuildCommand(template_dir, command, "development")
|
|
499
|
+
.then(() => {
|
|
500
|
+
rebuilding.delete(key);
|
|
501
|
+
build_failed.delete(key);
|
|
502
|
+
}, (error) => {
|
|
503
|
+
rebuilding.delete(key);
|
|
504
|
+
build_failed.add(key);
|
|
505
|
+
throw error;
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
Promise.all(build_commands_to_run)
|
|
511
|
+
.then(() => {
|
|
512
|
+
if (rebuilding.size == 0) {
|
|
513
|
+
log.success("Build process complete.");
|
|
514
|
+
reloadTemplate();
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
.catch((error) => {
|
|
518
|
+
if (build_failed.size > 0 && rebuilding.size == 0) {
|
|
519
|
+
reloadTemplate(); // To pass the build_failed flags
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else if (should_reload) reloadTemplate();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
loadSDKTemplate()
|
|
528
|
+
.then((sdk_template) => {
|
|
529
|
+
return Promise.all([
|
|
530
|
+
sdk_template, loadTemplate(template_dir, sdk_template)
|
|
531
|
+
]);
|
|
532
|
+
})
|
|
533
|
+
.then(([sdk_template, template]) => {
|
|
534
|
+
startServer(sdk_template, template);
|
|
535
|
+
})
|
|
536
|
+
.catch((error) => {
|
|
537
|
+
if (options.debug) log.problem("Failed to start server", error.message, error.stack);
|
|
538
|
+
else log.problem("Failed to start server", error.message);
|
|
539
|
+
});
|
|
540
|
+
};
|
|
@@ -916,12 +916,21 @@ describe("validate_config", function() {
|
|
|
916
916
|
"template.yml Setting import overrides must be an array"
|
|
917
917
|
);
|
|
918
918
|
});
|
|
919
|
-
it("should throw if an override is missing the 'property' property", function() {
|
|
919
|
+
it("should throw if an override is missing the 'property' or 'tag' property", function() {
|
|
920
920
|
expectFailure(
|
|
921
921
|
metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{}] }]}),
|
|
922
|
-
`template.yml Setting import overrides must each specify overridden “property”`
|
|
922
|
+
`template.yml Setting import overrides must each specify overridden “property” or “tag”`
|
|
923
923
|
);
|
|
924
924
|
});
|
|
925
|
+
it("should throw if an override has both 'property' and 'tag' property", function () {
|
|
926
|
+
expectFailure(
|
|
927
|
+
metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{property: "bg_color", tag: "categorical"}] }] }),
|
|
928
|
+
`template.yml Setting import overrides cannot contain both “property” and “tag” property`
|
|
929
|
+
);
|
|
930
|
+
});
|
|
931
|
+
it("should allow for an override to have 'tag' in place of 'property'", function () {
|
|
932
|
+
expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{ tag: "categorical" }] }] }));
|
|
933
|
+
});
|
|
925
934
|
it("should allow for an override without a 'method' property", function() {
|
|
926
935
|
expectSuccess(metadataPlus({ settings: [{ property: "imported_prop", import: "@flourish/layout", overrides: [{property: "bg_color"}] }]}));
|
|
927
936
|
});
|