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 CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "name": "quicklook-kit",
3
- "version": "0.1.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
+ ![License](https://img.shields.io/npm/l/quicklook-kit?style=for-the-badge)
4
+ [![Version](https://img.shields.io/npm/v/quicklook-kit?style=for-the-badge)](https://www.npmjs.org/package/quicklook-kit)
5
+ [![Build](https://img.shields.io/github/actions/workflow/status/idleberg/quicklook-kit/ci.yml?style=for-the-badge)](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 'quicklook-kit';
34
+ import { defineConfig } from "quicklook-kit";
31
35
 
32
36
  export default defineConfig({
33
- name: 'Example Preview', // app name, shown in Finder and System Settings
34
- bundleId: 'com.example.ExamplePreview',
35
- types: [
36
- {
37
- id: 'com.example.ExamplePreview.example',
38
- description: 'Example File',
39
- extensions: ['example'],
40
- conformsTo: ['public.plain-text'] // or 'public.data' for binary formats
41
- }
42
- ],
43
- framework: 'sveltekit' // or 'astro', 'solid-start', 'nuxt', 'analog'
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
- | SvelteKit | `@sveltejs/adapter-static` |
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
- | Analog | `@analogjs/vite-plugin-angular` |
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 'quicklook-kit/vite';
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 'virtual:quicklook';
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
- "scripts": {
116
- "dev": "quicklook dev",
117
- "build": "quicklook build",
118
- "register": "quicklook build && quicklook register",
119
- "unregister": "quicklook unregister"
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 'quicklook-kit';
158
+ import type { Integration } from "quicklook-kit";
154
159
 
155
160
  export default (options: { key: string }): Integration => ({
156
- name: 'my-integration',
157
- hooks: {
158
- // Adjust the resolved config.
159
- 'config:resolved'(config) {},
160
- // Post-process the framework's static output before it's bundled.
161
- async 'build:done'({ outDir, config }) {},
162
- // Edit the generated XcodeGen spec: Info.plist keys, entitlements, targets.
163
- 'xcode:spec'(spec, config) {
164
- spec.targets.ExamplePreviewExtension.info.properties.MyKey = options.key;
165
- },
166
- // Add Vite plugins wherever quicklook() is used.
167
- vite: () => []
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
- "integrations": ["quicklook-some-package", ["./my-integration.ts", { "key": "value" }]]
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: [`${native}/App`],
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
  }