quicklook-kit 0.1.0 → 0.2.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/package.json +14 -1
- package/readme.markdown +55 -47
- package/src/cli.ts +25 -21
- package/src/config.ts +9 -6
- package/src/runtime.ts +2 -2
- package/src/vite.ts +4 -4
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quicklook-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Build macOS QuickLook previews with any Vite-based web framework",
|
|
5
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
|
+
],
|
|
6
19
|
"type": "module",
|
|
7
20
|
"bin": {
|
|
8
21
|
"quicklook": "./src/cli.ts"
|
package/readme.markdown
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# quicklook-kit
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
[](https://www.npmjs.org/package/quicklook-kit)
|
|
5
|
+
[](https://github.com/idleberg/quicklook-kit/actions)
|
|
6
|
+
|
|
3
7
|
> Build macOS QuickLook previews with any Vite-based web framework, no Swift required
|
|
4
8
|
|
|
5
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.
|
|
@@ -27,33 +31,34 @@ To start from scratch, use one of the [templates](https://github.com/idleberg/qu
|
|
|
27
31
|
Create `quicklook.config.ts` in your project root:
|
|
28
32
|
|
|
29
33
|
```ts
|
|
30
|
-
import { defineConfig } from
|
|
34
|
+
import { defineConfig } from "quicklook-kit";
|
|
31
35
|
|
|
32
36
|
export default defineConfig({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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",
|
|
44
48
|
});
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
To preview a type macOS already knows about, use a plain string like `'public.json'` in place of an object.
|
|
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.
|
|
48
52
|
|
|
49
53
|
The config can also be `quicklook.config.js`, `.json`, `.yaml` or `.toml`, or a `.quicklookrc`. The keys are the same in every format.
|
|
50
54
|
|
|
51
55
|
| Key | Default | |
|
|
52
56
|
| -------------- | ------------ | ---------------------------------------------------------------------------------- |
|
|
53
|
-
| `name` | required | App name
|
|
57
|
+
| `name` | required | App name, shown in Finder and System Settings |
|
|
54
58
|
| `bundleId` | required | The extension gets `<bundleId>.Extension` |
|
|
55
59
|
| `types` | required | File types to preview |
|
|
56
60
|
| `framework` | required | `'sveltekit'`, `'astro'`, `'solid-start'`, `'nuxt'`, `'analog'`, or an integration |
|
|
61
|
+
| `icon` | none | Path to an `.icns` file for the app |
|
|
57
62
|
| `macOS` | `'14.0'` | Minimum macOS version |
|
|
58
63
|
| `integrations` | `[]` | See [Integrations](#integrations) |
|
|
59
64
|
| `csp` | locked down | Directives here replace the defaults of the same name |
|
|
@@ -66,16 +71,16 @@ Each framework needs a static, client-side build:
|
|
|
66
71
|
|
|
67
72
|
| Framework | Requirement |
|
|
68
73
|
| ---------- | ------------------------------------------------------ |
|
|
69
|
-
|
|
|
74
|
+
| Analog | `@analogjs/vite-plugin-angular` |
|
|
70
75
|
| Astro | the default `output: 'static'` |
|
|
71
|
-
| SolidStart | `ssr: false`, Nitro's `static` preset prerendering `/` |
|
|
72
76
|
| Nuxt | `ssr: false` |
|
|
73
|
-
|
|
|
77
|
+
| SolidStart | `ssr: false`, Nitro's `static` preset prerendering `/` |
|
|
78
|
+
| SvelteKit | `@sveltejs/adapter-static` |
|
|
74
79
|
|
|
75
80
|
### 2. Add the Vite plugin
|
|
76
81
|
|
|
77
82
|
```ts
|
|
78
|
-
import { quicklook } from
|
|
83
|
+
import { quicklook } from "quicklook-kit/vite";
|
|
79
84
|
|
|
80
85
|
// in the framework's Vite config
|
|
81
86
|
plugins: [quicklook()];
|
|
@@ -92,7 +97,7 @@ or in `tsconfig.json` under `compilerOptions.types` as `"quicklook-kit/virtual"`
|
|
|
92
97
|
### 3. Read the file
|
|
93
98
|
|
|
94
99
|
```ts
|
|
95
|
-
import { browserFile, config, nativeFile } from
|
|
100
|
+
import { browserFile, config, nativeFile } from "virtual:quicklook";
|
|
96
101
|
|
|
97
102
|
const file = nativeFile(); // the previewed file inside QuickLook, undefined in the browser
|
|
98
103
|
config.public; // values from quicklook.config.*
|
|
@@ -112,24 +117,24 @@ In the dev server, `nativeFile()` returns `undefined`. Pass a dropped or picked
|
|
|
112
117
|
|
|
113
118
|
```json
|
|
114
119
|
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
"scripts": {
|
|
121
|
+
"dev": "quicklook dev",
|
|
122
|
+
"build": "quicklook build",
|
|
123
|
+
"register": "quicklook build && quicklook register",
|
|
124
|
+
"unregister": "quicklook unregister"
|
|
125
|
+
}
|
|
121
126
|
}
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
## CLI
|
|
125
130
|
|
|
126
|
-
| Command | Description
|
|
127
|
-
| ---------------------- |
|
|
128
|
-
| `quicklook dev` | Starts the framework's dev server
|
|
129
|
-
| `quicklook build:web` | Builds the page only, into `.quicklook/web`
|
|
130
|
-
| `quicklook build` | Builds the app with its extension into `.quicklook/build`
|
|
131
|
-
| `quicklook register` | Copies the built app to `/Applications` and registers the extension
|
|
132
|
-
| `quicklook unregister` | Removes the app from `/Applications`
|
|
131
|
+
| Command | Description |
|
|
132
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
133
|
+
| `quicklook dev` | Starts the framework's dev server |
|
|
134
|
+
| `quicklook build:web` | Builds the page only, into `.quicklook/web` |
|
|
135
|
+
| `quicklook build` | Builds the app with its extension into `.quicklook/build` |
|
|
136
|
+
| `quicklook register` | Copies the built app to `/Applications` and registers the extension |
|
|
137
|
+
| `quicklook unregister` | Removes the app from `/Applications` |
|
|
133
138
|
|
|
134
139
|
After `register`, select a file in Finder and press Space. Run `build` and `register` again after every change.
|
|
135
140
|
|
|
@@ -150,22 +155,25 @@ Add `.quicklook` to your `.gitignore`.
|
|
|
150
155
|
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`.
|
|
151
156
|
|
|
152
157
|
```ts
|
|
153
|
-
import type { Integration } from
|
|
158
|
+
import type { Integration } from "quicklook-kit";
|
|
154
159
|
|
|
155
160
|
export default (options: { key: string }): Integration => ({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
name: "my-integration",
|
|
162
|
+
hooks: {
|
|
163
|
+
// Adjust the resolved config.
|
|
164
|
+
"config:resolved"(config) {},
|
|
165
|
+
|
|
166
|
+
// Post-process the framework's static output before it's bundled.
|
|
167
|
+
async "build:done"({ outDir, config }) {},
|
|
168
|
+
|
|
169
|
+
// Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets.
|
|
170
|
+
"xcode:spec"(spec, config) {
|
|
171
|
+
spec.targets.ExamplePreviewExtension.info.properties.MyKey = options.key;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Add Vite plugins wherever quicklook() is used.
|
|
175
|
+
vite: () => [],
|
|
176
|
+
},
|
|
169
177
|
});
|
|
170
178
|
```
|
|
171
179
|
|
|
@@ -173,7 +181,7 @@ Add it as an object, or by name so JSON and YAML configs can use it too. A name
|
|
|
173
181
|
|
|
174
182
|
```jsonc
|
|
175
183
|
{
|
|
176
|
-
|
|
184
|
+
"integrations": ["quicklook-some-package", ["./my-integration.ts", { "key": "value" }]],
|
|
177
185
|
}
|
|
178
186
|
```
|
|
179
187
|
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { cp, rm } from 'node:fs/promises';
|
|
7
|
-
import { resolve } from 'node:path';
|
|
7
|
+
import { basename, resolve } from 'node:path';
|
|
8
8
|
import process from 'node:process';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { $, Glob } from 'bun';
|
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
type DeclaredType,
|
|
14
14
|
loadConfig,
|
|
15
15
|
serializeCsp,
|
|
16
|
-
type XcodeSpec
|
|
16
|
+
type XcodeSpec,
|
|
17
17
|
} from './config.ts';
|
|
18
18
|
|
|
19
19
|
const config = await loadConfig();
|
|
20
|
-
const { root, name, bundleId, macOS, types, framework } = config;
|
|
20
|
+
const { root, name, bundleId, macOS, types, framework, icon } = config;
|
|
21
21
|
const target = name.replace(/[^A-Za-z0-9]/g, '');
|
|
22
22
|
// Everything generated lives here, away from the framework's own output.
|
|
23
23
|
const work = `${root}/.quicklook`;
|
|
@@ -35,7 +35,7 @@ const inlineScript = /<script\b(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
|
35
35
|
async function run(command: string[]) {
|
|
36
36
|
const child = Bun.spawn(['bun', 'run', ...command], {
|
|
37
37
|
cwd: root,
|
|
38
|
-
stdio: ['inherit', 'inherit', 'inherit']
|
|
38
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
39
39
|
});
|
|
40
40
|
const code = await child.exited;
|
|
41
41
|
if (code !== 0) {
|
|
@@ -81,20 +81,24 @@ async function build() {
|
|
|
81
81
|
CURRENT_PROJECT_VERSION: '1',
|
|
82
82
|
// Ad-hoc signing: enough for QuickLook to load it, no Apple Developer account needed.
|
|
83
83
|
CODE_SIGN_STYLE: 'Manual',
|
|
84
|
-
CODE_SIGN_IDENTITY: '-'
|
|
85
|
-
}
|
|
84
|
+
CODE_SIGN_IDENTITY: '-',
|
|
85
|
+
},
|
|
86
86
|
},
|
|
87
87
|
targets: {
|
|
88
88
|
[target]: {
|
|
89
89
|
type: 'application',
|
|
90
90
|
platform: 'macOS',
|
|
91
|
-
sources: [
|
|
91
|
+
sources: [
|
|
92
|
+
`${native}/App`,
|
|
93
|
+
...(icon ? [{ path: resolve(root, icon), buildPhase: 'resources' }] : []),
|
|
94
|
+
],
|
|
92
95
|
settings: { PRODUCT_NAME: name, PRODUCT_BUNDLE_IDENTIFIER: bundleId },
|
|
93
96
|
dependencies: [{ target: `${target}Extension`, embed: true, codeSign: true }],
|
|
94
97
|
info: {
|
|
95
98
|
path: `${work}/App/Info.plist`,
|
|
96
99
|
properties: {
|
|
97
100
|
CFBundleName: name,
|
|
101
|
+
...(icon && { CFBundleIconFile: basename(icon, '.icns') }),
|
|
98
102
|
LSMinimumSystemVersion: macOS,
|
|
99
103
|
UTExportedTypeDeclarations: types
|
|
100
104
|
.filter((type): type is DeclaredType => typeof type !== 'string')
|
|
@@ -102,10 +106,10 @@ async function build() {
|
|
|
102
106
|
UTTypeIdentifier: type.id,
|
|
103
107
|
UTTypeDescription: type.description,
|
|
104
108
|
UTTypeConformsTo: type.conformsTo,
|
|
105
|
-
UTTypeTagSpecification: { 'public.filename-extension': type.extensions }
|
|
106
|
-
}))
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
+
UTTypeTagSpecification: { 'public.filename-extension': type.extensions },
|
|
110
|
+
})),
|
|
111
|
+
},
|
|
112
|
+
},
|
|
109
113
|
},
|
|
110
114
|
[`${target}Extension`]: {
|
|
111
115
|
type: 'app-extension',
|
|
@@ -121,8 +125,8 @@ async function build() {
|
|
|
121
125
|
'com.apple.security.files.user-selected.read-only': true,
|
|
122
126
|
// WKWebView's content process won't launch in a sandbox without this,
|
|
123
127
|
// even for a page that loads nothing. The page's CSP blocks all requests.
|
|
124
|
-
'com.apple.security.network.client': true
|
|
125
|
-
}
|
|
128
|
+
'com.apple.security.network.client': true,
|
|
129
|
+
},
|
|
126
130
|
},
|
|
127
131
|
info: {
|
|
128
132
|
path: `${work}/Extension/Info.plist`,
|
|
@@ -136,15 +140,15 @@ async function build() {
|
|
|
136
140
|
NSExtensionPrincipalClass: 'PreviewViewController',
|
|
137
141
|
NSExtensionAttributes: {
|
|
138
142
|
QLSupportedContentTypes: types.map((type) =>
|
|
139
|
-
typeof type === 'string' ? type : type.id
|
|
143
|
+
typeof type === 'string' ? type : type.id,
|
|
140
144
|
),
|
|
141
|
-
QLSupportsSearchableItems: false
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
145
|
+
QLSupportsSearchableItems: false,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
148
152
|
};
|
|
149
153
|
for (const integration of config.integrations) {
|
|
150
154
|
await integration.hooks?.['xcode:spec']?.(spec, config);
|
package/src/config.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface QuickLookConfig {
|
|
|
52
52
|
/** App name, shown in Finder and System Settings. */
|
|
53
53
|
name: string;
|
|
54
54
|
bundleId: string;
|
|
55
|
+
/** Path to the app's .icns icon, relative to the project root. */
|
|
56
|
+
icon?: string;
|
|
55
57
|
/** Minimum macOS version. Defaults to 14.0. */
|
|
56
58
|
macOS?: string;
|
|
57
59
|
types: FileType[];
|
|
@@ -70,7 +72,8 @@ export interface QuickLookConfig {
|
|
|
70
72
|
export type PageConfig = Pick<ResolvedConfig, 'name' | 'bundleId' | 'types' | 'public'>;
|
|
71
73
|
|
|
72
74
|
export interface ResolvedConfig
|
|
73
|
-
extends Required<Omit<QuickLookConfig, 'framework' | 'integrations'
|
|
75
|
+
extends Required<Omit<QuickLookConfig, 'framework' | 'integrations' | 'icon'>>,
|
|
76
|
+
Pick<QuickLookConfig, 'icon'> {
|
|
74
77
|
root: string;
|
|
75
78
|
framework: Integration & { build: NonNullable<Integration['build']> };
|
|
76
79
|
/** The framework first, then the configured integrations, in order. */
|
|
@@ -84,7 +87,7 @@ export const defaultCsp: Csp = {
|
|
|
84
87
|
'style-src': ["'self'", "'unsafe-inline'"],
|
|
85
88
|
'img-src': ["'self'", 'data:', 'blob:'],
|
|
86
89
|
'font-src': ["'self'", 'data:'],
|
|
87
|
-
'media-src': ["'self'", 'data:', 'blob:']
|
|
90
|
+
'media-src': ["'self'", 'data:', 'blob:'],
|
|
88
91
|
};
|
|
89
92
|
|
|
90
93
|
export const defineConfig = (config: QuickLookConfig) => config;
|
|
@@ -97,7 +100,7 @@ export const serializeCsp = (csp: Csp) =>
|
|
|
97
100
|
/** `csp` with `sources` appended to `directive`. */
|
|
98
101
|
export const addSources = (csp: Csp, directive: string, ...sources: string[]): Csp => ({
|
|
99
102
|
...csp,
|
|
100
|
-
[directive]: [...(csp[directive] ?? []), ...sources]
|
|
103
|
+
[directive]: [...(csp[directive] ?? []), ...sources],
|
|
101
104
|
});
|
|
102
105
|
|
|
103
106
|
/**
|
|
@@ -114,7 +117,7 @@ const frameworkBuilds: Record<string, [dev: string[], command: string[], outDir:
|
|
|
114
117
|
// `solidStart({ ssr: false })` and Nitro's static preset prerendering `/`.
|
|
115
118
|
'solid-start': [['vite', 'dev'], ['vite', 'build'], '.output/public'],
|
|
116
119
|
// @sveltejs/adapter-static, writing `build`.
|
|
117
|
-
sveltekit: [['vite', 'dev'], ['vite', 'build'], 'build']
|
|
120
|
+
sveltekit: [['vite', 'dev'], ['vite', 'build'], 'build'],
|
|
118
121
|
};
|
|
119
122
|
|
|
120
123
|
function builtinFramework(name: string, options?: unknown): Integration | undefined {
|
|
@@ -125,7 +128,7 @@ function builtinFramework(name: string, options?: unknown): Integration | undefi
|
|
|
125
128
|
const [dev, command, outDir] = builtin;
|
|
126
129
|
return {
|
|
127
130
|
name,
|
|
128
|
-
build: { dev, command, outDir: (options as { outDir?: string } | undefined)?.outDir ?? outDir }
|
|
131
|
+
build: { dev, command, outDir: (options as { outDir?: string } | undefined)?.outDir ?? outDir },
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -167,7 +170,7 @@ export async function loadConfig(root = process.cwd()): Promise<ResolvedConfig>
|
|
|
167
170
|
csp: { ...defaultCsp, ...config.csp },
|
|
168
171
|
root,
|
|
169
172
|
framework: { ...framework, build: framework.build },
|
|
170
|
-
integrations
|
|
173
|
+
integrations,
|
|
171
174
|
};
|
|
172
175
|
for (const integration of integrations) {
|
|
173
176
|
await integration.hooks?.['config:resolved']?.(resolved);
|
package/src/runtime.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function nativeFile(): QuickLookFile | undefined {
|
|
|
22
22
|
return {
|
|
23
23
|
name: injected.name,
|
|
24
24
|
extension: injected.extension,
|
|
25
|
-
bytes: Uint8Array.from(atob(injected.base64), (c) => c.charCodeAt(0))
|
|
25
|
+
bytes: Uint8Array.from(atob(injected.base64), (c) => c.charCodeAt(0)),
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -31,6 +31,6 @@ export async function browserFile(file: File): Promise<QuickLookFile> {
|
|
|
31
31
|
return {
|
|
32
32
|
name: file.name,
|
|
33
33
|
extension: file.name.includes('.') ? (file.name.split('.').pop() ?? '').toLowerCase() : '',
|
|
34
|
-
bytes: new Uint8Array(await file.arrayBuffer())
|
|
34
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
35
35
|
};
|
|
36
36
|
}
|
package/src/vite.ts
CHANGED
|
@@ -20,8 +20,8 @@ export { browserFile, nativeFile } from 'quicklook-kit/runtime';`;
|
|
|
20
20
|
addSources(config.csp, 'script-src', "'unsafe-inline'"),
|
|
21
21
|
'connect-src',
|
|
22
22
|
"'self'",
|
|
23
|
-
'ws:'
|
|
24
|
-
)
|
|
23
|
+
'ws:',
|
|
24
|
+
),
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
return [
|
|
@@ -34,8 +34,8 @@ export { browserFile, nativeFile } from 'quicklook-kit/runtime';`;
|
|
|
34
34
|
response.setHeader('Content-Security-Policy-Report-Only', devCsp);
|
|
35
35
|
next();
|
|
36
36
|
});
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
38
|
},
|
|
39
|
-
...config.integrations.flatMap((integration) => integration.hooks?.vite?.() ?? [])
|
|
39
|
+
...config.integrations.flatMap((integration) => integration.hooks?.vite?.() ?? []),
|
|
40
40
|
];
|
|
41
41
|
}
|