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

source-academy / js-slang / 15237418122

25 May 2025 11:31AM UTC coverage: 77.048% (-3.5%) from 80.538%
15237418122

push

github

web-flow
Rewrite: Stepper (#1742)

3433 of 4826 branches covered (71.14%)

Branch coverage included in aggregate %.

1032 of 1260 new or added lines in 27 files covered. (81.9%)

440 existing lines in 29 files now uncovered.

10099 of 12737 relevant lines covered (79.29%)

142411.96 hits per line

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

84.62
/src/modules/loader/loaders.ts
1
import type { Context, Node } from '../../types'
2
import { timeoutPromise } from '../../utils/misc'
50✔
3
import { ModuleConnectionError, ModuleInternalError } from '../errors'
50✔
4
import type {
5
  ModuleBundle,
6
  ModuleDocumentation,
7
  ModuleFunctions,
8
  ModuleManifest
9
} from '../moduleTypes'
10
import { getRequireProvider } from './requireProvider'
50✔
11

12
/** Default modules static url. Exported for testing. */
13
export let MODULES_STATIC_URL = 'https://source-academy.github.io/modules'
50✔
14

15
export function setModulesStaticURL(url: string) {
50✔
16
  MODULES_STATIC_URL = url
×
17

18
  // Changing the module backend should clear these
19
  memoizedGetModuleDocsAsync.cache.clear()
×
20
  memoizedGetModuleManifestAsync.reset()
×
21
}
22

23
function wrapImporter<T>(func: (p: string) => Promise<T>) {
24
  return async (p: string): Promise<T> => {
100✔
25
    try {
5✔
26
      const result = await timeoutPromise(func(p), 10000)
5✔
27
      return result
4✔
28
    } catch (error) {
29
      // Before calling this function, the import analyzer should've been used to make sure
30
      // that the module being imported already exists, so the following errors should
31
      // be thrown only if the modules server is unreachable
32
      if (
1✔
33
        // In the browser, import statements should throw TypeError
34
        (typeof window !== 'undefined' && error instanceof TypeError) ||
3!
35
        // In Node a different error is thrown with the given code instead
36
        error.code === 'MODULE_NOT_FOUND' ||
37
        // Thrown specifically by jest
38
        error.code === 'ENOENT'
39
      ) {
40
        throw new ModuleConnectionError()
1✔
41
      }
UNCOV
42
      throw error
×
43
    }
44
  }
45
}
46

47
// Exported for testing
48
export const docsImporter = wrapImporter<{ default: any }>(async p => {
50✔
49
  // TODO: Use import attributes when they become supported
50
  // Import Assertions and Attributes are not widely supported by all
51
  // browsers yet, so we use fetch in the meantime
52
  const resp = await fetch(p)
1✔
53
  if (resp.status !== 200 && resp.status !== 304) {
1!
UNCOV
54
    throw new ModuleConnectionError()
×
55
  }
56

57
  const result = await resp.json()
1✔
58
  return { default: result }
1✔
59
})
60

61
// lodash's memoize function memoizes on errors. This is undesirable,
62
// so we have our own custom memoization that won't memoize on errors
63
function getManifestImporter() {
64
  let manifest: ModuleManifest | null = null
50✔
65

66
  async function func() {
67
    if (manifest !== null) {
6✔
68
      return manifest
2✔
69
    }
70

71
    ;({ default: manifest } = await docsImporter(`${MODULES_STATIC_URL}/modules.json`))
4✔
72

73
    return manifest!
3✔
74
  }
75

76
  func.reset = () => {
50✔
77
    manifest = null
2✔
78
  }
79

80
  return func
50✔
81
}
82

83
function getMemoizedDocsImporter() {
84
  const docs = new Map<string, ModuleDocumentation>()
50✔
85

86
  async function func(moduleName: string, throwOnError: true): Promise<ModuleDocumentation>
87
  async function func(moduleName: string, throwOnError?: false): Promise<ModuleDocumentation | null>
88
  async function func(moduleName: string, throwOnError?: boolean) {
89
    if (docs.has(moduleName)) {
9✔
90
      return docs.get(moduleName)!
4✔
91
    }
92

93
    try {
5✔
94
      const { default: loadedDocs } = await docsImporter(
5✔
95
        `${MODULES_STATIC_URL}/jsons/${moduleName}.json`
96
      )
97
      docs.set(moduleName, loadedDocs)
4✔
98
      return loadedDocs
4✔
99
    } catch (error) {
100
      if (throwOnError) throw error
1✔
UNCOV
101
      console.warn(`Failed to load documentation for ${moduleName}:`, error)
×
UNCOV
102
      return null
×
103
    }
104
  }
105

106
  func.cache = docs
50✔
107
  return func
50✔
108
}
109

110
export const memoizedGetModuleManifestAsync = getManifestImporter()
50✔
111
export const memoizedGetModuleDocsAsync = getMemoizedDocsImporter()
50✔
112

113
/*
114
  Browsers natively support esm's import() but Jest and Node do not. So we need
115
  to change which import function we use based on the environment.
116

117
  For the browser, we use the function constructor to hide the import calls from
118
  webpack so that webpack doesn't try to compile them away.
119
*/
120
const bundleAndTabImporter = wrapImporter<{ default: ModuleBundle }>(
50✔
121
  typeof window !== 'undefined' && process.env.NODE_ENV !== 'test'
150!
122
    ? (new Function('path', 'return import(`${path}?q=${Date.now()}`)') as any)
123
    : // eslint-disable-next-line @typescript-eslint/no-require-imports
124
      p => Promise.resolve(require(p))
4✔
125
)
126

127
export async function loadModuleBundleAsync(
50✔
128
  moduleName: string,
129
  context: Context,
130
  node?: Node
131
): Promise<ModuleFunctions> {
132
  const { default: result } = await bundleAndTabImporter(
2✔
133
    `${MODULES_STATIC_URL}/bundles/${moduleName}.js`
134
  )
135
  try {
1✔
136
    const loadedModule = result(getRequireProvider(context))
1✔
137
    return Object.entries(loadedModule).reduce((res, [name, value]) => {
1✔
138
      if (typeof value === 'function') {
2✔
139
        const repr = `function ${name} {\n\t[Function from ${moduleName}\n\tImplementation hidden]\n}`
2✔
140
        value[Symbol.toStringTag] = () => repr
2✔
141
        value.toString = () => repr
2✔
142
      }
143
      return {
2✔
144
        ...res,
145
        [name]: value
146
      }
147
    }, {})
148
  } catch (error) {
149
    throw new ModuleInternalError(moduleName, error, node)
×
150
  }
151
}
152

153
export async function loadModuleTabsAsync(moduleName: string) {
50✔
154
  const manifest = await memoizedGetModuleManifestAsync()
1✔
155
  const moduleInfo = manifest[moduleName]
1✔
156

157
  return Promise.all(
1✔
158
    moduleInfo.tabs.map(async tabName => {
159
      const { default: result } = await bundleAndTabImporter(
2✔
160
        `${MODULES_STATIC_URL}/tabs/${tabName}.js`
161
      )
162
      return result
2✔
163
    })
164
  )
165
}
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