Skip to content

Commit 89629cf

Browse files
committed
Add ESM build
1 parent 76d2e3c commit 89629cf

File tree

5 files changed

+413
-1
lines changed

5 files changed

+413
-1
lines changed

.npmignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.gitattributes
2+
.github
3+
scripts
4+
test
5+
**/*.test-d.ts

index.mjs

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
'use strict'
2+
3+
import { dequal as deepEqual } from 'dequal'
4+
5+
const jsonSchemaRefSymbol = Symbol.for('json-schema-ref')
6+
7+
class RefResolver {
8+
#schemas
9+
#derefSchemas
10+
#insertRefSymbol
11+
#allowEqualDuplicates
12+
#cloneSchemaWithoutRefs
13+
14+
constructor (opts = {}) {
15+
this.#schemas = {}
16+
this.#derefSchemas = {}
17+
this.#insertRefSymbol = opts.insertRefSymbol ?? false
18+
this.#allowEqualDuplicates = opts.allowEqualDuplicates ?? true
19+
this.#cloneSchemaWithoutRefs = opts.cloneSchemaWithoutRefs ?? false
20+
}
21+
22+
addSchema (schema, rootSchemaId, isRootSchema = true) {
23+
if (isRootSchema) {
24+
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
25+
// Schema has an $id that is not an anchor
26+
rootSchemaId = schema.$id
27+
} else {
28+
// Schema has no $id or $id is an anchor
29+
this.#insertSchemaBySchemaId(schema, rootSchemaId)
30+
}
31+
}
32+
33+
const schemaId = schema.$id
34+
if (schemaId !== undefined && typeof schemaId === 'string') {
35+
if (schemaId.charAt(0) === '#') {
36+
this.#insertSchemaByAnchor(schema, rootSchemaId, schemaId)
37+
} else {
38+
this.#insertSchemaBySchemaId(schema, schemaId)
39+
rootSchemaId = schemaId
40+
}
41+
}
42+
43+
const ref = schema.$ref
44+
if (ref !== undefined && typeof ref === 'string') {
45+
const { refSchemaId, refJsonPointer } = this.#parseSchemaRef(ref, rootSchemaId)
46+
this.#schemas[rootSchemaId].refs.push({
47+
schemaId: refSchemaId,
48+
jsonPointer: refJsonPointer
49+
})
50+
}
51+
52+
for (const key in schema) {
53+
if (typeof schema[key] === 'object' && schema[key] !== null) {
54+
this.addSchema(schema[key], rootSchemaId, false)
55+
}
56+
}
57+
}
58+
59+
getSchema (schemaId, jsonPointer = '#') {
60+
const schema = this.#schemas[schemaId]
61+
if (schema === undefined) {
62+
throw new Error(
63+
`Cannot resolve ref "${schemaId}${jsonPointer}". Schema with id "${schemaId}" is not found.`
64+
)
65+
}
66+
if (schema.anchors[jsonPointer] !== undefined) {
67+
return schema.anchors[jsonPointer]
68+
}
69+
return getDataByJSONPointer(schema.schema, jsonPointer)
70+
}
71+
72+
hasSchema (schemaId) {
73+
return this.#schemas[schemaId] !== undefined
74+
}
75+
76+
getSchemaRefs (schemaId) {
77+
const schema = this.#schemas[schemaId]
78+
if (schema === undefined) {
79+
throw new Error(`Schema with id "${schemaId}" is not found.`)
80+
}
81+
return schema.refs
82+
}
83+
84+
getSchemaDependencies (schemaId, dependencies = {}) {
85+
const schema = this.#schemas[schemaId]
86+
87+
for (const ref of schema.refs) {
88+
const dependencySchemaId = ref.schemaId
89+
if (
90+
dependencySchemaId === schemaId ||
91+
dependencies[dependencySchemaId] !== undefined
92+
) continue
93+
94+
dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)
95+
this.getSchemaDependencies(dependencySchemaId, dependencies)
96+
}
97+
98+
return dependencies
99+
}
100+
101+
derefSchema (schemaId) {
102+
if (this.#derefSchemas[schemaId] !== undefined) return
103+
104+
const schema = this.#schemas[schemaId]
105+
if (schema === undefined) {
106+
throw new Error(`Schema with id "${schemaId}" is not found.`)
107+
}
108+
109+
if (!this.#cloneSchemaWithoutRefs && schema.refs.length === 0) {
110+
this.#derefSchemas[schemaId] = {
111+
schema: schema.schema,
112+
anchors: schema.anchors
113+
}
114+
}
115+
116+
const refs = []
117+
this.#addDerefSchema(schema.schema, schemaId, true, refs)
118+
119+
const dependencies = this.getSchemaDependencies(schemaId)
120+
for (const schemaId in dependencies) {
121+
const schema = dependencies[schemaId]
122+
this.#addDerefSchema(schema, schemaId, true, refs)
123+
}
124+
125+
for (const ref of refs) {
126+
const {
127+
refSchemaId,
128+
refJsonPointer
129+
} = this.#parseSchemaRef(ref.ref, ref.sourceSchemaId)
130+
131+
const targetSchema = this.getDerefSchema(refSchemaId, refJsonPointer)
132+
if (targetSchema === null) {
133+
throw new Error(
134+
`Cannot resolve ref "${ref.ref}". Ref "${refJsonPointer}" is not found in schema "${refSchemaId}".`
135+
)
136+
}
137+
138+
ref.targetSchema = targetSchema
139+
ref.targetSchemaId = refSchemaId
140+
}
141+
142+
for (const ref of refs) {
143+
this.#resolveRef(ref, refs)
144+
}
145+
}
146+
147+
getDerefSchema (schemaId, jsonPointer = '#') {
148+
let derefSchema = this.#derefSchemas[schemaId]
149+
if (derefSchema === undefined) {
150+
this.derefSchema(schemaId)
151+
derefSchema = this.#derefSchemas[schemaId]
152+
}
153+
if (derefSchema.anchors[jsonPointer] !== undefined) {
154+
return derefSchema.anchors[jsonPointer]
155+
}
156+
return getDataByJSONPointer(derefSchema.schema, jsonPointer)
157+
}
158+
159+
#parseSchemaRef (ref, schemaId) {
160+
const sharpIndex = ref.indexOf('#')
161+
if (sharpIndex === -1) {
162+
return { refSchemaId: ref, refJsonPointer: '#' }
163+
}
164+
if (sharpIndex === 0) {
165+
return { refSchemaId: schemaId, refJsonPointer: ref }
166+
}
167+
return {
168+
refSchemaId: ref.slice(0, sharpIndex),
169+
refJsonPointer: ref.slice(sharpIndex)
170+
}
171+
}
172+
173+
#addDerefSchema (schema, rootSchemaId, isRootSchema, refs = []) {
174+
const derefSchema = Array.isArray(schema) ? [...schema] : { ...schema }
175+
176+
if (isRootSchema) {
177+
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
178+
// Schema has an $id that is not an anchor
179+
rootSchemaId = schema.$id
180+
} else {
181+
// Schema has no $id or $id is an anchor
182+
this.#insertDerefSchemaBySchemaId(derefSchema, rootSchemaId)
183+
}
184+
}
185+
186+
const schemaId = derefSchema.$id
187+
if (schemaId !== undefined && typeof schemaId === 'string') {
188+
if (schemaId.charAt(0) === '#') {
189+
this.#insertDerefSchemaByAnchor(derefSchema, rootSchemaId, schemaId)
190+
} else {
191+
this.#insertDerefSchemaBySchemaId(derefSchema, schemaId)
192+
rootSchemaId = schemaId
193+
}
194+
}
195+
196+
if (derefSchema.$ref !== undefined) {
197+
refs.push({
198+
ref: derefSchema.$ref,
199+
sourceSchemaId: rootSchemaId,
200+
sourceSchema: derefSchema
201+
})
202+
}
203+
204+
for (const key in derefSchema) {
205+
const value = derefSchema[key]
206+
if (typeof value === 'object' && value !== null) {
207+
derefSchema[key] = this.#addDerefSchema(value, rootSchemaId, false, refs)
208+
}
209+
}
210+
211+
return derefSchema
212+
}
213+
214+
#resolveRef (ref, refs) {
215+
const { sourceSchema, targetSchema } = ref
216+
217+
if (!sourceSchema.$ref) return
218+
if (this.#insertRefSymbol) {
219+
sourceSchema[jsonSchemaRefSymbol] = sourceSchema.$ref
220+
}
221+
222+
delete sourceSchema.$ref
223+
224+
if (targetSchema.$ref) {
225+
const targetSchemaRef = refs.find(ref => ref.sourceSchema === targetSchema)
226+
this.#resolveRef(targetSchemaRef, refs)
227+
}
228+
for (const key in targetSchema) {
229+
if (key === '$id') continue
230+
if (sourceSchema[key] !== undefined) {
231+
if (deepEqual(sourceSchema[key], targetSchema[key])) continue
232+
throw new Error(
233+
`Cannot resolve ref "${ref.ref}". Property "${key}" is already exist in schema "${ref.sourceSchemaId}".`
234+
)
235+
}
236+
sourceSchema[key] = targetSchema[key]
237+
}
238+
ref.isResolved = true
239+
}
240+
241+
#insertSchemaBySchemaId (schema, schemaId) {
242+
const foundSchema = this.#schemas[schemaId]
243+
if (foundSchema !== undefined) {
244+
if (this.#allowEqualDuplicates && deepEqual(schema, foundSchema.schema)) return
245+
throw new Error(`There is already another schema with id "${schemaId}".`)
246+
}
247+
this.#schemas[schemaId] = { schema, anchors: {}, refs: [] }
248+
}
249+
250+
#insertSchemaByAnchor (schema, schemaId, anchor) {
251+
const { anchors } = this.#schemas[schemaId]
252+
if (anchors[anchor] !== undefined) {
253+
throw new Error(`There is already another anchor "${anchor}" in a schema "${schemaId}".`)
254+
}
255+
anchors[anchor] = schema
256+
}
257+
258+
#insertDerefSchemaBySchemaId (schema, schemaId) {
259+
const foundSchema = this.#derefSchemas[schemaId]
260+
if (foundSchema !== undefined) return
261+
262+
this.#derefSchemas[schemaId] = { schema, anchors: {} }
263+
}
264+
265+
#insertDerefSchemaByAnchor (schema, schemaId, anchor) {
266+
const { anchors } = this.#derefSchemas[schemaId]
267+
anchors[anchor] = schema
268+
}
269+
}
270+
271+
function getDataByJSONPointer (data, jsonPointer) {
272+
const parts = jsonPointer.split('/')
273+
let current = data
274+
for (const part of parts) {
275+
if (part === '' || part === '#') continue
276+
if (typeof current !== 'object' || current === null) {
277+
return null
278+
}
279+
current = current[part]
280+
}
281+
return current ?? null
282+
}
283+
284+
export { RefResolver }

package.json

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,31 @@
55
"main": "index.js",
66
"type": "commonjs",
77
"types": "types/index.d.ts",
8+
"exports": {
9+
".": {
10+
"import": {
11+
"default": "./index.mjs",
12+
"types": "./types/index.d.mts"
13+
},
14+
"default": {
15+
"default": "./index.js",
16+
"types": "./types/index.d.ts"
17+
}
18+
},
19+
"./*": "./*"
20+
},
821
"scripts": {
922
"lint": "eslint",
1023
"lint:fix": "eslint --fix",
1124
"test:unit": "c8 --100 node --test",
1225
"test:typescript": "tsd",
13-
"test": "npm run lint && npm run test:unit && npm run test:typescript"
26+
"test": "npm run lint && npm run test:unit && npm run test:typescript",
27+
"build": "node ./scripts/generate-esm.mjs",
28+
"version": "npm run build"
1429
},
1530
"precommit": [
1631
"lint",
32+
"build",
1733
"test"
1834
],
1935
"repository": {

scripts/generate-esm.mjs

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import fs from 'node:fs'
2+
3+
const CWD = new URL('../', import.meta.url)
4+
5+
// list of all inputs/outputs, with replacements
6+
const SOURCE_FILES = [
7+
{
8+
source: './index.js',
9+
types: './types/index.d.ts',
10+
replacements: [
11+
[/const\s+([^=]+)=\s*require\(([^)]+)\)/g, (_, spec, moduleName) => {
12+
return `import ${spec.trim().replace(/:\s*/g, ' as ')} from ${moduleName.trim()}`
13+
}],
14+
[/module\.exports\s*=\s*({[^}]+})/, 'export $1'],
15+
]
16+
}
17+
]
18+
19+
// Build script
20+
for (const { source, types, replacements } of SOURCE_FILES) {
21+
// replace
22+
let output = fs.readFileSync(new URL(source, CWD), 'utf8')
23+
for (const [search, replaceValue] of replacements) {
24+
output = output.replace(search, replaceValue)
25+
}
26+
27+
// verify
28+
if (output.includes('require(')) {
29+
throw new Error('Could not convert all require() statements')
30+
}
31+
if (output.includes('module.exports')) {
32+
throw new Error('Could not convert module.exports statement')
33+
}
34+
35+
// write source
36+
fs.writeFileSync(new URL(source.replace(/\.js$/, '.mjs'), CWD), output)
37+
38+
// write types
39+
fs.copyFileSync(new URL(types, CWD), new URL(types.replace(/\.d\.ts$/, '.d.mts'), CWD))
40+
}

0 commit comments

Comments
 (0)