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

source-academy / js-slang / 5406352152

pending completion
5406352152

Pull #1428

github

web-flow
Merge 0380f5ed7 into 8618e26e4
Pull Request #1428: Further Enhancements to the Module System

3611 of 4728 branches covered (76.37%)

Branch coverage included in aggregate %.

831 of 831 new or added lines in 50 files covered. (100.0%)

10852 of 12603 relevant lines covered (86.11%)

93898.15 hits per line

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

27.12
/src/modules/utils.ts
1
import type { ImportDeclaration, Node } from 'estree'
2

3
import type { Context } from '..'
4
import assert from '../utils/assert'
67✔
5
import { getUniqueId } from '../utils/uniqueIds'
67✔
6
import { loadModuleTabs } from './moduleLoader'
67✔
7
import { loadModuleTabsAsync } from './moduleLoaderAsync'
67✔
8

9
/**
10
 * Create the module's context and load its tabs (if `loadTabs` is true)
11
 */
12
export async function initModuleContext(
67✔
13
  moduleName: string,
14
  context: Context,
15
  loadTabs: boolean,
16
  node?: Node
17
) {
18
  if (!(moduleName in context.moduleContexts)) {
1!
19
    context.moduleContexts[moduleName] = {
1✔
20
      state: null,
21
      tabs: loadTabs ? loadModuleTabs(moduleName, node) : null
1!
22
    }
23
  } else if (context.moduleContexts[moduleName].tabs === null && loadTabs) {
×
24
    context.moduleContexts[moduleName].tabs = loadModuleTabs(moduleName, node)
×
25
  }
26
}
27

28
/**
29
 * Create the module's context and load its tabs (if `loadTabs` is true)
30
 */
31
export async function initModuleContextAsync(
67✔
32
  moduleName: string,
33
  context: Context,
34
  loadTabs: boolean,
35
  node?: Node
36
) {
37
  if (!(moduleName in context.moduleContexts)) {
7!
38
    context.moduleContexts[moduleName] = {
7✔
39
      state: null,
40
      tabs: loadTabs ? await loadModuleTabsAsync(moduleName, node) : null
7✔
41
    }
42
  } else if (context.moduleContexts[moduleName].tabs === null && loadTabs) {
×
43
    context.moduleContexts[moduleName].tabs = await loadModuleTabsAsync(moduleName, node)
×
44
  }
45
}
46

47
/**
48
 * Represents a loaded Source module
49
 */
50
export type ModuleInfo<T> = {
51
  /**
52
   * `ImportDeclarations` that import from this module.
53
   */
54
  nodes: ImportDeclaration[]
55

56
  /**
57
   * Represents the loaded module. It can be the module's functions itself (see the ec-evaluator),
58
   * or just the module text (see the transpiler), or any other type.
59
   *
60
   * This field should not be null when the function returns.
61
   */
62
  content: T
63

64
  /**
65
   * The unique name given to this module. If `usedIdentifiers` is not provided, this field will be `null`.
66
   */
67
  namespaced: string | null
68
}
69

70
/**
71
 * Function that converts an `ImportSpecifier` into the given Transformed type.
72
 * It can be used as a `void` returning function as well, in case the specifiers
73
 * don't need to be transformed, just acted upon.
74
 * @example
75
 * ImportSpecifier(specifier, node, info) => {
76
 *  return create.constantDeclaration(
77
 *    spec.local.name,
78
 *    create.memberExpression(
79
 *      create.identifier(info.namespaced),
80
 *      spec.imported.name
81
 *    ),
82
 *  )
83
 * }
84
 */
85
export type SpecifierProcessor<Transformed, Content> = (
86
  spec: ImportDeclaration['specifiers'][0],
87
  moduleInfo: ModuleInfo<Content>,
88
  node: ImportDeclaration
89
) => Transformed
90

91
export type ImportSpecifierType =
92
  | 'ImportSpecifier'
93
  | 'ImportDefaultSpecifier'
94
  | 'ImportNamespaceSpecifier'
95

96
/**
97
 * This function is intended to unify how each of the different Source runners load imports. It handles
98
 * namespacing (if `usedIdentifiers` is provided), loading the module's context (if `context` is not `null`),
99
 * loading the module's tabs (if `loadTabs` is given as `true`) and the conversion
100
 * of import specifiers to the relevant type used by the runner.
101
 * @param nodes Nodes to transform
102
 * @param context Context to transform with, or `null`. Setting this to null prevents module contexts and tabs from being loaded.
103
 * @param loadTabs Set this to false to prevent tabs from being loaded even if a context is provided.
104
 * @param moduleLoader Function that takes the name of the module and returns its loaded representation.
105
 * @param processors Functions for working with each type of import specifier.
106
 * @param usedIdentifiers Set containing identifiers already used in code. If null, namespacing is not conducted.
107
 * @returns The loaded modules, along with the transformed versions of the given nodes
108
 */
109
export async function transformImportNodesAsync<Transformed, LoadedModule>(
67✔
110
  nodes: ImportDeclaration[],
111
  context: Context | null,
112
  loadTabs: boolean,
113
  moduleLoader: (name: string, node?: Node) => Promise<LoadedModule>,
114
  processors: Record<ImportSpecifierType, SpecifierProcessor<Transformed, LoadedModule>>,
115
  usedIdentifiers?: Set<string>
116
) {
117
  const internalLoader = async (name: string, node?: Node) => {
×
118
    // Make sure that module contexts are initialized before
119
    // loading the bundles
120
    if (context) {
×
121
      await initModuleContextAsync(name, context, loadTabs, node)
×
122
    }
123

124
    return moduleLoader(name, node)
×
125
  }
126

127
  const promises: Promise<void>[] = []
×
128
  const moduleInfos = nodes.reduce((res, node) => {
×
129
    const moduleName = node.source.value
×
130
    assert(
×
131
      typeof moduleName === 'string',
132
      `Expected ImportDeclaration to have a source of type string, got ${moduleName}`
133
    )
134

135
    if (!(moduleName in res)) {
×
136
      // First time we are loading this module
137
      res[moduleName] = {
×
138
        nodes: [],
139
        content: null as any,
140
        namespaced: null
141
      }
142
      const loadPromise = internalLoader(moduleName, node).then(content => {
×
143
        res[moduleName].content = content
×
144
      })
145

146
      promises.push(loadPromise)
×
147
    }
148

149
    res[moduleName].nodes.push(node)
×
150

151
    // Collate all the identifiers introduced by specifiers to prevent collisions when
152
    // the import declaration has aliases, e.g import { show as __MODULE__ } from 'rune';
153
    if (usedIdentifiers) {
×
154
      node.specifiers.forEach(spec => usedIdentifiers.add(spec.local.name))
×
155
    }
156
    return res
×
157
  }, {} as Record<string, ModuleInfo<LoadedModule>>)
158

159
  // Wait for all module and symbol loading to finish
160
  await Promise.all(promises)
×
161

162
  return Object.entries(moduleInfos).reduce((res, [moduleName, info]) => {
×
163
    // Now for each module, we give it a unique namespaced id
164
    const namespaced = usedIdentifiers ? getUniqueId(usedIdentifiers, '__MODULE__') : null
×
165
    info.namespaced = namespaced
×
166

167
    assert(info.content !== null, `${moduleName} was not loaded properly. This should never happen`)
×
168

169
    return {
×
170
      ...res,
171
      [moduleName]: {
172
        content: info.nodes.flatMap(node =>
173
          node.specifiers.flatMap(spec => {
×
174
            // Finally, transform that specifier into the form needed
175
            // by the runner
176
            return processors[spec.type](spec, info, node)
×
177
          })
178
        ),
179
        info
180
      }
181
    }
182
  }, {} as Record<string, { info: ModuleInfo<LoadedModule>; content: Transformed[] }>)
183
}
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