@rohal12/twee-ts 1.17.0 → 1.18.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.
@@ -1,4 +1,4 @@
1
- import { t as CompileOptions } from "../types-dR5vTVu2.js";
1
+ import { t as CompileOptions } from "../types-D8Xfe0l8.js";
2
2
  //#region src/plugins/diagnostics.d.ts
3
3
  /** An error carrying the file and line the bundler reports (and Vite shows in its overlay). */
4
4
  interface LocatedError extends Error {
@@ -10,6 +10,13 @@ interface LocatedError extends Error {
10
10
  };
11
11
  }
12
12
  //#endregion
13
+ //#region src/plugins/paths.d.ts
14
+ /** The output settings that say where a bundle is written, as Rollup and Vite pass them to generateBundle. */
15
+ interface OutputLocation {
16
+ readonly dir?: string | undefined;
17
+ readonly file?: string | undefined;
18
+ }
19
+ //#endregion
13
20
  //#region src/plugins/rollup.d.ts
14
21
  export interface TweeTsRollupPluginOptions {
15
22
  /** Source directories/files to compile. */
@@ -23,12 +30,14 @@ export interface TweeTsRollupPluginOptions {
23
30
  }
24
31
  export declare function tweeTsPlugin(options: TweeTsRollupPluginOptions): {
25
32
  name: string;
33
+ options(inputOptions: object): undefined;
26
34
  buildStart(this: {
27
35
  addWatchFile: (id: string) => void;
28
36
  meta: {
29
37
  watchMode: boolean;
30
38
  };
31
39
  }): void;
40
+ renderStart(outputOptions: OutputLocation): void;
32
41
  generateBundle(this: {
33
42
  emitFile: (opts: {
34
43
  type: 'asset';
@@ -37,6 +46,6 @@ export declare function tweeTsPlugin(options: TweeTsRollupPluginOptions): {
37
46
  }) => void;
38
47
  error: (error: LocatedError) => never;
39
48
  warn: (message: string) => void;
40
- }): Promise<void>;
49
+ }, outputOptions: OutputLocation, bundle: Readonly<Record<string, unknown>>): Promise<void>;
41
50
  };
42
51
  //#endregion
@@ -1,33 +1,87 @@
1
- import { i as compile, n as formatDiagnostic, r as splitDiagnostics, t as fatalError } from "../diagnostics-DZNS7W41.js";
2
- import { resolve } from "node:path";
1
+ import { c as splitDiagnostics, f as outputPaths, i as outputLocations, l as compileForOutputFile, m as walkedEntry, o as fatalError, p as realPathOf, s as formatDiagnostic, t as createOutputRecord } from "../paths-DgO9oo6G.js";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
3
4
  //#region src/plugins/rollup.ts
4
5
  /**
5
6
  * Ready-made Rollup plugin for twee-ts.
6
7
  * Compiles .tw files and emits HTML as an asset. Compile errors fail the build
7
8
  * and emit nothing; warnings go through Rollup's warnings.
8
9
  */
10
+ /**
11
+ * What `rollup --watch` watches for the input at `path`, whose real path is
12
+ * `real`. `isRoot`: the input itself, which is followed wherever it leads.
13
+ *
14
+ * Rollup watches a folder recursively and follows the links in it. So a folder
15
+ * is registered whole only when nothing under it is left out of the story: no
16
+ * file or folder the build writes (`outputs`), and no link to a folder, which
17
+ * source discovery doesn't follow and which may lead to the output. Any other
18
+ * folder is replaced by what it contains, minus those, so writing the outputs
19
+ * starts no build. A file added straight to such a folder is found at the next
20
+ * build. A path the build writes to is never registered itself: Rollup fails a
21
+ * watch build whose watched files include its `output.dir` or `output.file`.
22
+ */
23
+ function watchTargets(path, real, outputs, isRoot) {
24
+ if (outputs.isFile(real) || !isRoot && outputs.isDir(real)) return [];
25
+ let entry;
26
+ try {
27
+ entry = isRoot ? {
28
+ stat: statSync(path),
29
+ real
30
+ } : walkedEntry(path, real);
31
+ } catch {
32
+ return isRoot && !outputs.isDir(real) ? [path] : [];
33
+ }
34
+ if (entry === void 0) return [];
35
+ if (outputs.isFile(entry.real)) return [];
36
+ if (!entry.stat.isDirectory()) return [path];
37
+ let names;
38
+ try {
39
+ names = readdirSync(path);
40
+ } catch {
41
+ return isRoot && !outputs.isDir(real) ? [path] : [];
42
+ }
43
+ const children = names.map((name) => {
44
+ const child = join(path, name);
45
+ return {
46
+ path: child,
47
+ targets: watchTargets(child, join(entry.real, name), outputs, false)
48
+ };
49
+ });
50
+ return !outputs.holds(entry.real) && children.every(({ path: child, targets }) => targets.length === 1 && targets[0] === child) ? [path] : children.flatMap(({ targets }) => targets);
51
+ }
9
52
  function tweeTsPlugin(options) {
10
53
  const outputFilename = options.outputFilename ?? "index.html";
54
+ const record = createOutputRecord(outputFilename);
11
55
  return {
12
56
  name: "twee-ts",
57
+ options(inputOptions) {
58
+ const { output } = inputOptions;
59
+ for (const location of outputLocations(output)) record.addLocation(location);
60
+ },
13
61
  buildStart() {
14
62
  if (!this.meta.watchMode) return;
63
+ const outputs = outputPaths(record.outputs());
15
64
  const extra = options.compileOptions;
16
65
  const inputs = [
17
66
  ...options.sources,
18
67
  ...extra?.headFile ? [extra.headFile] : [],
19
68
  ...extra?.modules ?? []
20
69
  ];
21
- for (const input of inputs) this.addWatchFile(resolve(input));
70
+ for (const input of inputs) for (const target of watchTargets(resolve(input), realPathOf(input), outputs, true)) this.addWatchFile(target);
71
+ },
72
+ renderStart(outputOptions) {
73
+ record.addLocation(outputOptions);
22
74
  },
23
- async generateBundle() {
75
+ async generateBundle(outputOptions, bundle) {
76
+ record.addLocation(outputOptions);
77
+ record.addFiles(outputOptions, Object.keys(bundle));
24
78
  let result;
25
79
  try {
26
- result = await compile({
80
+ result = await compileForOutputFile({
27
81
  sources: options.sources,
28
82
  formatId: options.format,
29
83
  ...options.compileOptions
30
- });
84
+ }, record.outputs());
31
85
  } catch (e) {
32
86
  return this.error(fatalError(e));
33
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rollup.js","names":[],"sources":["../../src/plugins/rollup.ts"],"sourcesContent":["/**\n * Ready-made Rollup plugin for twee-ts.\n * Compiles .tw files and emits HTML as an asset. Compile errors fail the build\n * and emit nothing; warnings go through Rollup's warnings.\n */\nimport { resolve } from 'node:path';\nimport type { CompileOptions, CompileResult, Diagnostic } from '../types.js';\nimport { compile } from '../compiler.js';\nimport { fatalError, formatDiagnostic, splitDiagnostics } from './diagnostics.js';\nimport type { LocatedError } from './diagnostics.js';\n\nexport interface TweeTsRollupPluginOptions {\n /** Source directories/files to compile. */\n sources: string[];\n /** Story format ID. */\n format?: string;\n /** Output filename. Default: 'index.html'. */\n outputFilename?: string;\n /** Additional compile options. */\n compileOptions?: Partial<CompileOptions>;\n}\n\nexport function tweeTsPlugin(options: TweeTsRollupPluginOptions) {\n const outputFilename = options.outputFilename ?? 'index.html';\n\n return {\n name: 'twee-ts',\n\n // `rollup --watch` rebuilds for the files the build registers. The story's\n // inputs are read in generateBundle, outside the module graph, so they are\n // registered here. Rollup's watcher watches a folder recursively, files added\n // to it included.\n buildStart(this: { addWatchFile: (id: string) => void; meta: { watchMode: boolean } }) {\n if (!this.meta.watchMode) return;\n const extra = options.compileOptions;\n const inputs = [...options.sources, ...(extra?.headFile ? [extra.headFile] : []), ...(extra?.modules ?? [])];\n for (const input of inputs) this.addWatchFile(resolve(input));\n },\n\n async generateBundle(this: {\n emitFile: (opts: { type: 'asset'; fileName: string; source: string }) => void;\n error: (error: LocatedError) => never;\n warn: (message: string) => void;\n }): Promise<void> {\n let result: CompileResult;\n try {\n result = await compile({\n sources: options.sources,\n formatId: options.format,\n ...options.compileOptions,\n });\n } catch (e) {\n return this.error(fatalError(e));\n }\n let warnings: Diagnostic[];\n try {\n warnings = splitDiagnostics(result);\n } catch (e) {\n return this.error(e as LocatedError);\n }\n for (const w of warnings) this.warn(formatDiagnostic(w));\n this.emitFile({ type: 'asset', fileName: outputFilename, source: result.output });\n },\n };\n}\n"],"mappings":";;;;;;;;AAsBA,SAAgB,aAAa,SAAoC;CAC/D,MAAM,iBAAiB,QAAQ,kBAAkB;CAEjD,OAAO;EACL,MAAM;EAMN,aAAuF;GACrF,IAAI,CAAC,KAAK,KAAK,WAAW;GAC1B,MAAM,QAAQ,QAAQ;GACtB,MAAM,SAAS;IAAC,GAAG,QAAQ;IAAS,GAAI,OAAO,WAAW,CAAC,MAAM,QAAQ,IAAI,CAAC;IAAI,GAAI,OAAO,WAAW,CAAC;GAAE;GAC3G,KAAK,MAAM,SAAS,QAAQ,KAAK,aAAa,QAAQ,KAAK,CAAC;EAC9D;EAEA,MAAM,iBAIY;GAChB,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,QAAQ;KACrB,SAAS,QAAQ;KACjB,UAAU,QAAQ;KAClB,GAAG,QAAQ;IACb,CAAC;GACH,SAAS,GAAG;IACV,OAAO,KAAK,MAAM,WAAW,CAAC,CAAC;GACjC;GACA,IAAI;GACJ,IAAI;IACF,WAAW,iBAAiB,MAAM;GACpC,SAAS,GAAG;IACV,OAAO,KAAK,MAAM,CAAiB;GACrC;GACA,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,iBAAiB,CAAC,CAAC;GACvD,KAAK,SAAS;IAAE,MAAM;IAAS,UAAU;IAAgB,QAAQ,OAAO;GAAO,CAAC;EAClF;CACF;AACF"}
1
+ {"version":3,"file":"rollup.js","names":[],"sources":["../../src/plugins/rollup.ts"],"sourcesContent":["/**\n * Ready-made Rollup plugin for twee-ts.\n * Compiles .tw files and emits HTML as an asset. Compile errors fail the build\n * and emit nothing; warnings go through Rollup's warnings.\n */\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { CompileOptions, CompileResult, Diagnostic } from '../types.js';\nimport { compileForOutputFile } from '../compiler.js';\nimport { outputPaths, realPathOf, walkedEntry } from '../filesystem.js';\nimport type { OutputPaths } from '../filesystem.js';\nimport { fatalError, formatDiagnostic, splitDiagnostics } from './diagnostics.js';\nimport type { LocatedError } from './diagnostics.js';\nimport { createOutputRecord, outputLocations } from './paths.js';\nimport type { OutputLocation } from './paths.js';\n\nexport interface TweeTsRollupPluginOptions {\n /** Source directories/files to compile. */\n sources: string[];\n /** Story format ID. */\n format?: string;\n /** Output filename. Default: 'index.html'. */\n outputFilename?: string;\n /** Additional compile options. */\n compileOptions?: Partial<CompileOptions>;\n}\n\n/**\n * What `rollup --watch` watches for the input at `path`, whose real path is\n * `real`. `isRoot`: the input itself, which is followed wherever it leads.\n *\n * Rollup watches a folder recursively and follows the links in it. So a folder\n * is registered whole only when nothing under it is left out of the story: no\n * file or folder the build writes (`outputs`), and no link to a folder, which\n * source discovery doesn't follow and which may lead to the output. Any other\n * folder is replaced by what it contains, minus those, so writing the outputs\n * starts no build. A file added straight to such a folder is found at the next\n * build. A path the build writes to is never registered itself: Rollup fails a\n * watch build whose watched files include its `output.dir` or `output.file`.\n */\nfunction watchTargets(path: string, real: string, outputs: OutputPaths, isRoot: boolean): string[] {\n if (outputs.isFile(real) || (!isRoot && outputs.isDir(real))) return [];\n let entry;\n try {\n entry = isRoot ? { stat: statSync(path), real } : walkedEntry(path, real);\n } catch {\n // An input Rollup's watcher reports, unless it is an output location. Below an\n // input, what source discovery can't read (a link to nothing, say) is skipped.\n return isRoot && !outputs.isDir(real) ? [path] : [];\n }\n if (entry === undefined) return []; // A link to a folder, not followed.\n if (outputs.isFile(entry.real)) return [];\n if (!entry.stat.isDirectory()) return [path];\n let names: string[];\n try {\n names = readdirSync(path);\n } catch {\n return isRoot && !outputs.isDir(real) ? [path] : [];\n }\n const children = names.map((name) => {\n const child = join(path, name);\n return { path: child, targets: watchTargets(child, join(entry.real, name), outputs, false) };\n });\n const whole =\n !outputs.holds(entry.real) &&\n children.every(({ path: child, targets }) => targets.length === 1 && targets[0] === child);\n return whole ? [path] : children.flatMap(({ targets }) => targets);\n}\n\nexport function tweeTsPlugin(options: TweeTsRollupPluginOptions) {\n const outputFilename = options.outputFilename ?? 'index.html';\n // Every path this plugin's builds write: each output's folder or file, its story,\n // and the files of each bundle. A source folder may hold any of them, and none\n // may be read back as a source or rebuild under watch.\n const record = createOutputRecord(outputFilename);\n\n return {\n name: 'twee-ts',\n\n // Rollup's watch mode and CLI pass the whole config here, outputs included, so\n // they are known before the first build starts and before any is written.\n // Typed `object`: a type with only optional properties would share none with\n // Rollup's InputOptions, and the plugin would no longer be assignable to Plugin.\n options(inputOptions: object): undefined {\n const { output } = inputOptions as { readonly output?: unknown };\n for (const location of outputLocations(output)) record.addLocation(location);\n return undefined;\n },\n\n // `rollup --watch` rebuilds for the files the build registers. The story's\n // inputs are read in generateBundle, outside the module graph, so they are\n // registered here. Rollup's watcher watches a folder recursively, files added\n // to it included.\n buildStart(this: { addWatchFile: (id: string) => void; meta: { watchMode: boolean } }) {\n if (!this.meta.watchMode) return;\n const outputs = outputPaths(record.outputs());\n const extra = options.compileOptions;\n const inputs = [...options.sources, ...(extra?.headFile ? [extra.headFile] : []), ...(extra?.modules ?? [])];\n for (const input of inputs) {\n for (const target of watchTargets(resolve(input), realPathOf(input), outputs, true)) this.addWatchFile(target);\n }\n },\n\n // Runs for every output before any of them generates its bundle, when the\n // outputs are written together (as the CLI and watch mode write them).\n renderStart(outputOptions: OutputLocation): void {\n record.addLocation(outputOptions);\n },\n\n async generateBundle(\n this: {\n emitFile: (opts: { type: 'asset'; fileName: string; source: string }) => void;\n error: (error: LocatedError) => never;\n warn: (message: string) => void;\n },\n outputOptions: OutputLocation,\n bundle: Readonly<Record<string, unknown>>,\n ): Promise<void> {\n // Where this output writes the story and the bundle. A source folder may hold\n // them, or another output's, and no last build of them may be read back.\n record.addLocation(outputOptions);\n record.addFiles(outputOptions, Object.keys(bundle));\n let result: CompileResult;\n try {\n result = await compileForOutputFile(\n { sources: options.sources, formatId: options.format, ...options.compileOptions },\n record.outputs(),\n );\n } catch (e) {\n return this.error(fatalError(e));\n }\n let warnings: Diagnostic[];\n try {\n warnings = splitDiagnostics(result);\n } catch (e) {\n return this.error(e as LocatedError);\n }\n for (const w of warnings) this.warn(formatDiagnostic(w));\n this.emitFile({ type: 'asset', fileName: outputFilename, source: result.output });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,aAAa,MAAc,MAAc,SAAsB,QAA2B;CACjG,IAAI,QAAQ,OAAO,IAAI,KAAM,CAAC,UAAU,QAAQ,MAAM,IAAI,GAAI,OAAO,CAAC;CACtE,IAAI;CACJ,IAAI;EACF,QAAQ,SAAS;GAAE,MAAM,SAAS,IAAI;GAAG;EAAK,IAAI,YAAY,MAAM,IAAI;CAC1E,QAAQ;EAGN,OAAO,UAAU,CAAC,QAAQ,MAAM,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;CACpD;CACA,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,IAAI,QAAQ,OAAO,MAAM,IAAI,GAAG,OAAO,CAAC;CACxC,IAAI,CAAC,MAAM,KAAK,YAAY,GAAG,OAAO,CAAC,IAAI;CAC3C,IAAI;CACJ,IAAI;EACF,QAAQ,YAAY,IAAI;CAC1B,QAAQ;EACN,OAAO,UAAU,CAAC,QAAQ,MAAM,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;CACpD;CACA,MAAM,WAAW,MAAM,KAAK,SAAS;EACnC,MAAM,QAAQ,KAAK,MAAM,IAAI;EAC7B,OAAO;GAAE,MAAM;GAAO,SAAS,aAAa,OAAO,KAAK,MAAM,MAAM,IAAI,GAAG,SAAS,KAAK;EAAE;CAC7F,CAAC;CAID,OAFE,CAAC,QAAQ,MAAM,MAAM,IAAI,KACzB,SAAS,OAAO,EAAE,MAAM,OAAO,cAAc,QAAQ,WAAW,KAAK,QAAQ,OAAO,KAAK,IAC5E,CAAC,IAAI,IAAI,SAAS,SAAS,EAAE,cAAc,OAAO;AACnE;AAEA,SAAgB,aAAa,SAAoC;CAC/D,MAAM,iBAAiB,QAAQ,kBAAkB;CAIjD,MAAM,SAAS,mBAAmB,cAAc;CAEhD,OAAO;EACL,MAAM;EAMN,QAAQ,cAAiC;GACvC,MAAM,EAAE,WAAW;GACnB,KAAK,MAAM,YAAY,gBAAgB,MAAM,GAAG,OAAO,YAAY,QAAQ;EAE7E;EAMA,aAAuF;GACrF,IAAI,CAAC,KAAK,KAAK,WAAW;GAC1B,MAAM,UAAU,YAAY,OAAO,QAAQ,CAAC;GAC5C,MAAM,QAAQ,QAAQ;GACtB,MAAM,SAAS;IAAC,GAAG,QAAQ;IAAS,GAAI,OAAO,WAAW,CAAC,MAAM,QAAQ,IAAI,CAAC;IAAI,GAAI,OAAO,WAAW,CAAC;GAAE;GAC3G,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,UAAU,aAAa,QAAQ,KAAK,GAAG,WAAW,KAAK,GAAG,SAAS,IAAI,GAAG,KAAK,aAAa,MAAM;EAEjH;EAIA,YAAY,eAAqC;GAC/C,OAAO,YAAY,aAAa;EAClC;EAEA,MAAM,eAMJ,eACA,QACe;GAGf,OAAO,YAAY,aAAa;GAChC,OAAO,SAAS,eAAe,OAAO,KAAK,MAAM,CAAC;GAClD,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,qBACb;KAAE,SAAS,QAAQ;KAAS,UAAU,QAAQ;KAAQ,GAAG,QAAQ;IAAe,GAChF,OAAO,QAAQ,CACjB;GACF,SAAS,GAAG;IACV,OAAO,KAAK,MAAM,WAAW,CAAC,CAAC;GACjC;GACA,IAAI;GACJ,IAAI;IACF,WAAW,iBAAiB,MAAM;GACpC,SAAS,GAAG;IACV,OAAO,KAAK,MAAM,CAAiB;GACrC;GACA,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,iBAAiB,CAAC,CAAC;GACvD,KAAK,SAAS;IAAE,MAAM;IAAS,UAAU;IAAgB,QAAQ,OAAO;GAAO,CAAC;EAClF;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { t as CompileOptions } from "../types-dR5vTVu2.js";
1
+ import { t as CompileOptions } from "../types-D8Xfe0l8.js";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/plugins/vite.d.ts
4
4
  export interface TweeTsVitePluginOptions {
@@ -1,31 +1,8 @@
1
- import { a as compileIncremental, c as mediaTypeFromFilename, n as formatDiagnostic, o as getFilenames, r as splitDiagnostics, s as isExcluded, t as fatalError } from "../diagnostics-DZNS7W41.js";
1
+ import { a as toPosix, c as splitDiagnostics, d as isExcluded, f as outputPaths, h as mediaTypeFromFilename, i as outputLocations, l as compileForOutputFile, m as walkedEntry, n as isInside, o as fatalError, p as realPathOf, r as isViteConfigTemp, s as formatDiagnostic, t as createOutputRecord, u as getFilenames } from "../paths-DgO9oo6G.js";
2
2
  import { readdirSync, statSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { join, relative, resolve } from "node:path";
4
4
  import { stripVTControlCharacters } from "node:util";
5
5
  import { build, version } from "vite";
6
- //#region src/plugins/paths.ts
7
- /**
8
- * Path helpers for the Vite plugin. Vite reports module ids with forward
9
- * slashes on every platform, while node:path gives backslashes on Windows, so
10
- * the plugin compares paths in forward-slash form only.
11
- */
12
- /** The path with Windows separators turned into forward slashes. */
13
- function toPosix(path) {
14
- return path.replace(/\\/g, "/");
15
- }
16
- /** Whether `file` is one of `dirs` or inside one of them. All paths in forward-slash form. */
17
- function isInside(file, dirs) {
18
- return dirs.some((dir) => file === dir || file.startsWith(dir.endsWith("/") ? dir : `${dir}/`));
19
- }
20
- /**
21
- * Whether `file` is a temporary copy of a Vite config file. Vite writes one next
22
- * to the config file each time it loads it (when no node_modules folder is
23
- * above the config, and always under Deno), then deletes it.
24
- */
25
- function isViteConfigTemp(file) {
26
- return /\.timestamp-\d+-[0-9a-f]*\.mjs$/.test(file);
27
- }
28
- //#endregion
29
6
  //#region src/plugins/vite.ts
30
7
  /**
31
8
  * Ready-made Vite plugin for twee-ts.
@@ -141,49 +118,104 @@ function headInputs(options) {
141
118
  const extra = options.compileOptions;
142
119
  return [...extra?.headFile ? [extra.headFile] : [], ...extra?.modules ?? []].map((p) => toPosix(resolve(p)));
143
120
  }
144
- /** Whether a file (forward-slash path) is a source the compile leaves out; the head file and modules never are. */
145
- function excludedInput(options) {
121
+ /**
122
+ * Whether a file (forward-slash path) is a source `exclude` matches. `exclude`
123
+ * never applies to the head file and modules.
124
+ */
125
+ function excludedByGlob(options) {
146
126
  const exclude = options.compileOptions?.exclude ?? [];
147
127
  const notExcludable = headInputs(options);
148
128
  return (file) => isExcluded(file, exclude) && !isInside(file, notExcludable);
149
129
  }
130
+ /**
131
+ * Where a build with this config writes: each output's folder, the bundler's
132
+ * `output.dir` or else `build.outDir`, or the bundler's `output.file`. Vite
133
+ * resolves an `output.dir` against the root when it empties the folder and
134
+ * copies the public files into it, while the bundler resolves it against the
135
+ * working directory when it writes; both count.
136
+ */
137
+ function configOutputLocations(config) {
138
+ const outDir = resolve(config.root, config.build.outDir);
139
+ const option = config.build[viteMajor >= 8 ? "rolldownOptions" : "rollupOptions"]?.output;
140
+ return (option === void 0 ? [{}] : Array.isArray(option) ? option : [option]).flatMap((output) => {
141
+ const [location] = outputLocations(output);
142
+ if (location === void 0) return [{ dir: outDir }];
143
+ if (location.dir === void 0) return [location];
144
+ return [{ dir: resolve(location.dir) }, { dir: resolve(config.root, location.dir) }];
145
+ });
146
+ }
147
+ /**
148
+ * Every path a build with this config writes, as far as the config tells: each
149
+ * output's folder (see configOutputLocations), the story in it, and the copies
150
+ * of the public files Vite puts there. The plugin adds the files each bundle
151
+ * writes as it builds (see createOutputRecord).
152
+ */
153
+ function configOutputs(config, outputFilename) {
154
+ const record = createOutputRecord(outputFilename);
155
+ const publicFiles = config.build.copyPublicDir && config.publicDir ? getFilenames([config.publicDir]).filenames.map((file) => relative(config.publicDir, resolve(file))) : [];
156
+ for (const location of configOutputLocations(config)) {
157
+ record.addLocation(location);
158
+ record.addFiles(location, publicFiles);
159
+ }
160
+ return record.outputs();
161
+ }
162
+ /** Both builds' outputs together. */
163
+ function mergeOutputs(a, b) {
164
+ return {
165
+ files: [...a.files, ...b.files],
166
+ dirs: [...a.dirs, ...b.dirs]
167
+ };
168
+ }
150
169
  /** Absolute forward-slash paths whose changes recompile the story: sources, head file, modules. */
151
170
  function watchedInputs(options) {
152
171
  return [...options.sources.map((p) => toPosix(resolve(p))), ...headInputs(options)];
153
172
  }
154
173
  /**
155
174
  * What `vite build --watch` watches for the story: every file under `inputs`
156
- * (forward-slash paths) but those `skip` returns true for, and every folder
157
- * under them. The bundler watches each path on its own, not recursively, so a
158
- * folder is listed for the files added to it or deleted from it.
175
+ * (forward-slash paths) that source discovery reads, but those `skip` returns
176
+ * true for, and every folder under them. The bundler watches each path on its
177
+ * own, not recursively, so a folder is listed for the files added to it or
178
+ * deleted from it. As in source discovery, a link to a folder is not followed.
179
+ *
180
+ * A watched folder may still report changes in its subfolders (Rolldown's
181
+ * watcher on Linux does), so no path the build writes (`outputs`) is listed, nor
182
+ * a folder that holds one: writing the outputs would start the next build. A file
183
+ * added straight to such a folder is found when another change starts a build.
159
184
  */
160
- function buildWatchTargets(inputs, skip) {
185
+ function buildWatchTargets(inputs, skip, outputs) {
161
186
  const targets = [];
162
- const walk = (path) => {
163
- let entries;
187
+ const walk = (path, real, isRoot) => {
188
+ if (outputs.isFile(real) || !isRoot && outputs.isDir(real)) return;
189
+ let entry;
190
+ let names;
164
191
  try {
165
- if (!statSync(path).isDirectory()) {
192
+ entry = isRoot ? {
193
+ stat: statSync(path),
194
+ real
195
+ } : walkedEntry(path, real);
196
+ if (entry === void 0 || outputs.isFile(entry.real)) return;
197
+ if (!entry.stat.isDirectory()) {
166
198
  if (!skip(path)) targets.push(path);
167
199
  return;
168
200
  }
169
- entries = readdirSync(path);
201
+ names = readdirSync(path);
170
202
  } catch {
171
203
  return;
172
204
  }
173
- targets.push(path);
174
- for (const entry of entries) walk(`${path}/${entry}`);
205
+ if (!outputs.holds(entry.real)) targets.push(path);
206
+ for (const name of names) walk(`${path}/${name}`, join(entry.real, name), false);
175
207
  };
176
- for (const input of inputs) walk(input);
208
+ for (const input of inputs) walk(input, realPathOf(input), true);
177
209
  return targets;
178
210
  }
179
211
  /**
180
- * Every file under `inputs` (forward-slash paths) but those `skip` returns true
181
- * for, with what a change to it alters: modification time, size and inode (a
182
- * file replaced by a new one).
212
+ * Every file under `inputs` (forward-slash paths) that source discovery reads,
213
+ * leaving out `outputs`, but those `skip` returns true for, with what a change to
214
+ * it alters: modification time, size and inode (a file replaced by a new one).
183
215
  */
184
- function inputFiles(inputs, skip) {
216
+ function inputFiles(inputs, skip, outputs) {
185
217
  const files = /* @__PURE__ */ new Map();
186
- for (const filename of getFilenames([...inputs]).filenames) {
218
+ for (const filename of getFilenames(inputs, outputs).filenames) {
187
219
  const file = toPosix(resolve(filename));
188
220
  if (skip(file)) continue;
189
221
  try {
@@ -377,7 +409,9 @@ function tweeTsPlugin(options) {
377
409
  const cache = /* @__PURE__ */ new Map();
378
410
  let innerBuild = false;
379
411
  let building = false;
380
- let stopDev;
412
+ let buildConfig;
413
+ const record = createOutputRecord(outputFilename);
414
+ const allOutputs = (config) => mergeOutputs(configOutputs(config, outputFilename), record.outputs());
381
415
  if (options.entry && viteMajor < 8) throw new Error(`twee-ts: the entry option needs Vite 8 or newer (found ${version}).`);
382
416
  return {
383
417
  name: "twee-ts",
@@ -390,13 +424,16 @@ function tweeTsPlugin(options) {
390
424
  return { build: inputOnly(EMPTY_INPUT) };
391
425
  },
392
426
  configResolved(config) {
393
- if (innerBuild || !options.entry) return;
427
+ if (innerBuild) return;
428
+ if (building) buildConfig = config;
429
+ if (!options.entry) return;
394
430
  const sources = options.sources.map((p) => toPosix(resolve(p)));
395
431
  if (isInside(toPosix(resolve(options.entry)), sources)) config.logger.warn(`[twee-ts] The entry ${options.entry} is inside the story sources (${options.sources.join(", ")}). twee-ts also loads the .js and .css files it finds in source folders, unbundled, as Story JavaScript and Story Stylesheet; keep the entry's folder out of \`sources\`.`);
396
432
  },
397
433
  buildStart() {
398
- if (innerBuild || !building || !this.meta.watchMode) return;
399
- for (const target of buildWatchTargets(watchedInputs(options), excludedInput(options))) this.addWatchFile(target);
434
+ if (innerBuild || !building || !this.meta.watchMode || buildConfig === void 0) return;
435
+ const outputs = outputPaths(allOutputs(buildConfig));
436
+ for (const target of buildWatchTargets(watchedInputs(options), excludedByGlob(options), outputs)) this.addWatchFile(target);
400
437
  },
401
438
  watchChange(id) {
402
439
  if (innerBuild) return;
@@ -411,13 +448,16 @@ function tweeTsPlugin(options) {
411
448
  },
412
449
  generateBundle: {
413
450
  order: "post",
414
- async handler(_outputOptions, bundle) {
451
+ async handler(outputOptions, bundle) {
415
452
  if (innerBuild) return;
416
453
  for (const [fileName, item] of Object.entries(bundle)) if (item.type === "chunk" && item.facadeModuleId === RESOLVED_EMPTY_INPUT) delete bundle[fileName];
454
+ record.addLocation(outputOptions);
455
+ record.addFiles(outputOptions, Object.keys(bundle));
456
+ const outputs = buildConfig === void 0 ? record.outputs() : allOutputs(buildConfig);
417
457
  const entry = options.entry ? takeEntryFromBundle(bundle) : void 0;
418
458
  let result;
419
459
  try {
420
- result = await compileIncremental(buildCompileOptions(options, entry), cache);
460
+ result = await compileForOutputFile(buildCompileOptions(options, entry), outputs, cache);
421
461
  } catch (e) {
422
462
  return this.error(fatalError(e));
423
463
  }
@@ -440,7 +480,14 @@ function tweeTsPlugin(options) {
440
480
  const base = server.config.base;
441
481
  const servePaths = outputFilename === "index.html" ? [base, `${base}index.html`] : [`${base}${outputFilename}`];
442
482
  const inputs = watchedInputs(options);
443
- const excluded = excludedInput(options);
483
+ let outputs = allOutputs(server.config);
484
+ let output = outputPaths(outputs);
485
+ const refreshOutputs = () => {
486
+ outputs = allOutputs(server.config);
487
+ output = outputPaths(outputs);
488
+ };
489
+ const excludedGlob = excludedByGlob(options);
490
+ const excluded = (file) => excludedGlob(file) || output.isOutput(file, inputs);
444
491
  const entryPath = options.entry ? resolve(options.entry) : void 0;
445
492
  const root = toPosix(server.config.root);
446
493
  server.watcher.add(inputs);
@@ -454,15 +501,18 @@ function tweeTsPlugin(options) {
454
501
  let closed = false;
455
502
  let compiledInputs = /* @__PURE__ */ new Map();
456
503
  let catchingUp;
457
- stopDev = () => {
504
+ const closeWatcher = server.watcher.close.bind(server.watcher);
505
+ server.watcher.close = async () => {
458
506
  closed = true;
459
507
  clearTimeout(timer);
460
508
  pending.clear();
509
+ return closeWatcher();
461
510
  };
462
511
  const touchesEntry = (file) => entryPath !== void 0 && (entryStale ? isInside(file, [root]) : entry?.files.has(file) ?? false);
463
512
  const rebuild = async (changed, initial = false) => {
464
513
  if (closed) return;
465
- compiledInputs = inputFiles(inputs, excluded);
514
+ refreshOutputs();
515
+ compiledInputs = inputFiles(inputs, excludedGlob, outputs);
466
516
  for (const key of [...cache.keys()]) if (changed.has(toPosix(resolve(key)))) cache.delete(key);
467
517
  try {
468
518
  if (entryPath && (entryStale || [...changed].some(touchesEntry))) {
@@ -470,7 +520,7 @@ function tweeTsPlugin(options) {
470
520
  entry = await bundleEntryForDev(server.config, entryPath);
471
521
  entryStale = false;
472
522
  }
473
- const result = await compileIncremental(buildCompileOptions(options, entry), cache);
523
+ const result = await compileForOutputFile(buildCompileOptions(options, entry), outputs, cache);
474
524
  for (const w of splitDiagnostics(result)) server.config.logger.warn(`[twee-ts] ${formatDiagnostic(w)}`);
475
525
  html = injectViteClient(result.output, base);
476
526
  lastError = void 0;
@@ -506,7 +556,7 @@ function tweeTsPlugin(options) {
506
556
  const catchUp = () => {
507
557
  catchingUp ??= (async () => {
508
558
  await queue;
509
- const changed = filesChanged(compiledInputs, inputFiles(inputs, excluded));
559
+ const changed = filesChanged(compiledInputs, inputFiles(inputs, excludedGlob, outputs));
510
560
  if (changed.size === 0) return;
511
561
  for (const file of pending) changed.add(file);
512
562
  pending = /* @__PURE__ */ new Set();
@@ -543,9 +593,6 @@ function tweeTsPlugin(options) {
543
593
  }
544
594
  next();
545
595
  });
546
- },
547
- closeBundle() {
548
- stopDev?.();
549
596
  }
550
597
  };
551
598
  }