Repository: danvk/literate-ts (ts)

Issue:
Add a verifier directive to prepend a snippet as a module
Similar to #39, but for the case of the separate module contents coming from a previous snippet instead of some fictional file.

In this example, the first snippet is meant to describe an `a.ts` file imported by the `c.ts` file. 

> ```ts
> // a.ts
> export const shared = 'Cher';
> ```
> 
> ```ts
> import { shared } from './a';
> //       ~~~~~~
> // Error: Import declaration conflicts with local declaration of 'shared'.
> 
> export const shared = 'Cher';
> //           ~~~~~~
> // Error: Individual declarations in merged declaration
> // 'shared' must be all exported or all local.
> ```

Could we have some kind of verifier directive that indicates this? Maybe, `include-as-module:ID`?
prepend-to-following doesn't produce expected errors in first snippet
This pair of code snippets is meant to show TypeScript giving an error when in script (rather than module) mode:

> // verifier:prepend-to-following
> ```ts
> // a.ts
> const shared = "Cher";
> //    ~~~~~~
> // Cannot redeclare block-scoped variable 'shared'.
> ```
> 
> ```ts
> // b.ts
> const shared = "Cher";
> //    ~~~~~~
> // Cannot redeclare block-scoped variable 'shared'.
> ```

But, while the first snippet is prepended to the second, the first snippet isn't included in the second. Maybe this is a feature request for a `merge-with-following` verifier?

Pull request:
Implement prepend-as-file directive
Fixes #40 

This lets you test `import` scenarios. Also provides a different way to fix #41.

Patch:
diff --git a/README.md b/README.md
index a2b32d2..3cef109 100644
--- a/README.md
+++ b/README.md
@@ -251,12 +251,20 @@ See above for how to give directive to literate-ts in your source format.
   </dd>
   <dt>verifier:next-is-tsx</dt>
   <dd>Put the next sample in a `.tsx` file, e.g. if it uses JSX syntax.</dd>
+  <dt>verifier:prepend-as-file:filename.ts</dt>
+  <dd>
+    Usually samples are "prepending" by concatenating them. This directive says to prepend the
+    next snippet by writing it to a file instead. This is useful for verifying code involving
+    `import` statements. This is a property of the sample. If it's later prepended with
+    `prepend-id-to-following`, it will still be written to a file, not concatenated.
+  </dd>
   <dt>include-node-module:module-name</dt>
   <dd>
     Make `module-name` available during type checking for subsequent sample
     (until the next `reset`). This module must also be installed in the source file's
     `node_modules` directory. Particularly useful with `@types`, e.g.
     `verifier:include-node-module:@types/lodash`.
+    This will pull in all that module's transitive `dependencies` as well.
   </dd>
   <dt>verifier:done-with-file</dt>
   <dd>
diff --git a/src/code-sample.ts b/src/code-sample.ts
index 64077e1..97af7fe 100644
--- a/src/code-sample.ts
+++ b/src/code-sample.ts
@@ -39,6 +39,7 @@ function process(
   let tsOptions: {[key: string]: string | boolean} = {};
   let nextIsTSX = false;
   let nextShouldCheckJs = false;
+  let targetFilename: string | null = null;
 
   const p: Processor = {
     setLineNum(line) {
@@ -59,6 +60,7 @@ function process(
         nodeModules = [];
         nextIsTSX = false;
         nextShouldCheckJs = false;
+        targetFilename = null;
       } else if (directive === 'prepend-to-following') {
         prependNext = true;
       } else if (directive.startsWith('prepend-subset-to-following:')) {
@@ -92,6 +94,9 @@ function process(
         tsOptions['noEmit'] = true;
       } else if (directive.startsWith('replace-with-id')) {
         nextIsReplaced = directive.split(':', 2)[1];
+      } else if (directive.startsWith('prepend-as-file')) {
+        targetFilename = directive.split(':', 2)[1];
+        prependNext = true;
       } else {
         throw new Error(`Unknown directive: ${directive}`);
       }
@@ -125,9 +130,11 @@ function process(
           checkJS: nextShouldCheckJs,
           sourceFile,
           lineNumber: lineNum,
+          targetFilename,
           tsOptions: {...tsOptions},
           prefixesLength: 0,
           skip: skipNext || skipRemaining,
+          auxiliaryFiles: [],
         });
         if (prependNext) {
           prefixes = prefixes.concat([
@@ -148,6 +155,7 @@ function process(
       nextIsTSX = false;
       nextShouldCheckJs = false;
       nextIsReplaced = null;
+      targetFilename = null;
     },
   };
 
@@ -209,7 +217,8 @@ export function applyPrefixes(samples: PrefixedCodeSample[]): CodeSample[] {
       throw new Error(`Logic error: sample ${sample.id} was not replaced.`);
     }
     const prefixes = sample.id.endsWith('-output') ? [] : sample.prefixes;
-    const combinedPrefixes = prefixes.map(({id, lines}) =>
+    const [auxiliary, prepend] = _.partition(prefixes, ({id}) => !!idToSample[id].targetFilename);
+    const combinedPrefixes = prepend.map(({id, lines}) =>
       sliceLines(idToSample[id].content, lines),
     );
     const content = combinedPrefixes.concat([sample.content]).join('\n');
@@ -224,6 +233,11 @@ export function applyPrefixes(samples: PrefixedCodeSample[]): CodeSample[] {
       checkJS: sample.checkJS,
       sourceFile: sample.sourceFile,
       lineNumber: sample.lineNumber,
+      targetFilename: sample.targetFilename,
+      auxiliaryFiles: auxiliary.map(({id}) => ({
+        filename: idToSample[id].targetFilename!,
+        content: idToSample[id].content,
+      })),
       skip: sample.skip,
       prefixesLength: _.sum(combinedPrefixes.map(p => p.split('\n').length)),
       content,
diff --git a/src/ts-checker.ts b/src/ts-checker.ts
index a3a7a3e..18a3720 100644
--- a/src/ts-checker.ts
+++ b/src/ts-checker.ts
@@ -608,6 +608,10 @@ async function uncachedCheckTs(
   const fileName = id + (sample.isTSX ? '.tsx' : `.${sample.language}`);
   const tsFile = writeTempFile(fileName, content);
   const sampleDir = getTempDir();
+  for (const auxFile of sample.auxiliaryFiles) {
+    const destFile = sampleDir + '/' + auxFile.filename;
+    fs.writeFileSync(destFile, auxFile.content, 'utf-8');
+  }
 
   const options: ts.CompilerOptions = {
     ...config.options,
diff --git a/src/types.ts b/src/types.ts
index 4cbb472..5e8c73f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -19,6 +19,9 @@ export interface CodeSample {
   sourceFile: string;
   lineNumber: number;
   nodeModules: readonly string[];
+  /** If set, write this sample to a file with the given name instead of prepending. */
+  targetFilename: string | null;
+  auxiliaryFiles: AuxiliaryFile[];
   /** Combined length of prefixes, for offsetting error messages */
   prefixesLength: number;
   skip: boolean;
@@ -41,3 +44,8 @@ export interface SampleOutput {
   stderr: string;
   path: string;
 }
+
+export interface AuxiliaryFile {
+  filename: string;
+  content: string;
+}


Test patch:
diff --git a/src/test/__snapshots__/asciidoc.test.ts.snap b/src/test/__snapshots__/asciidoc.test.ts.snap
index dab398f..489b734 100644
--- a/src/test/__snapshots__/asciidoc.test.ts.snap
+++ b/src/test/__snapshots__/asciidoc.test.ts.snap
@@ -208,6 +208,40 @@ END #././src/test/inputs/prepend-and-skip.asciidoc:13 (--- ms)
 }
 `;
 
+exports[`checker asciidoc checker snapshots "./src/test/inputs/prepend-as-file.asciidoc": ./src/test/inputs/prepend-as-file.asciidoc 1`] = `
+{
+  "logs": [
+    "---- BEGIN FILE ./src/test/inputs/prepend-as-file.asciidoc
+",
+    "Found 2 code samples in ./src/test/inputs/prepend-as-file.asciidoc",
+    "BEGIN #././src/test/inputs/prepend-as-file.asciidoc:10 (--filter prepend-as-file-10)
+",
+    "Code passed type checker.",
+    "
+END #././src/test/inputs/prepend-as-file.asciidoc:10 (--- ms)
+",
+    "BEGIN #././src/test/inputs/prepend-as-file.asciidoc:21 (--filter prepend-as-file-21)
+",
+    "matched errors:",
+    "  expected: Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point' Object literal may only specify known properties, and 'z' does not exist in type 'Point'.",
+    "    actual: Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'.
+  Object literal may only specify known properties, and 'z' does not exist in type 'Point'.",
+    "  error messages could not be matched!",
+    "Matched 1/1 errors.",
+    "
+END #././src/test/inputs/prepend-as-file.asciidoc:21 (--- ms)
+",
+    "---- END FILE ./src/test/inputs/prepend-as-file.asciidoc
+",
+  ],
+  "statuses": [
+    "1/1: ././src/test/inputs/prepend-as-file.asciidoc",
+    "1/1: ././src/test/inputs/prepend-as-file.asciidoc: 1/2 ././src/test/inputs/prepend-as-file.asciidoc:10",
+    "1/1: ././src/test/inputs/prepend-as-file.asciidoc: 2/2 ././src/test/inputs/prepend-as-file.asciidoc:21",
+  ],
+}
+`;
+
 exports[`checker asciidoc checker snapshots "./src/test/inputs/president.asciidoc": ./src/test/inputs/president.asciidoc 1`] = `
 {
   "logs": [
@@ -317,6 +351,7 @@ END #././src/test/inputs/program-listing.asciidoc:40 (--- ms)
 exports[`extractSamples snapshot: backticks 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function greet(who: string) {
   console.log('Hello', who);
@@ -333,9 +368,11 @@ exports[`extractSamples snapshot: backticks 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "backticks.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "export const Example = () => <div />;",
     "descriptor": "./backticks.asciidoc:12",
@@ -350,9 +387,11 @@ exports[`extractSamples snapshot: backticks 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "backticks.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "console.log("end of file");",
     "descriptor": "./backticks.asciidoc:27",
@@ -367,6 +406,7 @@ exports[`extractSamples snapshot: backticks 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "backticks.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -377,6 +417,7 @@ exports[`extractSamples snapshot: backticks-disabled 1`] = `[]`;
 exports[`extractSamples snapshot: commented-sample 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function somethingDangerous() {}",
     "descriptor": "./commented-sample.asciidoc:7",
@@ -391,9 +432,11 @@ exports[`extractSamples snapshot: commented-sample 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "let val = null;  // Type is any
 try {
@@ -420,6 +463,7 @@ val  // Type is number | null",
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -428,6 +472,7 @@ val  // Type is number | null",
 exports[`extractSamples snapshot: commented-sample-with-error 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const value: number = "123";",
     "descriptor": "./commented-sample-with-error.asciidoc:7",
@@ -442,9 +487,11 @@ exports[`extractSamples snapshot: commented-sample-with-error 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample-with-error.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "console.log(value);
 //          ^? const value: number",
@@ -464,6 +511,7 @@ exports[`extractSamples snapshot: commented-sample-with-error 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample-with-error.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -472,6 +520,7 @@ exports[`extractSamples snapshot: commented-sample-with-error 1`] = `
 exports[`extractSamples snapshot: doc1 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function greet(who: string) {
   console.log('Hello', who);
@@ -488,6 +537,7 @@ exports[`extractSamples snapshot: doc1 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "doc1.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -496,6 +546,7 @@ exports[`extractSamples snapshot: doc1 1`] = `
 exports[`extractSamples snapshot: empty-twoslash 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "async function fetchANumber(...args: Parameters<typeof fetch>): Promise<number> {
   const response = await fetch(...args);
@@ -519,6 +570,7 @@ fetchANumber
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "empty-twoslash.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -527,6 +579,7 @@ fetchANumber
 exports[`extractSamples snapshot: equivalent 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "interface Point {
   x: number;
@@ -547,9 +600,11 @@ type T = keyof Point;
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type T2 = keyof Point;
 //   ^? type T2 = keyof Point (equivalent to "x" | "y")",
@@ -569,9 +624,11 @@ type T = keyof Point;
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type T2 = keyof Point;
 //   ^? type T2 = keyof Point (equivalent to "x" | "y" | "z")",
@@ -591,9 +648,11 @@ type T = keyof Point;
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type T2 = keyof Point;
 //   ^? type T2 = keyof Point
@@ -614,6 +673,7 @@ type T = keyof Point;
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -622,6 +682,7 @@ type T = keyof Point;
 exports[`extractSamples snapshot: inline-replacement 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function calculateAge(birthDate: Date): number {
   // ...
@@ -638,9 +699,11 @@ exports[`extractSamples snapshot: inline-replacement 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "inline-replacement.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function calculateAge(birthDate: Date): number {
   // COMPRESS
@@ -659,9 +722,11 @@ exports[`extractSamples snapshot: inline-replacement 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "inline-replacement.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function calculateAge(birthDate: string): number {
   // ...
@@ -678,6 +743,7 @@ exports[`extractSamples snapshot: inline-replacement 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "inline-replacement.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -686,6 +752,7 @@ exports[`extractSamples snapshot: inline-replacement 1`] = `
 exports[`extractSamples snapshot: multilinetype 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const o = {x: 1, y: 2};
 // type is {
@@ -704,9 +771,11 @@ exports[`extractSamples snapshot: multilinetype 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "multilinetype.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function addWithExtras(a: number, b: number) {
   const c = a + b;  // type is number
@@ -725,6 +794,7 @@ exports[`extractSamples snapshot: multilinetype 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "multilinetype.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -733,6 +803,7 @@ exports[`extractSamples snapshot: multilinetype 1`] = `
 exports[`extractSamples snapshot: noid 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "console.log('Hello', 'TS');",
     "descriptor": "./noid.asciidoc:10",
@@ -747,6 +818,7 @@ exports[`extractSamples snapshot: noid 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "noid.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -755,6 +827,7 @@ exports[`extractSamples snapshot: noid 1`] = `
 exports[`extractSamples snapshot: prepend 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
     "descriptor": "./prepend.asciidoc:2",
@@ -769,9 +842,11 @@ exports[`extractSamples snapshot: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const a: AB = 'a';",
     "descriptor": "./prepend.asciidoc:8",
@@ -790,9 +865,11 @@ exports[`extractSamples snapshot: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const a: AB = 'a';",
     "descriptor": "./prepend.asciidoc:16",
@@ -807,6 +884,7 @@ exports[`extractSamples snapshot: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -815,6 +893,7 @@ exports[`extractSamples snapshot: prepend 1`] = `
 exports[`extractSamples snapshot: prepend-and-skip 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "// a.ts
 const shared = "Cher";
@@ -832,9 +911,11 @@ const shared = "Cher";
     "sectionHeader": null,
     "skip": true,
     "sourceFile": "prepend-and-skip.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "// b.ts
 const shared = "Cher";
@@ -856,6 +937,65 @@ const shared = "Cher";
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-and-skip.asciidoc",
+    "targetFilename": null,
+    "tsOptions": {},
+  },
+]
+`;
+
+exports[`extractSamples snapshot: prepend-as-file 1`] = `
+[
+  {
+    "auxiliaryFiles": [],
+    "checkJS": false,
+    "content": "// point.ts
+export interface Point {
+  x: number;
+  y: number;
+}",
+    "descriptor": "./prepend-as-file.asciidoc:10",
+    "id": "prepend-as-file-10",
+    "isTSX": false,
+    "language": "ts",
+    "lineNumber": 9,
+    "nodeModules": [],
+    "prefixes": [],
+    "prefixesLength": 0,
+    "replacementId": undefined,
+    "sectionHeader": null,
+    "skip": false,
+    "sourceFile": "prepend-as-file.asciidoc",
+    "targetFilename": "point.ts",
+    "tsOptions": {},
+  },
+  {
+    "auxiliaryFiles": [],
+    "checkJS": false,
+    "content": "import { Point } from './point';
+const pt: Point = {
+    x: 3,
+    y: 4,
+    z: 5,
+//  ~ Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'
+//    Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
+}",
+    "descriptor": "./prepend-as-file.asciidoc:21",
+    "id": "prepend-as-file-21",
+    "isTSX": false,
+    "language": "ts",
+    "lineNumber": 20,
+    "nodeModules": [],
+    "prefixes": [
+      {
+        "id": "prepend-as-file-10",
+      },
+    ],
+    "prefixesLength": 0,
+    "replacementId": undefined,
+    "sectionHeader": null,
+    "skip": false,
+    "sourceFile": "prepend-as-file.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -864,6 +1004,7 @@ const shared = "Cher";
 exports[`extractSamples snapshot: prepend-multiple 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
     "descriptor": "./prepend-multiple.asciidoc:4",
@@ -878,9 +1019,11 @@ exports[`extractSamples snapshot: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type ABC = AB | 'c';",
     "descriptor": "./prepend-multiple.asciidoc:10",
@@ -899,9 +1042,11 @@ exports[`extractSamples snapshot: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const c: ABC = 'c';",
     "descriptor": "./prepend-multiple.asciidoc:15",
@@ -923,6 +1068,7 @@ exports[`extractSamples snapshot: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -931,6 +1077,7 @@ exports[`extractSamples snapshot: prepend-multiple 1`] = `
 exports[`extractSamples snapshot: president 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "declare let hasMiddle: boolean;
 const firstLast = {first: 'Harry', last: 'Truman'};
@@ -952,9 +1099,11 @@ const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "president.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "declare let hasMiddle: boolean;
 const firstLast = {first: 'Harry', last: 'Truman'};
@@ -972,6 +1121,7 @@ const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "president.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -980,6 +1130,7 @@ const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
 exports[`extractSamples snapshot: program-listing 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "<pre data-type="programlisting">&gt; <strong>x = {}</strong>
 {}
@@ -999,9 +1150,11 @@ exports[`extractSamples snapshot: program-listing 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "program-listing.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const place = {name: 'New York', latLng: [41.6868, -74.2692]};
 const loc = place.latLng;",
@@ -1017,9 +1170,11 @@ const loc = place.latLng;",
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "program-listing.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "<pre data-type="programlisting">&gt; <strong>loc[0] = 0;</strong>
 0
@@ -1041,9 +1196,11 @@ const loc = place.latLng;",
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "program-listing.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "<pre data-type="programlisting" data-code-language="javascript">&gt; <strong> RegExp.prototype.monkey = 'Capuchin'</strong>
 "Capuchin"
@@ -1065,6 +1222,7 @@ const loc = place.latLng;",
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "program-listing.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -1073,6 +1231,7 @@ const loc = place.latLng;",
 exports[`extractSamples snapshot: skip 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "keyof A&B = (keyof A) | (keyof B)",
     "descriptor": "./skip.asciidoc:4",
@@ -1087,6 +1246,7 @@ exports[`extractSamples snapshot: skip 1`] = `
     "sectionHeader": null,
     "skip": true,
     "sourceFile": "skip.asciidoc",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
diff --git a/src/test/__snapshots__/markdown.test.ts.snap b/src/test/__snapshots__/markdown.test.ts.snap
index 6968ad5..b4fc826 100644
--- a/src/test/__snapshots__/markdown.test.ts.snap
+++ b/src/test/__snapshots__/markdown.test.ts.snap
@@ -3,6 +3,7 @@
 exports[`markdown should match snapshots: commented-sample 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function somethingDangerous() {}",
     "descriptor": "./commented-sample.md:6",
@@ -17,9 +18,11 @@ exports[`markdown should match snapshots: commented-sample 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "let val = null;  // Type is any
 try {
@@ -46,6 +49,7 @@ val  // Type is number | null",
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "commented-sample.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -54,6 +58,7 @@ val  // Type is number | null",
 exports[`markdown should match snapshots: doc1 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function greet(who: string) {
   console.log('Hello', who);
@@ -70,9 +75,11 @@ exports[`markdown should match snapshots: doc1 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "doc1.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "console.log("end of file");
 ",
@@ -88,6 +95,7 @@ exports[`markdown should match snapshots: doc1 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "doc1.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -96,6 +104,7 @@ exports[`markdown should match snapshots: doc1 1`] = `
 exports[`markdown should match snapshots: multilinetype 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const o = {x: 1, y: 2};
 // type is {
@@ -114,9 +123,11 @@ exports[`markdown should match snapshots: multilinetype 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "multilinetype.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "function addWithExtras(a: number, b: number) {
   const c = a + b;  // type is number
@@ -135,6 +146,7 @@ exports[`markdown should match snapshots: multilinetype 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "multilinetype.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -143,6 +155,7 @@ exports[`markdown should match snapshots: multilinetype 1`] = `
 exports[`markdown should match snapshots: noid 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "console.log('Hello', 'TS');",
     "descriptor": "./noid.md:8",
@@ -157,6 +170,7 @@ exports[`markdown should match snapshots: noid 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "noid.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -165,6 +179,7 @@ exports[`markdown should match snapshots: noid 1`] = `
 exports[`markdown should match snapshots: prepend 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
     "descriptor": "./prepend.md:2",
@@ -179,9 +194,11 @@ exports[`markdown should match snapshots: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const a: AB = 'a';",
     "descriptor": "./prepend.md:7",
@@ -200,9 +217,11 @@ exports[`markdown should match snapshots: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const a: AB = 'a';",
     "descriptor": "./prepend.md:14",
@@ -217,6 +236,7 @@ exports[`markdown should match snapshots: prepend 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -225,6 +245,7 @@ exports[`markdown should match snapshots: prepend 1`] = `
 exports[`markdown should match snapshots: prepend-multiple 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
     "descriptor": "./prepend-multiple.md:3",
@@ -239,9 +260,11 @@ exports[`markdown should match snapshots: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "type ABC = AB | 'c';",
     "descriptor": "./prepend-multiple.md:8",
@@ -260,9 +283,11 @@ exports[`markdown should match snapshots: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "const c: ABC = 'c';",
     "descriptor": "./prepend-multiple.md:12",
@@ -284,6 +309,7 @@ exports[`markdown should match snapshots: prepend-multiple 1`] = `
     "sectionHeader": null,
     "skip": false,
     "sourceFile": "prepend-multiple.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
@@ -292,6 +318,7 @@ exports[`markdown should match snapshots: prepend-multiple 1`] = `
 exports[`markdown should match snapshots: skip 1`] = `
 [
   {
+    "auxiliaryFiles": [],
     "checkJS": false,
     "content": "keyof A&B = (keyof A) | (keyof B)",
     "descriptor": "./skip.md:3",
@@ -306,6 +333,7 @@ exports[`markdown should match snapshots: skip 1`] = `
     "sectionHeader": null,
     "skip": true,
     "sourceFile": "skip.md",
+    "targetFilename": null,
     "tsOptions": {},
   },
 ]
diff --git a/src/test/asciidoc.test.ts b/src/test/asciidoc.test.ts
index 8464ff9..a0893cc 100644
--- a/src/test/asciidoc.test.ts
+++ b/src/test/asciidoc.test.ts
@@ -468,6 +468,7 @@ describe('checker', () => {
     './src/test/inputs/empty-twoslash.asciidoc',
     './src/test/inputs/prepend-and-skip.asciidoc',
     './src/test/inputs/program-listing.asciidoc',
+    './src/test/inputs/prepend-as-file.asciidoc',
   ])(
     'asciidoc checker snapshots %p',
     async inputFile => {
diff --git a/src/test/common.ts b/src/test/common.ts
index 83d36e2..c0184f7 100644
--- a/src/test/common.ts
+++ b/src/test/common.ts
@@ -1,5 +1,7 @@
 // Shared test code
 
+import {CodeSample} from '../types.js';
+
 export const baseSample = {
   tsOptions: {},
   nodeModules: [],
@@ -9,7 +11,9 @@ export const baseSample = {
   sourceFile: 'source.asciidoc',
   prefixesLength: 0,
   skip: false,
-};
+  targetFilename: null,
+  auxiliaryFiles: [],
+} satisfies Partial<CodeSample>;
 
 export const baseExtract = {
   ...baseSample,
diff --git a/src/test/inputs/prepend-as-file.asciidoc b/src/test/inputs/prepend-as-file.asciidoc
new file mode 100644
index 0000000..ce81bee
--- /dev/null
+++ b/src/test/inputs/prepend-as-file.asciidoc
@@ -0,0 +1,30 @@
+See https://github.com/danvk/literate-ts/issues/40
+
+The `verifier:prepend-as-file` directive tells literate-ts to write code samples to a particular destination file (rather than concatenating them), which lets you verify code that involves `import` statements.
+
+This is a file that will be written out as a module:
+
+// verifier:prepend-as-file:point.ts
+[source,ts]
+----
+// point.ts
+export interface Point {
+  x: number;
+  y: number;
+}
+----
+
+You can import this from another snippet:
+
+[source,ts]
+----
+import { Point } from './point';
+const pt: Point = {
+    x: 3,
+    y: 4,
+    z: 5,
+//  ~ Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'
+//    Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
+}
+----
+// verifier:reset