@sasjs/cli 6.10.0 → 6.10.1

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.
@@ -954,6 +954,25 @@ const resolveProgram = (program) => {
954
954
  return null
955
955
  }
956
956
 
957
+ /**
958
+ * The header file a session starts with. sasjs/server seeds this exact line
959
+ * into a JS session (controllers/internal/Session.ts), so a service that
960
+ * declares no type answers text/html rather than leaving the choice to the
961
+ * browser - a mock build matches that here.
962
+ *
963
+ * The trailing newline is part of it: the server writes it for a JS session
964
+ * (`'content-type: text/html; charset=utf-8\n'` in Session.ts, as against the
965
+ * SAS 9 session's seed with no newline), and it is what keeps a service that
966
+ * appends a header without a leading newline on its own line - otherwise the two
967
+ * concatenate into one unusable value.
968
+ *
969
+ * It is a default, not a declaration: the worker ignores it for a byte body,
970
+ * so a service that writes bytes and names no type is still an undeclared
971
+ * download rather than being labelled text/html. The worker reads the value
972
+ * from HEADERS_SEED below, so the seed and the rule cannot drift.
973
+ */
974
+ const SEEDED_HEADERS = 'content-type: text/html; charset=utf-8\n'
975
+
957
976
  const runProgram = ({ program, code, params = {}, tables = [] }) => {
958
977
  const payloadCode = code
959
978
  const codePath = program ? resolveProgram(program) : null
@@ -977,7 +996,7 @@ const runProgram = ({ program, code, params = {}, tables = [] }) => {
977
996
  const headersPath = `${sessionDir}/stpsrv_header.txt`
978
997
  const tokenFile = `${sessionDir}/reqHeaders.txt`
979
998
  FILES.set(weboutPath, '')
980
- FILES.set(headersPath, '')
999
+ FILES.set(headersPath, SEEDED_HEADERS)
981
1000
  FILES.set(tokenFile, '')
982
1001
 
983
1002
  const varStatements = Object.entries(params)
@@ -1338,6 +1357,9 @@ const api = {
1338
1357
  list: (folder) => listFolder(folder),
1339
1358
  read: (file) => readDriveFile(file),
1340
1359
  tree: () => buildTree(),
1360
+ // the content type a session's header file is seeded with, so the worker can
1361
+ // tell a default from a declaration rather than carrying a second literal
1362
+ HEADERS_SEED: SEEDED_HEADERS,
1341
1363
  // the format the snapshot download stamps, so persist.js writes the number
1342
1364
  // this runtime reads rather than a second literal
1343
1365
  DRIVE_FORMAT
@@ -101,6 +101,14 @@ const parseHeaders = (content) => {
101
101
  return headers
102
102
  }
103
103
 
104
+ /**
105
+ * The content type a session's header file is seeded with - the server's
106
+ * default, not something the service asked for. The runtime owns the seed line,
107
+ * so the value is taken from it rather than repeated here: a byte body that
108
+ * carries only this type is an undeclared download, not a text/html one.
109
+ */
110
+ const SEEDED_HEADERS = String(R.HEADERS_SEED)
111
+
104
112
  /**
105
113
  * The reply a run produces: the webout, in the shape and under the name the
106
114
  * service asked for. A debug reply is the webout framed with the log, so it
@@ -114,6 +122,12 @@ const weboutResponse = (result, debug) => {
114
122
 
115
123
  const headers = parseHeaders(result.headers)
116
124
  if (result.binary) {
125
+ // the seed is a default, so a byte body that leaves the seeded file untouched
126
+ // declares nothing: it keeps the type an undeclared download gets. Compared as
127
+ // the raw text rather than as the parsed value, so a service that declares
128
+ // this same content type itself keeps it - only the untouched seed is dropped.
129
+ if (String(result.headers) === SEEDED_HEADERS)
130
+ delete headers['content-type']
117
131
  if (!headers['content-type'])
118
132
  headers['content-type'] = 'application/octet-stream'
119
133
  return new Response(toBytes(result.webout), { status: 200, headers })
@@ -175,23 +175,27 @@ describe('browser runtime', function () {
175
175
  expect(parsed.SYSPROCESSNAME).toEqual('Browser');
176
176
  expect(parsed.SYSSCPL).toEqual('browser');
177
177
  // The content type is the point: the block sets it, so the worker hands the
178
- // app JSON rather than a string. The mock seeds the file empty (the server
179
- // seeds text/html), so the block's record is the only one here.
178
+ // app JSON rather than a string. The session seed is text/html, so this
179
+ // record has to come last for the last-line-wins rule to land on JSON.
180
180
  expect(result.headers).toContain('content-type: application/json');
181
+ expect(result.headers.trim().split('\n').pop()).toEqual('content-type: application/json');
181
182
  return [2 /*return*/];
182
183
  }
183
184
  });
184
185
  }); });
185
- it('leaves the headers alone for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
186
+ it('seeds the headers the way a JS session does, for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
186
187
  var runtime, result;
187
188
  return __generator(this, function (_a) {
188
189
  runtime = loadRuntime();
189
190
  result = runtime.run({ code: "_webout = '<h1>raw</h1>'" });
190
191
  expect(result.error).toBeNull();
191
192
  expect(result.webout).toEqual('<h1>raw</h1>');
192
- // Unchanged behaviour: a raw assignment declares no type here, and the
193
- // worker's binary fallback depends on this file staying empty.
194
- expect(result.headers).toEqual('');
193
+ // A service that declares nothing answers text/html on a server, so the
194
+ // mock starts from the same line rather than from an empty file. The worker
195
+ // treats this value as undeclared for a byte body, which is why the seed
196
+ // does not turn a download into text/html.
197
+ expect(result.headers).toEqual('content-type: text/html; charset=utf-8\n');
198
+ expect(runtime.HEADERS_SEED).toEqual('content-type: text/html; charset=utf-8\n');
195
199
  return [2 /*return*/];
196
200
  });
197
201
  }); });
@@ -239,6 +239,92 @@ describe('service worker host', function () {
239
239
  }
240
240
  });
241
241
  }); });
242
+ it('keeps a header appended without a leading newline on its own line', function () { return __awaiter(void 0, void 0, void 0, function () {
243
+ var dispatch, response, _a;
244
+ return __generator(this, function (_b) {
245
+ switch (_b.label) {
246
+ case 0:
247
+ dispatch = streaming([
248
+ "fs.appendFileSync(_SASJS_WEBOUT_HEADERS, 'Content-type: application/json')",
249
+ "_webout = '{}'"
250
+ ].join('\n')).dispatch;
251
+ return [4 /*yield*/, dispatch(downloadPath())];
252
+ case 1:
253
+ response = _b.sent();
254
+ expect(response.headers.get('content-type')).toBe('application/json');
255
+ _a = expect;
256
+ return [4 /*yield*/, response.text()];
257
+ case 2:
258
+ _a.apply(void 0, [_b.sent()]).toBe('{}');
259
+ return [2 /*return*/];
260
+ }
261
+ });
262
+ }); });
263
+ it('keeps a declared type that happens to equal the seed', function () { return __awaiter(void 0, void 0, void 0, function () {
264
+ var dispatch, response;
265
+ return __generator(this, function (_a) {
266
+ switch (_a.label) {
267
+ case 0:
268
+ dispatch = streaming([
269
+ "fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'content-type: text/html; charset=utf-8')",
270
+ 'fs.writeFileSync(weboutPath, Uint8Array.from([1, 2, 3]))'
271
+ ].join('\n')).dispatch;
272
+ return [4 /*yield*/, dispatch(downloadPath())];
273
+ case 1:
274
+ response = _a.sent();
275
+ expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8');
276
+ return [2 /*return*/];
277
+ }
278
+ });
279
+ }); });
280
+ it('answers text/html for a service that declares no type, as a server does', function () { return __awaiter(void 0, void 0, void 0, function () {
281
+ var dispatch, response, _a;
282
+ return __generator(this, function (_b) {
283
+ switch (_b.label) {
284
+ case 0:
285
+ dispatch = seeded().dispatch;
286
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
287
+ encodeURIComponent("".concat(appLoc, "/services/public/getdata")))
288
+ // The session header file is seeded with the server's line, so a service
289
+ // that assigns _webout and stops answers with a content type instead of
290
+ // leaving the choice to the browser.
291
+ ];
292
+ case 1:
293
+ response = _b.sent();
294
+ // The session header file is seeded with the server's line, so a service
295
+ // that assigns _webout and stops answers with a content type instead of
296
+ // leaving the choice to the browser.
297
+ expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8');
298
+ _a = expect;
299
+ return [4 /*yield*/, response.text()];
300
+ case 2:
301
+ _a.apply(void 0, [_b.sent()]).toBe('seeded');
302
+ return [2 /*return*/];
303
+ }
304
+ });
305
+ }); });
306
+ it('keeps a declared text/html on a byte body, and ignores only the seed', function () { return __awaiter(void 0, void 0, void 0, function () {
307
+ var dispatch, response;
308
+ return __generator(this, function (_a) {
309
+ switch (_a.label) {
310
+ case 0:
311
+ dispatch = streaming([
312
+ "fs.appendFileSync(_SASJS_WEBOUT_HEADERS, '\\nContent-type: text/html')",
313
+ 'fs.writeFileSync(weboutPath, Uint8Array.from([1, 2, 3]))'
314
+ ].join('\n')).dispatch;
315
+ return [4 /*yield*/, dispatch(downloadPath())
316
+ // only the seeded value is treated as undeclared; the same type written by
317
+ // the service is a declaration and is applied
318
+ ];
319
+ case 1:
320
+ response = _a.sent();
321
+ // only the seeded value is treated as undeclared; the same type written by
322
+ // the service is a declaration and is applied
323
+ expect(response.headers.get('content-type')).toBe('text/html');
324
+ return [2 /*return*/];
325
+ }
326
+ });
327
+ }); });
242
328
  it('keeps a debug reply text, as the body is the framed log', function () { return __awaiter(void 0, void 0, void 0, function () {
243
329
  var dispatch, response, _a;
244
330
  return __generator(this, function (_b) {
package/package.json CHANGED
@@ -123,5 +123,5 @@
123
123
  "ws": "8.21.0",
124
124
  "nanoid": "3.3.18"
125
125
  },
126
- "version": "6.10.0"
126
+ "version": "6.10.1"
127
127
  }