quicklook-kit 0.0.0-stage → 0.1.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/native/App/HostApp.swift +21 -0
- package/native/Extension/PreviewViewController.swift +124 -0
- package/package.json +43 -3
- package/readme.markdown +200 -0
- package/src/cli.ts +200 -0
- package/src/config.ts +176 -0
- package/src/runtime.ts +36 -0
- package/src/virtual.d.ts +7 -0
- package/src/vite.ts +41 -0
- package/tsconfig.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// QuickLook extensions have to ship inside an app, and macOS registers them
|
|
4
|
+
/// when that app is first launched. This is all the app does.
|
|
5
|
+
@main
|
|
6
|
+
struct HostApp: App {
|
|
7
|
+
private let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? ""
|
|
8
|
+
|
|
9
|
+
var body: some Scene {
|
|
10
|
+
Window(name, id: "main") {
|
|
11
|
+
VStack(spacing: 8) {
|
|
12
|
+
Text(name).font(.headline)
|
|
13
|
+
Text("The QuickLook preview is installed. You can close this window.")
|
|
14
|
+
.foregroundStyle(.secondary)
|
|
15
|
+
}
|
|
16
|
+
.padding(32)
|
|
17
|
+
.fixedSize()
|
|
18
|
+
}
|
|
19
|
+
.windowResizability(.contentSize)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import QuickLookUI
|
|
2
|
+
import UniformTypeIdentifiers
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
/// Loads the bundled page over `quicklook://app/` and hands it the previewed
|
|
6
|
+
/// file as `window.quicklook`. See packages/quicklook-kit/src/runtime.ts.
|
|
7
|
+
@objc(PreviewViewController)
|
|
8
|
+
final class PreviewViewController: NSViewController, QLPreviewingController {
|
|
9
|
+
/// Larger files throw, which makes QuickLook fall back to its generic preview
|
|
10
|
+
/// instead of stalling while megabytes of base64 cross into the page.
|
|
11
|
+
private static let maxFileSize =
|
|
12
|
+
Bundle.main.object(forInfoDictionaryKey: "QLMaxFileSize") as? Int ?? 10_000_000
|
|
13
|
+
|
|
14
|
+
private let loadWaiter = LoadWaiter()
|
|
15
|
+
|
|
16
|
+
private lazy var webView: WKWebView = {
|
|
17
|
+
let configuration = WKWebViewConfiguration()
|
|
18
|
+
configuration.setURLSchemeHandler(BundleSchemeHandler(), forURLScheme: "quicklook")
|
|
19
|
+
let webView = WKWebView(frame: .zero, configuration: configuration)
|
|
20
|
+
webView.navigationDelegate = loadWaiter
|
|
21
|
+
return webView
|
|
22
|
+
}()
|
|
23
|
+
|
|
24
|
+
override func loadView() {
|
|
25
|
+
view = webView
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func preparePreviewOfFile(at url: URL) async throws {
|
|
29
|
+
let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
|
|
30
|
+
guard size <= Self.maxFileSize else { throw CocoaError(.fileReadTooLarge) }
|
|
31
|
+
|
|
32
|
+
let file = [
|
|
33
|
+
"name": url.lastPathComponent,
|
|
34
|
+
"extension": url.pathExtension.lowercased(),
|
|
35
|
+
"base64": try Data(contentsOf: url).base64EncodedString(),
|
|
36
|
+
]
|
|
37
|
+
// JSON is a valid JavaScript expression, so this needs no escaping of its own.
|
|
38
|
+
let json = String(decoding: try JSONSerialization.data(withJSONObject: file), as: UTF8.self)
|
|
39
|
+
let scripts = webView.configuration.userContentController
|
|
40
|
+
scripts.removeAllUserScripts()
|
|
41
|
+
scripts.addUserScript(
|
|
42
|
+
WKUserScript(
|
|
43
|
+
source: "window.quicklook = \(json);", injectionTime: .atDocumentStart,
|
|
44
|
+
forMainFrameOnly: true))
|
|
45
|
+
|
|
46
|
+
// QuickLook displays the view as soon as this returns, so wait for the
|
|
47
|
+
// page to actually finish loading — otherwise larger files show blank.
|
|
48
|
+
let page = URLRequest(url: URL(string: "quicklook://app/")!)
|
|
49
|
+
try await loadWaiter.load { webView.load(page) }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Serves the bundled `web` folder. A real origin, unlike file://, lets any
|
|
54
|
+
/// framework's build load its ES modules and chunks as `'self'`, and every
|
|
55
|
+
/// response carries the CSP the build wrote into Info.plist.
|
|
56
|
+
private final class BundleSchemeHandler: NSObject, WKURLSchemeHandler {
|
|
57
|
+
private let root = Bundle.main.resourceURL!.appendingPathComponent("web").standardizedFileURL
|
|
58
|
+
private let csp =
|
|
59
|
+
Bundle.main.object(forInfoDictionaryKey: "QLWebContentSecurityPolicy") as? String
|
|
60
|
+
?? "default-src 'none'"
|
|
61
|
+
|
|
62
|
+
func webView(_ webView: WKWebView, start task: any WKURLSchemeTask) {
|
|
63
|
+
guard let url = task.request.url else {
|
|
64
|
+
task.didFailWithError(URLError(.badURL))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
var path = url.path(percentEncoded: false)
|
|
68
|
+
if path.isEmpty || path.hasSuffix("/") { path += "index.html" }
|
|
69
|
+
let file = root.appendingPathComponent(path).standardizedFileURL
|
|
70
|
+
// `..%2f` arrives decoded as a real `..`; never serve anything outside web/.
|
|
71
|
+
let data = file.path.hasPrefix(root.path + "/") ? try? Data(contentsOf: file) : nil
|
|
72
|
+
let type =
|
|
73
|
+
UTType(filenameExtension: file.pathExtension)?.preferredMIMEType ?? "application/octet-stream"
|
|
74
|
+
let response = HTTPURLResponse(
|
|
75
|
+
url: url, statusCode: data == nil ? 404 : 200, httpVersion: "HTTP/1.1",
|
|
76
|
+
headerFields: ["Content-Type": type, "Content-Security-Policy": csp])!
|
|
77
|
+
task.didReceive(response)
|
|
78
|
+
task.didReceive(data ?? Data())
|
|
79
|
+
task.didFinish()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func webView(_ webView: WKWebView, stop task: any WKURLSchemeTask) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Bridges `WKNavigationDelegate` callbacks back to `async`.
|
|
86
|
+
@MainActor
|
|
87
|
+
private final class LoadWaiter: NSObject, WKNavigationDelegate {
|
|
88
|
+
private var continuation: CheckedContinuation<Void, Error>?
|
|
89
|
+
|
|
90
|
+
func load(_ start: () -> Void) async throws {
|
|
91
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
92
|
+
self.continuation = continuation
|
|
93
|
+
start()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func finish(_ result: Result<Void, Error>) {
|
|
98
|
+
continuation?.resume(with: result)
|
|
99
|
+
continuation = nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
103
|
+
finish(.success(()))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func webView(
|
|
107
|
+
_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error
|
|
108
|
+
) {
|
|
109
|
+
finish(.failure(error))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func webView(
|
|
113
|
+
_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!,
|
|
114
|
+
withError error: Error
|
|
115
|
+
) {
|
|
116
|
+
finish(.failure(error))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// A crashed content process fires no navigation callback at all — without
|
|
120
|
+
/// this the await never returns and QuickLook spins forever.
|
|
121
|
+
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
122
|
+
finish(.failure(CocoaError(.featureUnsupported)))
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quicklook-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Build macOS QuickLook previews with any Vite-based web framework",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"quicklook",
|
|
8
|
+
"quicklook-plugin",
|
|
9
|
+
"sveltekit",
|
|
10
|
+
"svelte",
|
|
11
|
+
"astro",
|
|
12
|
+
"solid",
|
|
13
|
+
"solid-start",
|
|
14
|
+
"vue",
|
|
15
|
+
"nuxt",
|
|
16
|
+
"angular",
|
|
17
|
+
"analog"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"quicklook": "./src/cli.ts"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/config.ts",
|
|
25
|
+
"./vite": "./src/vite.ts",
|
|
26
|
+
"./runtime": "./src/runtime.ts",
|
|
27
|
+
"./virtual": {
|
|
28
|
+
"types": "./src/virtual.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"c12": "3.3.4",
|
|
33
|
+
"jiti": "2.7.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/bun": "1.3.9",
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vite": "8.3.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"vite": ">=7"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"check": "tsc --noEmit"
|
|
45
|
+
}
|
|
6
46
|
}
|
package/readme.markdown
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# quicklook-kit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://www.npmjs.org/package/quicklook-kit)
|
|
5
|
+
[](https://github.com/idleberg/quicklook-kit/actions)
|
|
6
|
+
|
|
7
|
+
> Build macOS QuickLook previews with any Vite-based web framework, no Swift required
|
|
8
|
+
|
|
9
|
+
Your preview is a web page. quicklook-kit builds it with your framework, wraps it in a small native QuickLook extension and installs the resulting app. You never touch Swift or Xcode.
|
|
10
|
+
|
|
11
|
+
Built-in support for SvelteKit, Astro, SolidStart, Nuxt and Analog. Any other framework that builds with Vite and writes a static `index.html` works through an integration.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- macOS 14 or later, with [Xcode](https://apps.apple.com/app/xcode/id497799835) installed
|
|
16
|
+
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
|
|
17
|
+
- [Bun](https://bun.sh), which runs the `quicklook` CLI
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bun add --dev quicklook-kit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To start from scratch, use one of the [templates](https://github.com/idleberg/quicklook-kit/tree/main/templates) instead.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
### 1. Configure
|
|
30
|
+
|
|
31
|
+
Create `quicklook.config.ts` in your project root:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { defineConfig } from "quicklook-kit";
|
|
35
|
+
|
|
36
|
+
export default defineConfig({
|
|
37
|
+
name: "Example Preview",
|
|
38
|
+
bundleId: "com.example.ExamplePreview",
|
|
39
|
+
types: [
|
|
40
|
+
{
|
|
41
|
+
id: "com.example.ExamplePreview.example",
|
|
42
|
+
description: "Example File",
|
|
43
|
+
extensions: ["example"],
|
|
44
|
+
conformsTo: ["public.plain-text"],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
framework: "sveltekit",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A new type conforms to `public.plain-text` for text formats, or `public.data` for binary ones. To preview a type macOS already knows about, use a plain string like `'public.json'` in place of an object.
|
|
52
|
+
|
|
53
|
+
The config can also be `quicklook.config.js`, `.json`, `.yaml` or `.toml`, or a `.quicklookrc`. The keys are the same in every format.
|
|
54
|
+
|
|
55
|
+
| Key | Default | |
|
|
56
|
+
| -------------- | ------------ | ---------------------------------------------------------------------------------- |
|
|
57
|
+
| `name` | required | App name, shown in Finder and System Settings |
|
|
58
|
+
| `bundleId` | required | The extension gets `<bundleId>.Extension` |
|
|
59
|
+
| `types` | required | File types to preview |
|
|
60
|
+
| `framework` | required | `'sveltekit'`, `'astro'`, `'solid-start'`, `'nuxt'`, `'analog'`, or an integration |
|
|
61
|
+
| `macOS` | `'14.0'` | Minimum macOS version |
|
|
62
|
+
| `integrations` | `[]` | See [Integrations](#integrations) |
|
|
63
|
+
| `csp` | locked down | Directives here replace the defaults of the same name |
|
|
64
|
+
| `maxFileSize` | `10_000_000` | Larger files get QuickLook's generic preview |
|
|
65
|
+
| `public` | `{}` | Values the page can read at build time |
|
|
66
|
+
|
|
67
|
+
A framework can take options as a `[name, options]` pair, e.g. `['sveltekit', { outDir: 'dist' }]` when your adapter writes somewhere other than the default.
|
|
68
|
+
|
|
69
|
+
Each framework needs a static, client-side build:
|
|
70
|
+
|
|
71
|
+
| Framework | Requirement |
|
|
72
|
+
| ---------- | ------------------------------------------------------ |
|
|
73
|
+
| Analog | `@analogjs/vite-plugin-angular` |
|
|
74
|
+
| Astro | the default `output: 'static'` |
|
|
75
|
+
| Nuxt | `ssr: false` |
|
|
76
|
+
| SolidStart | `ssr: false`, Nitro's `static` preset prerendering `/` |
|
|
77
|
+
| SvelteKit | `@sveltejs/adapter-static` |
|
|
78
|
+
|
|
79
|
+
### 2. Add the Vite plugin
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { quicklook } from "quicklook-kit/vite";
|
|
83
|
+
|
|
84
|
+
// in the framework's Vite config
|
|
85
|
+
plugins: [quicklook()];
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For TypeScript, add the types of the virtual module, either in a `.d.ts` file:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
/// <reference types="quicklook-kit/virtual" />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
or in `tsconfig.json` under `compilerOptions.types` as `"quicklook-kit/virtual"`.
|
|
95
|
+
|
|
96
|
+
### 3. Read the file
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { browserFile, config, nativeFile } from "virtual:quicklook";
|
|
100
|
+
|
|
101
|
+
const file = nativeFile(); // the previewed file inside QuickLook, undefined in the browser
|
|
102
|
+
config.public; // values from quicklook.config.*
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Field | Type | |
|
|
106
|
+
| ----------- | ------------ | ------------------------------ |
|
|
107
|
+
| `name` | `string` | `"notes.example"` |
|
|
108
|
+
| `extension` | `string` | `"example"`, lowercase, no dot |
|
|
109
|
+
| `bytes` | `Uint8Array` | the raw file contents |
|
|
110
|
+
|
|
111
|
+
Decode `bytes` however your format needs: `new TextDecoder().decode(file.bytes)` for text, or a parser for binary formats.
|
|
112
|
+
|
|
113
|
+
In the dev server, `nativeFile()` returns `undefined`. Pass a dropped or picked `File` to `browserFile()` to get the same shape.
|
|
114
|
+
|
|
115
|
+
### 4. Add scripts
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"scripts": {
|
|
120
|
+
"dev": "quicklook dev",
|
|
121
|
+
"build": "quicklook build",
|
|
122
|
+
"register": "quicklook build && quicklook register",
|
|
123
|
+
"unregister": "quicklook unregister"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## CLI
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
132
|
+
| `quicklook dev` | Starts the framework's dev server |
|
|
133
|
+
| `quicklook build:web` | Builds the page only, into `.quicklook/web` |
|
|
134
|
+
| `quicklook build` | Builds the app with its extension into `.quicklook/build` |
|
|
135
|
+
| `quicklook register` | Copies the built app to `/Applications` and registers the extension |
|
|
136
|
+
| `quicklook unregister` | Removes the app from `/Applications` |
|
|
137
|
+
|
|
138
|
+
After `register`, select a file in Finder and press Space. Run `build` and `register` again after every change.
|
|
139
|
+
|
|
140
|
+
If the preview doesn't show up, check `pluginkit -mAv | grep <your bundleId>`, and make sure no other app claims the same file type.
|
|
141
|
+
|
|
142
|
+
Add `.quicklook` to your `.gitignore`.
|
|
143
|
+
|
|
144
|
+
## Things to know
|
|
145
|
+
|
|
146
|
+
- **Nothing can be loaded from the network.** The extension serves your build from inside the app with a strict Content Security Policy. Import images, fonts and CSS normally and they get bundled. The dev server only reports violations in the console, without blocking them.
|
|
147
|
+
- **Inline scripts are allowed by hash.** Frameworks that bootstrap with inline `<script>` tags keep working; the hashes are added to the CSP at build time.
|
|
148
|
+
- **Test in Safari.** It uses the same WebKit engine as QuickLook's WKWebView.
|
|
149
|
+
- **The light/dark theme follows the system.** Use `prefers-color-scheme` in CSS.
|
|
150
|
+
- **The app is signed ad hoc.** Other people have to allow it in System Settings → Privacy & Security on first launch. Distributing it without that step requires an Apple Developer account and notarization.
|
|
151
|
+
|
|
152
|
+
## Integrations
|
|
153
|
+
|
|
154
|
+
An integration is an object with a `name` and any of these hooks. They run in order: the framework first, then each entry in `integrations`.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import type { Integration } from "quicklook-kit";
|
|
158
|
+
|
|
159
|
+
export default (options: { key: string }): Integration => ({
|
|
160
|
+
name: "my-integration",
|
|
161
|
+
hooks: {
|
|
162
|
+
// Adjust the resolved config.
|
|
163
|
+
"config:resolved"(config) {},
|
|
164
|
+
|
|
165
|
+
// Post-process the framework's static output before it's bundled.
|
|
166
|
+
async "build:done"({ outDir, config }) {},
|
|
167
|
+
|
|
168
|
+
// Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets.
|
|
169
|
+
"xcode:spec"(spec, config) {
|
|
170
|
+
spec.targets.ExamplePreviewExtension.info.properties.MyKey = options.key;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Add Vite plugins wherever quicklook() is used.
|
|
174
|
+
vite: () => [],
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Add it as an object, or by name so JSON and YAML configs can use it too. A name is imported from the project: a package or a relative path, whose default export is the integration or a function returning it.
|
|
180
|
+
|
|
181
|
+
```jsonc
|
|
182
|
+
{
|
|
183
|
+
"integrations": ["quicklook-some-package", ["./my-integration.ts", { "key": "value" }]],
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A framework is an integration with a `build` field:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
framework: {
|
|
191
|
+
name: 'my-framework',
|
|
192
|
+
build: { dev: ['vite', 'dev'], command: ['vite', 'build'], outDir: 'dist' }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The commands run with the project's own binaries.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
This work is licensed under [The MIT License](https://opensource.org/licenses/MIT).
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Builds the page with the configured framework, wraps it in a QuickLook
|
|
3
|
+
// extension, and installs it. Usage: quicklook dev|build:web|build|register|unregister
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { cp, rm } from 'node:fs/promises';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { $, Glob } from 'bun';
|
|
11
|
+
import {
|
|
12
|
+
addSources,
|
|
13
|
+
type DeclaredType,
|
|
14
|
+
loadConfig,
|
|
15
|
+
serializeCsp,
|
|
16
|
+
type XcodeSpec
|
|
17
|
+
} from './config.ts';
|
|
18
|
+
|
|
19
|
+
const config = await loadConfig();
|
|
20
|
+
const { root, name, bundleId, macOS, types, framework } = config;
|
|
21
|
+
const target = name.replace(/[^A-Za-z0-9]/g, '');
|
|
22
|
+
// Everything generated lives here, away from the framework's own output.
|
|
23
|
+
const work = `${root}/.quicklook`;
|
|
24
|
+
const web = `${work}/web`;
|
|
25
|
+
const built = `${work}/build/${name}.app`;
|
|
26
|
+
const installed = `/Applications/${name}.app`;
|
|
27
|
+
const native = fileURLToPath(new URL('../native', import.meta.url));
|
|
28
|
+
const lsregister =
|
|
29
|
+
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
|
|
30
|
+
|
|
31
|
+
const leadingV = /^v/;
|
|
32
|
+
const inlineScript = /<script\b(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
33
|
+
|
|
34
|
+
/** Runs a framework command with the project's own binaries. */
|
|
35
|
+
async function run(command: string[]) {
|
|
36
|
+
const child = Bun.spawn(['bun', 'run', ...command], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
stdio: ['inherit', 'inherit', 'inherit']
|
|
39
|
+
});
|
|
40
|
+
const code = await child.exited;
|
|
41
|
+
if (code !== 0) {
|
|
42
|
+
process.exit(code);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Frameworks bootstrap with inline scripts; the CSP allows exactly those, by hash. */
|
|
47
|
+
async function inlineScriptHashes() {
|
|
48
|
+
const hashes = new Set<string>();
|
|
49
|
+
for await (const file of new Glob('**/*.html').scan(web)) {
|
|
50
|
+
const html = await Bun.file(`${web}/${file}`).text();
|
|
51
|
+
for (const [, body = ''] of html.matchAll(inlineScript)) {
|
|
52
|
+
hashes.add(`'sha256-${createHash('sha256').update(body).digest('base64')}'`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...hashes];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Builds the page with the framework and copies it to `.quicklook/web`. */
|
|
59
|
+
async function buildWeb() {
|
|
60
|
+
await run(framework.build.command);
|
|
61
|
+
const outDir = resolve(root, framework.build.outDir);
|
|
62
|
+
for (const integration of config.integrations) {
|
|
63
|
+
await integration.hooks?.['build:done']?.({ outDir, config });
|
|
64
|
+
}
|
|
65
|
+
// Copied so the bundle's folder is always named `web`, whatever the framework calls it.
|
|
66
|
+
await rm(web, { recursive: true, force: true });
|
|
67
|
+
await cp(outDir, web, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function build() {
|
|
71
|
+
await buildWeb();
|
|
72
|
+
|
|
73
|
+
const csp = addSources(config.csp, 'script-src', ...(await inlineScriptHashes()));
|
|
74
|
+
const spec: XcodeSpec = {
|
|
75
|
+
name: target,
|
|
76
|
+
options: { deploymentTarget: { macOS }, createIntermediateGroups: true },
|
|
77
|
+
settings: {
|
|
78
|
+
base: {
|
|
79
|
+
SWIFT_VERSION: '6.0',
|
|
80
|
+
MARKETING_VERSION: process.env.VERSION?.replace(leadingV, '') ?? '1.0',
|
|
81
|
+
CURRENT_PROJECT_VERSION: '1',
|
|
82
|
+
// Ad-hoc signing: enough for QuickLook to load it, no Apple Developer account needed.
|
|
83
|
+
CODE_SIGN_STYLE: 'Manual',
|
|
84
|
+
CODE_SIGN_IDENTITY: '-'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
targets: {
|
|
88
|
+
[target]: {
|
|
89
|
+
type: 'application',
|
|
90
|
+
platform: 'macOS',
|
|
91
|
+
sources: [`${native}/App`],
|
|
92
|
+
settings: { PRODUCT_NAME: name, PRODUCT_BUNDLE_IDENTIFIER: bundleId },
|
|
93
|
+
dependencies: [{ target: `${target}Extension`, embed: true, codeSign: true }],
|
|
94
|
+
info: {
|
|
95
|
+
path: `${work}/App/Info.plist`,
|
|
96
|
+
properties: {
|
|
97
|
+
CFBundleName: name,
|
|
98
|
+
LSMinimumSystemVersion: macOS,
|
|
99
|
+
UTExportedTypeDeclarations: types
|
|
100
|
+
.filter((type): type is DeclaredType => typeof type !== 'string')
|
|
101
|
+
.map((type) => ({
|
|
102
|
+
UTTypeIdentifier: type.id,
|
|
103
|
+
UTTypeDescription: type.description,
|
|
104
|
+
UTTypeConformsTo: type.conformsTo,
|
|
105
|
+
UTTypeTagSpecification: { 'public.filename-extension': type.extensions }
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[`${target}Extension`]: {
|
|
111
|
+
type: 'app-extension',
|
|
112
|
+
platform: 'macOS',
|
|
113
|
+
sources: [`${native}/Extension`, { path: web, type: 'folder', buildPhase: 'resources' }],
|
|
114
|
+
// macOS silently ignores an extension whose bundle id isn't prefixed by its app's.
|
|
115
|
+
settings: { PRODUCT_BUNDLE_IDENTIFIER: `${bundleId}.Extension` },
|
|
116
|
+
dependencies: [{ sdk: 'QuickLookUI.framework' }, { sdk: 'WebKit.framework' }],
|
|
117
|
+
entitlements: {
|
|
118
|
+
path: `${work}/Extension/Extension.entitlements`,
|
|
119
|
+
properties: {
|
|
120
|
+
'com.apple.security.app-sandbox': true,
|
|
121
|
+
'com.apple.security.files.user-selected.read-only': true,
|
|
122
|
+
// WKWebView's content process won't launch in a sandbox without this,
|
|
123
|
+
// even for a page that loads nothing. The page's CSP blocks all requests.
|
|
124
|
+
'com.apple.security.network.client': true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
info: {
|
|
128
|
+
path: `${work}/Extension/Info.plist`,
|
|
129
|
+
properties: {
|
|
130
|
+
CFBundleDisplayName: name,
|
|
131
|
+
// Read by PreviewViewController.swift.
|
|
132
|
+
QLWebContentSecurityPolicy: serializeCsp(csp),
|
|
133
|
+
QLMaxFileSize: config.maxFileSize,
|
|
134
|
+
NSExtension: {
|
|
135
|
+
NSExtensionPointIdentifier: 'com.apple.quicklook.preview',
|
|
136
|
+
NSExtensionPrincipalClass: 'PreviewViewController',
|
|
137
|
+
NSExtensionAttributes: {
|
|
138
|
+
QLSupportedContentTypes: types.map((type) =>
|
|
139
|
+
typeof type === 'string' ? type : type.id
|
|
140
|
+
),
|
|
141
|
+
QLSupportsSearchableItems: false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
for (const integration of config.integrations) {
|
|
150
|
+
await integration.hooks?.['xcode:spec']?.(spec, config);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// JSON is valid YAML.
|
|
154
|
+
await Bun.write(`${work}/project.yml`, JSON.stringify(spec, null, 2));
|
|
155
|
+
await $`xcodegen generate --quiet --spec ${work}/project.yml --project ${work}`;
|
|
156
|
+
await $`xcodebuild -quiet -project ${work}/${target}.xcodeproj -scheme ${target} -configuration Release -destination generic/platform=macOS CONFIGURATION_BUILD_DIR=${work}/build build`;
|
|
157
|
+
// Finder keys its icon cache on the bundle's mtime, which xcodebuild leaves alone.
|
|
158
|
+
await $`touch ${built}`;
|
|
159
|
+
console.log(`Built ${built}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
switch (process.argv[2]) {
|
|
163
|
+
case 'dev':
|
|
164
|
+
await run(framework.build.dev);
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'build:web':
|
|
168
|
+
await buildWeb();
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'build':
|
|
172
|
+
await build();
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
// macOS only loads extensions from apps in a standard location, and caches
|
|
176
|
+
// registrations aggressively, including for apps since renamed or deleted.
|
|
177
|
+
case 'register':
|
|
178
|
+
if (!existsSync(built)) {
|
|
179
|
+
throw new Error(`No build at ${built}, run 'quicklook build' first.`);
|
|
180
|
+
}
|
|
181
|
+
await $`rm -rf ${installed}`;
|
|
182
|
+
await $`cp -R ${built} ${installed}`;
|
|
183
|
+
await $`${lsregister} -f -R ${installed}`;
|
|
184
|
+
await $`qlmanage -r`.quiet().nothrow();
|
|
185
|
+
await $`qlmanage -r cache`.quiet().nothrow();
|
|
186
|
+
// Launching the app is what registers its embedded extension.
|
|
187
|
+
await $`open -g ${installed}`;
|
|
188
|
+
console.log(`Installed ${installed}\nVerify with: pluginkit -mAv | grep ${bundleId}`);
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'unregister':
|
|
192
|
+
await $`${lsregister} -u ${installed}`.quiet().nothrow();
|
|
193
|
+
await $`rm -rf ${installed}`;
|
|
194
|
+
await $`qlmanage -r`.quiet().nothrow();
|
|
195
|
+
console.log(`Removed ${installed}`);
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
default:
|
|
199
|
+
throw new Error('Usage: quicklook dev|build:web|build|register|unregister');
|
|
200
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Loads quicklook.config.* (any format c12 reads: ts, js, json, yaml, toml…)
|
|
2
|
+
// and resolves its framework and integrations.
|
|
3
|
+
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { loadConfig as loadC12 } from 'c12';
|
|
7
|
+
import { createJiti } from 'jiti';
|
|
8
|
+
import type { Plugin } from 'vite';
|
|
9
|
+
|
|
10
|
+
/** An existing type id such as "public.json", or a new type the app declares. */
|
|
11
|
+
export type FileType = string | DeclaredType;
|
|
12
|
+
|
|
13
|
+
/** A file type the app declares itself, as a UTExportedTypeDeclaration. */
|
|
14
|
+
export interface DeclaredType {
|
|
15
|
+
id: string;
|
|
16
|
+
description: string;
|
|
17
|
+
extensions: string[];
|
|
18
|
+
conformsTo: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** CSP directives, e.g. `{ 'img-src': ["'self'", 'https:'] }`. */
|
|
22
|
+
export type Csp = Partial<Record<string, string[]>>;
|
|
23
|
+
|
|
24
|
+
/** The xcodegen project spec, written as project.yml. */
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: hooks edit arbitrary xcodegen keys.
|
|
26
|
+
export type XcodeSpec = Record<string, any>;
|
|
27
|
+
|
|
28
|
+
export interface Integration {
|
|
29
|
+
name: string;
|
|
30
|
+
/** Set by frameworks only: how to run and build the page, and where it lands. */
|
|
31
|
+
build?: { dev: string[]; command: string[]; outDir: string };
|
|
32
|
+
hooks?: {
|
|
33
|
+
'config:resolved'?: (config: ResolvedConfig) => void | Promise<void>;
|
|
34
|
+
/** Runs on the framework's static output, before it's bundled into the app. */
|
|
35
|
+
'build:done'?: (context: { outDir: string; config: ResolvedConfig }) => void | Promise<void>;
|
|
36
|
+
/** Edit the generated Xcode project: Info.plist keys, entitlements, targets. */
|
|
37
|
+
'xcode:spec'?: (spec: XcodeSpec, config: ResolvedConfig) => void | Promise<void>;
|
|
38
|
+
/** Vite plugins to add wherever the `quicklook()` Vite plugin is used. */
|
|
39
|
+
vite?: () => Plugin[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A built-in framework or package name, the same with options, or an
|
|
45
|
+
* integration object. Names are imported from the project, so JSON and YAML
|
|
46
|
+
* configs can use third-party integrations too: the module's default export is
|
|
47
|
+
* either an integration or a function that takes the options and returns one.
|
|
48
|
+
*/
|
|
49
|
+
export type IntegrationRef = Integration | string | [name: string, options?: unknown];
|
|
50
|
+
|
|
51
|
+
export interface QuickLookConfig {
|
|
52
|
+
/** App name, shown in Finder and System Settings. */
|
|
53
|
+
name: string;
|
|
54
|
+
bundleId: string;
|
|
55
|
+
/** Minimum macOS version. Defaults to 14.0. */
|
|
56
|
+
macOS?: string;
|
|
57
|
+
types: FileType[];
|
|
58
|
+
/** `sveltekit`, `astro`, `solid-start`, `nuxt`, `analog`, or any integration that sets `build`. */
|
|
59
|
+
framework: IntegrationRef;
|
|
60
|
+
integrations?: IntegrationRef[];
|
|
61
|
+
/** Directives given here replace the defaults of the same name. */
|
|
62
|
+
csp?: Csp;
|
|
63
|
+
/** Larger files get QuickLook's generic preview. Defaults to 10 MB. */
|
|
64
|
+
maxFileSize?: number;
|
|
65
|
+
/** Exposed to the page as `config.public` from `virtual:quicklook`. */
|
|
66
|
+
public?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What the page sees as `config` from `virtual:quicklook`. */
|
|
70
|
+
export type PageConfig = Pick<ResolvedConfig, 'name' | 'bundleId' | 'types' | 'public'>;
|
|
71
|
+
|
|
72
|
+
export interface ResolvedConfig
|
|
73
|
+
extends Required<Omit<QuickLookConfig, 'framework' | 'integrations'>> {
|
|
74
|
+
root: string;
|
|
75
|
+
framework: Integration & { build: NonNullable<Integration['build']> };
|
|
76
|
+
/** The framework first, then the configured integrations, in order. */
|
|
77
|
+
integrations: Integration[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The previewed file is untrusted input; nothing on the page needs the network. */
|
|
81
|
+
export const defaultCsp: Csp = {
|
|
82
|
+
'default-src': ["'none'"],
|
|
83
|
+
'script-src': ["'self'"],
|
|
84
|
+
'style-src': ["'self'", "'unsafe-inline'"],
|
|
85
|
+
'img-src': ["'self'", 'data:', 'blob:'],
|
|
86
|
+
'font-src': ["'self'", 'data:'],
|
|
87
|
+
'media-src': ["'self'", 'data:', 'blob:']
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const defineConfig = (config: QuickLookConfig) => config;
|
|
91
|
+
|
|
92
|
+
export const serializeCsp = (csp: Csp) =>
|
|
93
|
+
Object.entries(csp)
|
|
94
|
+
.map(([directive, sources = []]) => [directive, ...sources].join(' '))
|
|
95
|
+
.join('; ');
|
|
96
|
+
|
|
97
|
+
/** `csp` with `sources` appended to `directive`. */
|
|
98
|
+
export const addSources = (csp: Csp, directive: string, ...sources: string[]): Csp => ({
|
|
99
|
+
...csp,
|
|
100
|
+
[directive]: [...(csp[directive] ?? []), ...sources]
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Built-in frameworks: their dev and build commands, and the default `outDir`
|
|
105
|
+
* of a static, client-side build. Each takes `{ outDir }` to override it.
|
|
106
|
+
*/
|
|
107
|
+
const frameworkBuilds: Record<string, [dev: string[], command: string[], outDir: string]> = {
|
|
108
|
+
// @analogjs/vite-plugin-angular, writing Vite's default `dist`.
|
|
109
|
+
analog: [['vite', 'dev'], ['vite', 'build'], 'dist'],
|
|
110
|
+
// Astro's default `output: 'static'`.
|
|
111
|
+
astro: [['astro', 'dev'], ['astro', 'build'], 'dist'],
|
|
112
|
+
// `ssr: false`; `nuxt generate` writes `.output/public`.
|
|
113
|
+
nuxt: [['nuxt', 'dev'], ['nuxt', 'generate'], '.output/public'],
|
|
114
|
+
// `solidStart({ ssr: false })` and Nitro's static preset prerendering `/`.
|
|
115
|
+
'solid-start': [['vite', 'dev'], ['vite', 'build'], '.output/public'],
|
|
116
|
+
// @sveltejs/adapter-static, writing `build`.
|
|
117
|
+
sveltekit: [['vite', 'dev'], ['vite', 'build'], 'build']
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function builtinFramework(name: string, options?: unknown): Integration | undefined {
|
|
121
|
+
const builtin = frameworkBuilds[name];
|
|
122
|
+
if (!builtin) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const [dev, command, outDir] = builtin;
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
build: { dev, command, outDir: (options as { outDir?: string } | undefined)?.outDir ?? outDir }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function resolveIntegration(ref: IntegrationRef, root: string): Promise<Integration> {
|
|
133
|
+
if (typeof ref === 'object' && !Array.isArray(ref)) {
|
|
134
|
+
return ref;
|
|
135
|
+
}
|
|
136
|
+
const [name, options] = typeof ref === 'string' ? [ref] : ref;
|
|
137
|
+
const builtin = builtinFramework(name, options);
|
|
138
|
+
if (builtin) {
|
|
139
|
+
return builtin;
|
|
140
|
+
}
|
|
141
|
+
const exported = await createJiti(join(root, 'quicklook.config')).import<
|
|
142
|
+
Integration | ((options?: unknown) => Integration)
|
|
143
|
+
>(name, { default: true });
|
|
144
|
+
return typeof exported === 'function' ? exported(options) : exported;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function loadConfig(root = process.cwd()): Promise<ResolvedConfig> {
|
|
148
|
+
const { config } = await loadC12<QuickLookConfig>({ name: 'quicklook', cwd: root });
|
|
149
|
+
if (!config?.name) {
|
|
150
|
+
throw new Error(`No quicklook.config.* with a name found in ${root}`);
|
|
151
|
+
}
|
|
152
|
+
const { framework: frameworkRef, integrations: refs = [], ...rest } = config;
|
|
153
|
+
const framework = await resolveIntegration(frameworkRef, root);
|
|
154
|
+
if (!framework.build) {
|
|
155
|
+
throw new Error(`Framework "${framework.name}" doesn't say how to build the page`);
|
|
156
|
+
}
|
|
157
|
+
const integrations = [framework];
|
|
158
|
+
for (const ref of refs) {
|
|
159
|
+
integrations.push(await resolveIntegration(ref, root));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const resolved: ResolvedConfig = {
|
|
163
|
+
macOS: '14.0',
|
|
164
|
+
maxFileSize: 10_000_000,
|
|
165
|
+
public: {},
|
|
166
|
+
...rest,
|
|
167
|
+
csp: { ...defaultCsp, ...config.csp },
|
|
168
|
+
root,
|
|
169
|
+
framework: { ...framework, build: framework.build },
|
|
170
|
+
integrations
|
|
171
|
+
};
|
|
172
|
+
for (const integration of integrations) {
|
|
173
|
+
await integration.hooks?.['config:resolved']?.(resolved);
|
|
174
|
+
}
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The file QuickLook asked to preview. */
|
|
2
|
+
export interface QuickLookFile {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Lowercase, without the dot. */
|
|
5
|
+
extension: string;
|
|
6
|
+
bytes: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
/** Injected by the extension before the page loads. See native/Extension/PreviewViewController.swift. */
|
|
12
|
+
quicklook?: { name: string; extension: string; base64: string };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The previewed file inside QuickLook; `undefined` in the browser. */
|
|
17
|
+
export function nativeFile(): QuickLookFile | undefined {
|
|
18
|
+
const injected = window.quicklook;
|
|
19
|
+
if (!injected) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
name: injected.name,
|
|
24
|
+
extension: injected.extension,
|
|
25
|
+
bytes: Uint8Array.from(atob(injected.base64), (c) => c.charCodeAt(0))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The same shape from a file picked or dropped in the browser, for the dev server. */
|
|
30
|
+
export async function browserFile(file: File): Promise<QuickLookFile> {
|
|
31
|
+
return {
|
|
32
|
+
name: file.name,
|
|
33
|
+
extension: file.name.includes('.') ? (file.name.split('.').pop() ?? '').toLowerCase() : '',
|
|
34
|
+
bytes: new Uint8Array(await file.arrayBuffer())
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/virtual.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Types for the module the `quicklook()` Vite plugin provides. Reference it
|
|
2
|
+
// with `/// <reference types="quicklook-kit/virtual" />`.
|
|
3
|
+
declare module 'virtual:quicklook' {
|
|
4
|
+
/** Build-time values from quicklook.config.*. */
|
|
5
|
+
export const config: import('./config.ts').PageConfig;
|
|
6
|
+
export { browserFile, nativeFile, type QuickLookFile } from 'quicklook-kit/runtime';
|
|
7
|
+
}
|
package/src/vite.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { addSources, loadConfig, type PageConfig, serializeCsp } from './config.ts';
|
|
3
|
+
|
|
4
|
+
const virtualId = 'virtual:quicklook';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provides `virtual:quicklook`, adds the integrations' Vite plugins, and makes
|
|
8
|
+
* the dev server report what the extension's CSP would block.
|
|
9
|
+
*/
|
|
10
|
+
export async function quicklook(): Promise<Plugin[]> {
|
|
11
|
+
const config = await loadConfig();
|
|
12
|
+
const { name, bundleId, types } = config;
|
|
13
|
+
const pageConfig: PageConfig = { name, bundleId, types, public: config.public };
|
|
14
|
+
const virtualSource = `export const config = ${JSON.stringify(pageConfig)};
|
|
15
|
+
export { browserFile, nativeFile } from 'quicklook-kit/runtime';`;
|
|
16
|
+
// Report-only, so violations show in the console without breaking HMR. Inline
|
|
17
|
+
// scripts are only hashed at build time, and the dev server needs its socket.
|
|
18
|
+
const devCsp = serializeCsp(
|
|
19
|
+
addSources(
|
|
20
|
+
addSources(config.csp, 'script-src', "'unsafe-inline'"),
|
|
21
|
+
'connect-src',
|
|
22
|
+
"'self'",
|
|
23
|
+
'ws:'
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
name: 'quicklook',
|
|
30
|
+
resolveId: (source) => (source === virtualId ? `\0${virtualId}` : undefined),
|
|
31
|
+
load: (source) => (source === `\0${virtualId}` ? virtualSource : undefined),
|
|
32
|
+
configureServer(server) {
|
|
33
|
+
server.middlewares.use((_request, response, next) => {
|
|
34
|
+
response.setHeader('Content-Security-Policy-Report-Only', devCsp);
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
...config.integrations.flatMap((integration) => integration.hooks?.vite?.() ?? [])
|
|
40
|
+
];
|
|
41
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"module": "preserve",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["es2023", "dom"],
|
|
7
|
+
"types": ["bun"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
package/README.md
DELETED