bare-script 3.7.5 → 3.8.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/lib/bare.js CHANGED
@@ -8,6 +8,7 @@ import {executeScriptAsync} from './runtimeAsync.js';
8
8
  import {fileURLToPath} from 'url';
9
9
  import {lintScript} from './model.js';
10
10
  import {readFile} from 'fs/promises';
11
+ import {systemGlobalIncludesName} from './library.js';
11
12
  import {urlFileRelative} from './options.js';
12
13
  import {valueBoolean} from './value.js';
13
14
 
@@ -55,12 +56,20 @@ export async function main(options) {
55
56
 
56
57
  // Get the scripts to run
57
58
  let {scripts} = args;
59
+ let ixUserScript = 0;
58
60
  if (args.markdownUp) {
59
61
  scripts = [['code', 'include <markdownUp.bare>'], ...scripts];
62
+ ixUserScript = 1;
63
+
64
+ // Add unittest.bare argument globals
65
+ globals.vUnittestReport = true;
66
+ if (args.static) {
67
+ globals.vUnittestDisabled = true;
68
+ }
60
69
  }
61
70
 
62
71
  // Parse and execute all source files in order
63
- for (const [scriptType, scriptValue] of scripts) {
72
+ for (const [ixScript, [scriptType, scriptValue]] of scripts.entries()) {
64
73
  // Get the script source
65
74
  let scriptName;
66
75
  let scriptSource;
@@ -80,16 +89,58 @@ export async function main(options) {
80
89
  }
81
90
  } else {
82
91
  inlineCount += 1;
83
- scriptName = `<string${inlineCount > 1 ? inlineCount : ''}>`;
92
+ const inlineDisplay = inlineCount - ixUserScript;
93
+ scriptName = `<string${inlineDisplay > 1 ? inlineDisplay : ''}>`;
84
94
  scriptSource = scriptValue;
85
95
  }
86
96
 
87
97
  // Parse the script source
88
98
  const script = parseScript(scriptSource, 1, scriptName);
89
99
 
100
+ // Execute?
101
+ let staticGlobals = null;
102
+ if (args.static !== 's') {
103
+ // Set the globals to use for static analysis (below)
104
+ if (!args.static || ixScript < ixUserScript) {
105
+ staticGlobals = globals;
106
+ } else {
107
+ // Copy global to keep each script as isolated as possible
108
+ staticGlobals = {...globals};
109
+ const globalIncludes = staticGlobals[systemGlobalIncludesName] ?? null;
110
+ if (globalIncludes !== null && typeof globalIncludes === 'object') {
111
+ staticGlobals[systemGlobalIncludesName] = {...globalIncludes};
112
+ }
113
+ }
114
+
115
+ // Execute the script
116
+ const timeBegin = performance.now();
117
+ const {fetchFn} = options;
118
+ const fetchIncludeFn = (fetchURL, fetchOptions) => fetchInclude(fetchFn, fetchURL, fetchOptions);
119
+ const result = await executeScriptAsync(script, {
120
+ 'debug': args.debug ?? false,
121
+ 'fetchFn': fetchIncludeFn,
122
+ 'globals': staticGlobals,
123
+ 'logFn': options.logFn,
124
+ 'systemPrefix': fetchIncludePrefix,
125
+ 'urlFn': scriptType === 'file' ? (url) => urlFileRelative(scriptName, url) : null
126
+
127
+ });
128
+ if (Number.isInteger(result) && result >= 0 && result <= 255) {
129
+ statusCode = result || statusCode;
130
+ } else {
131
+ statusCode = (valueBoolean(result) ? 1 : 0) || statusCode;
132
+ }
133
+
134
+ // Log script execution end with timing
135
+ if (args.debug && ixScript >= ixUserScript) {
136
+ const timeEnd = performance.now();
137
+ options.logFn(`BareScript executed in ${(timeEnd - timeBegin).toFixed(1)} milliseconds`);
138
+ }
139
+ }
140
+
90
141
  // Run the bare-script linter?
91
- if (args.static || args.debug) {
92
- const warnings = lintScript(script);
142
+ if (args.static && ixScript >= ixUserScript) {
143
+ const warnings = lintScript(script, staticGlobals);
93
144
  if (warnings.length === 0) {
94
145
  options.logFn(`BareScript static analysis "${scriptName}" ... OK`);
95
146
  } else {
@@ -99,43 +150,12 @@ export async function main(options) {
99
150
  for (const warning of warnings) {
100
151
  options.logFn(warning);
101
152
  }
102
- if (args.static) {
103
- statusCode = 1;
104
- break;
105
- }
153
+ statusCode = 1;
106
154
  }
107
155
  }
108
- if (args.static) {
109
- continue;
110
- }
111
-
112
- // Execute the script
113
- const timeBegin = performance.now();
114
- const {fetchFn} = options;
115
- const fetchIncludeFn = (fetchURL, fetchOptions) => fetchInclude(fetchFn, fetchURL, fetchOptions);
116
- const result = await executeScriptAsync(script, {
117
- 'debug': args.debug ?? false,
118
- 'fetchFn': fetchIncludeFn,
119
- 'globals': globals,
120
- 'logFn': options.logFn,
121
- 'systemPrefix': fetchIncludePrefix,
122
- 'urlFn': scriptType === 'file' ? (url) => urlFileRelative(scriptName, url) : null
123
-
124
- });
125
- if (Number.isInteger(result) && result >= 0 && result <= 255) {
126
- statusCode = result;
127
- } else {
128
- statusCode = valueBoolean(result) ? 1 : 0;
129
- }
130
-
131
- // Log script execution end with timing
132
- if (args.debug) {
133
- const timeEnd = performance.now();
134
- options.logFn(`BareScript executed in ${(timeEnd - timeBegin).toFixed(1)} milliseconds`);
135
- }
136
156
 
137
157
  // Stop on error status code
138
- if (statusCode !== 0) {
158
+ if (statusCode !== 0 && !args.static) {
139
159
  break;
140
160
  }
141
161
  }
@@ -202,7 +222,9 @@ export function parseArgs(argv) {
202
222
  } else if (arg === '-m' || arg === '--markdown-up') {
203
223
  args.markdownUp = true;
204
224
  } else if (arg === '-s' || arg === '--static') {
205
- args.static = true;
225
+ args.static = 's';
226
+ } else if (arg === '-x' || arg === '--staticx') {
227
+ args.static = 'x';
206
228
  } else if (arg === '-v' || arg === '--var') {
207
229
  if (iArg + 2 >= argv.length) {
208
230
  throw new Error('argument -v/--var: expected 2 arguments');
@@ -222,7 +244,7 @@ export function parseArgs(argv) {
222
244
 
223
245
  // The command-line interface (CLI) help text
224
246
  export const helpText = `\
225
- usage: bare [-h] [-c CODE] [-d] [-m] [-s] [-v VAR EXPR] [file ...]
247
+ usage: bare [-h] [-c CODE] [-d] [-m] [-s] [-x] [-v VAR EXPR] [file ...]
226
248
 
227
249
  The BareScript command-line interface
228
250
 
@@ -235,4 +257,5 @@ options:
235
257
  -d, --debug enable debug mode
236
258
  -m, --markdown-up run with MarkdownUp stubs
237
259
  -s, --static perform static analysis
260
+ -x, --staticx perform static analysis with execution
238
261
  -v, --var VAR EXPR set a global variable to an expression value`;
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('argsSentinel'):
7
- return
8
- endif
9
- argsSentinel = true
10
-
11
-
12
5
  # The URL arguments model
13
6
  argsTypes = schemaParse( \
14
7
  'group "args.bare"', \
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('dataTableSentinel'):
7
- return
8
- endif
9
- dataTableSentinel = true
10
-
11
-
12
5
  # The data table model's Schema Markdown
13
6
  dataTableTypes = schemaParse( \
14
7
  'group "Data Table"', \
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('diffSentinel'):
7
- return
8
- endif
9
- diffSentinel = true
10
-
11
-
12
5
  # The text line difference model
13
6
  diffTypes = schemaParse( \
14
7
  'group "diff.bare"', \
@@ -104,7 +97,7 @@ function diffLines(left, right):
104
97
  endif
105
98
 
106
99
  # Look ahead to find next matching point
107
- foundMatch = False
100
+ foundMatch = false
108
101
  ixLeftTmp = ixLeft
109
102
  while ixLeftTmp < leftLength:
110
103
  ixRightTmp = ixRight
@@ -152,4 +145,4 @@ endfunction
152
145
 
153
146
 
154
147
  # Regex for splitting lines
155
- diffRegexLineSplit = regexNew(stringFromCharCode(13) + '?' + stringFromCharCode(10))
148
+ diffRegexLineSplit = regexNew('\r?\n')
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('formsSentinel'):
7
- return
8
- endif
9
- formsSentinel = true
10
-
11
-
12
5
  # $function: formsTextElements
13
6
  # $group: forms.bare
14
7
  # $doc: Create a text input [element model](https://github.com/craigahobbs/element-model#readme)
@@ -1,14 +1,6 @@
1
1
  # Licensed under the MIT License
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
-
5
- # Include sentinel
6
- if systemGlobalGet('markdownUpSentinel'):
7
- return
8
- endif
9
- markdownUpSentinel = true
10
-
11
-
12
4
  include <dataTable.bare>
13
5
 
14
6
 
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('pagerSentinel'):
7
- return
8
- endif
9
- pagerSentinel = true
10
-
11
-
12
5
  include <args.bare>
13
6
 
14
7
 
@@ -2,69 +2,60 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('unittestSentinel'):
7
- return
8
- endif
9
- unittestSentinel = true
10
-
11
-
12
5
  include <args.bare>
13
6
  include <dataTable.bare>
14
7
  include <diff.bare>
15
8
 
16
9
 
17
- # Test statistics
18
- unittestTests = {}
19
- unittestWarnings = []
20
-
21
-
22
10
  # $function: unittestRunTest
23
11
  # $group: unittest.bare
24
12
  # $doc: Run a unit test
25
13
  # $arg testName: The test function name
26
14
  async function unittestRunTest(testName):
27
- testFn = unittestRunTestHelper(testName)
28
- if testFn:
29
- testFn()
30
- systemGlobalSet('unittestTestName', null)
15
+ # Tests disabled?
16
+ if systemGlobalGet('vUnittestDisabled'):
17
+ return
31
18
  endif
32
- endfunction
33
19
 
34
-
35
- # For backward compatibility
36
- async function unittestRunTestAsync(testName):
37
- systemLogDebug('unittest.bare: unittestRunTestAsync is deprecated - use unittestRunTest')
38
- return unittestRunTest(testName)
39
- endfunction
40
-
41
-
42
- # unittestRunTest helper function
43
- function unittestRunTestHelper(testName):
44
20
  # Single test argument?
21
+ vUnittestTest = systemGlobalGet('vUnittestTest')
45
22
  if vUnittestTest && vUnittestTest != testName:
46
- return null
23
+ return
47
24
  endif
48
25
 
26
+ # Get the global unittest data
27
+ unittestData = unittestDataGet()
28
+ unittestTests = objectGet(unittestData, 'tests')
29
+ unittestWarnings = objectGet(unittestData, 'warnings')
30
+
49
31
  # Test run multiple times?
50
32
  if objectHas(unittestTests, testName):
51
33
  arrayPush(unittestWarnings, 'Test "' + testName + '" run multiple times')
52
- return null
34
+ return
53
35
  endif
54
36
 
55
37
  # Get the test func
56
38
  testFn = systemGlobalGet(testName)
57
39
  if !testFn:
58
40
  arrayPush(unittestWarnings, 'Test "' + testName + '" not found')
59
- return null
41
+ return
60
42
  endif
61
43
 
62
44
  # Add the unit test result array
63
45
  testFailures = []
64
46
  objectSet(unittestTests, testName, testFailures)
65
- systemGlobalSet('unittestTestName', testName)
66
47
 
67
- return testFn
48
+ # Run the test
49
+ objectSet(unittestData, 'current', testName)
50
+ testFn()
51
+ objectSet(unittestData, 'current', null)
52
+ endfunction
53
+
54
+
55
+ # For backward compatibility
56
+ async function unittestRunTestAsync(testName):
57
+ systemLogDebug('unittest.bare: unittestRunTestAsync is deprecated - use unittestRunTest')
58
+ return unittestRunTest(testName)
68
59
  endfunction
69
60
 
70
61
 
@@ -79,8 +70,13 @@ function unittestEqual(actual, expected, description):
79
70
  return
80
71
  endif
81
72
 
73
+ # Get the global unittest data
74
+ unittestData = unittestDataGet()
75
+ unittestTests = objectGet(unittestData, 'tests')
76
+ unittestCurrent = objectGet(unittestData, 'current')
77
+ testFailures = objectGet(unittestTests, unittestCurrent)
78
+
82
79
  # Add the test failure error lines
83
- testFailures = objectGet(unittestTests, unittestTestName)
84
80
  errorLines = [ \
85
81
  'Equal:', \
86
82
  '', \
@@ -143,8 +139,13 @@ function unittestDeepEqual(actual, expected, description):
143
139
  arrayPush(errorLines, '```')
144
140
  endif
145
141
 
142
+ # Get the global unittest data
143
+ unittestData = unittestDataGet()
144
+ unittestTests = objectGet(unittestData, 'tests')
145
+ unittestCurrent = objectGet(unittestData, 'current')
146
+ testFailures = objectGet(unittestTests, unittestCurrent)
147
+
146
148
  # Add the test failure error lines
147
- testFailures = objectGet(unittestTests, unittestTestName)
148
149
  if description:
149
150
  arrayPush(testFailures, arrayExtend([markdownEscape(description), ''], errorLines))
150
151
  else:
@@ -153,6 +154,17 @@ function unittestDeepEqual(actual, expected, description):
153
154
  endfunction
154
155
 
155
156
 
157
+ # Helper to get the global unittest data object
158
+ function unittestDataGet():
159
+ unittestData = systemGlobalGet('unittestData')
160
+ if unittestData == null:
161
+ unittestData = {'tests': {}, 'warnings': [], 'current': null}
162
+ systemGlobalSet('unittestData', unittestData)
163
+ endif
164
+ return unittestData
165
+ endfunction
166
+
167
+
156
168
  # $function: unittestReport
157
169
  # $group: unittest.bare
158
170
  # $doc: Render the unit test report
@@ -171,10 +183,16 @@ function unittestReport(options):
171
183
 
172
184
  # Parse arguments
173
185
  args = argsParse(unittestReportArguments)
174
- testNameArg = objectGet(args, 'test')
175
- scriptName = objectGet(args, 'script')
186
+ isDisabled = objectGet(args, 'disabled')
176
187
  hideTests = objectGet(args, 'hideTests')
177
188
  isReport = objectGet(args, 'report')
189
+ scriptName = objectGet(args, 'script')
190
+ testNameArg = objectGet(args, 'test')
191
+
192
+ # Disabled?
193
+ if isDisabled:
194
+ return 0
195
+ endif
178
196
 
179
197
  # Script coverage details page?
180
198
  if scriptName:
@@ -182,6 +200,11 @@ function unittestReport(options):
182
200
  return 0
183
201
  endif
184
202
 
203
+ # Get the global unittest data
204
+ unittestData = unittestDataGet()
205
+ unittestTests = objectGet(unittestData, 'tests')
206
+ unittestWarnings = objectGet(unittestData, 'warnings')
207
+
185
208
  # Compute test statistics
186
209
  testNames = arraySort(objectKeys(unittestTests))
187
210
  testCount = arrayLength(testNames)
@@ -318,10 +341,11 @@ endfunction
318
341
 
319
342
  # unittestReport application arguments
320
343
  unittestReportArguments = argsValidate([ \
321
- {'name': 'test', 'global': 'vUnittestTest'}, \
322
- {'name': 'script', 'global': 'vUnittestScript'}, \
344
+ {'name': 'disabled', 'global': 'vUnittestDisabled', 'type': 'bool', 'default': false}, \
323
345
  {'name': 'hideTests', 'global': 'vUnittestHideTests', 'type': 'bool', 'default': false}, \
324
- {'name': 'report', 'global': 'vUnittestReport', 'type': 'bool', 'default': false} \
346
+ {'name': 'report', 'global': 'vUnittestReport', 'type': 'bool', 'default': false}, \
347
+ {'name': 'script', 'global': 'vUnittestScript'}, \
348
+ {'name': 'test', 'global': 'vUnittestTest'} \
325
349
  ])
326
350
 
327
351
 
@@ -393,7 +417,7 @@ endfunction
393
417
 
394
418
  function unittestReportScriptLines(codeLineElements, lines, color, noNewline):
395
419
  if lines:
396
- linesStr = arrayJoin(lines, stringFromCharCode(10)) + if(noNewline, '', stringFromCharCode(10))
420
+ linesStr = arrayJoin(lines, '\n') + if(noNewline, '', '\n')
397
421
  if !color:
398
422
  arrayPush(codeLineElements, {'text': linesStr})
399
423
  else:
@@ -2,13 +2,6 @@
2
2
  # https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
3
3
 
4
4
 
5
- # Include sentinel
6
- if systemGlobalGet('unittestMockSentinel'):
7
- return
8
- endif
9
- unittestMockSentinel = true
10
-
11
-
12
5
  # Constants
13
6
  unittestMockPixelsPerPoint = 4 / 3
14
7
  unittestMockDefaultFontSizePx = 12 * unittestMockPixelsPerPoint
package/lib/library.js CHANGED
@@ -1987,6 +1987,29 @@ struct SystemFetchRequest
1987
1987
  `);
1988
1988
 
1989
1989
 
1990
+ // System includes object global variable name
1991
+ export const systemGlobalIncludesName = '__bareScriptIncludes';
1992
+
1993
+
1994
+ // $function: systemGlobalIncludesGet
1995
+ // $group: System
1996
+ // $doc: Get the global system includes object
1997
+ // $return: The global system includes object
1998
+ function systemGlobalIncludesGet(unusedArgs, options) {
1999
+ const globals = (options !== null ? (options.globals ?? null) : null);
2000
+ return (globals !== null ? (globals[systemGlobalIncludesName] ?? null) : null);
2001
+ }
2002
+
2003
+
2004
+ // $function: systemGlobalIncludesName
2005
+ // $group: System
2006
+ // $doc: Get the system includes object global variable name
2007
+ // $return: The system includes object global variable name
2008
+ function systemGlobalIncludesNameFn() {
2009
+ return systemGlobalIncludesName;
2010
+ }
2011
+
2012
+
1990
2013
  // $function: systemGlobalGet
1991
2014
  // $group: System
1992
2015
  // $doc: Get a global variable value
@@ -2248,6 +2271,8 @@ export const scriptFunctions = {
2248
2271
  systemCompare,
2249
2272
  systemFetch,
2250
2273
  systemGlobalGet,
2274
+ systemGlobalIncludesGet,
2275
+ 'systemGlobalIncludesName': systemGlobalIncludesNameFn,
2251
2276
  systemGlobalSet,
2252
2277
  systemIs,
2253
2278
  systemLog,
package/lib/model.js CHANGED
@@ -320,9 +320,10 @@ export function validateExpression(expr) {
320
320
  * Lint a BareScript script model
321
321
  *
322
322
  * @param {Object} script - The [BareScript model](./model/#var.vName='BareScript')
323
+ * @param {?Object} globals - The script global variables
323
324
  * @returns {string[]} The array of lint warnings
324
325
  */
325
- export function lintScript(script) {
326
+ export function lintScript(script, globals = null) {
326
327
  const warnings = [];
327
328
  const {statements} = script;
328
329
 
@@ -341,6 +342,15 @@ export function lintScript(script) {
341
342
  }
342
343
  }
343
344
 
345
+ // Unknown global variable?
346
+ if (globals !== null) {
347
+ for (const varName of Object.keys(varUses).sort()) {
348
+ if (!(varName in varAssigns) && !(varName in globals) && !builtinGlobals.has(varName)) {
349
+ lintScriptWarning(warnings, script, statements[varUses[varName]], `Unknown global variable "${varName}"`);
350
+ }
351
+ }
352
+ }
353
+
344
354
  // Iterate global statements
345
355
  const functionsDefined = {};
346
356
  const labelsDefined = {};
@@ -361,7 +371,8 @@ export function lintScript(script) {
361
371
  const fnVarAssigns = {};
362
372
  const fnVarUses = {};
363
373
  const args = (statement.function.args ?? null);
364
- getVariableAssignmentsAndUses(statement.function.statements, fnVarAssigns, fnVarUses);
374
+ const fnStatements = statement.function.statements;
375
+ getVariableAssignmentsAndUses(fnStatements, fnVarAssigns, fnVarUses);
365
376
  for (const varName of Object.keys(fnVarAssigns)) {
366
377
  // Ignore re-assigned function arguments
367
378
  if (args !== null && args.indexOf(varName) !== -1) {
@@ -369,7 +380,7 @@ export function lintScript(script) {
369
380
  }
370
381
  if (varName in fnVarUses && fnVarUses[varName] <= fnVarAssigns[varName]) {
371
382
  lintScriptWarning(
372
- warnings, script, statement,
383
+ warnings, script, fnStatements[fnVarUses[varName]],
373
384
  `Variable "${varName}" of function "${statement.function.name}" used before assignment`
374
385
  );
375
386
  }
@@ -379,12 +390,24 @@ export function lintScript(script) {
379
390
  for (const varName of Object.keys(fnVarAssigns)) {
380
391
  if (!(varName in fnVarUses)) {
381
392
  lintScriptWarning(
382
- warnings, script, statement,
393
+ warnings, script, fnStatements[fnVarAssigns[varName]],
383
394
  `Unused variable "${varName}" defined in function "${statement.function.name}"`
384
395
  );
385
396
  }
386
397
  }
387
398
 
399
+ // Unknown global variable?
400
+ if (globals !== null) {
401
+ for (const varName of Object.keys(fnVarUses).sort()) {
402
+ if (!(varName in fnVarAssigns) && (args === null || args.indexOf(varName) === -1) &&
403
+ !(varName in globals) && !builtinGlobals.has(varName)) {
404
+ lintScriptWarning(
405
+ warnings, script, fnStatements[fnVarUses[varName]], `Unknown global variable "${varName}"`
406
+ );
407
+ }
408
+ }
409
+ }
410
+
388
411
  // Function argument checks
389
412
  if (args !== null) {
390
413
  const argsDefined = new Set();
@@ -410,7 +433,7 @@ export function lintScript(script) {
410
433
  // Iterate function statements
411
434
  const fnLabelsDefined = {};
412
435
  const fnLabelsUsed = {};
413
- for (const [ixFnStatement, fnStatement] of statement.function.statements.entries()) {
436
+ for (const [ixFnStatement, fnStatement] of fnStatements.entries()) {
414
437
  const [fnStatementKey] = Object.keys(fnStatement);
415
438
 
416
439
  // Function expression statement checks
@@ -498,6 +521,10 @@ export function lintScript(script) {
498
521
  }
499
522
 
500
523
 
524
+ // Builtin global variable names
525
+ const builtinGlobals = new Set(['false', 'if', 'null', 'true']);
526
+
527
+
501
528
  // Helper to format static analysis warnings
502
529
  function lintScriptWarning(warnings, script, statement, message) {
503
530
  const scriptName = script.scriptName ?? '';
@@ -5,7 +5,7 @@
5
5
 
6
6
  import {BareScriptRuntimeError, evaluateExpression, recordStatementCoverage, scriptFunction} from './runtime.js';
7
7
  import {ValueArgsError, valueBoolean, valueCompare, valueString} from './value.js';
8
- import {coverageGlobalName, defaultMaxStatements, expressionFunctions, scriptFunctions} from './library.js';
8
+ import {coverageGlobalName, defaultMaxStatements, expressionFunctions, scriptFunctions, systemGlobalIncludesName} from './library.js';
9
9
  import {lintScript} from './model.js';
10
10
  import {parseScript} from './parser.js';
11
11
  import {urlFileRelative} from './options.js';
@@ -123,9 +123,9 @@ async function executeScriptHelperAsync(script, statements, options, locals) {
123
123
 
124
124
  // Include?
125
125
  } else if (statementKey === 'include') {
126
- // Fetch the include script text
126
+ // Compute the include script URLs
127
127
  const urlFn = options.urlFn ?? null;
128
- const includeURLs = statement.include.includes.map(({url, system = false}) => {
128
+ const unfilteredIncludeURLs = statement.include.includes.map(({url, system = false}) => {
129
129
  let includeURL;
130
130
  if (system && 'systemPrefix' in options) {
131
131
  includeURL = urlFileRelative(options.systemPrefix, url);
@@ -134,6 +134,21 @@ async function executeScriptHelperAsync(script, statements, options, locals) {
134
134
  }
135
135
  return {includeURL, 'systemInclude': system};
136
136
  });
137
+
138
+ // Filter already included
139
+ let globalIncludes = globals[systemGlobalIncludesName] ?? null;
140
+ if (globalIncludes === null || typeof globalIncludes !== 'object') {
141
+ globalIncludes = {};
142
+ globals[systemGlobalIncludesName] = globalIncludes;
143
+ }
144
+ const includeURLs = unfilteredIncludeURLs.filter(({includeURL}) => {
145
+ if (globalIncludes[includeURL]) {
146
+ return false;
147
+ }
148
+ return true;
149
+ });
150
+
151
+ // Fetch the include script text
137
152
  const responses = await Promise.all(includeURLs.map(async ({includeURL, systemInclude}) => {
138
153
  try {
139
154
  const response = ('fetchFn' in options ? await options.fetchFn(includeURL) : null);
@@ -160,6 +175,12 @@ async function executeScriptHelperAsync(script, statements, options, locals) {
160
175
  throw new BareScriptRuntimeError(script, statement, `Include of "${includeURL}" failed`);
161
176
  }
162
177
 
178
+ // Mark as included. Check again if the URL is included.
179
+ if (globalIncludes[includeURL]) {
180
+ continue;
181
+ }
182
+ globalIncludes[includeURL] = true;
183
+
163
184
  // Parse the include script
164
185
  const includeScript = parseScript(includeText, 1, includeURL);
165
186
  if (systemInclude) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "bare-script",
4
- "version": "3.7.5",
4
+ "version": "3.8.0",
5
5
  "description": "BareScript; a lightweight scripting and expression language",
6
6
  "keywords": [
7
7
  "expression",