@platformatic/runtime 0.26.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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +13 -0
  3. package/README.md +13 -0
  4. package/fixtures/configs/invalid-entrypoint.json +14 -0
  5. package/fixtures/configs/missing-property.config.json +13 -0
  6. package/fixtures/configs/missing-service-config.json +7 -0
  7. package/fixtures/configs/monorepo-composer.json +19 -0
  8. package/fixtures/configs/monorepo-create-cycle.json +21 -0
  9. package/fixtures/configs/monorepo-no-cycles.json +20 -0
  10. package/fixtures/configs/monorepo.json +20 -0
  11. package/fixtures/configs/no-services.config.json +5 -0
  12. package/fixtures/configs/no-sources.config.json +4 -0
  13. package/fixtures/configs/service-throws-on-start.json +11 -0
  14. package/fixtures/monorepo/composerApp/platformatic.composer.json +39 -0
  15. package/fixtures/monorepo/docs/README.md +1 -0
  16. package/fixtures/monorepo/serviceApp/deps/dep1.js +7 -0
  17. package/fixtures/monorepo/serviceApp/deps/dep2.mjs +2 -0
  18. package/fixtures/monorepo/serviceApp/deps/dep3.mjs +2 -0
  19. package/fixtures/monorepo/serviceApp/deps/dep4.js +1 -0
  20. package/fixtures/monorepo/serviceApp/platformatic.service.json +22 -0
  21. package/fixtures/monorepo/serviceApp/plugin.js +25 -0
  22. package/fixtures/monorepo/serviceApp/with-logger/package.json +5 -0
  23. package/fixtures/monorepo/serviceApp/with-logger/with-logger.cjs +25 -0
  24. package/fixtures/monorepo/serviceApp/with-logger/with-logger.d.ts +38 -0
  25. package/fixtures/monorepo/serviceApp/with-logger/with-logger.openapi.json +22 -0
  26. package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +19 -0
  27. package/fixtures/monorepo/serviceAppWithLogger/plugin.js +8 -0
  28. package/fixtures/monorepo/serviceAppWithMultiplePlugins/platformatic.service.json +26 -0
  29. package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin.js +8 -0
  30. package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin2.mjs +5 -0
  31. package/fixtures/serviceAppThrowsOnStart/platformatic.service.json +13 -0
  32. package/fixtures/serviceAppThrowsOnStart/plugin.js +6 -0
  33. package/help/help.txt +5 -0
  34. package/help/start.txt +5 -0
  35. package/index.d.ts +13 -0
  36. package/index.js +33 -0
  37. package/index.test-d.ts +18 -0
  38. package/lib/app.js +261 -0
  39. package/lib/config.js +209 -0
  40. package/lib/loader.mjs +101 -0
  41. package/lib/schema.js +89 -0
  42. package/lib/start.js +77 -0
  43. package/lib/worker.js +88 -0
  44. package/package.json +52 -0
  45. package/runtime.mjs +43 -0
  46. package/test/app.test.js +245 -0
  47. package/test/cli/helper.mjs +46 -0
  48. package/test/cli/start.test.mjs +61 -0
  49. package/test/cli/validations.test.mjs +52 -0
  50. package/test/cli/watch.test.mjs +131 -0
  51. package/test/config.test.js +58 -0
  52. package/test/schema.test.js +13 -0
  53. package/test/start.test.js +101 -0
  54. package/test/worker.test.js +21 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.20.0/service",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": 0
6
+ },
7
+ "plugins": {
8
+ "hotReload": true,
9
+ "paths": [
10
+ "plugin.js"
11
+ ]
12
+ }
13
+ }
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ /** @param {import('fastify').FastifyInstance} app */
4
+ module.exports = async function (app) {
5
+ throw new Error('boom')
6
+ }
package/help/help.txt ADDED
@@ -0,0 +1,5 @@
1
+ Available commands:
2
+
3
+ * `help` - show this help message.
4
+ * `help <command>` - shows more information about a command.
5
+ * `start` - start the application.
package/help/start.txt ADDED
@@ -0,0 +1,5 @@
1
+ Start the Platformatic Runtime with the following command:
2
+
3
+ ``` bash
4
+ $ platformatic runtime start
5
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { InjectOptions, LightMyRequestResponse } from 'fastify'
2
+
3
+ export type pltRuntimeBuildServer = {
4
+ address: string
5
+ port: number
6
+ restart: () => Promise<void>
7
+ stop: () => Promise<void>
8
+ inject: (opts: InjectOptions | string) => Promise<LightMyRequestResponse>
9
+ }
10
+
11
+ declare module '@platformatic/runtime' {
12
+ export function buildServer(opts: object): Promise<pltRuntimeBuildServer>
13
+ }
package/index.js ADDED
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+ const ConfigManager = require('@platformatic/config')
3
+ const { platformaticRuntime } = require('./lib/config')
4
+ const { start, startWithConfig } = require('./lib/start')
5
+
6
+ async function buildServer (options = {}) {
7
+ if (!options.configManager) {
8
+ // Instantiate a new config manager from the current options.
9
+ const cm = new ConfigManager({
10
+ ...platformaticRuntime.configManagerConfig,
11
+ source: options
12
+ })
13
+ await cm.parseAndValidate()
14
+
15
+ if (typeof options === 'string') {
16
+ options = { configManager: cm }
17
+ } else {
18
+ options.configManager = cm
19
+ }
20
+ }
21
+
22
+ // The transformConfig() function can't be sent between threads.
23
+ delete options.configManager._transformConfig
24
+
25
+ return startWithConfig(options.configManager)
26
+ }
27
+
28
+ module.exports = {
29
+ buildServer,
30
+ platformaticRuntime,
31
+ schema: platformaticRuntime.schema,
32
+ start
33
+ }
@@ -0,0 +1,18 @@
1
+ import { expectError, expectType } from 'tsd';
2
+ import { LightMyRequestResponse } from 'fastify';
3
+ import { pltRuntimeBuildServer } from '.';
4
+
5
+ const server: pltRuntimeBuildServer = {
6
+ address: 'localhost',
7
+ port: 3000,
8
+ restart: async () => {},
9
+ stop: async () => {},
10
+ inject: async () => ({} as LightMyRequestResponse),
11
+ };
12
+
13
+ expectType<pltRuntimeBuildServer>(server);
14
+ expectError<pltRuntimeBuildServer>({...server, address: 42 });
15
+ expectError<pltRuntimeBuildServer>({...server, port: 'WRONG' });
16
+ expectError<pltRuntimeBuildServer>({...server, restart: 'WRONG' });
17
+ expectError<pltRuntimeBuildServer>({...server, stop: 'WRONG' });
18
+ expectError<pltRuntimeBuildServer>({...server, inject: 'WRONG' });
package/lib/app.js ADDED
@@ -0,0 +1,261 @@
1
+ 'use strict'
2
+ const { once } = require('node:events')
3
+ const { dirname } = require('node:path')
4
+ const {
5
+ addLoggerToTheConfig
6
+ } = require('@platformatic/service')
7
+ const {
8
+ buildServer,
9
+ loadConfig
10
+ } = require('@platformatic/start')
11
+ const { FileWatcher } = require('@platformatic/utils')
12
+
13
+ class PlatformaticApp {
14
+ #hotReload
15
+ #loaderPort
16
+ #restarting
17
+ #started
18
+ #originalWatch
19
+
20
+ constructor (appConfig, loaderPort) {
21
+ this.appConfig = appConfig
22
+ this.config = null
23
+ this.#hotReload = false
24
+ this.#loaderPort = loaderPort
25
+ this.#restarting = false
26
+ this.server = null
27
+ this.#started = false
28
+ this.#originalWatch = null
29
+ }
30
+
31
+ async restart (force) {
32
+ if (this.#restarting) {
33
+ return
34
+ }
35
+
36
+ if (!this.#hotReload && !force) {
37
+ return
38
+ }
39
+
40
+ this.#restarting = true
41
+ await this.stop()
42
+
43
+ /* c8 ignore next 4 - tests may not pass in a MessagePort. */
44
+ if (this.#loaderPort) {
45
+ this.#loaderPort.postMessage('plt:clear-cache')
46
+ await once(this.#loaderPort, 'message')
47
+ }
48
+
49
+ await this.start()
50
+ this.#restarting = false
51
+ }
52
+
53
+ async start () {
54
+ if (this.#started) {
55
+ throw new Error('application is already started')
56
+ }
57
+
58
+ await this.#initializeConfig()
59
+ const { configManager } = this.config
60
+ const config = configManager.current
61
+
62
+ this.#originalWatch = config.watch
63
+ config.watch = false
64
+
65
+ try {
66
+ // If this is a restart, have the fastify server restart itself. If this
67
+ // is not a restart, then create a new server.
68
+ if (this.#restarting) {
69
+ await this.server.restart()
70
+ } else {
71
+ this.server = await buildServer({
72
+ ...config,
73
+ configManager
74
+ })
75
+ }
76
+ } catch (err) {
77
+ this.#logAndExit(err)
78
+ }
79
+
80
+ this.server.platformatic.configManager = configManager
81
+ this.server.platformatic.config = config
82
+
83
+ if (config.plugins !== undefined && this.#originalWatch !== false) {
84
+ this.#startFileWatching()
85
+ }
86
+
87
+ this.#started = true
88
+
89
+ if (this.appConfig.entrypoint && !this.#restarting) {
90
+ try {
91
+ await this.server.start()
92
+ /* c8 ignore next 5 */
93
+ } catch (err) {
94
+ this.server.log.error({ err })
95
+ process.exit(1)
96
+ }
97
+ }
98
+ }
99
+
100
+ async stop () {
101
+ if (!this.#started) {
102
+ throw new Error('application has not been started')
103
+ }
104
+
105
+ if (!this.#restarting) {
106
+ await this.server.close()
107
+ }
108
+
109
+ await this.#stopFileWatching()
110
+ this.#started = false
111
+ }
112
+
113
+ async handleProcessLevelEvent ({ msg, signal, err }) {
114
+ if (msg === 'plt:start') {
115
+ await this.start()
116
+ return
117
+ }
118
+
119
+ if (!this.server) {
120
+ return false
121
+ }
122
+
123
+ if (msg === 'plt:restart') {
124
+ await this.restart(true)
125
+ return
126
+ }
127
+
128
+ if (msg === 'plt:stop') {
129
+ await this.stop()
130
+ return
131
+ }
132
+
133
+ if (signal === 'SIGUSR2') {
134
+ this.server.log.info('reloading configuration')
135
+ await this.restart()
136
+ return false
137
+ }
138
+
139
+ if (err) {
140
+ this.server.log.error({
141
+ err: {
142
+ message: err?.message,
143
+ stack: err?.stack
144
+ }
145
+ }, 'exiting')
146
+ } else if (signal) {
147
+ this.server.log.info({ signal }, 'received signal')
148
+ }
149
+
150
+ if (this.#started) {
151
+ await this.stop()
152
+ }
153
+ }
154
+
155
+ async #initializeConfig () {
156
+ const appConfig = this.appConfig
157
+
158
+ this.config = await loadConfig({}, ['-c', appConfig.config], null, {
159
+ watch: true,
160
+ onMissingEnv (key) {
161
+ return appConfig.localServiceEnvVars.get(key)
162
+ }
163
+ })
164
+ const { args, configManager } = this.config
165
+
166
+ if (appConfig._configOverrides instanceof Map) {
167
+ try {
168
+ appConfig._configOverrides.forEach((value, key) => {
169
+ if (typeof key !== 'string') {
170
+ throw new Error('config path must be a string.')
171
+ }
172
+
173
+ const parts = key.split('.')
174
+ let next = configManager.current
175
+ let obj
176
+
177
+ for (let i = 0; next !== undefined && i < parts.length; ++i) {
178
+ obj = next
179
+ next = obj[parts[i]]
180
+ }
181
+
182
+ if (next !== undefined) {
183
+ obj[parts.at(-1)] = value
184
+ }
185
+ })
186
+ } catch (err) {
187
+ configManager.stopWatching()
188
+ throw err
189
+ }
190
+ }
191
+
192
+ // Set the logger if not present (and the config supports it).
193
+ if (configManager.current.server) {
194
+ addLoggerToTheConfig(configManager.current)
195
+ configManager.current.server.logger.name = this.appConfig.id
196
+ }
197
+
198
+ this.#hotReload = args.hotReload && this.appConfig.hotReload
199
+
200
+ if (configManager.current.plugins) {
201
+ if (this.#hotReload) {
202
+ this.#hotReload = configManager.current.plugins.hotReload
203
+ }
204
+
205
+ configManager.current.plugins.hotReload = false
206
+ }
207
+
208
+ configManager.on('update', async (newConfig) => {
209
+ /* c8 ignore next 4 */
210
+ this.server.platformatic.config = newConfig
211
+ this.server.log.debug('config changed')
212
+ this.server.log.trace({ newConfig }, 'new config')
213
+ await this.restart()
214
+ })
215
+
216
+ configManager.on('error', (err) => {
217
+ /* c8 ignore next */
218
+ this.server.log.error({ err }, 'error reloading the configuration')
219
+ })
220
+ }
221
+
222
+ #startFileWatching () {
223
+ const server = this.server
224
+ const { configManager } = server.platformatic
225
+ const fileWatcher = new FileWatcher({
226
+ path: dirname(configManager.fullPath),
227
+ /* c8 ignore next 2 */
228
+ allowToWatch: this.#originalWatch?.allow,
229
+ watchIgnore: this.#originalWatch?.ignore
230
+ })
231
+
232
+ fileWatcher.on('update', async () => {
233
+ this.server.log.debug('files changed')
234
+ this.restart()
235
+ })
236
+
237
+ fileWatcher.startWatching()
238
+ server.log.debug('start watching files')
239
+ server.platformatic.fileWatcher = fileWatcher
240
+ server.platformatic.configManager.startWatching()
241
+ }
242
+
243
+ async #stopFileWatching () {
244
+ const watcher = this.server.platformatic.fileWatcher
245
+
246
+ if (watcher) {
247
+ await watcher.stopWatching()
248
+ this.server.log.debug('stop watching files')
249
+ this.server.platformatic.fileWatcher = undefined
250
+ this.server.platformatic.configManager.stopWatching()
251
+ }
252
+ }
253
+
254
+ #logAndExit (err) {
255
+ this.config?.configManager?.stopWatching()
256
+ console.error(err)
257
+ process.exit(1)
258
+ }
259
+ }
260
+
261
+ module.exports = { PlatformaticApp }
package/lib/config.js ADDED
@@ -0,0 +1,209 @@
1
+ 'use strict'
2
+ const { readFile, readdir } = require('node:fs/promises')
3
+ const { join, resolve: pathResolve } = require('node:path')
4
+ const Topo = require('@hapi/topo')
5
+ const ConfigManager = require('@platformatic/config')
6
+ const { schema } = require('./schema')
7
+
8
+ async function _transformConfig (configManager) {
9
+ const config = configManager.current
10
+ const services = config.services ?? []
11
+
12
+ if (config.autoload) {
13
+ const { path, exclude = [], mappings = {} } = config.autoload
14
+ const entries = await readdir(path, { withFileTypes: true })
15
+
16
+ for (let i = 0; i < entries.length; ++i) {
17
+ const entry = entries[i]
18
+
19
+ if (exclude.includes(entry.name) || !entry.isDirectory()) {
20
+ continue
21
+ }
22
+
23
+ const mapping = mappings[entry.name] ?? {}
24
+ const id = mapping.id ?? entry.name
25
+ const entryPath = join(path, entry.name)
26
+ const configFilename = mapping.config ?? await ConfigManager.findConfigFile(entryPath)
27
+
28
+ if (typeof configFilename !== 'string') {
29
+ throw new Error(`no config file found for service '${id}'`)
30
+ }
31
+
32
+ const config = join(entryPath, configFilename)
33
+
34
+ services.push({ id, config, path: entryPath })
35
+ }
36
+ }
37
+
38
+ configManager.current.allowCycles = !!configManager.current.allowCycles
39
+ configManager.current.serviceMap = new Map()
40
+
41
+ let hasValidEntrypoint = false
42
+
43
+ for (let i = 0; i < services.length; ++i) {
44
+ const service = services[i]
45
+
46
+ service.config = pathResolve(service.path, service.config)
47
+ service.entrypoint = service.id === config.entrypoint
48
+ service.hotReload = !!config.hotReload
49
+ service.dependencies = []
50
+ service.dependents = []
51
+ service.localServiceEnvVars = new Map()
52
+ service.localUrl = `http://${service.id}.plt.local`
53
+
54
+ if (service.entrypoint) {
55
+ hasValidEntrypoint = true
56
+ }
57
+
58
+ configManager.current.serviceMap.set(service.id, service)
59
+ }
60
+
61
+ if (!hasValidEntrypoint) {
62
+ throw new Error(`invalid entrypoint: '${config.entrypoint}' does not exist`)
63
+ }
64
+
65
+ configManager.current.services = services
66
+ await parseClientsAndComposer(configManager)
67
+
68
+ if (!configManager.current.allowCycles) {
69
+ topologicalSort(configManager)
70
+ }
71
+ }
72
+
73
+ async function parseClientsAndComposer (configManager) {
74
+ for (let i = 0; i < configManager.current.services.length; ++i) {
75
+ const service = configManager.current.services[i]
76
+ const cm = new ConfigManager({ source: service.config })
77
+ const configString = await cm.load()
78
+ const parsed = cm._parser(configString)
79
+
80
+ if (Array.isArray(parsed.composer?.services)) {
81
+ for (let i = 0; i < parsed.composer.services.length; ++i) {
82
+ const dep = parsed.composer.services[i]
83
+ /* c8 ignore next 4 - why c8? */
84
+ const clientName = dep.id ?? ''
85
+ const dependency = configManager.current.serviceMap.get(clientName)
86
+
87
+ if (dependency === undefined) {
88
+ /* c8 ignore next 2 */
89
+ throw new Error(`service '${service.id}' has unknown dependency: '${clientName}'`)
90
+ }
91
+
92
+ dependency.dependents.push(service.id)
93
+
94
+ if (dep.origin) {
95
+ try {
96
+ await cm.replaceEnv(dep.origin)
97
+ /* c8 ignore next 4 */
98
+ } catch (err) {
99
+ if (err.name !== 'MissingValueError') {
100
+ throw err
101
+ }
102
+
103
+ if (dep.origin === `{${err.key}}`) {
104
+ service.localServiceEnvVars.set(err.key, `${clientName}.plt.local`)
105
+ }
106
+ }
107
+ }
108
+
109
+ service.dependencies.push({
110
+ id: clientName,
111
+ url: `http://${clientName}.plt.local`,
112
+ local: true
113
+ })
114
+ }
115
+ }
116
+
117
+ if (Array.isArray(parsed.clients)) {
118
+ const promises = parsed.clients.map((client) => {
119
+ // eslint-disable-next-line no-async-promise-executor
120
+ return new Promise(async (resolve, reject) => {
121
+ let clientUrl
122
+ let missingKey
123
+
124
+ try {
125
+ clientUrl = await cm.replaceEnv(client.url)
126
+ /* c8 ignore next 2 - unclear why c8 is unhappy here */
127
+ } catch (err) {
128
+ if (err.name !== 'MissingValueError') {
129
+ /* c8 ignore next 3 */
130
+ reject(err)
131
+ return
132
+ }
133
+
134
+ missingKey = err.key
135
+ }
136
+
137
+ const isLocal = missingKey && client.url === `{${missingKey}}`
138
+ const clientAbsolutePath = pathResolve(service.path, client.path)
139
+ const clientPackageJson = join(clientAbsolutePath, 'package.json')
140
+ const clientMetadata = JSON.parse(await readFile(clientPackageJson, 'utf8'))
141
+ /* c8 ignore next 20 - unclear why c8 is unhappy for nearly 20 lines here */
142
+ const clientName = clientMetadata.name ?? ''
143
+
144
+ if (clientUrl === undefined) {
145
+ // Combine the service name with the client name to avoid collisions
146
+ // if two or more services have a client with the same name pointing
147
+ // to different services.
148
+ clientUrl = isLocal ? `http://${clientName}.plt.local` : client.url
149
+ }
150
+
151
+ service.dependencies.push({
152
+ id: clientName,
153
+ url: clientUrl,
154
+ local: isLocal
155
+ })
156
+
157
+ const dependency = configManager.current.serviceMap.get(clientName)
158
+
159
+ if (dependency === undefined) {
160
+ /* c8 ignore next 3 */
161
+ reject(new Error(`service '${service.id}' has unknown dependency: '${clientName}'`))
162
+ return
163
+ }
164
+
165
+ dependency.dependents.push(service.id)
166
+
167
+ if (isLocal) {
168
+ service.localServiceEnvVars.set(missingKey, clientUrl)
169
+ }
170
+
171
+ resolve()
172
+ })
173
+ })
174
+
175
+ await Promise.all(promises)
176
+ }
177
+ }
178
+ }
179
+
180
+ function topologicalSort (configManager) {
181
+ const { services } = configManager.current
182
+ const topo = new Topo.Sorter()
183
+
184
+ for (let i = 0; i < services.length; ++i) {
185
+ const service = services[i]
186
+ const dependencyIds = service.dependencies.map(dep => dep.id)
187
+
188
+ topo.add(service, { group: service.id, after: dependencyIds, manual: true })
189
+ }
190
+
191
+ configManager.current.services = topo.sort()
192
+ }
193
+
194
+ async function platformaticRuntime () {
195
+ // No-op. Here for consistency with other app types.
196
+ }
197
+
198
+ platformaticRuntime[Symbol.for('skip-override')] = true
199
+ platformaticRuntime.schema = schema
200
+ platformaticRuntime.configType = 'runtime'
201
+ platformaticRuntime.configManagerConfig = {
202
+ schema,
203
+ allowToWatch: ['.env'],
204
+ async transformConfig () {
205
+ await _transformConfig(this)
206
+ }
207
+ }
208
+
209
+ module.exports = { platformaticRuntime }
package/lib/loader.mjs ADDED
@@ -0,0 +1,101 @@
1
+ import { createRequire, isBuiltin } from 'node:module'
2
+ import { dirname, isAbsolute, resolve as pathResolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ const require = createRequire(import.meta.url)
5
+ const thisFile = fileURLToPath(import.meta.url)
6
+ const isWindows = process.platform === 'win32'
7
+ let timestamp = process.hrtime.bigint()
8
+ let port
9
+
10
+ function bustEsmCache () {
11
+ timestamp = process.hrtime.bigint()
12
+ }
13
+
14
+ function clearCjsCache () {
15
+ // This evicts all of the modules from the require() cache.
16
+ // Note: This does not clean up children references to the deleted module.
17
+ // It's likely not a big deal for most cases, but it is a leak. The child
18
+ // references can be cleaned up, but it is expensive and involves walking
19
+ // the entire require() cache. See the DEP0144 documentation for how to do
20
+ // it.
21
+ Object.keys(require.cache).forEach((key) => {
22
+ delete require.cache[key]
23
+ })
24
+ }
25
+
26
+ function isRelativePath (p) {
27
+ // This function is extracted from Node core, so it should work.
28
+ return p.charAt(0) === '.' &&
29
+ /* c8 ignore next 9 */
30
+ (
31
+ p.length === 1 ||
32
+ p.charAt(1) === '/' ||
33
+ (isWindows && p.charAt(1) === '\\') ||
34
+ (p.charAt(1) === '.' && ((
35
+ p.length === 2 ||
36
+ p.charAt(2) === '/') ||
37
+ (isWindows && p.charAt(2) === '\\')))
38
+ )
39
+ }
40
+
41
+ function specifierToPath (specifier, referencingModuleId) {
42
+ // Convert the specifier into an absolute path if possible. If the specifier
43
+ // cannot be converted to a path (for example for a core module), then return
44
+ // null.
45
+ try {
46
+ const url = new URL(specifier)
47
+
48
+ if (url.protocol === 'file:') {
49
+ specifier = url.pathname
50
+ } else {
51
+ return null
52
+ }
53
+ } catch {
54
+ // Ignore error.
55
+ }
56
+
57
+ if (isBuiltin(specifier)) {
58
+ return null
59
+ }
60
+
61
+ if (isAbsolute(specifier)) {
62
+ return specifier
63
+ }
64
+
65
+ /* c8 ignore next 3 */
66
+ if (!referencingModuleId) {
67
+ throw new Error(`cannot map '${specifier}' to an absolute path`)
68
+ }
69
+
70
+ if (isRelativePath(specifier)) {
71
+ return pathResolve(dirname(fileURLToPath(referencingModuleId)), specifier)
72
+ } else {
73
+ // The specifier is something in node_modules/.
74
+ const req = createRequire(referencingModuleId)
75
+
76
+ return req.resolve(specifier)
77
+ }
78
+ }
79
+
80
+ export async function resolve (specifier, context, nextResolve) {
81
+ const path = specifierToPath(specifier, context.parentURL)
82
+
83
+ // If the specifier could not be mapped to a file, or the path is this file,
84
+ // then don't do anything.
85
+ if (typeof path !== 'string' || path === thisFile) {
86
+ return nextResolve(specifier, context)
87
+ }
88
+
89
+ return nextResolve(`${path}?ts=${timestamp}`, context)
90
+ }
91
+
92
+ export function globalPreload (context) {
93
+ port = context.port
94
+ port.on('message', () => {
95
+ bustEsmCache()
96
+ clearCjsCache()
97
+ port.postMessage('plt:cache-cleared')
98
+ })
99
+
100
+ return 'globalThis.LOADER_PORT = port;'
101
+ }