Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ module.exports = {
rules: [
{
test: /\.ya?ml$/,
type: 'json', // Required by Webpack v4
use: 'yaml-loader'
}
]
Expand Down Expand Up @@ -45,6 +44,10 @@ file.hello === 'world'

In addition to all [`yaml` options](https://eemeli.org/yaml/#options), the loader supports the following additional options:

### `asJSON`

If enabled, the loader output is stringified JSON rather than stringified JavaScript. For Webpack v4, you'll need to set the rule to have `type: "json"`. Also useful for chaining with other loaders that expect JSON input.

### `asStream`

If enabled, parses the source file as a stream of YAML documents. With this, the output will always be an array, with entries for each document. If set, `namespace` is ignored.
Expand Down
51 changes: 38 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
var loaderUtils = require('loader-utils')
var YAML = require('yaml')
const loaderUtils = require('loader-utils')
const { stringify } = require('javascript-stringify')
const YAML = require('yaml')

const makeIdIterator = (prefix = 'v', i = 1) => ({ next: () => prefix + i++ })

module.exports = function yamlLoader(src) {
const { asStream, namespace, ...options } = Object.assign(
const { asJSON, asStream, namespace, ...options } = Object.assign(
{ prettyErrors: true },
loaderUtils.getOptions(this)
)

// keep track of repeated object references
const refs = new Map()
const idIter = makeIdIterator()
function addRef(ref, count) {
if (ref && typeof ref === 'object' && count > 1)
refs.set(ref, { id: idIter.next(), seen: false })
}
const stringifyWithRefs = value =>
stringify(value, (value, space, next) => {
const v = refs.get(value)
if (v) {
if (v.seen) return v.id
v.seen = true
}
return next(value)
})

let res
if (asStream) {
const stream = YAML.parseAllDocuments(src, options)
const res = []
res = []
for (const doc of stream) {
for (const warn of doc.warnings) this.emitWarning(warn)
for (const err of doc.errors) throw err
res.push(doc.toJSON())
res.push(doc.toJSON(null, addRef))
}
return JSON.stringify(res)
} else {
const doc = YAML.parseDocument(src, options)
for (const warn of doc.warnings) this.emitWarning(warn)
for (const err of doc.errors) throw err
if (namespace) doc.contents = doc.getIn(namespace.split('.'))
res = doc.toJSON(null, addRef)
}

let res = YAML.parse(src, options)
if (namespace) {
res = namespace.split('.').reduce(function(acc, name) {
return acc[name]
}, res)
}
return JSON.stringify(res)
if (asJSON) return JSON.stringify(res)
let str = ''
for (const [obj, { id }] of refs.entries())
str += `var ${id} = ${stringifyWithRefs(obj)};\n`
str += `export default ${stringifyWithRefs(res)};`
return str
}
19 changes: 12 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
"singleQuote": true
},
"dependencies": {
"javascript-stringify": "^2.0.1",
"loader-utils": "^1.4.0",
"yaml": "^1.8.3"
"yaml": "^1.9.1"
},
"devDependencies": {
"jest": "^25.1.0",
Expand Down
85 changes: 53 additions & 32 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
const loader = require('../')

test('return stringify version of the yaml file', () => {
const res = loader('---\nhello: world')
expect(res).toBe('{"hello":"world"}')
describe('aliased objects', () => {
test('single document', () => {
const ctx = {}
const src = 'foo: &foo [&val foo]\nbar: *foo'
const res = loader.call(ctx, src)
expect(res).toBe("var v1 = ['foo'];\nexport default {foo:v1,bar:v1};")
})
test('document stream', () => {
const ctx = { query: { asStream: true } }
const src = 'foo: &foo [foo]\nbar: *foo\n---\nfoo: &foo [foo]\nbar: *foo'
const res = loader.call(ctx, src)
expect(res).toBe("var v1 = ['foo'];\nvar v2 = ['foo'];\nexport default [{foo:v1,bar:v1},{foo:v2,bar:v2}];")
})
})

test('throw error if there is a parse error', () => {
let msg = null
try {
loader('---\nhello: world\nhello: 2')
} catch (error) {
msg = error.message
}
expect(msg).toMatch(/^Map keys must be unique; "hello" is repeated/)
})
describe('options.asJSON', () => {
test('return stringify version of the yaml file', () => {
const ctx = { query: { asJSON: true } }
const src = '---\nhello: world'
const res = loader.call(ctx, src)
expect(res).toBe('{"hello":"world"}')
})

test('return a part of the yaml', () => {
const ctx = { query: '?namespace=hello' }
const res = loader.call(ctx, '---\nhello:\n world: plop')
expect(res).toBe('{"world":"plop"}')
test('throw error if there is a parse error', () => {
const ctx = { query: { asJSON: true } }
const src = '---\nhello: world\nhello: 2'
expect(() => loader.call(ctx, src)).toThrow(
/^Map keys must be unique; "hello" is repeated/
)
})
})

test('return a sub-part of the yaml', () => {
const ctx = { query: '?namespace=hello.world' }
const res = loader.call(ctx, '---\nhello:\n world: plop')
expect(res).toBe('"plop"')
})
describe('options.namespace', () => {
test('return a part of the yaml', () => {
const ctx = { query: '?namespace=hello' }
const res = loader.call(ctx, '---\nhello:\n world: plop')
expect(res).toBe("export default {world:'plop'};")
})

test('with asStream, parse multiple documents', () => {
const ctx = { query: { asStream: true } }
const src = 'hello: world\n---\nsecond: document\n'
const res = loader.call(ctx, src)
expect(res).toBe('[{"hello":"world"},{"second":"document"}]')
test('return a sub-part of the yaml', () => {
const ctx = { query: '?namespace=hello.world' }
const res = loader.call(ctx, '---\nhello:\n world: plop')
expect(res).toBe("export default 'plop';")
})
})

test('without asStream, fail to parse multiple documents', () => {
const ctx = { query: { asStream: false } }
const src = 'hello: world\n---\nsecond: document\n'
expect(() => loader.call(ctx, src)).toThrow(
/^Source contains multiple documents/
)
describe('options.asStream', () => {
test('with asStream, parse multiple documents', () => {
const ctx = { query: { asStream: true } }
const src = 'hello: world\n---\nsecond: document\n'
const res = loader.call(ctx, src)
expect(res).toBe("export default [{hello:'world'},{second:'document'}];")
})

test('without asStream, fail to parse multiple documents', () => {
const ctx = { query: { asStream: false } }
const src = 'hello: world\n---\nsecond: document\n'
expect(() => loader.call(ctx, src)).toThrow(
/^Source contains multiple documents/
)
})
})