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/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-BQ-9qYNV.mjs";
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
- //#region src/cli.ts
10
+ import { parseArgs } from "node:util";
11
+ // src/windows.ts
12
+ const xmlEscapes = {
13
+ "&": "&",
14
+ "<": "&lt;",
15
+ ">": "&gt;"
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, bundleId, macOS, types, framework, icon } = config;
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: root,
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, { env: {
43
- ...process.env,
44
- PATH: binPath()
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: process.env.VERSION?.replace(leadingV, "") ?? "1.0",
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: [`${native}/App`, ...icon ? [{
91
- path: resolve(root, icon),
92
- buildPhase: "resources"
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: macOS,
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: [`${native}/Extension`, {
122
- path: web,
123
- type: "folder",
124
- buildPhase: "resources"
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=${work}/build`,
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
- switch (process.argv[2]) {
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 };