@oddsquad/vite-plugin-lit 0.0.5 → 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/lib/plugin.d.ts CHANGED
@@ -46,6 +46,17 @@ export interface HmrOptions {
46
46
  */
47
47
  indicator?: boolean | HmrIndicatorOptions;
48
48
  }
49
+ /**
50
+ * What a `?css-sheet` import compiles to under `vite build`.
51
+ *
52
+ * - `'url'`: fetch-backed sheet over the emitted `.css` asset.
53
+ * - `'inline'`: css text embedded in the JS chunk, processed by the css
54
+ * pipeline (`?inline`).
55
+ * - `'inline-raw'`: css text embedded verbatim, skipping the css pipeline
56
+ * (`?raw`).
57
+ * - `'auto'`: `'inline'` when `build.lib` is set, `'url'` otherwise.
58
+ */
59
+ export type CssSheetBuild = 'auto' | 'url' | 'inline' | 'inline-raw';
49
60
  /**
50
61
  * Options for the Lit Vite plugin.
51
62
  *
@@ -77,14 +88,59 @@ export interface LitPluginOptions {
77
88
  * `true` enables all built-in layers with defaults.
78
89
  */
79
90
  timeline?: boolean;
91
+ /**
92
+ * What a `?css-sheet` import compiles to under `vite build`.
93
+ * - `'url'`: fetch-backed sheet over the emitted `.css` asset
94
+ * - `'inline'`: css text embedded in the JS chunk, processed by the css
95
+ * pipeline (`?inline`)
96
+ * - `'inline-raw'`: css text embedded verbatim, skipping the css pipeline
97
+ * (`?raw`)
98
+ * - `'auto'`: `'inline'` when `build.lib` is set, `'url'` otherwise
99
+ *
100
+ * Dev is always the fetch-backed HMR form regardless of this setting.
101
+ * Defaults to `'auto'`.
102
+ */
103
+ cssSheetBuild?: CssSheetBuild;
104
+ }
105
+ /** Plugin options after merging explicit options, env vars, and defaults. */
106
+ export interface ResolvedOptions {
107
+ hmrEnabled: boolean;
108
+ reconnect: boolean;
109
+ onIncompatible: 'reload' | 'warn';
110
+ indicator: false | {
111
+ count: boolean;
112
+ };
113
+ sourceOverlay: false | SourceOverlayOptions;
114
+ timeline: boolean;
115
+ cssSheetBuild: CssSheetBuild;
80
116
  }
117
+ /**
118
+ * Merge explicit options over env vars over defaults (in that precedence) into
119
+ * the flat shape the plugin hooks consume.
120
+ */
121
+ export declare const resolveOptions: (options: LitPluginOptions, env: Record<string, string>) => ResolvedOptions;
122
+ export declare const createOpenInEditorMiddleware: (allowedRoots: readonly string[]) => (req: {
123
+ url?: string;
124
+ headers?: {
125
+ origin?: string;
126
+ host?: string;
127
+ };
128
+ }, res: {
129
+ statusCode: number;
130
+ end: (msg: string) => void;
131
+ }, next: (err?: unknown) => void) => void;
81
132
  /**
82
133
  * Import-query support, served in dev and build alike (source code using
83
134
  * `?hmr-url`/`?css-sheet` must keep working under `vite build`, where the HMR
84
135
  * plugin doesn't apply). Exported for the baseline e2e run, which needs the
85
136
  * queries working without the HMR plugin.
137
+ *
138
+ * `getCssSheetBuild` is a getter, not a value: `litPlugin()` re-resolves its
139
+ * options against the loaded env in a `config` hook, which runs after the
140
+ * plugin array is built. Standalone (no getter) it behaves like the `'auto'`
141
+ * default.
86
142
  */
87
- export declare const litCssQueries: () => Plugin;
143
+ export declare const litCssQueries: (getCssSheetBuild?: () => CssSheetBuild) => Plugin;
88
144
  /**
89
145
  * Vite plugin set providing HMR, CSS helpers, and Lightning CSS processing
90
146
  * for Lit projects.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../packages/labs/vite-plugin-lit/src/lib/plugin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAA2B,KAAK,MAAM,EAAC,MAAM,MAAM,CAAC;AAI3D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,YAAY,CAAC;AAKrD;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;CAC3C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAE3B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IAE/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA6PD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QAAO,MA4D/B,CAAC;AAuFH;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,UAAS,gBAAqB,KAAG,MAAM,EAsQhE,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../packages/labs/vite-plugin-lit/src/lib/plugin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAA2B,KAAK,MAAM,EAAC,MAAM,MAAM,CAAC;AAI3D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,YAAY,CAAC;AAKrD;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;CAC3C;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,YAAY,CAAC;AASrE;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAE3B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IAE/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,QAAQ,GAAG,MAAM,CAAC;IAClC,SAAS,EAAE,KAAK,GAAG;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC,CAAC;IACpC,aAAa,EAAE,KAAK,GAAG,oBAAoB,CAAC;IAC5C,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAuCD;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,SAAS,gBAAgB,EACzB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC1B,eA4DF,CAAC;AA8DF,eAAO,MAAM,4BAA4B,GACvC,cAAc,SAAS,MAAM,EAAE,MAM7B,KAAK;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAC,CAAA;CAAC,EAC/D,KAAK;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAC,EACrD,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,SAkFhC,CAAC;AAoCF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GACxB,mBAAkB,MAAM,aAA4B,KACnD,MAwFF,CAAC;AAuFF;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,UAAS,gBAAqB,KAAG,MAAM,EA0RhE,CAAC"}
package/lib/plugin.js CHANGED
@@ -12,6 +12,12 @@ import { injectSourceMeta } from './source-meta.js';
12
12
  import { INSTALL_ID, VIRTUAL_PREFIX, transformLitModule } from './transform.js';
13
13
  import { WRAP_TABLE } from './wrap-table.js';
14
14
  import { litTimelinePlugin } from './timeline-plugin.js';
15
+ const CSS_SHEET_BUILD_MODES = [
16
+ 'auto',
17
+ 'url',
18
+ 'inline',
19
+ 'inline-raw',
20
+ ];
15
21
  /** Env var prefix consumed at config time. */
16
22
  const ENV_PREFIX = 'LIT_PLUGIN';
17
23
  /** Parse a boolean-ish env string; `undefined` when unset/unrecognized. */
@@ -27,11 +33,25 @@ const envNum = (v) => {
27
33
  const n = Number(v);
28
34
  return Number.isNaN(n) ? undefined : n;
29
35
  };
36
+ /**
37
+ * Parse a string against a closed set of values; `undefined` (with a warning)
38
+ * when set to something outside it, so a typo falls back to the default
39
+ * instead of silently disabling a feature.
40
+ */
41
+ const envEnum = (v, allowed, source) => {
42
+ if (v === undefined || v === '')
43
+ return undefined;
44
+ if (allowed.includes(v))
45
+ return v;
46
+ console.warn(`[lit-plugin] ignoring ${source}=${JSON.stringify(v)}; expected one of ` +
47
+ allowed.map((a) => JSON.stringify(a)).join(', '));
48
+ return undefined;
49
+ };
30
50
  /**
31
51
  * Merge explicit options over env vars over defaults (in that precedence) into
32
52
  * the flat shape the plugin hooks consume.
33
53
  */
34
- const resolveOptions = (options, env) => {
54
+ export const resolveOptions = (options, env) => {
35
55
  const hmr = options.hmr;
36
56
  const hmrObj = typeof hmr === 'object' ? hmr : undefined;
37
57
  const hmrEnabled = (typeof hmr === 'boolean' ? hmr : hmrObj?.enabled) ??
@@ -61,6 +81,9 @@ const resolveOptions = (options, env) => {
61
81
  sourceOverlay = base;
62
82
  }
63
83
  const timeline = options.timeline ?? envBool(env[`${ENV_PREFIX}_TIMELINE`]) ?? false;
84
+ const cssSheetBuild = envEnum(options.cssSheetBuild, CSS_SHEET_BUILD_MODES, 'cssSheetBuild') ??
85
+ envEnum(env[`${ENV_PREFIX}_CSS_SHEET_BUILD`], CSS_SHEET_BUILD_MODES, `${ENV_PREFIX}_CSS_SHEET_BUILD`) ??
86
+ 'auto';
64
87
  return {
65
88
  hmrEnabled,
66
89
  reconnect,
@@ -68,6 +91,7 @@ const resolveOptions = (options, env) => {
68
91
  indicator,
69
92
  sourceOverlay,
70
93
  timeline,
94
+ cssSheetBuild,
71
95
  };
72
96
  };
73
97
  /**
@@ -126,7 +150,10 @@ const isSameOrigin = (headers) => {
126
150
  return false;
127
151
  }
128
152
  };
129
- const createOpenInEditorMiddleware = (root) => {
153
+ export const createOpenInEditorMiddleware = (allowedRoots) => {
154
+ // First entry is the primary root: relative `file` params resolve against
155
+ // it. Every entry grants open access to files beneath it.
156
+ const roots = allowedRoots.map((r) => resolvePath(r));
130
157
  return (req, res, next) => {
131
158
  if (req.url === undefined) {
132
159
  next();
@@ -147,14 +174,16 @@ const createOpenInEditorMiddleware = (root) => {
147
174
  res.end('missing file parameter');
148
175
  return;
149
176
  }
150
- // Confine the open to the project root: resolve the requested path and
151
- // reject anything that escapes `root` (path traversal, absolute paths to
152
- // arbitrary files). `launch-editor` spawns the user's editor on this path,
153
- // so an unvalidated `file` is a local-file-open / arg-injection vector.
154
- const resolved = resolvePath(root, file);
155
- if (resolved !== root && !resolved.startsWith(root + sep)) {
177
+ // Confine the open to the allowed roots: resolve the requested path and
178
+ // reject anything that escapes all of them (path traversal, absolute
179
+ // paths to arbitrary files). `launch-editor` spawns the user's editor on
180
+ // this path, so an unvalidated `file` is a local-file-open /
181
+ // arg-injection vector.
182
+ const resolved = resolvePath(roots[0] ?? process.cwd(), file);
183
+ const allowed = roots.some((root) => resolved === root || resolved.startsWith(root + sep));
184
+ if (!allowed) {
156
185
  res.statusCode = 403;
157
- res.end('file outside project root');
186
+ res.end('file outside allowed roots');
158
187
  return;
159
188
  }
160
189
  if (!existsSync(resolved)) {
@@ -170,14 +199,24 @@ const createOpenInEditorMiddleware = (root) => {
170
199
  import('launch-editor')
171
200
  .then((mod) => {
172
201
  const launch = (mod.default ?? mod);
202
+ // `launch-editor` invokes the callback only on failure — synchronously
203
+ // when it can't guess an editor, later if the spawn errors. Reply `ok`
204
+ // right after launching; the guard keeps a sync failure's 500 first
205
+ // and drops a late async failure (the response is long gone).
206
+ let finished = false;
207
+ const finish = (statusCode, msg) => {
208
+ if (finished)
209
+ return;
210
+ finished = true;
211
+ res.statusCode = statusCode;
212
+ res.end(msg);
213
+ };
173
214
  launch(fileRef, (_fileName, errorMessage) => {
174
215
  if (errorMessage !== null) {
175
- res.statusCode = 500;
176
- res.end(errorMessage);
177
- return;
216
+ finish(500, errorMessage);
178
217
  }
179
- res.end('ok');
180
218
  });
219
+ finish(200, 'ok');
181
220
  })
182
221
  .catch(next);
183
222
  };
@@ -203,6 +242,11 @@ const CSS_URL_VIRTUAL_PREFIX = '\0lit-plugin:hmr-url:';
203
242
  * a plugin-generated module: the `import.meta.hot.accept` lives here (where
204
243
  * Vite's static analysis sees the literal specifier), so callers write a bare
205
244
  * import and never touch `import.meta.hot` themselves.
245
+ *
246
+ * Under `vite build` the generated module can instead inline the css text (see
247
+ * `cssSheetBuild`) — a library's consumers bundle its JS only, so a
248
+ * fetch-backed sheet would 404 on the `.css` asset that never reached their
249
+ * build.
206
250
  */
207
251
  const CSS_SHEET_QUERY_RE = /^([^?]+\.css)\?(?:[^&]*&)*css-sheet(?:&.*)?$/;
208
252
  const CSS_SHEET_VIRTUAL_PREFIX = '\0lit-plugin:css-sheet:';
@@ -214,60 +258,91 @@ const CSS_VIRTUAL_SUFFIX = '.js';
214
258
  * `?hmr-url`/`?css-sheet` must keep working under `vite build`, where the HMR
215
259
  * plugin doesn't apply). Exported for the baseline e2e run, which needs the
216
260
  * queries working without the HMR plugin.
261
+ *
262
+ * `getCssSheetBuild` is a getter, not a value: `litPlugin()` re-resolves its
263
+ * options against the loaded env in a `config` hook, which runs after the
264
+ * plugin array is built. Standalone (no getter) it behaves like the `'auto'`
265
+ * default.
217
266
  */
218
- export const litCssQueries = () => ({
219
- name: 'lit-css-query',
220
- // Vite's core resolver claims `./x.css?hmr-url` for the CSS pipeline
221
- // before normal plugins get a look, so resolve ahead of it.
222
- enforce: 'pre',
223
- async resolveId(id, importer) {
224
- const prefix = CSS_URL_QUERY_RE.test(id)
225
- ? CSS_URL_VIRTUAL_PREFIX
226
- : CSS_SHEET_QUERY_RE.test(id)
227
- ? CSS_SHEET_VIRTUAL_PREFIX
228
- : null;
229
- if (prefix === null) {
230
- return null;
231
- }
232
- const file = id.slice(0, id.indexOf('?'));
233
- const resolved = await this.resolve(file, importer);
234
- if (resolved === null) {
267
+ export const litCssQueries = (getCssSheetBuild = () => 'auto') => {
268
+ let isBuild = false;
269
+ let isLib = false;
270
+ return {
271
+ name: 'lit-css-query',
272
+ // Vite's core resolver claims `./x.css?hmr-url` for the CSS pipeline
273
+ // before normal plugins get a look, so resolve ahead of it.
274
+ enforce: 'pre',
275
+ configResolved(config) {
276
+ isBuild = config.command === 'build';
277
+ isLib = config.build.lib !== false && config.build.lib !== undefined;
278
+ },
279
+ async resolveId(id, importer) {
280
+ const prefix = CSS_URL_QUERY_RE.test(id)
281
+ ? CSS_URL_VIRTUAL_PREFIX
282
+ : CSS_SHEET_QUERY_RE.test(id)
283
+ ? CSS_SHEET_VIRTUAL_PREFIX
284
+ : null;
285
+ if (prefix === null) {
286
+ return null;
287
+ }
288
+ const file = id.slice(0, id.indexOf('?'));
289
+ const resolved = await this.resolve(file, importer);
290
+ if (resolved === null) {
291
+ return null;
292
+ }
293
+ return prefix + resolved.id + CSS_VIRTUAL_SUFFIX;
294
+ },
295
+ load(id) {
296
+ const helperPath = resolveRuntimeModule('css');
297
+ if (id.startsWith(CSS_URL_VIRTUAL_PREFIX)) {
298
+ const file = id.slice(CSS_URL_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
299
+ return {
300
+ code: `import url from ${JSON.stringify(`${file}?url`)};\n` +
301
+ `import {devCacheBust} from ${JSON.stringify(helperPath)};\n` +
302
+ `export default devCacheBust(url);\n`,
303
+ moduleType: 'js',
304
+ };
305
+ }
306
+ if (id.startsWith(CSS_SHEET_VIRTUAL_PREFIX)) {
307
+ const file = id.slice(CSS_SHEET_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
308
+ const mode = getCssSheetBuild();
309
+ const inlineQuery = !isBuild || mode === 'url' || (mode === 'auto' && !isLib)
310
+ ? null
311
+ : mode === 'inline-raw'
312
+ ? '?raw'
313
+ : '?inline';
314
+ if (inlineQuery !== null) {
315
+ // Build only, so no `import.meta.hot` block. The sheet is still
316
+ // constructed once per virtual module, so adopters share it exactly
317
+ // as they do in the fetch-backed form.
318
+ return {
319
+ code: `import css from ${JSON.stringify(`${file}${inlineQuery}`)};\n` +
320
+ `const sheet = new CSSStyleSheet();\n` +
321
+ `sheet.replaceSync(css);\n` +
322
+ `export default sheet;\n`,
323
+ moduleType: 'js',
324
+ };
325
+ }
326
+ // The accept specifier must be byte-identical to the import above —
327
+ // Vite resolves accepted HMR deps by static analysis. Generating both
328
+ // here is exactly what frees the caller from writing it. The swap is
329
+ // self-accepted at this boundary, so it never propagates to adopters.
330
+ const urlSpecifier = JSON.stringify(`${file}?url`);
331
+ return {
332
+ code: `import url from ${urlSpecifier};\n` +
333
+ `import {urlSheet} from ${JSON.stringify(helperPath)};\n` +
334
+ `const {sheet, onHotUpdate} = urlSheet(url);\n` +
335
+ `export default sheet;\n` +
336
+ `if (import.meta.hot) {\n` +
337
+ ` import.meta.hot.accept(${urlSpecifier}, onHotUpdate);\n` +
338
+ `}\n`,
339
+ moduleType: 'js',
340
+ };
341
+ }
235
342
  return null;
236
- }
237
- return prefix + resolved.id + CSS_VIRTUAL_SUFFIX;
238
- },
239
- load(id) {
240
- const helperPath = resolveRuntimeModule('css');
241
- if (id.startsWith(CSS_URL_VIRTUAL_PREFIX)) {
242
- const file = id.slice(CSS_URL_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
243
- return {
244
- code: `import url from ${JSON.stringify(`${file}?url`)};\n` +
245
- `import {devCacheBust} from ${JSON.stringify(helperPath)};\n` +
246
- `export default devCacheBust(url);\n`,
247
- moduleType: 'js',
248
- };
249
- }
250
- if (id.startsWith(CSS_SHEET_VIRTUAL_PREFIX)) {
251
- const file = id.slice(CSS_SHEET_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
252
- // The accept specifier must be byte-identical to the import above —
253
- // Vite resolves accepted HMR deps by static analysis. Generating both
254
- // here is exactly what frees the caller from writing it. The swap is
255
- // self-accepted at this boundary, so it never propagates to adopters.
256
- const urlSpecifier = JSON.stringify(`${file}?url`);
257
- return {
258
- code: `import url from ${urlSpecifier};\n` +
259
- `import {urlSheet} from ${JSON.stringify(helperPath)};\n` +
260
- `const {sheet, onHotUpdate} = urlSheet(url);\n` +
261
- `export default sheet;\n` +
262
- `if (import.meta.hot) {\n` +
263
- ` import.meta.hot.accept(${urlSpecifier}, onHotUpdate);\n` +
264
- `}\n`,
265
- moduleType: 'js',
266
- };
267
- }
268
- return null;
269
- },
270
- });
343
+ },
344
+ };
345
+ };
271
346
  /**
272
347
  * A `css` tagged template literal with no interpolations and no escape
273
348
  * sequences — the only kind we can hand to a CSS parser as-is. Literals
@@ -354,10 +429,30 @@ const litCssLiterals = () => {
354
429
  */
355
430
  export const litPlugin = (options = {}) => {
356
431
  // Resolved from explicit options first, env vars second, defaults last. The
357
- // `config` hook re-resolves with the loaded env once Vite hands us the mode;
358
- // this initial pass covers code paths that run before (or without) it.
432
+ // `lit-plugin-options` `config` hook below re-resolves with the loaded env
433
+ // once Vite hands us the mode; this initial pass covers code paths that run
434
+ // before (or without) it.
359
435
  let resolved = resolveOptions(options, {});
360
436
  let root = '';
437
+ // Env resolution lives in its own always-applied plugin: `?css-sheet` is a
438
+ // build-time feature too, and the HMR plugin (`apply: 'serve'`) never gets a
439
+ // `config` hook under `vite build`. `enforce: 'pre'` and first position put
440
+ // it ahead of every other hook that reads `resolved`.
441
+ const optionsPlugin = {
442
+ name: 'lit-plugin-options',
443
+ enforce: 'pre',
444
+ config(viteConfig, { mode }) {
445
+ // `loadEnv` reads `.env*` files from the env dir (root by default) and
446
+ // merges in matching `process.env` keys, filtered to the `LIT_PLUGIN`
447
+ // prefix. Explicit options still win (handled in resolveOptions).
448
+ const envDir = viteConfig.envDir
449
+ ? resolvePath(viteConfig.envDir)
450
+ : viteConfig.root
451
+ ? resolvePath(viteConfig.root)
452
+ : process.cwd();
453
+ resolved = resolveOptions(options, loadEnv(mode, envDir, ENV_PREFIX));
454
+ },
455
+ };
361
456
  const sourceOverlayPlugin = {
362
457
  name: 'lit-source-overlay',
363
458
  apply: 'serve',
@@ -377,7 +472,12 @@ export const litPlugin = (options = {}) => {
377
472
  configureServer(server) {
378
473
  if (!resolved.sourceOverlay)
379
474
  return;
380
- server.middlewares.use(OPEN_IN_EDITOR_PATH, createOpenInEditorMiddleware(root));
475
+ // Allow opens from everything vite itself is willing to serve
476
+ // (`server.fs.allow` defaults to the workspace root), not just
477
+ // `config.root` — in monorepos, component sources regularly live in
478
+ // sibling packages outside the served app's root.
479
+ const fsAllow = server.config.server.fs?.allow ?? [];
480
+ server.middlewares.use(OPEN_IN_EDITOR_PATH, createOpenInEditorMiddleware([root, ...fsAllow]));
381
481
  },
382
482
  transform(code, id, transformOptions) {
383
483
  if (!resolved.sourceOverlay)
@@ -428,17 +528,9 @@ export const litPlugin = (options = {}) => {
428
528
  const hmr = {
429
529
  name: 'lit-plugin',
430
530
  apply: 'serve',
431
- config: (viteConfig, { mode }) => {
432
- // Resolve options against the loaded env now that Vite hands us the mode.
433
- // `loadEnv` reads `.env*` files from the env dir (root by default) and
434
- // merges in matching `process.env` keys, filtered to the `LIT_PLUGIN`
435
- // prefix. Explicit options still win (handled in resolveOptions).
436
- const envDir = viteConfig.envDir
437
- ? resolvePath(viteConfig.envDir)
438
- : viteConfig.root
439
- ? resolvePath(viteConfig.root)
440
- : process.cwd();
441
- resolved = resolveOptions(options, loadEnv(mode, envDir, ENV_PREFIX));
531
+ config: () => {
532
+ // Options are already resolved against the loaded env by
533
+ // `lit-plugin-options`, which runs first.
442
534
  if (!resolved.hmrEnabled) {
443
535
  return;
444
536
  }
@@ -578,7 +670,10 @@ export const litPlugin = (options = {}) => {
578
670
  // feature is disabled (which env may decide), so inclusion can't be gated
579
671
  // on the synchronously-known options here.
580
672
  const plugins = [
581
- litCssQueries(),
673
+ optionsPlugin,
674
+ // Getter, not a snapshot: `resolved` is re-resolved against the loaded env
675
+ // in `optionsPlugin`'s `config` hook, which runs after this array is built.
676
+ litCssQueries(() => resolved.cssSheetBuild),
582
677
  litCssLiterals(),
583
678
  sourceOverlayPlugin,
584
679
  hmr,