@knighted/css 1.0.0-rc.4 → 1.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/css.cjs CHANGED
@@ -12,6 +12,7 @@ const node_fs_1 = require("node:fs");
12
12
  const dependency_tree_1 = __importDefault(require("dependency-tree"));
13
13
  const lightningcss_1 = require("lightningcss");
14
14
  const helpers_js_1 = require("./helpers.cjs");
15
+ const sassInternals_js_1 = require("./sassInternals.cjs");
15
16
  exports.DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
16
17
  async function css(entry, options = {}) {
17
18
  const { css: output } = await cssWithMeta(entry, options);
@@ -35,6 +36,7 @@ async function cssWithMeta(entry, options = {}) {
35
36
  const chunk = await compileStyleModule(file, {
36
37
  cwd,
37
38
  peerResolver: options.peerResolver,
39
+ resolver: options.resolver,
38
40
  });
39
41
  if (chunk) {
40
42
  chunks.push(chunk);
@@ -105,13 +107,17 @@ function matchExtension(filePath, extensions) {
105
107
  const lower = filePath.toLowerCase();
106
108
  return extensions.find(ext => lower.endsWith(ext));
107
109
  }
108
- async function compileStyleModule(file, { cwd, peerResolver }) {
110
+ async function compileStyleModule(file, { cwd, peerResolver, resolver, }) {
109
111
  switch (file.ext) {
110
112
  case '.css':
111
113
  return node_fs_1.promises.readFile(file.path, 'utf8');
112
114
  case '.scss':
113
115
  case '.sass':
114
- return compileSass(file.path, file.ext === '.sass', peerResolver);
116
+ return compileSass(file.path, file.ext === '.sass', {
117
+ cwd,
118
+ peerResolver,
119
+ resolver,
120
+ });
115
121
  case '.less':
116
122
  return compileLess(file.path, peerResolver);
117
123
  case '.css.ts':
@@ -120,12 +126,14 @@ async function compileStyleModule(file, { cwd, peerResolver }) {
120
126
  return '';
121
127
  }
122
128
  }
123
- async function compileSass(filePath, indented, peerResolver) {
129
+ async function compileSass(filePath, indented, { cwd, peerResolver, resolver, }) {
124
130
  const sassModule = await optionalPeer('sass', 'Sass', peerResolver);
125
131
  const sass = sassModule;
126
- const result = sass.compile(filePath, {
132
+ const importer = (0, sassInternals_js_1.createSassImporter)({ cwd, resolver });
133
+ const result = await sass.compileAsync(filePath, {
127
134
  style: 'expanded',
128
135
  loadPaths: buildSassLoadPaths(filePath),
136
+ importers: importer ? [importer] : undefined,
129
137
  });
130
138
  return result.css;
131
139
  }
@@ -1,11 +1,10 @@
1
1
  import type { Options as DependencyTreeOpts } from 'dependency-tree';
2
2
  import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
3
3
  import { type SpecificitySelector, type SpecificityStrategy } from './helpers.cjs';
4
+ import type { CssResolver } from './sassInternals.cjs';
5
+ export type { CssResolver } from './sassInternals.cjs';
4
6
  export declare const DEFAULT_EXTENSIONS: string[];
5
7
  type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
6
- export type CssResolver = (specifier: string, ctx: {
7
- cwd: string;
8
- }) => string | Promise<string | undefined>;
9
8
  type PeerLoader = (name: string) => Promise<unknown>;
10
9
  export interface CssOptions {
11
10
  extensions?: string[];
@@ -35,4 +34,3 @@ export interface CssResult {
35
34
  export declare function css(entry: string, options?: CssOptions): Promise<string>;
36
35
  export declare function cssWithMeta(entry: string, options?: CssOptions): Promise<CssResult>;
37
36
  export declare function compileVanillaModule(filePath: string, cwd: string, peerResolver?: PeerLoader): Promise<VanillaCompileResult>;
38
- export {};
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.pitch = void 0;
4
4
  const css_js_1 = require("./css.cjs");
5
+ const moduleInfo_js_1 = require("./moduleInfo.cjs");
6
+ const loaderInternals_js_1 = require("./loaderInternals.cjs");
5
7
  const DEFAULT_EXPORT_NAME = 'knightedCss';
6
- const COMBINED_QUERY_FLAG = 'combined';
7
8
  const loader = async function loader(source) {
8
9
  const { cssOptions, vanillaOptions } = resolveLoaderOptions(this);
9
10
  const css = await extractCss(this, cssOptions);
@@ -47,7 +48,18 @@ const pitch = function pitch() {
47
48
  }
48
49
  const request = buildProxyRequest(this);
49
50
  const { cssOptions } = resolveLoaderOptions(this);
50
- return extractCss(this, cssOptions).then(css => createCombinedModule(request, css));
51
+ const skipSyntheticDefault = hasNamedOnlyQueryFlag(this.resourceQuery);
52
+ const defaultSignalPromise = skipSyntheticDefault
53
+ ? Promise.resolve('unknown')
54
+ : (0, moduleInfo_js_1.detectModuleDefaultExport)(this.resourcePath);
55
+ return Promise.all([extractCss(this, cssOptions), defaultSignalPromise]).then(([css, defaultSignal]) => {
56
+ const emitDefault = (0, loaderInternals_js_1.shouldEmitCombinedDefault)({
57
+ request,
58
+ skipSyntheticDefault,
59
+ detection: defaultSignal,
60
+ });
61
+ return createCombinedModule(request, css, { emitDefault });
62
+ });
51
63
  };
52
64
  exports.pitch = pitch;
53
65
  loader.pitch = exports.pitch;
@@ -87,10 +99,16 @@ function hasCombinedQuery(query) {
87
99
  return trimmed
88
100
  .split('&')
89
101
  .filter(Boolean)
90
- .some(part => isQueryFlag(part, COMBINED_QUERY_FLAG));
102
+ .some(part => (0, loaderInternals_js_1.isQueryFlag)(part, loaderInternals_js_1.COMBINED_QUERY_FLAG));
103
+ }
104
+ function hasNamedOnlyQueryFlag(query) {
105
+ if (!query)
106
+ return false;
107
+ const entries = (0, loaderInternals_js_1.splitQuery)(query);
108
+ return entries.some(part => loaderInternals_js_1.NAMED_ONLY_QUERY_FLAGS.some(flag => (0, loaderInternals_js_1.isQueryFlag)(part, flag)));
91
109
  }
92
110
  function buildProxyRequest(ctx) {
93
- const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery);
111
+ const sanitizedQuery = (0, loaderInternals_js_1.buildSanitizedQuery)(ctx.resourceQuery);
94
112
  const rawRequest = getRawRequest(ctx);
95
113
  if (rawRequest) {
96
114
  const stripped = stripResourceQuery(rawRequest);
@@ -115,36 +133,14 @@ function stripResourceQuery(request) {
115
133
  const idx = request.indexOf('?');
116
134
  return idx >= 0 ? request.slice(0, idx) : request;
117
135
  }
118
- function buildSanitizedQuery(query) {
119
- if (!query)
120
- return '';
121
- const entries = splitQuery(query).filter(part => {
122
- return !isQueryFlag(part, COMBINED_QUERY_FLAG) && !isQueryFlag(part, 'knighted-css');
123
- });
124
- return entries.length > 0 ? `?${entries.join('&')}` : '';
125
- }
126
- function splitQuery(query) {
127
- const trimmed = query.startsWith('?') ? query.slice(1) : query;
128
- if (!trimmed)
129
- return [];
130
- return trimmed.split('&').filter(Boolean);
131
- }
132
- function isQueryFlag(entry, flag) {
133
- const [rawKey] = entry.split('=');
134
- try {
135
- return decodeURIComponent(rawKey) === flag;
136
- }
137
- catch {
138
- return rawKey === flag;
139
- }
140
- }
141
- function createCombinedModule(request, css) {
136
+ function createCombinedModule(request, css, options) {
137
+ const shouldEmitDefault = options?.emitDefault ?? (0, loaderInternals_js_1.shouldForwardDefaultExport)(request);
142
138
  const requestLiteral = JSON.stringify(request);
143
139
  const lines = [
144
140
  `import * as __knightedModule from ${requestLiteral};`,
145
141
  `export * from ${requestLiteral};`,
146
142
  ];
147
- if (shouldForwardDefaultExport(request)) {
143
+ if (shouldEmitDefault) {
148
144
  lines.push(`const __knightedDefault =
149
145
  typeof __knightedModule.default !== 'undefined'
150
146
  ? __knightedModule.default
@@ -153,13 +149,3 @@ typeof __knightedModule.default !== 'undefined'
153
149
  lines.push(buildInjection(css));
154
150
  return lines.join('\n');
155
151
  }
156
- function shouldForwardDefaultExport(request) {
157
- const [pathPart] = request.split('?');
158
- if (!pathPart)
159
- return true;
160
- const lower = pathPart.toLowerCase();
161
- if (lower.endsWith('.css.ts') || lower.endsWith('.css.js')) {
162
- return false;
163
- }
164
- return true;
165
- }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__loaderInternals = exports.NAMED_ONLY_QUERY_FLAGS = exports.COMBINED_QUERY_FLAG = void 0;
4
+ exports.splitQuery = splitQuery;
5
+ exports.isQueryFlag = isQueryFlag;
6
+ exports.buildSanitizedQuery = buildSanitizedQuery;
7
+ exports.shouldForwardDefaultExport = shouldForwardDefaultExport;
8
+ exports.shouldEmitCombinedDefault = shouldEmitCombinedDefault;
9
+ exports.COMBINED_QUERY_FLAG = 'combined';
10
+ exports.NAMED_ONLY_QUERY_FLAGS = ['named-only', 'no-default'];
11
+ function splitQuery(query) {
12
+ const trimmed = query.startsWith('?') ? query.slice(1) : query;
13
+ if (!trimmed)
14
+ return [];
15
+ return trimmed.split('&').filter(Boolean);
16
+ }
17
+ function isQueryFlag(entry, flag) {
18
+ const [rawKey] = entry.split('=');
19
+ try {
20
+ return decodeURIComponent(rawKey) === flag;
21
+ }
22
+ catch {
23
+ return rawKey === flag;
24
+ }
25
+ }
26
+ function buildSanitizedQuery(query) {
27
+ if (!query)
28
+ return '';
29
+ const entries = splitQuery(query).filter(part => {
30
+ if (isQueryFlag(part, exports.COMBINED_QUERY_FLAG)) {
31
+ return false;
32
+ }
33
+ if (isQueryFlag(part, 'knighted-css')) {
34
+ return false;
35
+ }
36
+ if (exports.NAMED_ONLY_QUERY_FLAGS.some(flag => isQueryFlag(part, flag))) {
37
+ return false;
38
+ }
39
+ return true;
40
+ });
41
+ return entries.length > 0 ? `?${entries.join('&')}` : '';
42
+ }
43
+ function shouldForwardDefaultExport(request) {
44
+ const [pathPart] = request.split('?');
45
+ if (!pathPart)
46
+ return true;
47
+ const lower = pathPart.toLowerCase();
48
+ if (lower.endsWith('.css.ts') || lower.endsWith('.css.js')) {
49
+ return false;
50
+ }
51
+ return true;
52
+ }
53
+ function shouldEmitCombinedDefault(options) {
54
+ if (options.skipSyntheticDefault) {
55
+ return false;
56
+ }
57
+ if (!shouldForwardDefaultExport(options.request)) {
58
+ return false;
59
+ }
60
+ if (options.detection === 'has-default') {
61
+ return true;
62
+ }
63
+ if (options.detection === 'no-default') {
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ exports.__loaderInternals = {
69
+ buildSanitizedQuery,
70
+ shouldEmitCombinedDefault,
71
+ };
@@ -0,0 +1,16 @@
1
+ import type { ModuleDefaultSignal } from './moduleInfo.cjs';
2
+ export declare const COMBINED_QUERY_FLAG = "combined";
3
+ export declare const NAMED_ONLY_QUERY_FLAGS: readonly ["named-only", "no-default"];
4
+ export declare function splitQuery(query: string): string[];
5
+ export declare function isQueryFlag(entry: string, flag: string): boolean;
6
+ export declare function buildSanitizedQuery(query?: string | null): string;
7
+ export declare function shouldForwardDefaultExport(request: string): boolean;
8
+ export declare function shouldEmitCombinedDefault(options: {
9
+ detection: ModuleDefaultSignal;
10
+ request: string;
11
+ skipSyntheticDefault: boolean;
12
+ }): boolean;
13
+ export declare const __loaderInternals: {
14
+ buildSanitizedQuery: typeof buildSanitizedQuery;
15
+ shouldEmitCombinedDefault: typeof shouldEmitCombinedDefault;
16
+ };
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.__moduleInfoInternals = void 0;
7
+ exports.detectModuleDefaultExport = detectModuleDefaultExport;
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const es_module_lexer_1 = require("es-module-lexer");
11
+ const DETECTABLE_EXTENSIONS = new Set([
12
+ '.js',
13
+ '.jsx',
14
+ '.ts',
15
+ '.tsx',
16
+ '.mjs',
17
+ '.mts',
18
+ '.cjs',
19
+ '.cts',
20
+ ]);
21
+ let lexerInit;
22
+ let lexerOverrides;
23
+ function ensureLexerInitialized() {
24
+ if (!lexerInit) {
25
+ lexerInit = es_module_lexer_1.init;
26
+ }
27
+ return lexerInit;
28
+ }
29
+ async function detectModuleDefaultExport(filePath) {
30
+ if (!DETECTABLE_EXTENSIONS.has(node_path_1.default.extname(filePath))) {
31
+ return 'unknown';
32
+ }
33
+ let source;
34
+ try {
35
+ source = await (0, promises_1.readFile)(filePath, 'utf8');
36
+ }
37
+ catch {
38
+ return 'unknown';
39
+ }
40
+ try {
41
+ await ensureLexerInitialized();
42
+ const [, exports] = (lexerOverrides?.parse ?? es_module_lexer_1.parse)(source, filePath);
43
+ if (exports.some(entry => entry.n === 'default')) {
44
+ return 'has-default';
45
+ }
46
+ if (exports.length === 0) {
47
+ return 'unknown';
48
+ }
49
+ return 'no-default';
50
+ }
51
+ catch {
52
+ return 'unknown';
53
+ }
54
+ }
55
+ exports.__moduleInfoInternals = {
56
+ setLexerOverrides(overrides) {
57
+ lexerOverrides = overrides;
58
+ if (!overrides) {
59
+ lexerInit = undefined;
60
+ }
61
+ },
62
+ };
@@ -0,0 +1,10 @@
1
+ import { parse } from 'es-module-lexer';
2
+ export type ModuleDefaultSignal = 'has-default' | 'no-default' | 'unknown';
3
+ type LexerOverrides = {
4
+ parse?: typeof parse;
5
+ };
6
+ export declare function detectModuleDefaultExport(filePath: string): Promise<ModuleDefaultSignal>;
7
+ export declare const __moduleInfoInternals: {
8
+ setLexerOverrides(overrides?: LexerOverrides): void;
9
+ };
10
+ export {};
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.__sassInternals = void 0;
7
+ exports.createSassImporter = createSassImporter;
8
+ exports.resolveAliasSpecifier = resolveAliasSpecifier;
9
+ exports.shouldNormalizeSpecifier = shouldNormalizeSpecifier;
10
+ exports.ensureSassPath = ensureSassPath;
11
+ exports.resolveRelativeSpecifier = resolveRelativeSpecifier;
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const node_fs_1 = require("node:fs");
14
+ const node_url_1 = require("node:url");
15
+ function createSassImporter({ cwd, resolver, }) {
16
+ if (!resolver)
17
+ return undefined;
18
+ const debug = process.env.KNIGHTED_CSS_DEBUG_SASS === '1';
19
+ return {
20
+ async canonicalize(url, context) {
21
+ if (debug) {
22
+ console.error('[knighted-css:sass] canonicalize request:', url);
23
+ if (context?.containingUrl) {
24
+ console.error('[knighted-css:sass] containing url:', context.containingUrl.href);
25
+ }
26
+ }
27
+ if (shouldNormalizeSpecifier(url)) {
28
+ const resolvedPath = await resolveAliasSpecifier(url, resolver, cwd);
29
+ if (!resolvedPath) {
30
+ if (debug) {
31
+ console.error('[knighted-css:sass] resolver returned no result for', url);
32
+ }
33
+ return null;
34
+ }
35
+ const fileUrl = (0, node_url_1.pathToFileURL)(resolvedPath);
36
+ if (debug) {
37
+ console.error('[knighted-css:sass] canonical url:', fileUrl.href);
38
+ }
39
+ return fileUrl;
40
+ }
41
+ const relativePath = resolveRelativeSpecifier(url, context?.containingUrl);
42
+ if (relativePath) {
43
+ const fileUrl = (0, node_url_1.pathToFileURL)(relativePath);
44
+ if (debug) {
45
+ console.error('[knighted-css:sass] canonical url:', fileUrl.href);
46
+ }
47
+ return fileUrl;
48
+ }
49
+ return null;
50
+ },
51
+ async load(canonicalUrl) {
52
+ if (debug) {
53
+ console.error('[knighted-css:sass] load request:', canonicalUrl.href);
54
+ }
55
+ const filePath = (0, node_url_1.fileURLToPath)(canonicalUrl);
56
+ const contents = await node_fs_1.promises.readFile(filePath, 'utf8');
57
+ return {
58
+ contents,
59
+ syntax: inferSassSyntax(filePath),
60
+ };
61
+ },
62
+ };
63
+ }
64
+ async function resolveAliasSpecifier(specifier, resolver, cwd) {
65
+ const resolved = await resolver(specifier, { cwd });
66
+ if (!resolved) {
67
+ return undefined;
68
+ }
69
+ if (resolved.startsWith('file://')) {
70
+ return ensureSassPath((0, node_url_1.fileURLToPath)(new URL(resolved)));
71
+ }
72
+ const normalized = node_path_1.default.isAbsolute(resolved) ? resolved : node_path_1.default.resolve(cwd, resolved);
73
+ return ensureSassPath(normalized);
74
+ }
75
+ function shouldNormalizeSpecifier(specifier) {
76
+ const schemeMatch = specifier.match(/^([a-z][\w+.-]*):/i);
77
+ if (!schemeMatch) {
78
+ return false;
79
+ }
80
+ const scheme = schemeMatch[1].toLowerCase();
81
+ if (scheme === 'file' ||
82
+ scheme === 'http' ||
83
+ scheme === 'https' ||
84
+ scheme === 'data' ||
85
+ scheme === 'sass') {
86
+ return false;
87
+ }
88
+ return true;
89
+ }
90
+ function inferSassSyntax(filePath) {
91
+ return filePath.endsWith('.sass') ? 'indented' : 'scss';
92
+ }
93
+ function ensureSassPath(filePath) {
94
+ if ((0, node_fs_1.existsSync)(filePath)) {
95
+ return filePath;
96
+ }
97
+ const ext = node_path_1.default.extname(filePath);
98
+ const dir = node_path_1.default.dirname(filePath);
99
+ const base = node_path_1.default.basename(filePath, ext);
100
+ const partialCandidate = node_path_1.default.join(dir, `_${base}${ext}`);
101
+ if (ext && (0, node_fs_1.existsSync)(partialCandidate)) {
102
+ return partialCandidate;
103
+ }
104
+ const indexCandidate = node_path_1.default.join(dir, base, `index${ext}`);
105
+ if (ext && (0, node_fs_1.existsSync)(indexCandidate)) {
106
+ return indexCandidate;
107
+ }
108
+ const partialIndexCandidate = node_path_1.default.join(dir, base, `_index${ext}`);
109
+ if (ext && (0, node_fs_1.existsSync)(partialIndexCandidate)) {
110
+ return partialIndexCandidate;
111
+ }
112
+ return undefined;
113
+ }
114
+ function resolveRelativeSpecifier(specifier, containingUrl) {
115
+ if (!containingUrl || containingUrl.protocol !== 'file:') {
116
+ return undefined;
117
+ }
118
+ if (/^[a-z][\w+.-]*:/i.test(specifier)) {
119
+ return undefined;
120
+ }
121
+ const containingPath = (0, node_url_1.fileURLToPath)(containingUrl);
122
+ const baseDir = node_path_1.default.dirname(containingPath);
123
+ const candidate = node_path_1.default.resolve(baseDir, specifier);
124
+ return ensureSassPath(candidate);
125
+ }
126
+ exports.__sassInternals = {
127
+ createSassImporter,
128
+ resolveAliasSpecifier,
129
+ shouldNormalizeSpecifier,
130
+ ensureSassPath,
131
+ resolveRelativeSpecifier,
132
+ };
@@ -0,0 +1,26 @@
1
+ export type CssResolver = (specifier: string, ctx: {
2
+ cwd: string;
3
+ }) => string | Promise<string | undefined>;
4
+ export declare function createSassImporter({ cwd, resolver, }: {
5
+ cwd: string;
6
+ resolver?: CssResolver;
7
+ }): {
8
+ canonicalize(url: string, context?: {
9
+ containingUrl?: URL | null;
10
+ }): Promise<import("node:url").URL | null>;
11
+ load(canonicalUrl: URL): Promise<{
12
+ contents: string;
13
+ syntax: "scss" | "indented";
14
+ }>;
15
+ } | undefined;
16
+ export declare function resolveAliasSpecifier(specifier: string, resolver: CssResolver, cwd: string): Promise<string | undefined>;
17
+ export declare function shouldNormalizeSpecifier(specifier: string): boolean;
18
+ export declare function ensureSassPath(filePath: string): string | undefined;
19
+ export declare function resolveRelativeSpecifier(specifier: string, containingUrl?: URL | null): string | undefined;
20
+ export declare const __sassInternals: {
21
+ createSassImporter: typeof createSassImporter;
22
+ resolveAliasSpecifier: typeof resolveAliasSpecifier;
23
+ shouldNormalizeSpecifier: typeof shouldNormalizeSpecifier;
24
+ ensureSassPath: typeof ensureSassPath;
25
+ resolveRelativeSpecifier: typeof resolveRelativeSpecifier;
26
+ };
package/dist/css.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import type { Options as DependencyTreeOpts } from 'dependency-tree';
2
2
  import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
3
3
  import { type SpecificitySelector, type SpecificityStrategy } from './helpers.js';
4
+ import type { CssResolver } from './sassInternals.js';
5
+ export type { CssResolver } from './sassInternals.js';
4
6
  export declare const DEFAULT_EXTENSIONS: string[];
5
7
  type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
6
- export type CssResolver = (specifier: string, ctx: {
7
- cwd: string;
8
- }) => string | Promise<string | undefined>;
9
8
  type PeerLoader = (name: string) => Promise<unknown>;
10
9
  export interface CssOptions {
11
10
  extensions?: string[];
@@ -35,4 +34,3 @@ export interface CssResult {
35
34
  export declare function css(entry: string, options?: CssOptions): Promise<string>;
36
35
  export declare function cssWithMeta(entry: string, options?: CssOptions): Promise<CssResult>;
37
36
  export declare function compileVanillaModule(filePath: string, cwd: string, peerResolver?: PeerLoader): Promise<VanillaCompileResult>;
38
- export {};
package/dist/css.js CHANGED
@@ -3,6 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import dependencyTree from 'dependency-tree';
4
4
  import { composeVisitors, transform as lightningTransform, } from 'lightningcss';
5
5
  import { applyStringSpecificityBoost, buildSpecificityVisitor, } from './helpers.js';
6
+ import { createSassImporter } from './sassInternals.js';
6
7
  export const DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
7
8
  export async function css(entry, options = {}) {
8
9
  const { css: output } = await cssWithMeta(entry, options);
@@ -26,6 +27,7 @@ export async function cssWithMeta(entry, options = {}) {
26
27
  const chunk = await compileStyleModule(file, {
27
28
  cwd,
28
29
  peerResolver: options.peerResolver,
30
+ resolver: options.resolver,
29
31
  });
30
32
  if (chunk) {
31
33
  chunks.push(chunk);
@@ -96,13 +98,17 @@ function matchExtension(filePath, extensions) {
96
98
  const lower = filePath.toLowerCase();
97
99
  return extensions.find(ext => lower.endsWith(ext));
98
100
  }
99
- async function compileStyleModule(file, { cwd, peerResolver }) {
101
+ async function compileStyleModule(file, { cwd, peerResolver, resolver, }) {
100
102
  switch (file.ext) {
101
103
  case '.css':
102
104
  return fs.readFile(file.path, 'utf8');
103
105
  case '.scss':
104
106
  case '.sass':
105
- return compileSass(file.path, file.ext === '.sass', peerResolver);
107
+ return compileSass(file.path, file.ext === '.sass', {
108
+ cwd,
109
+ peerResolver,
110
+ resolver,
111
+ });
106
112
  case '.less':
107
113
  return compileLess(file.path, peerResolver);
108
114
  case '.css.ts':
@@ -111,12 +117,14 @@ async function compileStyleModule(file, { cwd, peerResolver }) {
111
117
  return '';
112
118
  }
113
119
  }
114
- async function compileSass(filePath, indented, peerResolver) {
120
+ async function compileSass(filePath, indented, { cwd, peerResolver, resolver, }) {
115
121
  const sassModule = await optionalPeer('sass', 'Sass', peerResolver);
116
122
  const sass = sassModule;
117
- const result = sass.compile(filePath, {
123
+ const importer = createSassImporter({ cwd, resolver });
124
+ const result = await sass.compileAsync(filePath, {
118
125
  style: 'expanded',
119
126
  loadPaths: buildSassLoadPaths(filePath),
127
+ importers: importer ? [importer] : undefined,
120
128
  });
121
129
  return result.css;
122
130
  }
package/dist/loader.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { cssWithMeta, compileVanillaModule } from './css.js';
2
+ import { detectModuleDefaultExport } from './moduleInfo.js';
3
+ import { buildSanitizedQuery, COMBINED_QUERY_FLAG, isQueryFlag, NAMED_ONLY_QUERY_FLAGS, shouldEmitCombinedDefault, shouldForwardDefaultExport, splitQuery, } from './loaderInternals.js';
2
4
  const DEFAULT_EXPORT_NAME = 'knightedCss';
3
- const COMBINED_QUERY_FLAG = 'combined';
4
5
  const loader = async function loader(source) {
5
6
  const { cssOptions, vanillaOptions } = resolveLoaderOptions(this);
6
7
  const css = await extractCss(this, cssOptions);
@@ -44,7 +45,18 @@ export const pitch = function pitch() {
44
45
  }
45
46
  const request = buildProxyRequest(this);
46
47
  const { cssOptions } = resolveLoaderOptions(this);
47
- return extractCss(this, cssOptions).then(css => createCombinedModule(request, css));
48
+ const skipSyntheticDefault = hasNamedOnlyQueryFlag(this.resourceQuery);
49
+ const defaultSignalPromise = skipSyntheticDefault
50
+ ? Promise.resolve('unknown')
51
+ : detectModuleDefaultExport(this.resourcePath);
52
+ return Promise.all([extractCss(this, cssOptions), defaultSignalPromise]).then(([css, defaultSignal]) => {
53
+ const emitDefault = shouldEmitCombinedDefault({
54
+ request,
55
+ skipSyntheticDefault,
56
+ detection: defaultSignal,
57
+ });
58
+ return createCombinedModule(request, css, { emitDefault });
59
+ });
48
60
  };
49
61
  loader.pitch = pitch;
50
62
  export default loader;
@@ -85,6 +97,12 @@ function hasCombinedQuery(query) {
85
97
  .filter(Boolean)
86
98
  .some(part => isQueryFlag(part, COMBINED_QUERY_FLAG));
87
99
  }
100
+ function hasNamedOnlyQueryFlag(query) {
101
+ if (!query)
102
+ return false;
103
+ const entries = splitQuery(query);
104
+ return entries.some(part => NAMED_ONLY_QUERY_FLAGS.some(flag => isQueryFlag(part, flag)));
105
+ }
88
106
  function buildProxyRequest(ctx) {
89
107
  const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery);
90
108
  const rawRequest = getRawRequest(ctx);
@@ -111,36 +129,14 @@ function stripResourceQuery(request) {
111
129
  const idx = request.indexOf('?');
112
130
  return idx >= 0 ? request.slice(0, idx) : request;
113
131
  }
114
- function buildSanitizedQuery(query) {
115
- if (!query)
116
- return '';
117
- const entries = splitQuery(query).filter(part => {
118
- return !isQueryFlag(part, COMBINED_QUERY_FLAG) && !isQueryFlag(part, 'knighted-css');
119
- });
120
- return entries.length > 0 ? `?${entries.join('&')}` : '';
121
- }
122
- function splitQuery(query) {
123
- const trimmed = query.startsWith('?') ? query.slice(1) : query;
124
- if (!trimmed)
125
- return [];
126
- return trimmed.split('&').filter(Boolean);
127
- }
128
- function isQueryFlag(entry, flag) {
129
- const [rawKey] = entry.split('=');
130
- try {
131
- return decodeURIComponent(rawKey) === flag;
132
- }
133
- catch {
134
- return rawKey === flag;
135
- }
136
- }
137
- function createCombinedModule(request, css) {
132
+ function createCombinedModule(request, css, options) {
133
+ const shouldEmitDefault = options?.emitDefault ?? shouldForwardDefaultExport(request);
138
134
  const requestLiteral = JSON.stringify(request);
139
135
  const lines = [
140
136
  `import * as __knightedModule from ${requestLiteral};`,
141
137
  `export * from ${requestLiteral};`,
142
138
  ];
143
- if (shouldForwardDefaultExport(request)) {
139
+ if (shouldEmitDefault) {
144
140
  lines.push(`const __knightedDefault =
145
141
  typeof __knightedModule.default !== 'undefined'
146
142
  ? __knightedModule.default
@@ -149,13 +145,3 @@ typeof __knightedModule.default !== 'undefined'
149
145
  lines.push(buildInjection(css));
150
146
  return lines.join('\n');
151
147
  }
152
- function shouldForwardDefaultExport(request) {
153
- const [pathPart] = request.split('?');
154
- if (!pathPart)
155
- return true;
156
- const lower = pathPart.toLowerCase();
157
- if (lower.endsWith('.css.ts') || lower.endsWith('.css.js')) {
158
- return false;
159
- }
160
- return true;
161
- }
@@ -0,0 +1,16 @@
1
+ import type { ModuleDefaultSignal } from './moduleInfo.js';
2
+ export declare const COMBINED_QUERY_FLAG = "combined";
3
+ export declare const NAMED_ONLY_QUERY_FLAGS: readonly ["named-only", "no-default"];
4
+ export declare function splitQuery(query: string): string[];
5
+ export declare function isQueryFlag(entry: string, flag: string): boolean;
6
+ export declare function buildSanitizedQuery(query?: string | null): string;
7
+ export declare function shouldForwardDefaultExport(request: string): boolean;
8
+ export declare function shouldEmitCombinedDefault(options: {
9
+ detection: ModuleDefaultSignal;
10
+ request: string;
11
+ skipSyntheticDefault: boolean;
12
+ }): boolean;
13
+ export declare const __loaderInternals: {
14
+ buildSanitizedQuery: typeof buildSanitizedQuery;
15
+ shouldEmitCombinedDefault: typeof shouldEmitCombinedDefault;
16
+ };
@@ -0,0 +1,63 @@
1
+ export const COMBINED_QUERY_FLAG = 'combined';
2
+ export const NAMED_ONLY_QUERY_FLAGS = ['named-only', 'no-default'];
3
+ export function splitQuery(query) {
4
+ const trimmed = query.startsWith('?') ? query.slice(1) : query;
5
+ if (!trimmed)
6
+ return [];
7
+ return trimmed.split('&').filter(Boolean);
8
+ }
9
+ export function isQueryFlag(entry, flag) {
10
+ const [rawKey] = entry.split('=');
11
+ try {
12
+ return decodeURIComponent(rawKey) === flag;
13
+ }
14
+ catch {
15
+ return rawKey === flag;
16
+ }
17
+ }
18
+ export function buildSanitizedQuery(query) {
19
+ if (!query)
20
+ return '';
21
+ const entries = splitQuery(query).filter(part => {
22
+ if (isQueryFlag(part, COMBINED_QUERY_FLAG)) {
23
+ return false;
24
+ }
25
+ if (isQueryFlag(part, 'knighted-css')) {
26
+ return false;
27
+ }
28
+ if (NAMED_ONLY_QUERY_FLAGS.some(flag => isQueryFlag(part, flag))) {
29
+ return false;
30
+ }
31
+ return true;
32
+ });
33
+ return entries.length > 0 ? `?${entries.join('&')}` : '';
34
+ }
35
+ export function shouldForwardDefaultExport(request) {
36
+ const [pathPart] = request.split('?');
37
+ if (!pathPart)
38
+ return true;
39
+ const lower = pathPart.toLowerCase();
40
+ if (lower.endsWith('.css.ts') || lower.endsWith('.css.js')) {
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ export function shouldEmitCombinedDefault(options) {
46
+ if (options.skipSyntheticDefault) {
47
+ return false;
48
+ }
49
+ if (!shouldForwardDefaultExport(options.request)) {
50
+ return false;
51
+ }
52
+ if (options.detection === 'has-default') {
53
+ return true;
54
+ }
55
+ if (options.detection === 'no-default') {
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ export const __loaderInternals = {
61
+ buildSanitizedQuery,
62
+ shouldEmitCombinedDefault,
63
+ };
@@ -0,0 +1,10 @@
1
+ import { parse } from 'es-module-lexer';
2
+ export type ModuleDefaultSignal = 'has-default' | 'no-default' | 'unknown';
3
+ type LexerOverrides = {
4
+ parse?: typeof parse;
5
+ };
6
+ export declare function detectModuleDefaultExport(filePath: string): Promise<ModuleDefaultSignal>;
7
+ export declare const __moduleInfoInternals: {
8
+ setLexerOverrides(overrides?: LexerOverrides): void;
9
+ };
10
+ export {};
@@ -0,0 +1,55 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { init, parse } from 'es-module-lexer';
4
+ const DETECTABLE_EXTENSIONS = new Set([
5
+ '.js',
6
+ '.jsx',
7
+ '.ts',
8
+ '.tsx',
9
+ '.mjs',
10
+ '.mts',
11
+ '.cjs',
12
+ '.cts',
13
+ ]);
14
+ let lexerInit;
15
+ let lexerOverrides;
16
+ function ensureLexerInitialized() {
17
+ if (!lexerInit) {
18
+ lexerInit = init;
19
+ }
20
+ return lexerInit;
21
+ }
22
+ export async function detectModuleDefaultExport(filePath) {
23
+ if (!DETECTABLE_EXTENSIONS.has(path.extname(filePath))) {
24
+ return 'unknown';
25
+ }
26
+ let source;
27
+ try {
28
+ source = await readFile(filePath, 'utf8');
29
+ }
30
+ catch {
31
+ return 'unknown';
32
+ }
33
+ try {
34
+ await ensureLexerInitialized();
35
+ const [, exports] = (lexerOverrides?.parse ?? parse)(source, filePath);
36
+ if (exports.some(entry => entry.n === 'default')) {
37
+ return 'has-default';
38
+ }
39
+ if (exports.length === 0) {
40
+ return 'unknown';
41
+ }
42
+ return 'no-default';
43
+ }
44
+ catch {
45
+ return 'unknown';
46
+ }
47
+ }
48
+ export const __moduleInfoInternals = {
49
+ setLexerOverrides(overrides) {
50
+ lexerOverrides = overrides;
51
+ if (!overrides) {
52
+ lexerInit = undefined;
53
+ }
54
+ },
55
+ };
@@ -0,0 +1,26 @@
1
+ export type CssResolver = (specifier: string, ctx: {
2
+ cwd: string;
3
+ }) => string | Promise<string | undefined>;
4
+ export declare function createSassImporter({ cwd, resolver, }: {
5
+ cwd: string;
6
+ resolver?: CssResolver;
7
+ }): {
8
+ canonicalize(url: string, context?: {
9
+ containingUrl?: URL | null;
10
+ }): Promise<import("node:url").URL | null>;
11
+ load(canonicalUrl: URL): Promise<{
12
+ contents: string;
13
+ syntax: "scss" | "indented";
14
+ }>;
15
+ } | undefined;
16
+ export declare function resolveAliasSpecifier(specifier: string, resolver: CssResolver, cwd: string): Promise<string | undefined>;
17
+ export declare function shouldNormalizeSpecifier(specifier: string): boolean;
18
+ export declare function ensureSassPath(filePath: string): string | undefined;
19
+ export declare function resolveRelativeSpecifier(specifier: string, containingUrl?: URL | null): string | undefined;
20
+ export declare const __sassInternals: {
21
+ createSassImporter: typeof createSassImporter;
22
+ resolveAliasSpecifier: typeof resolveAliasSpecifier;
23
+ shouldNormalizeSpecifier: typeof shouldNormalizeSpecifier;
24
+ ensureSassPath: typeof ensureSassPath;
25
+ resolveRelativeSpecifier: typeof resolveRelativeSpecifier;
26
+ };
@@ -0,0 +1,121 @@
1
+ import path from 'node:path';
2
+ import { existsSync, promises as fs } from 'node:fs';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ export function createSassImporter({ cwd, resolver, }) {
5
+ if (!resolver)
6
+ return undefined;
7
+ const debug = process.env.KNIGHTED_CSS_DEBUG_SASS === '1';
8
+ return {
9
+ async canonicalize(url, context) {
10
+ if (debug) {
11
+ console.error('[knighted-css:sass] canonicalize request:', url);
12
+ if (context?.containingUrl) {
13
+ console.error('[knighted-css:sass] containing url:', context.containingUrl.href);
14
+ }
15
+ }
16
+ if (shouldNormalizeSpecifier(url)) {
17
+ const resolvedPath = await resolveAliasSpecifier(url, resolver, cwd);
18
+ if (!resolvedPath) {
19
+ if (debug) {
20
+ console.error('[knighted-css:sass] resolver returned no result for', url);
21
+ }
22
+ return null;
23
+ }
24
+ const fileUrl = pathToFileURL(resolvedPath);
25
+ if (debug) {
26
+ console.error('[knighted-css:sass] canonical url:', fileUrl.href);
27
+ }
28
+ return fileUrl;
29
+ }
30
+ const relativePath = resolveRelativeSpecifier(url, context?.containingUrl);
31
+ if (relativePath) {
32
+ const fileUrl = pathToFileURL(relativePath);
33
+ if (debug) {
34
+ console.error('[knighted-css:sass] canonical url:', fileUrl.href);
35
+ }
36
+ return fileUrl;
37
+ }
38
+ return null;
39
+ },
40
+ async load(canonicalUrl) {
41
+ if (debug) {
42
+ console.error('[knighted-css:sass] load request:', canonicalUrl.href);
43
+ }
44
+ const filePath = fileURLToPath(canonicalUrl);
45
+ const contents = await fs.readFile(filePath, 'utf8');
46
+ return {
47
+ contents,
48
+ syntax: inferSassSyntax(filePath),
49
+ };
50
+ },
51
+ };
52
+ }
53
+ export async function resolveAliasSpecifier(specifier, resolver, cwd) {
54
+ const resolved = await resolver(specifier, { cwd });
55
+ if (!resolved) {
56
+ return undefined;
57
+ }
58
+ if (resolved.startsWith('file://')) {
59
+ return ensureSassPath(fileURLToPath(new URL(resolved)));
60
+ }
61
+ const normalized = path.isAbsolute(resolved) ? resolved : path.resolve(cwd, resolved);
62
+ return ensureSassPath(normalized);
63
+ }
64
+ export function shouldNormalizeSpecifier(specifier) {
65
+ const schemeMatch = specifier.match(/^([a-z][\w+.-]*):/i);
66
+ if (!schemeMatch) {
67
+ return false;
68
+ }
69
+ const scheme = schemeMatch[1].toLowerCase();
70
+ if (scheme === 'file' ||
71
+ scheme === 'http' ||
72
+ scheme === 'https' ||
73
+ scheme === 'data' ||
74
+ scheme === 'sass') {
75
+ return false;
76
+ }
77
+ return true;
78
+ }
79
+ function inferSassSyntax(filePath) {
80
+ return filePath.endsWith('.sass') ? 'indented' : 'scss';
81
+ }
82
+ export function ensureSassPath(filePath) {
83
+ if (existsSync(filePath)) {
84
+ return filePath;
85
+ }
86
+ const ext = path.extname(filePath);
87
+ const dir = path.dirname(filePath);
88
+ const base = path.basename(filePath, ext);
89
+ const partialCandidate = path.join(dir, `_${base}${ext}`);
90
+ if (ext && existsSync(partialCandidate)) {
91
+ return partialCandidate;
92
+ }
93
+ const indexCandidate = path.join(dir, base, `index${ext}`);
94
+ if (ext && existsSync(indexCandidate)) {
95
+ return indexCandidate;
96
+ }
97
+ const partialIndexCandidate = path.join(dir, base, `_index${ext}`);
98
+ if (ext && existsSync(partialIndexCandidate)) {
99
+ return partialIndexCandidate;
100
+ }
101
+ return undefined;
102
+ }
103
+ export function resolveRelativeSpecifier(specifier, containingUrl) {
104
+ if (!containingUrl || containingUrl.protocol !== 'file:') {
105
+ return undefined;
106
+ }
107
+ if (/^[a-z][\w+.-]*:/i.test(specifier)) {
108
+ return undefined;
109
+ }
110
+ const containingPath = fileURLToPath(containingUrl);
111
+ const baseDir = path.dirname(containingPath);
112
+ const candidate = path.resolve(baseDir, specifier);
113
+ return ensureSassPath(candidate);
114
+ }
115
+ export const __sassInternals = {
116
+ createSassImporter,
117
+ resolveAliasSpecifier,
118
+ shouldNormalizeSpecifier,
119
+ ensureSassPath,
120
+ resolveRelativeSpecifier,
121
+ };
@@ -19,3 +19,11 @@ declare module '*?knighted-css&combined' {
19
19
  export default combined
20
20
  export const knightedCss: string
21
21
  }
22
+
23
+ declare module '*?knighted-css&combined&named-only*' {
24
+ export const knightedCss: string
25
+ }
26
+
27
+ declare module '*?knighted-css&combined&no-default*' {
28
+ export const knightedCss: string
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/css",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
4
4
  "description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.",
5
5
  "type": "module",
6
6
  "main": "./dist/css.js",
@@ -66,6 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "dependency-tree": "^11.2.0",
69
+ "es-module-lexer": "^2.0.0",
69
70
  "lightningcss": "^1.30.2"
70
71
  },
71
72
  "overrides": {