@inkandswitch/patchwork 0.6.1 → 0.7.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/CHANGELOG.md +54 -0
- package/dist/vite/build-info-plugin.d.ts +8 -0
- package/dist/vite/build-info-plugin.js +67 -0
- package/dist/vite/dev-plugin.js +12 -0
- package/dist/vite/patchwork-plugin.d.ts +15 -0
- package/dist/vite/patchwork-plugin.js +4 -0
- package/dist/vite/static-plugin.d.ts +46 -0
- package/dist/vite/static-plugin.js +212 -0
- package/package.json +4 -4
- package/src/vite/build-info-plugin.ts +84 -0
- package/src/vite/dev-plugin.ts +19 -0
- package/src/vite/patchwork-plugin.ts +20 -0
- package/src/vite/static-plugin.ts +279 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# @inkandswitch/patchwork
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8b9206d: Don't copy `static` sources or write `build-info.json` when a dev server shuts
|
|
8
|
+
down. `closeBundle` runs then too, so stopping `vite` was filling `dist/` with
|
|
9
|
+
a copy of every static source.
|
|
10
|
+
|
|
11
|
+
## 0.7.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 5be751b: Add `static` and `buildInfo` options to the vite plugin.
|
|
16
|
+
|
|
17
|
+
`static` mounts file trees into the site — a package of Patchwork modules, a
|
|
18
|
+
sibling repo's build output, a hand-written `modules.json`. Each source is
|
|
19
|
+
served by the dev server and copied into the site at build:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
patchwork({
|
|
23
|
+
static: [
|
|
24
|
+
{ from: "modules.json" },
|
|
25
|
+
"@inkandswitch/patchwork-pkg-base",
|
|
26
|
+
{
|
|
27
|
+
from: "../notebook/dist",
|
|
28
|
+
to: "/packages/notebook",
|
|
29
|
+
watch: ".watch-ready",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A source is either a package specifier or a path relative to the site root, and
|
|
36
|
+
either a file or a directory. `to` mounts it somewhere other than the site root.
|
|
37
|
+
`watch` names a file inside the source that another build touches when it
|
|
38
|
+
finishes; writing to it full-reloads the dev page, which is how a sibling repo
|
|
39
|
+
in watch mode drives the site's dev server.
|
|
40
|
+
|
|
41
|
+
Sources never overwrite the site's own files — not the build's output, not
|
|
42
|
+
`public/`, not an earlier source in the list — so precedence is list order. A
|
|
43
|
+
site that wants its own `modules.json` lists it before the package it takes the
|
|
44
|
+
rest of its modules from. Every file a source didn't get to write is logged at
|
|
45
|
+
the end of the build, so a collision you didn't mean to have is visible.
|
|
46
|
+
|
|
47
|
+
A package can say which of its directories is the static tree with a
|
|
48
|
+
`"patchwork": {"static": "static-dist"}` field in its package.json. Without one,
|
|
49
|
+
the package root is mounted (minus its manifest, `node_modules` and `.git`).
|
|
50
|
+
|
|
51
|
+
`buildInfo` writes `build-info.json`: the site's git revision, the version and
|
|
52
|
+
revision of the patchwork that built it, and every `static` source. Pass an
|
|
53
|
+
object to merge extra fields into it.
|
|
54
|
+
|
|
55
|
+
Both options are off unless set, so existing sites are unaffected.
|
|
56
|
+
|
|
3
57
|
## 0.6.1
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
3
|
+
/**
|
|
4
|
+
* Writes build-info.json: what this site was built from. The site's own
|
|
5
|
+
* revision, the version of patchwork that built it, and every `static` source,
|
|
6
|
+
* plus whatever extra fields the site passes as the option value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildInfoPlugin(options?: PatchworkVitePluginOptions): Plugin | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveStatic } from "./static-plugin.js";
|
|
7
|
+
function revision(directory) {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
10
|
+
cwd: directory,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
}).trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What a package directory is: its name and version, plus the revision it was
|
|
21
|
+
* built from when it isn't an installed copy. A workspace link or a checkout
|
|
22
|
+
* resolves outside node_modules, and its version alone doesn't say which build
|
|
23
|
+
* of it this was.
|
|
24
|
+
*/
|
|
25
|
+
function describe(directory) {
|
|
26
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
27
|
+
return {
|
|
28
|
+
name: manifest.name,
|
|
29
|
+
version: manifest.version,
|
|
30
|
+
revision: directory.split(sep).includes("node_modules")
|
|
31
|
+
? undefined
|
|
32
|
+
: revision(directory),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Writes build-info.json: what this site was built from. The site's own
|
|
37
|
+
* revision, the version of patchwork that built it, and every `static` source,
|
|
38
|
+
* plus whatever extra fields the site passes as the option value.
|
|
39
|
+
*/
|
|
40
|
+
export function buildInfoPlugin(options = {}) {
|
|
41
|
+
if (!options.buildInfo)
|
|
42
|
+
return null;
|
|
43
|
+
let root;
|
|
44
|
+
let outDir;
|
|
45
|
+
let serve = false;
|
|
46
|
+
return {
|
|
47
|
+
name: "@patchwork/build-info",
|
|
48
|
+
configResolved(config) {
|
|
49
|
+
root = config.root;
|
|
50
|
+
outDir = join(config.root, config.build.outDir);
|
|
51
|
+
serve = config.command === "serve";
|
|
52
|
+
},
|
|
53
|
+
async closeBundle() {
|
|
54
|
+
if (serve)
|
|
55
|
+
return;
|
|
56
|
+
const sources = resolveStatic(options, root).map((source) => source.packageDirectory
|
|
57
|
+
? { from: source.from, ...describe(source.packageDirectory) }
|
|
58
|
+
: { from: source.from, revision: revision(source.path) });
|
|
59
|
+
await writeFile(join(outDir, "build-info.json"), `${JSON.stringify({
|
|
60
|
+
site: { revision: revision(root) },
|
|
61
|
+
patchwork: describe(join(dirname(fileURLToPath(import.meta.url)), "..", "..")),
|
|
62
|
+
static: sources.length ? sources : undefined,
|
|
63
|
+
...(options.buildInfo === true ? {} : options.buildInfo),
|
|
64
|
+
}, null, 2)}\n`);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/vite/dev-plugin.js
CHANGED
|
@@ -14,6 +14,7 @@ const stylesheets = {
|
|
|
14
14
|
[PATCHWORK_CSS]: fileURLToPath(import.meta.resolve("@inkandswitch/patchwork/global.css")),
|
|
15
15
|
[BOOTLOADER_CSS]: fileURLToPath(import.meta.resolve("@inkandswitch/patchwork-bootloader/global.css")),
|
|
16
16
|
};
|
|
17
|
+
const builtinPaths = new Map(Object.entries(builtins).map(([id, fileName]) => [fileName, id]));
|
|
17
18
|
/**
|
|
18
19
|
* Workers are `type: "module"` scripts the browser fetches directly, so import
|
|
19
20
|
* maps don't apply to them and their heavy imports have to resolve to real
|
|
@@ -92,6 +93,17 @@ export function devPlugin(options = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
96
|
+
// A worker script is fetched by URL, and import maps don't apply to
|
|
97
|
+
// those, so code that starts one reaches for the /packages/… path the
|
|
98
|
+
// build emits. In dev those are the optimized deps the page's import
|
|
99
|
+
// map points at — same module, one URL over.
|
|
100
|
+
const builtin = builtinPaths.get(pathname);
|
|
101
|
+
if (builtin) {
|
|
102
|
+
response.statusCode = 302;
|
|
103
|
+
response.setHeader("Location", `/@id/${encodeURI(devDependencyId(builtin))}`);
|
|
104
|
+
response.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
95
107
|
const binary = wasm.get(pathname);
|
|
96
108
|
if (binary) {
|
|
97
109
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Plugin, ServerOptions, PreviewOptions, BuildOptions } from "vite";
|
|
2
|
+
import { type PatchworkStaticSource } from "./static-plugin.js";
|
|
2
3
|
import type { PatchworkSiteOptions } from "../site-kit/options.js";
|
|
3
4
|
/**
|
|
4
5
|
* The patchwork vite plugin. A site's vite.config.ts can shrink down to
|
|
@@ -31,9 +32,23 @@ export type ImportMap = {
|
|
|
31
32
|
[scope: string]: Imports;
|
|
32
33
|
};
|
|
33
34
|
};
|
|
35
|
+
export type { PatchworkStaticSource } from "./static-plugin.js";
|
|
34
36
|
export type { PatchworkSiteOptions, PatchworkIconsOptions, PatchworkHtmlOptions, PatchworkNetlifyOptions, PatchworkKeyhiveSyncServer, PatchworkSyncServersOptions, } from "../site-kit/options.js";
|
|
35
37
|
export interface PatchworkVitePluginOptions extends PatchworkSiteOptions {
|
|
36
38
|
importmap?: ImportMap;
|
|
39
|
+
/**
|
|
40
|
+
* File trees to mount into the site, in order of precedence — a package of
|
|
41
|
+
* Patchwork modules, a sibling repo's build output, a hand-written
|
|
42
|
+
* modules.json. Served in dev, copied into the site at build, and never
|
|
43
|
+
* overwriting the site's own files. A bare string is `{from: string}`.
|
|
44
|
+
*/
|
|
45
|
+
static?: (string | PatchworkStaticSource)[];
|
|
46
|
+
/**
|
|
47
|
+
* Write build-info.json: this site's revision, the patchwork version that
|
|
48
|
+
* built it, and every `static` source. An object is merged into it, for
|
|
49
|
+
* whatever else the site wants recorded.
|
|
50
|
+
*/
|
|
51
|
+
buildInfo?: boolean | Record<string, unknown>;
|
|
37
52
|
server?: false | ServerOptions;
|
|
38
53
|
preview?: false | PreviewOptions;
|
|
39
54
|
worker?: false | {
|
|
@@ -6,6 +6,8 @@ import { iconsPlugin } from "./icons.js";
|
|
|
6
6
|
import { htmlPlugin } from "./html-plugin.js";
|
|
7
7
|
import { manifestPlugin } from "./manifest-plugin.js";
|
|
8
8
|
import { netlifyPlugin } from "./netlify-plugin.js";
|
|
9
|
+
import { staticPlugin } from "./static-plugin.js";
|
|
10
|
+
import { buildInfoPlugin } from "./build-info-plugin.js";
|
|
9
11
|
/**
|
|
10
12
|
* The patchwork vite plugin. A site's vite.config.ts can shrink down to
|
|
11
13
|
* `plugins: [patchwork({...})]` plus a single source icon file — this plugin
|
|
@@ -38,5 +40,7 @@ export default function patchwork(options) {
|
|
|
38
40
|
importmap(options),
|
|
39
41
|
serviceworker(),
|
|
40
42
|
devPlugin(options),
|
|
43
|
+
staticPlugin(options),
|
|
44
|
+
buildInfoPlugin(options),
|
|
41
45
|
].filter((plugin) => plugin != null);
|
|
42
46
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
3
|
+
/**
|
|
4
|
+
* A tree of files to mount into the site — a package of Patchwork modules, a
|
|
5
|
+
* sibling repo's build output, a single hand-written file.
|
|
6
|
+
*
|
|
7
|
+
* `from` is either a package specifier or a path relative to the site root. A
|
|
8
|
+
* package says where its static tree lives with a `"patchwork": {"static":
|
|
9
|
+
* "static-dist"}` field in its own package.json; without one, the whole
|
|
10
|
+
* package directory is mounted.
|
|
11
|
+
*/
|
|
12
|
+
export interface PatchworkStaticSource {
|
|
13
|
+
from: string;
|
|
14
|
+
/**
|
|
15
|
+
* URL path to mount `from` at. Defaults to `/`. For a file source a trailing
|
|
16
|
+
* slash means "this directory, under the file's own name", so `{from:
|
|
17
|
+
* "build/modules.json"}` lands at `/modules.json`.
|
|
18
|
+
*/
|
|
19
|
+
to?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Path, within `from`, of a file another build touches when it finishes.
|
|
22
|
+
* Writing to it full-reloads the dev page, and it is never copied into the
|
|
23
|
+
* site. This is how a sibling repo in watch mode drives the site's dev
|
|
24
|
+
* server.
|
|
25
|
+
*/
|
|
26
|
+
watch?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ResolvedStaticSource extends PatchworkStaticSource {
|
|
29
|
+
/** absolute path of the file or directory `from` resolved to */
|
|
30
|
+
path: string;
|
|
31
|
+
/** normalised mount point: "" for the site root, else "/packages/x" */
|
|
32
|
+
mount: string;
|
|
33
|
+
file: boolean;
|
|
34
|
+
/** set when `from` was a package specifier rather than a path */
|
|
35
|
+
packageDirectory?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolveStatic(options: PatchworkVitePluginOptions, root: string): ResolvedStaticSource[];
|
|
38
|
+
/**
|
|
39
|
+
* Mounts `static` sources: served in dev, copied into the site at build.
|
|
40
|
+
*
|
|
41
|
+
* Sources never overwrite the site's own output — neither the build's, nor
|
|
42
|
+
* `public/`, nor an earlier source in the list. So precedence is just list
|
|
43
|
+
* order, and a site keeps its own `modules.json` by listing it before the
|
|
44
|
+
* package it borrows the rest of the packages from.
|
|
45
|
+
*/
|
|
46
|
+
export declare function staticPlugin(options?: PatchworkVitePluginOptions): Plugin | null;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
const CONTENT_TYPES = {
|
|
6
|
+
".css": "text/css",
|
|
7
|
+
".html": "text/html",
|
|
8
|
+
".js": "text/javascript",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".map": "application/json",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".wasm": "application/wasm",
|
|
14
|
+
};
|
|
15
|
+
/** Never site content, whatever a source directory happens to contain. */
|
|
16
|
+
const SKIP = new Set(["node_modules", ".git"]);
|
|
17
|
+
/**
|
|
18
|
+
* Resolved from the site, not from here: the site is what depends on these
|
|
19
|
+
* packages, and under pnpm this plugin lives somewhere that can't see them.
|
|
20
|
+
*/
|
|
21
|
+
function packageDirectory(name, root) {
|
|
22
|
+
const require = createRequire(join(root, "package.json"));
|
|
23
|
+
try {
|
|
24
|
+
return dirname(require.resolve(`${name}/package.json`));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// an "exports" map can hide both the manifest and the package's own main,
|
|
28
|
+
// so fall back to looking for it where node would have found it
|
|
29
|
+
for (const directory of require.resolve.paths(name) ?? []) {
|
|
30
|
+
if (existsSync(join(directory, name, "package.json"))) {
|
|
31
|
+
return join(directory, name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`[patchwork] can't find the package "${name}" — a static source has to be a dependency of the site`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function resolveStatic(options, root) {
|
|
38
|
+
return (options.static ?? []).map((source) => {
|
|
39
|
+
const entry = typeof source === "string" ? { from: source } : source;
|
|
40
|
+
const isPath = entry.from.startsWith(".") || isAbsolute(entry.from);
|
|
41
|
+
const directory = isPath ? undefined : packageDirectory(entry.from, root);
|
|
42
|
+
const path = directory
|
|
43
|
+
? join(directory, JSON.parse(readFileSync(join(directory, "package.json"), "utf8"))
|
|
44
|
+
.patchwork?.static ?? ".")
|
|
45
|
+
: resolve(root, entry.from);
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
throw new Error(`[patchwork] static source not found: ${entry.from}`);
|
|
48
|
+
}
|
|
49
|
+
const file = statSync(path).isFile();
|
|
50
|
+
const to = entry.to ?? "/";
|
|
51
|
+
return {
|
|
52
|
+
...entry,
|
|
53
|
+
path,
|
|
54
|
+
file,
|
|
55
|
+
packageDirectory: directory,
|
|
56
|
+
mount: (file && to.endsWith("/") ? join(to, basename(path)) : to).replace(/\/+$/, ""),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function locate(source, pathname) {
|
|
61
|
+
if (source.file)
|
|
62
|
+
return pathname === source.mount ? source.path : undefined;
|
|
63
|
+
if (pathname !== source.mount && !pathname.startsWith(`${source.mount}/`)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const path = resolve(source.path, `.${pathname.slice(source.mount.length)}`);
|
|
67
|
+
const within = relative(source.path, path);
|
|
68
|
+
if (within.startsWith(".."))
|
|
69
|
+
return undefined;
|
|
70
|
+
if (within.split(sep).some((part) => SKIP.has(part)))
|
|
71
|
+
return undefined;
|
|
72
|
+
if (within === source.watch)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (within === "package.json" && source.packageDirectory)
|
|
75
|
+
return undefined;
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
/** How a source is named in messages: a specifier, or the shorter of the paths. */
|
|
79
|
+
function label(source, root) {
|
|
80
|
+
if (source.packageDirectory)
|
|
81
|
+
return source.from;
|
|
82
|
+
const path = relative(root, source.path);
|
|
83
|
+
return path.split(sep).filter((part) => part === "..").length > 1
|
|
84
|
+
? source.path
|
|
85
|
+
: path;
|
|
86
|
+
}
|
|
87
|
+
async function files(source, directory = source) {
|
|
88
|
+
const paths = [];
|
|
89
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
90
|
+
if (SKIP.has(entry.name))
|
|
91
|
+
continue;
|
|
92
|
+
const path = join(directory, entry.name);
|
|
93
|
+
if (entry.isDirectory())
|
|
94
|
+
paths.push(...(await files(source, path)));
|
|
95
|
+
if (entry.isFile())
|
|
96
|
+
paths.push(relative(source, path));
|
|
97
|
+
}
|
|
98
|
+
return paths;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mounts `static` sources: served in dev, copied into the site at build.
|
|
102
|
+
*
|
|
103
|
+
* Sources never overwrite the site's own output — neither the build's, nor
|
|
104
|
+
* `public/`, nor an earlier source in the list. So precedence is just list
|
|
105
|
+
* order, and a site keeps its own `modules.json` by listing it before the
|
|
106
|
+
* package it borrows the rest of the packages from.
|
|
107
|
+
*/
|
|
108
|
+
export function staticPlugin(options = {}) {
|
|
109
|
+
if (!options.static?.length)
|
|
110
|
+
return null;
|
|
111
|
+
let sources = [];
|
|
112
|
+
let root;
|
|
113
|
+
let outDir;
|
|
114
|
+
let base = "/";
|
|
115
|
+
let serve = false;
|
|
116
|
+
let logger;
|
|
117
|
+
return {
|
|
118
|
+
name: "@patchwork/static",
|
|
119
|
+
configResolved(config) {
|
|
120
|
+
sources = resolveStatic(options, config.root);
|
|
121
|
+
root = config.root;
|
|
122
|
+
outDir = resolve(config.root, config.build.outDir);
|
|
123
|
+
base = config.base;
|
|
124
|
+
serve = config.command === "serve";
|
|
125
|
+
logger = config.logger;
|
|
126
|
+
},
|
|
127
|
+
async closeBundle() {
|
|
128
|
+
// also called when a dev server shuts down, which has nothing to copy
|
|
129
|
+
if (serve)
|
|
130
|
+
return;
|
|
131
|
+
for (const source of sources) {
|
|
132
|
+
const paths = source.file ? [""] : await files(source.path);
|
|
133
|
+
const kept = [];
|
|
134
|
+
for (const path of paths) {
|
|
135
|
+
if (path === source.watch)
|
|
136
|
+
continue;
|
|
137
|
+
// a package that doesn't declare a static directory mounts its whole
|
|
138
|
+
// root, and its manifest isn't part of the site
|
|
139
|
+
if (path === "package.json" && source.packageDirectory)
|
|
140
|
+
continue;
|
|
141
|
+
const to = join(outDir, `.${source.mount}`, path);
|
|
142
|
+
await mkdir(dirname(to), { recursive: true });
|
|
143
|
+
try {
|
|
144
|
+
// EXCL rather than a check, so the build's own output and public/
|
|
145
|
+
// win a collision no matter what order the copies happen in.
|
|
146
|
+
await copyFile(join(source.path, path), to, constants.COPYFILE_EXCL);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code !== "EEXIST")
|
|
150
|
+
throw error;
|
|
151
|
+
kept.push(join(source.mount, "/", path));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (kept.length) {
|
|
155
|
+
logger.info(`[patchwork] ${label(source, root)}: ${kept.length} ${kept.length === 1 ? "file was" : "files were"} already in the site and not copied over — ${kept
|
|
156
|
+
.slice(0, 5)
|
|
157
|
+
.join(", ")}${kept.length > 5 ? `, and ${kept.length - 5} more` : ""}`, { timestamp: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
configureServer(server) {
|
|
162
|
+
const watched = new Map(sources
|
|
163
|
+
.filter((source) => source.watch)
|
|
164
|
+
.map((source) => [join(source.path, source.watch), source]));
|
|
165
|
+
for (const path of watched.keys())
|
|
166
|
+
server.watcher.add(path);
|
|
167
|
+
server.watcher.on("change", (path) => {
|
|
168
|
+
if (watched.has(path))
|
|
169
|
+
server.ws.send({ type: "full-reload" });
|
|
170
|
+
});
|
|
171
|
+
// Returned, so this runs after vite's own public/static middlewares and
|
|
172
|
+
// dev matches the build: sources fill in what the site doesn't have.
|
|
173
|
+
// By then the SPA fallback has rewritten req.url to /index.html, so the
|
|
174
|
+
// path being asked for is the one connect saved on the way in.
|
|
175
|
+
return () => {
|
|
176
|
+
server.middlewares.use(async (request, response, next) => {
|
|
177
|
+
const url = request
|
|
178
|
+
.originalUrl ??
|
|
179
|
+
request.url ??
|
|
180
|
+
"/";
|
|
181
|
+
let pathname;
|
|
182
|
+
try {
|
|
183
|
+
pathname = decodeURIComponent(new URL(url, "http://localhost").pathname);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return next();
|
|
187
|
+
}
|
|
188
|
+
if (base !== "/" && pathname.startsWith(base)) {
|
|
189
|
+
pathname = pathname.slice(base.length - 1);
|
|
190
|
+
}
|
|
191
|
+
for (const source of sources) {
|
|
192
|
+
const path = locate(source, pathname);
|
|
193
|
+
if (!path)
|
|
194
|
+
continue;
|
|
195
|
+
try {
|
|
196
|
+
if (!statSync(path).isFile())
|
|
197
|
+
continue;
|
|
198
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
199
|
+
response.setHeader("Content-Type", CONTENT_TYPES[extname(path)] ?? "application/octet-stream");
|
|
200
|
+
response.end(await readFile(path));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git+https://github.com/inkandswitch/patchwork-system.git",
|
|
6
6
|
"directory": "core/patchwork"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.7.1",
|
|
9
9
|
"author": "Ink & Switch",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"license": "MIT",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"sharp": "^0.35.3",
|
|
45
45
|
"vite-plugin-wasm": "^3.6.0",
|
|
46
46
|
"@inkandswitch/patchwork-bootloader": "^0.6.2",
|
|
47
|
-
"@inkandswitch/patchwork-plugins": "^1.2.0",
|
|
48
47
|
"@inkandswitch/patchwork-filesystem": "^0.2.5",
|
|
49
|
-
"@inkandswitch/patchwork-
|
|
50
|
-
"@inkandswitch/patchwork-
|
|
48
|
+
"@inkandswitch/patchwork-providers": "^0.5.0",
|
|
49
|
+
"@inkandswitch/patchwork-plugins": "^1.2.0",
|
|
50
|
+
"@inkandswitch/patchwork-elements": "^6.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"rollup": "^4.61.1",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
8
|
+
import { resolveStatic } from "./static-plugin.js";
|
|
9
|
+
|
|
10
|
+
function revision(directory: string) {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
13
|
+
cwd: directory,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
+
}).trim();
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a package directory is: its name and version, plus the revision it was
|
|
24
|
+
* built from when it isn't an installed copy. A workspace link or a checkout
|
|
25
|
+
* resolves outside node_modules, and its version alone doesn't say which build
|
|
26
|
+
* of it this was.
|
|
27
|
+
*/
|
|
28
|
+
function describe(directory: string) {
|
|
29
|
+
const manifest = JSON.parse(
|
|
30
|
+
readFileSync(join(directory, "package.json"), "utf8")
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
name: manifest.name,
|
|
34
|
+
version: manifest.version,
|
|
35
|
+
revision: directory.split(sep).includes("node_modules")
|
|
36
|
+
? undefined
|
|
37
|
+
: revision(directory),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writes build-info.json: what this site was built from. The site's own
|
|
43
|
+
* revision, the version of patchwork that built it, and every `static` source,
|
|
44
|
+
* plus whatever extra fields the site passes as the option value.
|
|
45
|
+
*/
|
|
46
|
+
export function buildInfoPlugin(
|
|
47
|
+
options: PatchworkVitePluginOptions = {}
|
|
48
|
+
): Plugin | null {
|
|
49
|
+
if (!options.buildInfo) return null;
|
|
50
|
+
let root: string;
|
|
51
|
+
let outDir: string;
|
|
52
|
+
let serve = false;
|
|
53
|
+
return {
|
|
54
|
+
name: "@patchwork/build-info",
|
|
55
|
+
configResolved(config) {
|
|
56
|
+
root = config.root;
|
|
57
|
+
outDir = join(config.root, config.build.outDir);
|
|
58
|
+
serve = config.command === "serve";
|
|
59
|
+
},
|
|
60
|
+
async closeBundle() {
|
|
61
|
+
if (serve) return;
|
|
62
|
+
const sources = resolveStatic(options, root).map((source) =>
|
|
63
|
+
source.packageDirectory
|
|
64
|
+
? { from: source.from, ...describe(source.packageDirectory) }
|
|
65
|
+
: { from: source.from, revision: revision(source.path) }
|
|
66
|
+
);
|
|
67
|
+
await writeFile(
|
|
68
|
+
join(outDir, "build-info.json"),
|
|
69
|
+
`${JSON.stringify(
|
|
70
|
+
{
|
|
71
|
+
site: { revision: revision(root) },
|
|
72
|
+
patchwork: describe(
|
|
73
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
74
|
+
),
|
|
75
|
+
static: sources.length ? sources : undefined,
|
|
76
|
+
...(options.buildInfo === true ? {} : options.buildInfo),
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2
|
|
80
|
+
)}\n`
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/vite/dev-plugin.ts
CHANGED
|
@@ -23,6 +23,10 @@ const stylesheets: Record<string, string> = {
|
|
|
23
23
|
),
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const builtinPaths = new Map(
|
|
27
|
+
Object.entries(builtins).map(([id, fileName]) => [fileName, id])
|
|
28
|
+
);
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* Workers are `type: "module"` scripts the browser fetches directly, so import
|
|
28
32
|
* maps don't apply to them and their heavy imports have to resolve to real
|
|
@@ -117,6 +121,21 @@ export function devPlugin(options: PatchworkVitePluginOptions = {}): Plugin {
|
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
// A worker script is fetched by URL, and import maps don't apply to
|
|
125
|
+
// those, so code that starts one reaches for the /packages/… path the
|
|
126
|
+
// build emits. In dev those are the optimized deps the page's import
|
|
127
|
+
// map points at — same module, one URL over.
|
|
128
|
+
const builtin = builtinPaths.get(pathname);
|
|
129
|
+
if (builtin) {
|
|
130
|
+
response.statusCode = 302;
|
|
131
|
+
response.setHeader(
|
|
132
|
+
"Location",
|
|
133
|
+
`/@id/${encodeURI(devDependencyId(builtin))}`
|
|
134
|
+
);
|
|
135
|
+
response.end();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
120
139
|
const binary = wasm.get(pathname);
|
|
121
140
|
if (binary) {
|
|
122
141
|
try {
|
|
@@ -8,6 +8,8 @@ import { iconsPlugin } from "./icons.js";
|
|
|
8
8
|
import { htmlPlugin } from "./html-plugin.js";
|
|
9
9
|
import { manifestPlugin } from "./manifest-plugin.js";
|
|
10
10
|
import { netlifyPlugin } from "./netlify-plugin.js";
|
|
11
|
+
import { staticPlugin, type PatchworkStaticSource } from "./static-plugin.js";
|
|
12
|
+
import { buildInfoPlugin } from "./build-info-plugin.js";
|
|
11
13
|
import type { PatchworkSiteOptions } from "../site-kit/options.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -42,6 +44,8 @@ export default function patchwork(options?: PatchworkVitePluginOptions) {
|
|
|
42
44
|
importmap(options),
|
|
43
45
|
serviceworker(),
|
|
44
46
|
devPlugin(options),
|
|
47
|
+
staticPlugin(options),
|
|
48
|
+
buildInfoPlugin(options),
|
|
45
49
|
].filter((plugin): plugin is Plugin => plugin != null);
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -51,6 +55,8 @@ export type ImportMap = {
|
|
|
51
55
|
scopes?: { [scope: string]: Imports };
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
export type { PatchworkStaticSource } from "./static-plugin.js";
|
|
59
|
+
|
|
54
60
|
export type {
|
|
55
61
|
PatchworkSiteOptions,
|
|
56
62
|
PatchworkIconsOptions,
|
|
@@ -63,6 +69,20 @@ export type {
|
|
|
63
69
|
export interface PatchworkVitePluginOptions extends PatchworkSiteOptions {
|
|
64
70
|
importmap?: ImportMap;
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* File trees to mount into the site, in order of precedence — a package of
|
|
74
|
+
* Patchwork modules, a sibling repo's build output, a hand-written
|
|
75
|
+
* modules.json. Served in dev, copied into the site at build, and never
|
|
76
|
+
* overwriting the site's own files. A bare string is `{from: string}`.
|
|
77
|
+
*/
|
|
78
|
+
static?: (string | PatchworkStaticSource)[];
|
|
79
|
+
/**
|
|
80
|
+
* Write build-info.json: this site's revision, the patchwork version that
|
|
81
|
+
* built it, and every `static` source. An object is merged into it, for
|
|
82
|
+
* whatever else the site wants recorded.
|
|
83
|
+
*/
|
|
84
|
+
buildInfo?: boolean | Record<string, unknown>;
|
|
85
|
+
|
|
66
86
|
server?: false | ServerOptions;
|
|
67
87
|
preview?: false | PreviewOptions;
|
|
68
88
|
worker?: false | { format?: "es" | "iife" };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Logger, Plugin } from "vite";
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
3
|
+
import { constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { copyFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import {
|
|
6
|
+
basename,
|
|
7
|
+
dirname,
|
|
8
|
+
extname,
|
|
9
|
+
isAbsolute,
|
|
10
|
+
join,
|
|
11
|
+
relative,
|
|
12
|
+
resolve,
|
|
13
|
+
sep,
|
|
14
|
+
} from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import type { PatchworkVitePluginOptions } from "./patchwork-plugin.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A tree of files to mount into the site — a package of Patchwork modules, a
|
|
20
|
+
* sibling repo's build output, a single hand-written file.
|
|
21
|
+
*
|
|
22
|
+
* `from` is either a package specifier or a path relative to the site root. A
|
|
23
|
+
* package says where its static tree lives with a `"patchwork": {"static":
|
|
24
|
+
* "static-dist"}` field in its own package.json; without one, the whole
|
|
25
|
+
* package directory is mounted.
|
|
26
|
+
*/
|
|
27
|
+
export interface PatchworkStaticSource {
|
|
28
|
+
from: string;
|
|
29
|
+
/**
|
|
30
|
+
* URL path to mount `from` at. Defaults to `/`. For a file source a trailing
|
|
31
|
+
* slash means "this directory, under the file's own name", so `{from:
|
|
32
|
+
* "build/modules.json"}` lands at `/modules.json`.
|
|
33
|
+
*/
|
|
34
|
+
to?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Path, within `from`, of a file another build touches when it finishes.
|
|
37
|
+
* Writing to it full-reloads the dev page, and it is never copied into the
|
|
38
|
+
* site. This is how a sibling repo in watch mode drives the site's dev
|
|
39
|
+
* server.
|
|
40
|
+
*/
|
|
41
|
+
watch?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
45
|
+
".css": "text/css",
|
|
46
|
+
".html": "text/html",
|
|
47
|
+
".js": "text/javascript",
|
|
48
|
+
".json": "application/json",
|
|
49
|
+
".map": "application/json",
|
|
50
|
+
".png": "image/png",
|
|
51
|
+
".svg": "image/svg+xml",
|
|
52
|
+
".wasm": "application/wasm",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Never site content, whatever a source directory happens to contain. */
|
|
56
|
+
const SKIP = new Set(["node_modules", ".git"]);
|
|
57
|
+
|
|
58
|
+
export interface ResolvedStaticSource extends PatchworkStaticSource {
|
|
59
|
+
/** absolute path of the file or directory `from` resolved to */
|
|
60
|
+
path: string;
|
|
61
|
+
/** normalised mount point: "" for the site root, else "/packages/x" */
|
|
62
|
+
mount: string;
|
|
63
|
+
file: boolean;
|
|
64
|
+
/** set when `from` was a package specifier rather than a path */
|
|
65
|
+
packageDirectory?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolved from the site, not from here: the site is what depends on these
|
|
70
|
+
* packages, and under pnpm this plugin lives somewhere that can't see them.
|
|
71
|
+
*/
|
|
72
|
+
function packageDirectory(name: string, root: string) {
|
|
73
|
+
const require = createRequire(join(root, "package.json"));
|
|
74
|
+
try {
|
|
75
|
+
return dirname(require.resolve(`${name}/package.json`));
|
|
76
|
+
} catch {
|
|
77
|
+
// an "exports" map can hide both the manifest and the package's own main,
|
|
78
|
+
// so fall back to looking for it where node would have found it
|
|
79
|
+
for (const directory of require.resolve.paths(name) ?? []) {
|
|
80
|
+
if (existsSync(join(directory, name, "package.json"))) {
|
|
81
|
+
return join(directory, name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[patchwork] can't find the package "${name}" — a static source has to be a dependency of the site`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveStatic(
|
|
91
|
+
options: PatchworkVitePluginOptions,
|
|
92
|
+
root: string
|
|
93
|
+
): ResolvedStaticSource[] {
|
|
94
|
+
return (options.static ?? []).map((source) => {
|
|
95
|
+
const entry = typeof source === "string" ? { from: source } : source;
|
|
96
|
+
const isPath = entry.from.startsWith(".") || isAbsolute(entry.from);
|
|
97
|
+
const directory = isPath ? undefined : packageDirectory(entry.from, root);
|
|
98
|
+
const path = directory
|
|
99
|
+
? join(
|
|
100
|
+
directory,
|
|
101
|
+
JSON.parse(readFileSync(join(directory, "package.json"), "utf8"))
|
|
102
|
+
.patchwork?.static ?? "."
|
|
103
|
+
)
|
|
104
|
+
: resolve(root, entry.from);
|
|
105
|
+
if (!existsSync(path)) {
|
|
106
|
+
throw new Error(`[patchwork] static source not found: ${entry.from}`);
|
|
107
|
+
}
|
|
108
|
+
const file = statSync(path).isFile();
|
|
109
|
+
const to = entry.to ?? "/";
|
|
110
|
+
return {
|
|
111
|
+
...entry,
|
|
112
|
+
path,
|
|
113
|
+
file,
|
|
114
|
+
packageDirectory: directory,
|
|
115
|
+
mount: (file && to.endsWith("/") ? join(to, basename(path)) : to).replace(
|
|
116
|
+
/\/+$/,
|
|
117
|
+
""
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function locate(source: ResolvedStaticSource, pathname: string) {
|
|
124
|
+
if (source.file) return pathname === source.mount ? source.path : undefined;
|
|
125
|
+
if (pathname !== source.mount && !pathname.startsWith(`${source.mount}/`)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const path = resolve(source.path, `.${pathname.slice(source.mount.length)}`);
|
|
129
|
+
const within = relative(source.path, path);
|
|
130
|
+
if (within.startsWith("..")) return undefined;
|
|
131
|
+
if (within.split(sep).some((part) => SKIP.has(part))) return undefined;
|
|
132
|
+
if (within === source.watch) return undefined;
|
|
133
|
+
if (within === "package.json" && source.packageDirectory) return undefined;
|
|
134
|
+
return path;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** How a source is named in messages: a specifier, or the shorter of the paths. */
|
|
138
|
+
function label(source: ResolvedStaticSource, root: string) {
|
|
139
|
+
if (source.packageDirectory) return source.from;
|
|
140
|
+
const path = relative(root, source.path);
|
|
141
|
+
return path.split(sep).filter((part) => part === "..").length > 1
|
|
142
|
+
? source.path
|
|
143
|
+
: path;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function files(source: string, directory = source): Promise<string[]> {
|
|
147
|
+
const paths: string[] = [];
|
|
148
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
149
|
+
if (SKIP.has(entry.name)) continue;
|
|
150
|
+
const path = join(directory, entry.name);
|
|
151
|
+
if (entry.isDirectory()) paths.push(...(await files(source, path)));
|
|
152
|
+
if (entry.isFile()) paths.push(relative(source, path));
|
|
153
|
+
}
|
|
154
|
+
return paths;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mounts `static` sources: served in dev, copied into the site at build.
|
|
159
|
+
*
|
|
160
|
+
* Sources never overwrite the site's own output — neither the build's, nor
|
|
161
|
+
* `public/`, nor an earlier source in the list. So precedence is just list
|
|
162
|
+
* order, and a site keeps its own `modules.json` by listing it before the
|
|
163
|
+
* package it borrows the rest of the packages from.
|
|
164
|
+
*/
|
|
165
|
+
export function staticPlugin(
|
|
166
|
+
options: PatchworkVitePluginOptions = {}
|
|
167
|
+
): Plugin | null {
|
|
168
|
+
if (!options.static?.length) return null;
|
|
169
|
+
let sources: ResolvedStaticSource[] = [];
|
|
170
|
+
let root: string;
|
|
171
|
+
let outDir: string;
|
|
172
|
+
let base = "/";
|
|
173
|
+
let serve = false;
|
|
174
|
+
let logger: Logger;
|
|
175
|
+
return {
|
|
176
|
+
name: "@patchwork/static",
|
|
177
|
+
configResolved(config) {
|
|
178
|
+
sources = resolveStatic(options, config.root);
|
|
179
|
+
root = config.root;
|
|
180
|
+
outDir = resolve(config.root, config.build.outDir);
|
|
181
|
+
base = config.base;
|
|
182
|
+
serve = config.command === "serve";
|
|
183
|
+
logger = config.logger;
|
|
184
|
+
},
|
|
185
|
+
async closeBundle() {
|
|
186
|
+
// also called when a dev server shuts down, which has nothing to copy
|
|
187
|
+
if (serve) return;
|
|
188
|
+
for (const source of sources) {
|
|
189
|
+
const paths = source.file ? [""] : await files(source.path);
|
|
190
|
+
const kept: string[] = [];
|
|
191
|
+
for (const path of paths) {
|
|
192
|
+
if (path === source.watch) continue;
|
|
193
|
+
// a package that doesn't declare a static directory mounts its whole
|
|
194
|
+
// root, and its manifest isn't part of the site
|
|
195
|
+
if (path === "package.json" && source.packageDirectory) continue;
|
|
196
|
+
const to = join(outDir, `.${source.mount}`, path);
|
|
197
|
+
await mkdir(dirname(to), { recursive: true });
|
|
198
|
+
try {
|
|
199
|
+
// EXCL rather than a check, so the build's own output and public/
|
|
200
|
+
// win a collision no matter what order the copies happen in.
|
|
201
|
+
await copyFile(
|
|
202
|
+
join(source.path, path),
|
|
203
|
+
to,
|
|
204
|
+
constants.COPYFILE_EXCL
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
208
|
+
kept.push(join(source.mount, "/", path));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (kept.length) {
|
|
212
|
+
logger.info(
|
|
213
|
+
`[patchwork] ${label(source, root)}: ${kept.length} ${
|
|
214
|
+
kept.length === 1 ? "file was" : "files were"
|
|
215
|
+
} already in the site and not copied over — ${kept
|
|
216
|
+
.slice(0, 5)
|
|
217
|
+
.join(
|
|
218
|
+
", "
|
|
219
|
+
)}${kept.length > 5 ? `, and ${kept.length - 5} more` : ""}`,
|
|
220
|
+
{ timestamp: true }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
configureServer(server) {
|
|
226
|
+
const watched = new Map(
|
|
227
|
+
sources
|
|
228
|
+
.filter((source) => source.watch)
|
|
229
|
+
.map((source) => [join(source.path, source.watch!), source])
|
|
230
|
+
);
|
|
231
|
+
for (const path of watched.keys()) server.watcher.add(path);
|
|
232
|
+
server.watcher.on("change", (path) => {
|
|
233
|
+
if (watched.has(path)) server.ws.send({ type: "full-reload" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Returned, so this runs after vite's own public/static middlewares and
|
|
237
|
+
// dev matches the build: sources fill in what the site doesn't have.
|
|
238
|
+
// By then the SPA fallback has rewritten req.url to /index.html, so the
|
|
239
|
+
// path being asked for is the one connect saved on the way in.
|
|
240
|
+
return () => {
|
|
241
|
+
server.middlewares.use(async (request, response, next) => {
|
|
242
|
+
const url =
|
|
243
|
+
(request as IncomingMessage & { originalUrl?: string })
|
|
244
|
+
.originalUrl ??
|
|
245
|
+
request.url ??
|
|
246
|
+
"/";
|
|
247
|
+
let pathname: string;
|
|
248
|
+
try {
|
|
249
|
+
pathname = decodeURIComponent(
|
|
250
|
+
new URL(url, "http://localhost").pathname
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
return next();
|
|
254
|
+
}
|
|
255
|
+
if (base !== "/" && pathname.startsWith(base)) {
|
|
256
|
+
pathname = pathname.slice(base.length - 1);
|
|
257
|
+
}
|
|
258
|
+
for (const source of sources) {
|
|
259
|
+
const path = locate(source, pathname);
|
|
260
|
+
if (!path) continue;
|
|
261
|
+
try {
|
|
262
|
+
if (!statSync(path).isFile()) continue;
|
|
263
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
264
|
+
response.setHeader(
|
|
265
|
+
"Content-Type",
|
|
266
|
+
CONTENT_TYPES[extname(path)] ?? "application/octet-stream"
|
|
267
|
+
);
|
|
268
|
+
response.end(await readFile(path));
|
|
269
|
+
return;
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
next();
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|