@sembl/compiler 0.1.0 → 0.2.1

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.
package/dist/cli/index.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- emitSchemas,
4
- extractSchemas
5
- } from "../chunk-Y4FKH2ZU.js";
6
2
 
7
3
  // src/cli/index.ts
8
4
  import { Command } from "commander";
@@ -10,6 +6,459 @@ import { Command } from "commander";
10
6
  // src/cli/commands/extract.ts
11
7
  import { resolve } from "path";
12
8
  import { glob } from "glob";
9
+
10
+ // src/extractor/ast-extractor.ts
11
+ import { Project, ScriptTarget } from "ts-morph";
12
+
13
+ // src/extractor/decorator-parser.ts
14
+ import {
15
+ Node,
16
+ SyntaxKind
17
+ } from "ts-morph";
18
+ var CONSTRAINT_KEYS = {
19
+ maxLength: "number",
20
+ minLength: "number",
21
+ minimum: "number",
22
+ maximum: "number",
23
+ minItems: "number",
24
+ maxItems: "number",
25
+ pattern: "string"
26
+ };
27
+ function getDecoratorStringArg(decorator) {
28
+ if (!decorator.isDecoratorFactory()) {
29
+ return void 0;
30
+ }
31
+ const args = decorator.getArguments();
32
+ if (args.length === 0) {
33
+ return void 0;
34
+ }
35
+ const arg = args[0];
36
+ const text = arg.getText();
37
+ if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
38
+ return text.slice(1, -1);
39
+ }
40
+ if (text.startsWith("`") && text.endsWith("`")) {
41
+ return text.slice(1, -1);
42
+ }
43
+ return void 0;
44
+ }
45
+ function parseSchemaDecorator(classDecl) {
46
+ const decorator = classDecl.getDecorator("Schema");
47
+ if (!decorator) {
48
+ return void 0;
49
+ }
50
+ return getDecoratorStringArg(decorator);
51
+ }
52
+ function parseDescribeDecorator(propDecl) {
53
+ const decorator = propDecl.getDecorator("Describe");
54
+ if (!decorator) {
55
+ return void 0;
56
+ }
57
+ return getDecoratorStringArg(decorator);
58
+ }
59
+ function readNumberLiteral(node) {
60
+ if (Node.isNumericLiteral(node)) {
61
+ return node.getLiteralValue();
62
+ }
63
+ if (Node.isPrefixUnaryExpression(node)) {
64
+ const operand = node.getOperand();
65
+ if (Node.isNumericLiteral(operand)) {
66
+ const operator = node.getOperatorToken();
67
+ if (operator === SyntaxKind.MinusToken) {
68
+ return -operand.getLiteralValue();
69
+ }
70
+ if (operator === SyntaxKind.PlusToken) {
71
+ return operand.getLiteralValue();
72
+ }
73
+ }
74
+ }
75
+ return void 0;
76
+ }
77
+ function readStringLiteral(node) {
78
+ if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
79
+ return node.getLiteralValue();
80
+ }
81
+ return void 0;
82
+ }
83
+ function readConstraintEntry(property, constraints, scope) {
84
+ if (!Node.isPropertyAssignment(property)) {
85
+ scope.context.warn(
86
+ scope,
87
+ `@Constrain entry \`${property.getText()}\` is not a \`key: value\` pair of compile-time constants and cannot be read from source. Skipping it.`
88
+ );
89
+ return false;
90
+ }
91
+ const nameNode = property.getNameNode();
92
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
93
+ scope.context.warn(
94
+ scope,
95
+ `@Constrain key \`${nameNode.getText()}\` is computed and cannot be read from source. Skipping it.`
96
+ );
97
+ return false;
98
+ }
99
+ const key = nameNode.getText().replace(/^["']|["']$/g, "");
100
+ if (!Object.prototype.hasOwnProperty.call(CONSTRAINT_KEYS, key)) {
101
+ scope.context.warn(
102
+ scope,
103
+ `@Constrain key "${key}" is not a FieldConstraints property. Expected one of: ${Object.keys(CONSTRAINT_KEYS).join(", ")}. Skipping it.`
104
+ );
105
+ return false;
106
+ }
107
+ const expected = CONSTRAINT_KEYS[key];
108
+ const initializer = property.getInitializerOrThrow();
109
+ const value = expected === "number" ? readNumberLiteral(initializer) : readStringLiteral(initializer);
110
+ if (value === void 0) {
111
+ scope.context.warn(
112
+ scope,
113
+ `@Constrain value for "${key}" is \`${initializer.getText()}\`, which is not a ${expected} literal the compiler can read from source. Skipping it.`
114
+ );
115
+ return false;
116
+ }
117
+ constraints[key] = value;
118
+ return true;
119
+ }
120
+ function parseConstrainDecorator(propDecl, scope) {
121
+ const decorator = propDecl.getDecorator("Constrain");
122
+ if (!decorator) {
123
+ return void 0;
124
+ }
125
+ const args = decorator.isDecoratorFactory() ? decorator.getArguments() : [];
126
+ const argument = args[0];
127
+ if (argument === void 0 || !Node.isObjectLiteralExpression(argument)) {
128
+ scope.context.warn(
129
+ scope,
130
+ `@Constrain expects an inline object literal of compile-time constants, but ${argument === void 0 ? "it was called with no argument" : `was given \`${argument.getText()}\``}. Ignoring the decorator.`
131
+ );
132
+ return void 0;
133
+ }
134
+ const constraints = {};
135
+ let accepted = 0;
136
+ for (const property of argument.getProperties()) {
137
+ if (readConstraintEntry(property, constraints, scope)) {
138
+ accepted += 1;
139
+ }
140
+ }
141
+ return accepted > 0 ? constraints : void 0;
142
+ }
143
+ function parseValuesFromDecorator(propDecl, scope) {
144
+ const decorator = propDecl.getDecorator("ValuesFrom");
145
+ if (!decorator) {
146
+ return void 0;
147
+ }
148
+ const sourceId = getDecoratorStringArg(decorator);
149
+ if (sourceId === void 0) {
150
+ scope.context.warn(
151
+ scope,
152
+ `@ValuesFrom expects a string literal naming the enum source, which the caller resolves at coercion time. Ignoring the decorator.`
153
+ );
154
+ return void 0;
155
+ }
156
+ return sourceId;
157
+ }
158
+
159
+ // src/extractor/extraction-context.ts
160
+ import { createHash } from "crypto";
161
+ function createExtractionContext() {
162
+ const warnings = [];
163
+ const synthesizedSchemas = {};
164
+ const nestedReferences = [];
165
+ return {
166
+ warnings,
167
+ synthesizedSchemas,
168
+ warn(scope, message) {
169
+ warnings.push(`${scope.className}.${scope.propertyPath}: ${message}`);
170
+ },
171
+ registerSynthesizedSchema(schema) {
172
+ synthesizedSchemas[schema.id] = schema;
173
+ },
174
+ recordNestedReference(scope, nestedSchemaId, typeText) {
175
+ nestedReferences.push({
176
+ className: scope.className,
177
+ propertyPath: scope.propertyPath,
178
+ nestedSchemaId,
179
+ typeText
180
+ });
181
+ },
182
+ reportUnresolvedNestedSchemas(knownSchemaIds) {
183
+ for (const reference of nestedReferences) {
184
+ if (knownSchemaIds.has(reference.nestedSchemaId)) {
185
+ continue;
186
+ }
187
+ warnings.push(
188
+ `${reference.className}.${reference.propertyPath}: type \`${reference.typeText}\` resolves to nested schema "${reference.nestedSchemaId}", which is not a @Schema-decorated class in this extraction. It will emit as an object with no properties. Decorate it with @Schema if it is yours to change, or use a type the contract supports \u2014 a \`Date\`, for instance, extracts as an ISO-8601 string.`
189
+ );
190
+ }
191
+ }
192
+ };
193
+ }
194
+ function synthesizedSchemaId(scope, structuralSignature) {
195
+ const path = `${scope.className}_${scope.propertyPath}`.replace(
196
+ /[^A-Za-z0-9_]/g,
197
+ "_"
198
+ );
199
+ const digest = createHash("sha256").update(structuralSignature).digest("hex").slice(0, 8);
200
+ return `${path}__${digest}`;
201
+ }
202
+
203
+ // src/extractor/type-resolver.ts
204
+ function reportUnsupported(type, scope, reason) {
205
+ scope.context.warn(
206
+ scope,
207
+ `unsupported type \`${type.getText(scope.node)}\` \u2014 ${reason}. Falling back to string.`
208
+ );
209
+ return { kind: "string" };
210
+ }
211
+ function resolveInlineObjectType(type, scope) {
212
+ const properties = type.getProperties();
213
+ if (properties.length === 0) {
214
+ const isMap = type.getStringIndexType() !== void 0 || type.getNumberIndexType() !== void 0;
215
+ return reportUnsupported(
216
+ type,
217
+ scope,
218
+ isMap ? "a map with an index signature has no FieldType equivalent, so its entries cannot be described to the model; declare a @Schema class with the keys you expect, or take the value as a JSON string and parse it yourself" : "an object type with no properties has nothing to extract"
219
+ );
220
+ }
221
+ const fields = properties.map((property) => {
222
+ const name = property.getName();
223
+ return {
224
+ name,
225
+ // Members of an inline type carry no @Describe, so the owning field's
226
+ // description is the only semantics the model gets for them.
227
+ description: "",
228
+ type: resolveFieldType(property.getTypeAtLocation(scope.node), {
229
+ ...scope,
230
+ propertyPath: `${scope.propertyPath}.${name}`
231
+ }),
232
+ required: !property.isOptional()
233
+ };
234
+ });
235
+ const id = synthesizedSchemaId(
236
+ scope,
237
+ JSON.stringify(fields.map((f) => [f.name, f.required, f.type]))
238
+ );
239
+ scope.context.registerSynthesizedSchema({
240
+ id,
241
+ description: `Inline object type declared at ${scope.className}.${scope.propertyPath}.`,
242
+ fields
243
+ });
244
+ return { kind: "object", nestedSchemaId: id };
245
+ }
246
+ function resolveFieldType(type, scope) {
247
+ if (type.isUnion()) {
248
+ const members = type.getUnionTypes().filter((t) => !t.isUndefined() && !t.isNull());
249
+ if (members.length === 1) {
250
+ return resolveFieldType(members[0], scope);
251
+ }
252
+ if (members.length === 0) {
253
+ return reportUnsupported(type, scope, "there is no value type to extract");
254
+ }
255
+ if (members.every((t) => t.isStringLiteral())) {
256
+ return {
257
+ kind: "enum",
258
+ values: members.map((t) => t.getLiteralValue())
259
+ };
260
+ }
261
+ if (members.every((t) => t.isBoolean() || t.isBooleanLiteral())) {
262
+ return { kind: "boolean" };
263
+ }
264
+ if (members.every((t) => t.isNumber() || t.isNumberLiteral())) {
265
+ return { kind: "number" };
266
+ }
267
+ return reportUnsupported(
268
+ type,
269
+ scope,
270
+ "a union mixing several kinds of value has no single FieldType; split it into separate fields, or narrow it to one kind"
271
+ );
272
+ }
273
+ if (type.isString() || type.isStringLiteral()) {
274
+ return { kind: "string" };
275
+ }
276
+ if (type.isNumber() || type.isNumberLiteral()) {
277
+ return { kind: "number" };
278
+ }
279
+ if (type.isBoolean() || type.isBooleanLiteral()) {
280
+ return { kind: "boolean" };
281
+ }
282
+ if (type.isArray()) {
283
+ const elementType = type.getArrayElementTypeOrThrow();
284
+ return { kind: "array", items: resolveFieldType(elementType, scope) };
285
+ }
286
+ if (type.isEnum()) {
287
+ const members = type.getUnionTypes().map((t) => t.getLiteralValue()).filter((v) => typeof v === "string");
288
+ if (members.length > 0) {
289
+ return { kind: "enum", values: members };
290
+ }
291
+ return reportUnsupported(
292
+ type,
293
+ scope,
294
+ "its members are not string values, so they cannot be offered to the model as an enum"
295
+ );
296
+ }
297
+ if (type.isObject()) {
298
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
299
+ const typeName = symbol?.getName();
300
+ if (typeName && typeName !== "__type" && typeName !== "Object") {
301
+ scope.context.recordNestedReference(
302
+ scope,
303
+ typeName,
304
+ type.getText(scope.node)
305
+ );
306
+ return { kind: "object", nestedSchemaId: typeName };
307
+ }
308
+ return resolveInlineObjectType(type, scope);
309
+ }
310
+ return reportUnsupported(
311
+ type,
312
+ scope,
313
+ "it maps to none of string, number, boolean, array, enum, or a @Schema class"
314
+ );
315
+ }
316
+
317
+ // src/extractor/class-visitor.ts
318
+ function describeKind(type) {
319
+ return type.kind === "array" ? `${describeKind(type.items)}[]` : type.kind;
320
+ }
321
+ function applyValuesFrom(type, sourceId, scope) {
322
+ if (type.kind === "string") {
323
+ return { kind: "dynamicEnum", sourceId };
324
+ }
325
+ if (type.kind === "array" && type.items.kind === "string") {
326
+ return { kind: "array", items: { kind: "dynamicEnum", sourceId } };
327
+ }
328
+ scope.context.warn(
329
+ scope,
330
+ `@ValuesFrom("${sourceId}") applies to a string or string[] field, but this field resolved to ${describeKind(type)}. Leaving the type unchanged.`
331
+ );
332
+ return type;
333
+ }
334
+ function visitClass(classDecl, context) {
335
+ const description = parseSchemaDecorator(classDecl);
336
+ if (description === void 0) {
337
+ return void 0;
338
+ }
339
+ const className = classDecl.getName();
340
+ if (!className) {
341
+ return void 0;
342
+ }
343
+ const fields = [];
344
+ for (const prop of classDecl.getProperties()) {
345
+ const fieldDescription = parseDescribeDecorator(prop);
346
+ if (fieldDescription === void 0) {
347
+ continue;
348
+ }
349
+ const name = prop.getName();
350
+ const isOptional = prop.hasQuestionToken();
351
+ const scope = {
352
+ className,
353
+ propertyPath: name,
354
+ node: prop,
355
+ context
356
+ };
357
+ let type = resolveFieldType(prop.getType(), scope);
358
+ const sourceId = parseValuesFromDecorator(prop, scope);
359
+ if (sourceId !== void 0) {
360
+ type = applyValuesFrom(type, sourceId, scope);
361
+ }
362
+ const constraints = parseConstrainDecorator(prop, scope);
363
+ fields.push({
364
+ name,
365
+ description: fieldDescription,
366
+ type,
367
+ required: !isOptional,
368
+ ...constraints !== void 0 ? { constraints } : {}
369
+ });
370
+ }
371
+ return {
372
+ id: className,
373
+ description,
374
+ fields
375
+ };
376
+ }
377
+
378
+ // src/extractor/ast-extractor.ts
379
+ function extractSchemas(options) {
380
+ const project = new Project({
381
+ tsConfigFilePath: options.tsconfigPath,
382
+ skipAddingFilesFromTsConfig: true,
383
+ compilerOptions: {
384
+ experimentalDecorators: true,
385
+ strict: true,
386
+ // Without a target the default lib is ES5, so anything newer — `Map`,
387
+ // `Set` — resolves to `any` and lands in the unsupported-type warning as
388
+ // "any", naming a type the author never wrote.
389
+ target: ScriptTarget.ES2022
390
+ }
391
+ });
392
+ for (const pattern of options.filePatterns) {
393
+ project.addSourceFilesAtPaths(pattern);
394
+ }
395
+ const context = createExtractionContext();
396
+ const schemas = {};
397
+ for (const sourceFile of project.getSourceFiles()) {
398
+ for (const classDecl of sourceFile.getClasses()) {
399
+ const schema = visitClass(classDecl, context);
400
+ if (schema) {
401
+ schemas[schema.id] = schema;
402
+ }
403
+ }
404
+ }
405
+ for (const [id, schema] of Object.entries(context.synthesizedSchemas)) {
406
+ schemas[id] = schema;
407
+ }
408
+ context.reportUnresolvedNestedSchemas(new Set(Object.keys(schemas)));
409
+ return { schemas, warnings: [...context.warnings] };
410
+ }
411
+
412
+ // src/generator/schema-emitter.ts
413
+ import { mkdirSync, writeFileSync } from "fs";
414
+ import { join } from "path";
415
+ function schemaToSource(schema) {
416
+ const json = JSON.stringify(schema, null, 2);
417
+ return `// Auto-generated by sembl extract \u2014 do not edit
418
+ import type { RuntimeSchema } from "@sembl/core";
419
+
420
+ export const ${schema.id}Schema: RuntimeSchema = ${json};
421
+ `;
422
+ }
423
+ function emitSchemas(bundle, outputDir) {
424
+ mkdirSync(outputDir, { recursive: true });
425
+ const emittedFiles = [];
426
+ const schemaIds = [];
427
+ for (const [id, schema] of Object.entries(bundle.schemas)) {
428
+ const fileName = `${id}.schema.ts`;
429
+ const filePath = join(outputDir, fileName);
430
+ writeFileSync(filePath, schemaToSource(schema), "utf-8");
431
+ emittedFiles.push(filePath);
432
+ schemaIds.push(id);
433
+ }
434
+ const indexLines = [
435
+ "// Auto-generated by sembl extract \u2014 do not edit",
436
+ 'import type { SchemaBundle } from "@sembl/core";',
437
+ ""
438
+ ];
439
+ for (const id of schemaIds) {
440
+ indexLines.push(`import { ${id}Schema } from "./${id}.schema.js";`);
441
+ }
442
+ indexLines.push("");
443
+ indexLines.push("export const bundle: SchemaBundle = {");
444
+ indexLines.push(" schemas: {");
445
+ for (const id of schemaIds) {
446
+ indexLines.push(` ${id}: ${id}Schema,`);
447
+ }
448
+ indexLines.push(" },");
449
+ indexLines.push("};");
450
+ indexLines.push("");
451
+ for (const id of schemaIds) {
452
+ indexLines.push(`export { ${id}Schema } from "./${id}.schema.js";`);
453
+ }
454
+ indexLines.push("");
455
+ const indexPath = join(outputDir, "index.ts");
456
+ writeFileSync(indexPath, indexLines.join("\n"), "utf-8");
457
+ emittedFiles.push(indexPath);
458
+ return emittedFiles;
459
+ }
460
+
461
+ // src/cli/commands/extract.ts
13
462
  async function extractCommand(options) {
14
463
  const inputDir = resolve(options.input);
15
464
  const outputDir = resolve(options.output);
@@ -60,9 +509,83 @@ async function extractCommand(options) {
60
509
  };
61
510
  }
62
511
 
512
+ // src/cli/commands/eval.ts
513
+ import { pathToFileURL } from "url";
514
+ import { resolve as resolve2, join as join2 } from "path";
515
+ import {
516
+ runEval,
517
+ loadFixtures,
518
+ loadReport,
519
+ saveReport,
520
+ diffReports,
521
+ formatReport,
522
+ replayOrRecord
523
+ } from "@sembl/testing";
524
+ async function loadConfig(path) {
525
+ const url = pathToFileURL(resolve2(path)).href;
526
+ const mod = await import(url);
527
+ const config = mod.default ?? mod;
528
+ if (!config.schema || typeof config.schema !== "object" || !("id" in config.schema)) {
529
+ throw new Error(`${path} must export a \`schema\` (a RuntimeSchema or defineSchema result)`);
530
+ }
531
+ if (!config.provider || typeof config.provider.complete !== "function") {
532
+ throw new Error(`${path} must export a \`provider\``);
533
+ }
534
+ return config;
535
+ }
536
+ async function evalCommand(options) {
537
+ let config;
538
+ let fixtures;
539
+ try {
540
+ config = await loadConfig(options.config);
541
+ fixtures = loadFixtures(options.fixtures);
542
+ } catch (error) {
543
+ console.error(`sembl eval: ${error instanceof Error ? error.message : String(error)}`);
544
+ return { exitCode: 1 };
545
+ }
546
+ if (fixtures.length === 0) {
547
+ console.error(`sembl eval: no fixtures found in ${resolve2(options.fixtures)}`);
548
+ return { exitCode: 1 };
549
+ }
550
+ const provider = options.replay ? replayOrRecord(resolve2(options.replay), config.provider) : config.provider;
551
+ const out = options.out ?? join2(resolve2(options.fixtures), ".sembl-eval", "last-run.json");
552
+ const previous = loadReport(out);
553
+ const report = await runEval({
554
+ ...config.coerceOptions,
555
+ schema: config.schema,
556
+ bundle: config.bundle,
557
+ enumResolver: config.enumResolver,
558
+ prices: config.prices,
559
+ provider,
560
+ fixtures,
561
+ mode: options.mode,
562
+ provenance: options.provenance,
563
+ concurrency: options.concurrency
564
+ });
565
+ const diff = previous ? diffReports(previous, report) : void 0;
566
+ console.log(formatReport(report, diff));
567
+ saveReport(report, out);
568
+ console.log(`
569
+ Report written to ${out}${previous ? " (deltas are against the previous run)" : ""}`);
570
+ const below = (value, floor) => floor !== void 0 && (value === null || value < floor);
571
+ let exitCode = 0;
572
+ if (below(report.totals.recall, options.minRecall)) {
573
+ console.error(`sembl eval: recall ${fmt(report.totals.recall)} is below --min-recall ${options.minRecall}`);
574
+ exitCode = 1;
575
+ }
576
+ if (below(report.totals.precision, options.minPrecision)) {
577
+ console.error(`sembl eval: precision ${fmt(report.totals.precision)} is below --min-precision ${options.minPrecision}`);
578
+ exitCode = 1;
579
+ }
580
+ return { report, diff, exitCode };
581
+ }
582
+ function fmt(value) {
583
+ return value === null ? "n/a" : value.toFixed(2);
584
+ }
585
+
63
586
  // src/cli/index.ts
64
587
  var program = new Command();
65
- program.name("sembl").description("SEMBL schema compiler \u2014 extract runtime schemas from decorated TypeScript classes").version("0.1.0");
588
+ program.name("sembl").description("SEMBL schema compiler \u2014 extract runtime schemas from decorated TypeScript classes").version("0.2.1");
66
589
  program.command("extract").description("Extract @Schema-decorated classes into RuntimeSchema files").requiredOption("-i, --input <path>", "Input directory containing decorated schema classes").requiredOption("-o, --output <path>", "Output directory for generated .schema.ts files").option("--tsconfig <path>", "Path to tsconfig.json").option("--strict", "Exit non-zero if extraction produced any warnings").action(async (options) => {
67
590
  const result = await extractCommand({
68
591
  input: options.input,
@@ -72,5 +595,20 @@ program.command("extract").description("Extract @Schema-decorated classes into R
72
595
  });
73
596
  process.exitCode = result.exitCode;
74
597
  });
598
+ program.command("eval").description("Run fixtures through a schema and report per-field precision and recall").requiredOption("-c, --config <path>", "JS module exporting { schema, provider, \u2026 }").requiredOption("-f, --fixtures <dir>", "Directory of fixture JSON files").option("-o, --out <file>", "Where to write the report (default: <fixtures>/.sembl-eval/last-run.json)").option("--mode <mode>", "coerce or partialCoerce", "coerce").option("--provenance", "Ask for provenance and show confidence on mismatches").option("--concurrency <n>", "Fixtures to run at once", "1").option("--replay <dir>", "Replay recordings from this directory; record misses through the provider").option("--min-recall <fraction>", "Fail when overall recall is below this").option("--min-precision <fraction>", "Fail when overall precision is below this").action(async (options) => {
599
+ const mode = options.mode === "partialCoerce" ? "partialCoerce" : "coerce";
600
+ const result = await evalCommand({
601
+ config: options.config,
602
+ fixtures: options.fixtures,
603
+ out: options.out,
604
+ mode,
605
+ provenance: options.provenance,
606
+ concurrency: Number(options.concurrency),
607
+ replay: options.replay,
608
+ minRecall: options.minRecall !== void 0 ? Number(options.minRecall) : void 0,
609
+ minPrecision: options.minPrecision !== void 0 ? Number(options.minPrecision) : void 0
610
+ });
611
+ process.exitCode = result.exitCode;
612
+ });
75
613
  program.parse();
76
614
  //# sourceMappingURL=index.js.map