• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 6107141959

07 Sep 2023 08:11AM UTC coverage: 82.853% (+0.2%) from 82.683%
6107141959

push

github

web-flow
Use Asynchronous Code for Loading Modules (#1471)

* Add async loading code

* Made infinite loop checking async

* Update stepper to be async

* Add assert and misc utils

* Update transpiler to be async

* Update runners

* Add new import options

* Ignore tests during typechecking

* Relocate typeguards

* Update with assertions and typeguards

* Update repl transpiler to be async

* Abstract out module context initialization and tab loading

* Renamed promise timeout error and added tests

* Remove old code

* Update options

* Add files to avoid line ending issues

* Use POSIX paths on Windows systems

* Ran format

* Update eslint rules to follow exclusion of tests

* Incorporate changes for posix path handling

* Minor change to resolve certain webpack issues with the frontend

* Use a different posix path import

* Allow loading of export default tabs

* Refactor collation of import declarations

---------

Co-authored-by: Ian Yong <ianyongyc@gmail.com>

3629 of 4814 branches covered (0.0%)

Branch coverage included in aggregate %.

266 of 266 new or added lines in 25 files covered. (100.0%)

10886 of 12705 relevant lines covered (85.68%)

96881.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.25
/src/modules/moduleLoader.ts
1
import es from 'estree'
2
import { memoize } from 'lodash'
66✔
3
import { XMLHttpRequest as NodeXMLHttpRequest } from 'xmlhttprequest-ts'
66✔
4

5
import {
66✔
6
  ModuleConnectionError,
7
  ModuleInternalError,
8
  ModuleNotFoundError
9
} from '../errors/moduleErrors'
10
import { Context } from '../types'
11
import { wrapSourceModule } from '../utils/operators'
66✔
12
import { ModuleBundle, ModuleDocumentation, ModuleFunctions, ModuleManifest } from './moduleTypes'
13
import { getRequireProvider } from './requireProvider'
66✔
14
import { evalRawTab } from './utils'
66✔
15

16
// Supports both JSDom (Web Browser) environment and Node environment
17
export const newHttpRequest = () =>
66✔
18
  typeof window === 'undefined' ? new NodeXMLHttpRequest() : new XMLHttpRequest()
24!
19

20
// Default modules static url. Exported for testing.
21
export let MODULES_STATIC_URL = 'https://source-academy.github.io/modules'
66✔
22

23
export function setModulesStaticURL(url: string) {
66✔
24
  MODULES_STATIC_URL = url
1✔
25
}
26

27
/**
28
 * Send a HTTP Get request to the specified endpoint.
29
 * @return NodeXMLHttpRequest | XMLHttpRequest
30
 */
31
export function httpGet(url: string): string {
66✔
32
  const request = newHttpRequest()
23✔
33
  try {
23✔
34
    // If running function in node environment, set request timeout
35
    if (typeof window === 'undefined') request.timeout = 10000
23!
36
    request.open('GET', url, false)
23✔
37
    request.send(null)
23✔
38
  } catch (error) {
39
    if (!(error instanceof DOMException)) throw error
×
40
  }
41
  if (request.status !== 200 && request.status !== 304) throw new ModuleConnectionError()
23✔
42
  return request.responseText
22✔
43
}
44

45
/**
46
 * Send a HTTP GET request to the modules endpoint to retrieve the manifest
47
 * @return Modules
48
 */
49
export const memoizedGetModuleManifest = memoize(getModuleManifest)
66✔
50
function getModuleManifest(): ModuleManifest {
51
  const rawManifest = httpGet(`${MODULES_STATIC_URL}/modules.json`)
9✔
52
  return JSON.parse(rawManifest)
9✔
53
}
54

55
/**
56
 * Send a HTTP GET request to the modules endpoint to retrieve the specified file
57
 * @return String of module file contents
58
 */
59

60
const memoizedGetModuleFileInternal = memoize(getModuleFile)
66✔
61
export const memoizedGetModuleFile = (name: string, type: 'tab' | 'bundle' | 'json') =>
66✔
62
  memoizedGetModuleFileInternal({ name, type })
12✔
63
function getModuleFile({ name, type }: { name: string; type: 'tab' | 'bundle' | 'json' }): string {
64
  return httpGet(`${MODULES_STATIC_URL}/${type}s/${name}.js${type === 'json' ? 'on' : ''}`)
12!
65
}
66

67
/**
68
 * Loads the respective module package (functions from the module)
69
 * @param path imported module name
70
 * @param context
71
 * @param node import declaration node
72
 * @returns the module's functions object
73
 */
74
export function loadModuleBundle(path: string, context: Context, node?: es.Node): ModuleFunctions {
66✔
75
  const modules = memoizedGetModuleManifest()
4✔
76

77
  // Check if the module exists
78
  const moduleList = Object.keys(modules)
4✔
79
  if (moduleList.includes(path) === false) throw new ModuleNotFoundError(path, node)
4!
80

81
  // Get module file
82
  const moduleText = memoizedGetModuleFile(path, 'bundle')
4✔
83
  try {
4✔
84
    const moduleBundle: ModuleBundle = eval(moduleText)
4✔
85
    return wrapSourceModule(path, moduleBundle, getRequireProvider(context))
3✔
86
  } catch (error) {
87
    // console.error("bundle error: ", error)
88
    throw new ModuleInternalError(path, error, node)
1✔
89
  }
90
}
91

92
/**
93
 * Loads the module contents of a package
94
 *
95
 * @param path imported module name
96
 * @param node import declaration node
97
 * @returns an array of functions
98
 */
99
export function loadModuleTabs(path: string, node?: es.Node) {
66✔
100
  const modules = memoizedGetModuleManifest()
5✔
101
  // Check if the module exists
102
  const moduleList = Object.keys(modules)
5✔
103
  if (moduleList.includes(path) === false) throw new ModuleNotFoundError(path, node)
5!
104

105
  // Retrieves the tabs the module has from modules.json
106
  const sideContentTabPaths: string[] = modules[path].tabs
5✔
107
  // Load the tabs for the current module
108
  return sideContentTabPaths.map(path => {
5✔
109
    const rawTabFile = memoizedGetModuleFile(path, 'tab')
6✔
110
    try {
6✔
111
      return evalRawTab(rawTabFile)
6✔
112
    } catch (error) {
113
      // console.error('tab error:', error);
114
      throw new ModuleInternalError(path, error, node)
1✔
115
    }
116
  })
117
}
118

119
export const memoizedloadModuleDocs = memoize(loadModuleDocs)
66✔
120
export function loadModuleDocs(path: string, node?: es.Node) {
66✔
121
  try {
×
122
    const modules = memoizedGetModuleManifest()
×
123
    // Check if the module exists
124
    const moduleList = Object.keys(modules)
×
125
    if (!moduleList.includes(path)) throw new ModuleNotFoundError(path, node)
×
126
    const result = getModuleFile({ name: path, type: 'json' })
×
127
    return JSON.parse(result) as ModuleDocumentation
×
128
  } catch (error) {
129
    console.warn('Failed to load module documentation')
×
130
    return null
×
131
  }
132
}
133

134
export function initModuleContext(
66✔
135
  moduleName: string,
136
  context: Context,
137
  loadTabs: boolean,
138
  node?: es.Node
139
) {
140
  if (!(moduleName in context.moduleContexts)) {
2!
141
    context.moduleContexts[moduleName] = {
2✔
142
      state: null,
143
      tabs: loadTabs ? loadModuleTabs(moduleName, node) : null
2!
144
    }
145
  } else if (context.moduleContexts[moduleName].tabs === null && loadTabs) {
×
146
    context.moduleContexts[moduleName].tabs = loadModuleTabs(moduleName, node)
×
147
  }
148
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc