quicklook-kit 0.5.0 → 0.7.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.
- package/README.markdown +146 -60
- package/dist/cli.mjs +250 -34
- package/dist/config-qdxXRvMf.mjs +271 -0
- package/dist/config.d.mts +64 -9
- package/dist/config.mjs +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/runtime.mjs +1 -2
- package/dist/vite.d.mts +1 -3
- package/dist/vite.mjs +32 -9
- package/native/App/HostApp.swift +224 -8
- package/native/Extension/PreviewViewController.swift +8 -1
- package/native/Shared/Settings.swift +129 -0
- package/native/Windows/Plugin.cs +232 -0
- package/package.json +6 -4
- package/src/virtual.d.ts +2 -0
- package/dist/config-BQ-9qYNV.mjs +0 -113
package/dist/cli.mjs
CHANGED
|
@@ -1,29 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as serializeCsp, i as loadConfig, t as addSources } from "./config-
|
|
2
|
+
import { a as serializeCsp, i as loadConfig, t as addSources } from "./config-qdxXRvMf.mjs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
-
import { cp, glob, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { cp, glob, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
|
|
10
|
+
import { parseArgs } from "node:util";
|
|
11
|
+
// src/windows.ts
|
|
12
|
+
const xmlEscapes = {
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">"
|
|
16
|
+
};
|
|
17
|
+
const xmlSpecial = /[&<>]/g;
|
|
18
|
+
function xml(text) {
|
|
19
|
+
return text.replace(xmlSpecial, (char) => xmlEscapes[char] ?? char);
|
|
20
|
+
}
|
|
21
|
+
/** The settings schema as a flat list keyed like QL-Win's settings file, `editor.fontSize`. */
|
|
22
|
+
function flatSettings(settings, prefix = "") {
|
|
23
|
+
return Object.entries(settings).flatMap(([name, setting]) => {
|
|
24
|
+
const key = prefix + name;
|
|
25
|
+
if (setting.type === "object") return flatSettings(setting.properties, `${key}.`);
|
|
26
|
+
const { type, default: fallback } = setting;
|
|
27
|
+
const { enum: options, minimum, maximum } = setting;
|
|
28
|
+
return [{
|
|
29
|
+
key,
|
|
30
|
+
type,
|
|
31
|
+
default: fallback,
|
|
32
|
+
...options && { enum: options.map((option) => typeof option === "object" && option ? option.value : option) },
|
|
33
|
+
...minimum !== void 0 && { minimum },
|
|
34
|
+
...maximum !== void 0 && { maximum }
|
|
35
|
+
}];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The plugin project's generated files, by name. The CLI adds the host source and `web/`. */
|
|
39
|
+
function windowsProject(config, version) {
|
|
40
|
+
const { id, extensions } = config.windows;
|
|
41
|
+
return {
|
|
42
|
+
[`${id}.csproj`]: `<Project Sdk="Microsoft.NET.Sdk">
|
|
43
|
+
<PropertyGroup>
|
|
44
|
+
<TargetFramework>net462</TargetFramework>
|
|
45
|
+
<AssemblyName>${id}</AssemblyName>
|
|
46
|
+
<RootNamespace>${id}</RootNamespace>
|
|
47
|
+
<Version>${xml(version)}</Version>
|
|
48
|
+
<LangVersion>latest</LangVersion>
|
|
49
|
+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
|
50
|
+
</PropertyGroup>
|
|
51
|
+
|
|
52
|
+
<ItemGroup>
|
|
53
|
+
<!-- Lets a non-Windows SDK compile against .NET Framework, WPF included, without UseWPF -->
|
|
54
|
+
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
|
55
|
+
<!-- QL-Win loads QuickLook.Common itself, so compile against it but don't ship it -->
|
|
56
|
+
<PackageReference Include="QuickLook.Common" Version="4.5.0" ExcludeAssets="runtime" />
|
|
57
|
+
<!-- Same version as QL-Win's built-in viewers -->
|
|
58
|
+
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4078.44" />
|
|
59
|
+
<Reference Include="PresentationCore" />
|
|
60
|
+
<Reference Include="PresentationFramework" />
|
|
61
|
+
<Reference Include="WindowsBase" />
|
|
62
|
+
<Reference Include="System.Xaml" />
|
|
63
|
+
<Reference Include="System.Web" />
|
|
64
|
+
<Reference Include="System.Web.Extensions" />
|
|
65
|
+
</ItemGroup>
|
|
66
|
+
|
|
67
|
+
<ItemGroup>
|
|
68
|
+
<None Include="QuickLook.Plugin.Metadata.config" CopyToOutputDirectory="PreserveNewest" />
|
|
69
|
+
<None Include="quicklook.json" CopyToOutputDirectory="PreserveNewest" />
|
|
70
|
+
<None Include="web/**" CopyToOutputDirectory="PreserveNewest" />
|
|
71
|
+
</ItemGroup>
|
|
72
|
+
</Project>
|
|
73
|
+
`,
|
|
74
|
+
"QuickLook.Plugin.Metadata.config": `<?xml version="1.0" encoding="utf-8"?>
|
|
75
|
+
<Metadata>
|
|
76
|
+
<Namespace>${id}</Namespace>
|
|
77
|
+
<Version>${xml(version)}</Version>
|
|
78
|
+
<Description>${xml(config.name)}</Description>
|
|
79
|
+
</Metadata>
|
|
80
|
+
`,
|
|
81
|
+
"quicklook.json": JSON.stringify({
|
|
82
|
+
id,
|
|
83
|
+
extensions: extensions.map((extension) => extension.toLowerCase()),
|
|
84
|
+
maxFileSize: config.maxFileSize,
|
|
85
|
+
csp: serializeCsp(config.csp),
|
|
86
|
+
settings: flatSettings(config.settings)
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// src/cli.ts
|
|
91
|
+
const platforms = ["macos", "windows"];
|
|
92
|
+
const { positionals: [subcommand], values: { platform } } = parseArgs({
|
|
93
|
+
allowPositionals: true,
|
|
94
|
+
options: { platform: { type: "string" } }
|
|
95
|
+
});
|
|
96
|
+
if (platform !== void 0 && !platforms.includes(platform)) throw new Error(`--platform: expected "macos" or "windows", got "${platform}"`);
|
|
11
97
|
const config = await loadConfig();
|
|
12
|
-
const { root, name,
|
|
98
|
+
const { root, name, framework } = config;
|
|
13
99
|
const target = name.replace(/[^A-Za-z0-9]/g, "");
|
|
14
100
|
const work = `${root}/.quicklook`;
|
|
15
101
|
const web = `${work}/web`;
|
|
16
|
-
const built = `${work}/build/${name}.app`;
|
|
102
|
+
const built = `${work}/build/macos/${name}.app`;
|
|
17
103
|
const installed = `/Applications/${name}.app`;
|
|
104
|
+
const version = process.env.VERSION?.replace(/^v/, "") ?? "1.0";
|
|
105
|
+
const onWindows = process.platform === "win32";
|
|
18
106
|
const native = fileURLToPath(new URL("../native", import.meta.url));
|
|
19
107
|
const lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
20
|
-
const leadingV = /^v/;
|
|
21
108
|
const inlineScript = /<script\b(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
109
|
+
/**
|
|
110
|
+
* The settings schema as nested arrays, each entry with its own `key`: Swift's
|
|
111
|
+
* JSONSerialization decodes objects into unordered dictionaries.
|
|
112
|
+
*/
|
|
113
|
+
function settingsSchema(settings) {
|
|
114
|
+
return Object.entries(settings).map(([key, setting]) => setting.type === "object" ? {
|
|
115
|
+
...setting,
|
|
116
|
+
key,
|
|
117
|
+
properties: settingsSchema(setting.properties)
|
|
118
|
+
} : {
|
|
119
|
+
...setting,
|
|
120
|
+
key
|
|
121
|
+
});
|
|
122
|
+
}
|
|
22
123
|
/** Runs a command, exiting with its status if it fails unless `nothrow` is set. */
|
|
23
|
-
function exec([file = "", ...args], { quiet = false, nothrow = false, env = process.env } = {}) {
|
|
124
|
+
function exec([file = "", ...args], { quiet = false, nothrow = false, env = process.env, cwd = root, shell = false } = {}) {
|
|
24
125
|
const { status, error } = spawnSync(file, args, {
|
|
25
|
-
cwd
|
|
126
|
+
cwd,
|
|
26
127
|
env,
|
|
128
|
+
shell,
|
|
27
129
|
stdio: quiet ? "ignore" : "inherit"
|
|
28
130
|
});
|
|
29
131
|
if (error) throw error;
|
|
@@ -37,12 +139,15 @@ function binPath() {
|
|
|
37
139
|
if (dir === dirname(dir)) return [...dirs, process.env.PATH].join(delimiter);
|
|
38
140
|
}
|
|
39
141
|
}
|
|
40
|
-
/** Runs a framework command with the project's own binaries. */
|
|
142
|
+
/** Runs a framework command with the project's own binaries, which are `.cmd` shims on Windows. */
|
|
41
143
|
function run(command) {
|
|
42
|
-
exec(command
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
144
|
+
exec(onWindows ? [command.join(" ")] : command, {
|
|
145
|
+
env: {
|
|
146
|
+
...process.env,
|
|
147
|
+
PATH: binPath()
|
|
148
|
+
},
|
|
149
|
+
shell: onWindows
|
|
150
|
+
});
|
|
46
151
|
}
|
|
47
152
|
/** Frameworks bootstrap with inline scripts; the CSP allows exactly those, by hash. */
|
|
48
153
|
async function inlineScriptHashes() {
|
|
@@ -67,18 +172,35 @@ async function buildWeb() {
|
|
|
67
172
|
});
|
|
68
173
|
await cp(outDir, web, { recursive: true });
|
|
69
174
|
}
|
|
175
|
+
function group(key) {
|
|
176
|
+
const options = config[key];
|
|
177
|
+
if (!options) throw new Error(`quicklook.config has no "${key}" group, so there is nothing to build for it.`);
|
|
178
|
+
return options;
|
|
179
|
+
}
|
|
180
|
+
/** Builds every configured platform, or just `--platform`. */
|
|
70
181
|
async function build() {
|
|
182
|
+
const targets = platform ? [platform] : platforms.filter((p) => config[p]);
|
|
183
|
+
targets.forEach(group);
|
|
184
|
+
if (targets.includes("macos") && process.platform !== "darwin") {
|
|
185
|
+
if (platform) throw new Error("Building for macOS needs a Mac.");
|
|
186
|
+
console.log("Skipping macOS, which only builds on a Mac.");
|
|
187
|
+
targets.splice(targets.indexOf("macos"), 1);
|
|
188
|
+
}
|
|
71
189
|
await buildWeb();
|
|
72
190
|
const csp = addSources(config.csp, "script-src", ...await inlineScriptHashes());
|
|
191
|
+
if (targets.includes("macos")) await buildMacos(csp);
|
|
192
|
+
if (targets.includes("windows")) await buildWindows(csp);
|
|
193
|
+
}
|
|
194
|
+
async function buildMacos(csp) {
|
|
195
|
+
const { bundleId, minimumVersion, types, icon } = group("macos");
|
|
196
|
+
const hasSettings = Object.keys(config.settings).length > 0;
|
|
197
|
+
const qlSettings = hasSettings && { QLSettings: JSON.stringify(settingsSchema(config.settings)) };
|
|
73
198
|
const spec = {
|
|
74
199
|
name: target,
|
|
75
|
-
options: {
|
|
76
|
-
deploymentTarget: { macOS },
|
|
77
|
-
createIntermediateGroups: true
|
|
78
|
-
},
|
|
200
|
+
options: { deploymentTarget: { macOS: minimumVersion } },
|
|
79
201
|
settings: { base: {
|
|
80
202
|
SWIFT_VERSION: "6.0",
|
|
81
|
-
MARKETING_VERSION:
|
|
203
|
+
MARKETING_VERSION: version,
|
|
82
204
|
CURRENT_PROJECT_VERSION: "1",
|
|
83
205
|
CODE_SIGN_STYLE: "Manual",
|
|
84
206
|
CODE_SIGN_IDENTITY: "-"
|
|
@@ -87,10 +209,14 @@ async function build() {
|
|
|
87
209
|
[target]: {
|
|
88
210
|
type: "application",
|
|
89
211
|
platform: "macOS",
|
|
90
|
-
sources: [
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
212
|
+
sources: [
|
|
213
|
+
`${native}/App`,
|
|
214
|
+
`${native}/Shared`,
|
|
215
|
+
...icon ? [{
|
|
216
|
+
path: resolve(root, icon),
|
|
217
|
+
buildPhase: "resources"
|
|
218
|
+
}] : []
|
|
219
|
+
],
|
|
94
220
|
settings: {
|
|
95
221
|
PRODUCT_NAME: name,
|
|
96
222
|
PRODUCT_BUNDLE_IDENTIFIER: bundleId
|
|
@@ -105,7 +231,8 @@ async function build() {
|
|
|
105
231
|
properties: {
|
|
106
232
|
CFBundleName: name,
|
|
107
233
|
...icon && { CFBundleIconFile: basename(icon, ".icns") },
|
|
108
|
-
LSMinimumSystemVersion:
|
|
234
|
+
LSMinimumSystemVersion: minimumVersion,
|
|
235
|
+
...qlSettings,
|
|
109
236
|
UTExportedTypeDeclarations: types.filter((type) => typeof type !== "string").map((type) => ({
|
|
110
237
|
UTTypeIdentifier: type.id,
|
|
111
238
|
UTTypeDescription: type.description,
|
|
@@ -118,11 +245,15 @@ async function build() {
|
|
|
118
245
|
[`${target}Extension`]: {
|
|
119
246
|
type: "app-extension",
|
|
120
247
|
platform: "macOS",
|
|
121
|
-
sources: [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
248
|
+
sources: [
|
|
249
|
+
`${native}/Extension`,
|
|
250
|
+
`${native}/Shared`,
|
|
251
|
+
{
|
|
252
|
+
path: web,
|
|
253
|
+
type: "folder",
|
|
254
|
+
buildPhase: "resources"
|
|
255
|
+
}
|
|
256
|
+
],
|
|
126
257
|
settings: { PRODUCT_BUNDLE_IDENTIFIER: `${bundleId}.Extension` },
|
|
127
258
|
dependencies: [{ sdk: "QuickLookUI.framework" }, { sdk: "WebKit.framework" }],
|
|
128
259
|
entitlements: {
|
|
@@ -130,7 +261,8 @@ async function build() {
|
|
|
130
261
|
properties: {
|
|
131
262
|
"com.apple.security.app-sandbox": true,
|
|
132
263
|
"com.apple.security.files.user-selected.read-only": true,
|
|
133
|
-
"com.apple.security.network.client": true
|
|
264
|
+
"com.apple.security.network.client": true,
|
|
265
|
+
...hasSettings && { "com.apple.security.temporary-exception.shared-preference.read-only": [bundleId] }
|
|
134
266
|
}
|
|
135
267
|
},
|
|
136
268
|
info: {
|
|
@@ -139,6 +271,7 @@ async function build() {
|
|
|
139
271
|
CFBundleDisplayName: name,
|
|
140
272
|
QLWebContentSecurityPolicy: serializeCsp(csp),
|
|
141
273
|
QLMaxFileSize: config.maxFileSize,
|
|
274
|
+
...qlSettings,
|
|
142
275
|
NSExtension: {
|
|
143
276
|
NSExtensionPointIdentifier: "com.apple.quicklook.preview",
|
|
144
277
|
NSExtensionPrincipalClass: "PreviewViewController",
|
|
@@ -174,13 +307,82 @@ async function build() {
|
|
|
174
307
|
"Release",
|
|
175
308
|
"-destination",
|
|
176
309
|
"generic/platform=macOS",
|
|
177
|
-
`CONFIGURATION_BUILD_DIR=${
|
|
310
|
+
`CONFIGURATION_BUILD_DIR=${dirname(built)}`,
|
|
178
311
|
"build"
|
|
179
312
|
]);
|
|
180
313
|
exec(["touch", built]);
|
|
181
314
|
console.log(`Built ${built}`);
|
|
182
315
|
}
|
|
183
|
-
|
|
316
|
+
/** The plugin folder `register` copies, and the `.qlplugin` users install. */
|
|
317
|
+
function windowsBuild() {
|
|
318
|
+
const { id } = group("windows");
|
|
319
|
+
return {
|
|
320
|
+
plugin: `${work}/build/windows/${id}`,
|
|
321
|
+
archive: `${work}/build/windows/${name}.qlplugin`
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async function buildWindows(csp) {
|
|
325
|
+
const windows = group("windows");
|
|
326
|
+
const { plugin, archive } = windowsBuild();
|
|
327
|
+
const project = `${work}/windows`;
|
|
328
|
+
await rm(`${project}/web`, {
|
|
329
|
+
recursive: true,
|
|
330
|
+
force: true
|
|
331
|
+
});
|
|
332
|
+
await cp(web, `${project}/web`, { recursive: true });
|
|
333
|
+
await cp(`${native}/Windows/Plugin.cs`, `${project}/Plugin.cs`);
|
|
334
|
+
for (const [file, content] of Object.entries(windowsProject({
|
|
335
|
+
...config,
|
|
336
|
+
csp,
|
|
337
|
+
windows
|
|
338
|
+
}, version))) await writeFile(`${project}/${file}`, content);
|
|
339
|
+
await rm(plugin, {
|
|
340
|
+
recursive: true,
|
|
341
|
+
force: true
|
|
342
|
+
});
|
|
343
|
+
exec([
|
|
344
|
+
"dotnet",
|
|
345
|
+
"build",
|
|
346
|
+
`${project}/${windows.id}.csproj`,
|
|
347
|
+
"--configuration",
|
|
348
|
+
"Release",
|
|
349
|
+
"--output",
|
|
350
|
+
plugin,
|
|
351
|
+
"--nologo",
|
|
352
|
+
"--verbosity",
|
|
353
|
+
"quiet"
|
|
354
|
+
]);
|
|
355
|
+
const tar = onWindows ? join(process.env.SystemRoot ?? "C:\\Windows", "System32\\tar.exe") : "tar";
|
|
356
|
+
await rm(archive, { force: true });
|
|
357
|
+
exec([
|
|
358
|
+
tar,
|
|
359
|
+
"--create",
|
|
360
|
+
"--format",
|
|
361
|
+
"zip",
|
|
362
|
+
"--file",
|
|
363
|
+
archive,
|
|
364
|
+
"--exclude",
|
|
365
|
+
"*.pdb",
|
|
366
|
+
...await readdir(plugin)
|
|
367
|
+
], { cwd: plugin });
|
|
368
|
+
console.log(`Built ${archive}`);
|
|
369
|
+
}
|
|
370
|
+
/** QL-Win's folder for plugins installed by the user. */
|
|
371
|
+
const qlWinPlugins = join(process.env.APPDATA ?? "", "pooi.moe/QuickLook/QuickLook.Plugin");
|
|
372
|
+
/** Removes an installed plugin, whose DLL is locked while QuickLook runs. */
|
|
373
|
+
async function removePlugin(folder) {
|
|
374
|
+
try {
|
|
375
|
+
await rm(folder, {
|
|
376
|
+
recursive: true,
|
|
377
|
+
force: true
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const { code } = error;
|
|
381
|
+
if (code === "EBUSY" || code === "EPERM") throw new Error(`Quit QuickLook first, it has ${folder} in use.`, { cause: error });
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
switch (subcommand) {
|
|
184
386
|
case "dev":
|
|
185
387
|
run(framework.build.dev);
|
|
186
388
|
break;
|
|
@@ -191,6 +393,15 @@ switch (process.argv[2]) {
|
|
|
191
393
|
await build();
|
|
192
394
|
break;
|
|
193
395
|
case "register":
|
|
396
|
+
if (onWindows) {
|
|
397
|
+
const { plugin } = windowsBuild();
|
|
398
|
+
if (!existsSync(plugin)) throw new Error(`No build at ${plugin}, run 'quicklook build' first.`);
|
|
399
|
+
const folder = join(qlWinPlugins, group("windows").id);
|
|
400
|
+
await removePlugin(folder);
|
|
401
|
+
await cp(plugin, folder, { recursive: true });
|
|
402
|
+
console.log(`Installed ${folder}\nQuit and restart QuickLook to load it.`);
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
194
405
|
if (!existsSync(built)) throw new Error(`No build at ${built}, run 'quicklook build' first.`);
|
|
195
406
|
await rm(installed, {
|
|
196
407
|
recursive: true,
|
|
@@ -225,9 +436,15 @@ switch (process.argv[2]) {
|
|
|
225
436
|
"-g",
|
|
226
437
|
installed
|
|
227
438
|
]);
|
|
228
|
-
console.log(`Installed ${installed}\nVerify with: pluginkit -mAv | grep ${bundleId}`);
|
|
439
|
+
console.log(`Installed ${installed}\nVerify with: pluginkit -mAv | grep ${group("macos").bundleId}`);
|
|
229
440
|
break;
|
|
230
441
|
case "unregister":
|
|
442
|
+
if (onWindows) {
|
|
443
|
+
const folder = join(qlWinPlugins, group("windows").id);
|
|
444
|
+
await removePlugin(folder);
|
|
445
|
+
console.log(`Removed ${folder}\nQuit and restart QuickLook to unload it.`);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
231
448
|
exec([
|
|
232
449
|
lsregister,
|
|
233
450
|
"-u",
|
|
@@ -246,7 +463,6 @@ switch (process.argv[2]) {
|
|
|
246
463
|
});
|
|
247
464
|
console.log(`Removed ${installed}`);
|
|
248
465
|
break;
|
|
249
|
-
default: throw new Error("Usage: quicklook dev|build:web|build|register|unregister");
|
|
466
|
+
default: throw new Error("Usage: quicklook dev|build:web|build|register|unregister [--platform macos|windows]");
|
|
250
467
|
}
|
|
251
|
-
//#endregion
|
|
252
468
|
export {};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { loadConfig } from "c12";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
// src/settings.ts
|
|
6
|
+
/** The fields each setting type allows, besides `type`, `title`, `description` and `order`. */
|
|
7
|
+
const settingFields = {
|
|
8
|
+
boolean: ["default"],
|
|
9
|
+
string: [
|
|
10
|
+
"default",
|
|
11
|
+
"enum",
|
|
12
|
+
"radio"
|
|
13
|
+
],
|
|
14
|
+
integer: [
|
|
15
|
+
"default",
|
|
16
|
+
"enum",
|
|
17
|
+
"radio",
|
|
18
|
+
"minimum",
|
|
19
|
+
"maximum"
|
|
20
|
+
],
|
|
21
|
+
number: [
|
|
22
|
+
"default",
|
|
23
|
+
"enum",
|
|
24
|
+
"radio",
|
|
25
|
+
"minimum",
|
|
26
|
+
"maximum"
|
|
27
|
+
],
|
|
28
|
+
object: ["properties"]
|
|
29
|
+
};
|
|
30
|
+
const commonFields = [
|
|
31
|
+
"type",
|
|
32
|
+
"title",
|
|
33
|
+
"description",
|
|
34
|
+
"order"
|
|
35
|
+
];
|
|
36
|
+
const knownFields = /* @__PURE__ */ new Set([...commonFields, ...Object.values(settingFields).flat()]);
|
|
37
|
+
function isString(value) {
|
|
38
|
+
return typeof value === "string";
|
|
39
|
+
}
|
|
40
|
+
/** Whether `value` is a valid value of setting type `type`. */
|
|
41
|
+
const valueChecks = {
|
|
42
|
+
boolean: (value) => typeof value === "boolean",
|
|
43
|
+
string: isString,
|
|
44
|
+
integer: Number.isInteger,
|
|
45
|
+
number: Number.isFinite
|
|
46
|
+
};
|
|
47
|
+
/** Optional fields with a fixed type, and the problem when it's wrong. */
|
|
48
|
+
const fieldChecks = [
|
|
49
|
+
[
|
|
50
|
+
"title",
|
|
51
|
+
isString,
|
|
52
|
+
"title must be a string"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"description",
|
|
56
|
+
isString,
|
|
57
|
+
"description must be a string"
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"order",
|
|
61
|
+
Number.isFinite,
|
|
62
|
+
"order must be a number"
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"minimum",
|
|
66
|
+
Number.isFinite,
|
|
67
|
+
"minimum must be a number"
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"maximum",
|
|
71
|
+
Number.isFinite,
|
|
72
|
+
"maximum must be a number"
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"radio",
|
|
76
|
+
(value) => typeof value === "boolean",
|
|
77
|
+
"radio must be true or false"
|
|
78
|
+
]
|
|
79
|
+
];
|
|
80
|
+
function isRecord(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
const settingKey = /^[A-Za-z_][\w-]*$/;
|
|
84
|
+
/**
|
|
85
|
+
* Throws on anything in a `settings` schema the app can't show, naming the key
|
|
86
|
+
* path, e.g. `settings.editor.tabSize: default 0 is below minimum 1`.
|
|
87
|
+
*/
|
|
88
|
+
function validateSettings(settings, path = "settings") {
|
|
89
|
+
if (!isRecord(settings)) throw new Error(`${path}: expected an object of settings`);
|
|
90
|
+
for (const [key, entry] of Object.entries(settings)) {
|
|
91
|
+
const at = `${path}.${key}`;
|
|
92
|
+
if (!settingKey.test(key)) throw new Error(`${at}: keys need a letter or _ first, then letters, digits, _ or -`);
|
|
93
|
+
if (!isRecord(entry)) throw new Error(`${at}: expected an object with a type`);
|
|
94
|
+
const problem = settingProblem(entry);
|
|
95
|
+
if (problem) throw new Error(`${at}: ${problem}`);
|
|
96
|
+
if (entry.type === "object") validateSettings(entry.properties, at);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** What's wrong with one entry, without looking into `properties`. */
|
|
100
|
+
function settingProblem(entry) {
|
|
101
|
+
const type = String(entry.type);
|
|
102
|
+
if (!(typeof entry.type === "string" && Object.hasOwn(settingFields, type))) return `unknown type ${JSON.stringify(entry.type)}`;
|
|
103
|
+
for (const field of Object.keys(entry)) {
|
|
104
|
+
if (!knownFields.has(field)) return `unknown field "${field}"`;
|
|
105
|
+
if (!(commonFields.includes(field) || settingFields[type]?.includes(field))) return `"${field}" isn't allowed on type "${type}"`;
|
|
106
|
+
}
|
|
107
|
+
const wrong = fieldChecks.find(([field, check]) => !(entry[field] === void 0 || check(entry[field])));
|
|
108
|
+
if (wrong) return wrong[2];
|
|
109
|
+
if (type === "object") return isRecord(entry.properties) && Object.keys(entry.properties).length > 0 ? void 0 : "an object needs non-empty properties";
|
|
110
|
+
return valueProblem(entry, type);
|
|
111
|
+
}
|
|
112
|
+
/** What's wrong with the default, the range and the enum of a non-object entry. */
|
|
113
|
+
function valueProblem(entry, type) {
|
|
114
|
+
if (!("default" in entry)) return "default is required";
|
|
115
|
+
const { default: value, minimum, maximum, radio, enum: options } = entry;
|
|
116
|
+
const shown = JSON.stringify(value);
|
|
117
|
+
if (!valueChecks[type]?.(value)) return `default ${shown} isn't of type "${type}"`;
|
|
118
|
+
if (typeof minimum === "number" && typeof maximum === "number" && minimum > maximum) return `minimum ${minimum} is above maximum ${maximum}`;
|
|
119
|
+
if (typeof minimum === "number" && value < minimum) return `default ${shown} is below minimum ${minimum}`;
|
|
120
|
+
if (typeof maximum === "number" && value > maximum) return `default ${shown} is above maximum ${maximum}`;
|
|
121
|
+
if (radio && options === void 0) return "radio needs an enum";
|
|
122
|
+
return options === void 0 ? void 0 : enumProblem(options, value, type);
|
|
123
|
+
}
|
|
124
|
+
/** An object entry other than `{ value, description? }`. */
|
|
125
|
+
function malformedOption(option) {
|
|
126
|
+
return !("value" in option && Object.keys(option).every((field) => field === "value" || field === "description") && (option.description === void 0 || isString(option.description)));
|
|
127
|
+
}
|
|
128
|
+
function enumProblem(options, value, type) {
|
|
129
|
+
if (!Array.isArray(options) || options.length === 0) return "enum must be a non-empty array";
|
|
130
|
+
const values = [];
|
|
131
|
+
for (const [index, option] of options.entries()) {
|
|
132
|
+
if (isRecord(option) && malformedOption(option)) return `enum[${index}] must be a value or { value, description }`;
|
|
133
|
+
const optionValue = isRecord(option) ? option.value : option;
|
|
134
|
+
if (!valueChecks[type]?.(optionValue)) return `enum[${index}] ${JSON.stringify(optionValue)} isn't of type "${type}"`;
|
|
135
|
+
values.push(optionValue);
|
|
136
|
+
}
|
|
137
|
+
return values.includes(value) ? void 0 : `default ${JSON.stringify(value)} isn't in the enum`;
|
|
138
|
+
}
|
|
139
|
+
// src/config.ts
|
|
140
|
+
/** The previewed file is untrusted input; nothing on the page needs the network. */
|
|
141
|
+
const defaultCsp = {
|
|
142
|
+
"default-src": ["'none'"],
|
|
143
|
+
"script-src": ["'self'"],
|
|
144
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
145
|
+
"img-src": [
|
|
146
|
+
"'self'",
|
|
147
|
+
"data:",
|
|
148
|
+
"blob:"
|
|
149
|
+
],
|
|
150
|
+
"font-src": ["'self'", "data:"],
|
|
151
|
+
"media-src": [
|
|
152
|
+
"'self'",
|
|
153
|
+
"data:",
|
|
154
|
+
"blob:"
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
const defineConfig = (config) => config;
|
|
158
|
+
const serializeCsp = (csp) => Object.entries(csp).map(([directive, sources = []]) => [directive, ...sources].join(" ")).join("; ");
|
|
159
|
+
/** `csp` with `sources` appended to `directive`. */
|
|
160
|
+
const addSources = (csp, directive, ...sources) => ({
|
|
161
|
+
...csp,
|
|
162
|
+
[directive]: [...csp[directive] ?? [], ...sources]
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Built-in frameworks: their dev and build commands, and the default `outDir`
|
|
166
|
+
* of a static, client-side build. Each takes `{ outDir }` to override it.
|
|
167
|
+
*/
|
|
168
|
+
const frameworkBuilds = {
|
|
169
|
+
analog: [
|
|
170
|
+
["vite", "dev"],
|
|
171
|
+
["vite", "build"],
|
|
172
|
+
"dist"
|
|
173
|
+
],
|
|
174
|
+
astro: [
|
|
175
|
+
["astro", "dev"],
|
|
176
|
+
["astro", "build"],
|
|
177
|
+
"dist"
|
|
178
|
+
],
|
|
179
|
+
nuxt: [
|
|
180
|
+
["nuxt", "dev"],
|
|
181
|
+
["nuxt", "generate"],
|
|
182
|
+
".output/public"
|
|
183
|
+
],
|
|
184
|
+
"solid-start": [
|
|
185
|
+
["vite", "dev"],
|
|
186
|
+
["vite", "build"],
|
|
187
|
+
".output/public"
|
|
188
|
+
],
|
|
189
|
+
sveltekit: [
|
|
190
|
+
["vite", "dev"],
|
|
191
|
+
["vite", "build"],
|
|
192
|
+
"build"
|
|
193
|
+
]
|
|
194
|
+
};
|
|
195
|
+
function builtinFramework(name, options) {
|
|
196
|
+
const builtin = frameworkBuilds[name];
|
|
197
|
+
if (!builtin) return;
|
|
198
|
+
const [dev, command, outDir] = builtin;
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
build: {
|
|
202
|
+
dev,
|
|
203
|
+
command,
|
|
204
|
+
outDir: options?.outDir ?? outDir
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function resolveIntegration(ref, root) {
|
|
209
|
+
if (typeof ref === "object" && !Array.isArray(ref)) return ref;
|
|
210
|
+
const [name, options] = typeof ref === "string" ? [ref] : ref;
|
|
211
|
+
const builtin = builtinFramework(name, options);
|
|
212
|
+
if (builtin) return builtin;
|
|
213
|
+
const exported = await createJiti(join(root, "quicklook.config")).import(name, { default: true });
|
|
214
|
+
return typeof exported === "function" ? exported(options) : exported;
|
|
215
|
+
}
|
|
216
|
+
/** Top-level fields from before the platform groups, and where they went. */
|
|
217
|
+
const movedFields = {
|
|
218
|
+
bundleId: "macos.bundleId",
|
|
219
|
+
icon: "macos.icon",
|
|
220
|
+
macOS: "macos.minimumVersion",
|
|
221
|
+
types: "macos.types, Windows uses windows.extensions"
|
|
222
|
+
};
|
|
223
|
+
const windowsId = /^QuickLook\.Plugin\.[A-Za-z0-9_.]+$/;
|
|
224
|
+
function validatePlatforms(config) {
|
|
225
|
+
const moved = Object.keys(movedFields).filter((field) => field in config).map((field) => `${field}: moved to ${movedFields[field]}`);
|
|
226
|
+
if (moved.length > 0) throw new Error(moved.join("\n"));
|
|
227
|
+
if (!(config.macos || config.windows)) throw new Error("quicklook.config: add a \"macos\" or \"windows\" group for each platform to build for");
|
|
228
|
+
if (!config.windows) return;
|
|
229
|
+
const { id, extensions } = config.windows;
|
|
230
|
+
if (typeof id !== "string" || !windowsId.test(id)) throw new Error(`windows.id: expected "QuickLook.Plugin." followed by a name, got ${JSON.stringify(id)}`);
|
|
231
|
+
if (!Array.isArray(extensions) || extensions.length === 0) throw new Error("windows.extensions: expected at least one extension");
|
|
232
|
+
const dotted = extensions.find((extension) => extension.startsWith("."));
|
|
233
|
+
if (dotted) throw new Error(`windows.extensions: write "${dotted.slice(1)}" without the dot`);
|
|
234
|
+
}
|
|
235
|
+
async function loadConfig$1(root = process.cwd()) {
|
|
236
|
+
const { config } = await loadConfig({
|
|
237
|
+
name: "quicklook",
|
|
238
|
+
cwd: root
|
|
239
|
+
});
|
|
240
|
+
if (!config?.name) throw new Error(`No quicklook.config.* with a name found in ${root}`);
|
|
241
|
+
validatePlatforms(config);
|
|
242
|
+
validateSettings(config.settings ?? {});
|
|
243
|
+
const { framework: frameworkRef, integrations: refs = [], macos, ...rest } = config;
|
|
244
|
+
const framework = await resolveIntegration(frameworkRef, root);
|
|
245
|
+
if (!framework.build) throw new Error(`Framework "${framework.name}" doesn't say how to build the page`);
|
|
246
|
+
const integrations = [framework];
|
|
247
|
+
for (const ref of refs) integrations.push(await resolveIntegration(ref, root));
|
|
248
|
+
const resolved = {
|
|
249
|
+
maxFileSize: 1e7,
|
|
250
|
+
public: {},
|
|
251
|
+
settings: {},
|
|
252
|
+
...rest,
|
|
253
|
+
...macos && { macos: {
|
|
254
|
+
minimumVersion: "14.0",
|
|
255
|
+
...macos
|
|
256
|
+
} },
|
|
257
|
+
csp: {
|
|
258
|
+
...defaultCsp,
|
|
259
|
+
...config.csp
|
|
260
|
+
},
|
|
261
|
+
root,
|
|
262
|
+
framework: {
|
|
263
|
+
...framework,
|
|
264
|
+
build: framework.build
|
|
265
|
+
},
|
|
266
|
+
integrations
|
|
267
|
+
};
|
|
268
|
+
for (const integration of integrations) await integration.hooks?.["config:resolved"]?.(resolved);
|
|
269
|
+
return resolved;
|
|
270
|
+
}
|
|
271
|
+
export { serializeCsp as a, loadConfig$1 as i, defaultCsp as n, defineConfig as r, addSources as t };
|