Skip to content

Commit b592b8c

Browse files
committed
Fix(#373): interop dynamically imported htmlnano module properly
1 parent 1cf5289 commit b592b8c

File tree

1 file changed

+73
-17
lines changed

1 file changed

+73
-17
lines changed

src/index.ts

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,63 @@ const optionalDependencies = {
5454
minifySvg: ['svgo']
5555
} satisfies Partial<Record<keyof HtmlnanoOptions, string[]>>;
5656

57-
const interop = <T>(imported: Promise<{ default: T }>): Promise<T> => imported.then(mod => mod.default);
57+
/**
58+
* And the old mixing named export and default export again.
59+
*
60+
* TL; DR: our bundler has bundled our mixed default/named export module into a "exports" object,
61+
* and when dynamically importing a CommonJS module using "import" instead of "require", Node.js wraps
62+
* another layer of default around the "exports" object.
63+
*
64+
* The longer version:
65+
*
66+
* The bundler we are using outputs:
67+
*
68+
* ESM: export { [named], xxx as default }
69+
* CJS: exports.default = xxx; exports.[named] = ...; exports.__esModule = true;
70+
*
71+
* With ESM, the Module object looks like this:
72+
*
73+
* ```js
74+
* Module {
75+
* default: xxx,
76+
* [named]: ...,
77+
* }
78+
* ```
79+
*
80+
* With CJS, Node.js handles dynamic import differently. Node.js doesn't respect `__esModule`,
81+
* and will wrongly treat a CommonJS module as ESM, i.e. assign the "exports" object on its
82+
* own "default" on the "Module" object.
83+
*
84+
* Now we have:
85+
*
86+
* ```js
87+
* Module {
88+
* // this is actually the "exports" inside among "exports.__esModule", "exports.[named]", and "exports.default"
89+
* default: {
90+
* __esModule: true,
91+
* // This is the actual "exports.default"
92+
* default: xxx
93+
* }
94+
* }
95+
* ```
96+
*/
97+
const interop = <T>(imported: Promise<object>): Promise<HtmlnanoModule<T>> => imported.then((mod) => {
98+
let htmlnanoModule;
99+
while ('default' in mod) {
100+
htmlnanoModule = mod;
101+
mod = mod.default as object;
102+
// If we find any htmlnano module hook methods, we know this object is a htmlnano module, return directly
103+
if ('onAttrs' in mod || 'onContent' in mod || 'onNode' in mod) {
104+
return mod as HtmlnanoModule<T>;
105+
}
106+
}
107+
108+
if (htmlnanoModule && typeof htmlnanoModule.default === 'function') {
109+
return htmlnanoModule as HtmlnanoModule<T>;
110+
}
111+
112+
throw new TypeError('The imported module is not a valid htmlnano module');
113+
});
58114

59115
const modules = {
60116
collapseAttributeWhitespace: () => interop(import('./_modules/collapseAttributeWhitespace')),
@@ -63,23 +119,23 @@ const modules = {
63119
custom: () => interop(import('./_modules/custom')),
64120
deduplicateAttributeValues: () => interop(import('./_modules/deduplicateAttributeValues')),
65121
// example: () => import('./_modules/example.mjs'),
66-
mergeScripts: () => interop(import('./_modules/mergeScripts.js')),
67-
mergeStyles: () => interop(import('./_modules/mergeStyles.js')),
68-
minifyConditionalComments: () => interop(import('./_modules/minifyConditionalComments.js')),
69-
minifyCss: () => interop(import('./_modules/minifyCss.js')),
70-
minifyJs: () => interop(import('./_modules/minifyJs.js')),
71-
minifyJson: () => interop(import('./_modules/minifyJson.js')),
72-
minifySvg: () => interop(import('./_modules/minifySvg.js')),
73-
minifyUrls: () => interop(import('./_modules/minifyUrls.js')),
122+
mergeScripts: () => interop(import('./_modules/mergeScripts')),
123+
mergeStyles: () => interop(import('./_modules/mergeStyles')),
124+
minifyConditionalComments: () => interop(import('./_modules/minifyConditionalComments')),
125+
minifyCss: () => interop(import('./_modules/minifyCss')),
126+
minifyJs: () => interop(import('./_modules/minifyJs')),
127+
minifyJson: () => interop(import('./_modules/minifyJson')),
128+
minifySvg: () => interop(import('./_modules/minifySvg')),
129+
minifyUrls: () => interop(import('./_modules/minifyUrls')),
74130
normalizeAttributeValues: () => interop(import('./_modules/normalizeAttributeValues')),
75-
removeAttributeQuotes: () => interop(import('./_modules/removeAttributeQuotes.js')),
76-
removeComments: () => interop(import('./_modules/removeComments.js')),
77-
removeEmptyAttributes: () => interop(import('./_modules/removeEmptyAttributes.js')),
78-
removeOptionalTags: () => interop(import('./_modules/removeOptionalTags.js')),
79-
removeRedundantAttributes: () => interop(import('./_modules/removeRedundantAttributes.js')),
80-
removeUnusedCss: () => interop(import('./_modules/removeUnusedCss.js')),
81-
sortAttributes: () => interop(import('./_modules/sortAttributes.js')),
82-
sortAttributesWithLists: () => interop(import('./_modules/sortAttributesWithLists.js'))
131+
removeAttributeQuotes: () => interop(import('./_modules/removeAttributeQuotes')),
132+
removeComments: () => interop(import('./_modules/removeComments')),
133+
removeEmptyAttributes: () => interop(import('./_modules/removeEmptyAttributes')),
134+
removeOptionalTags: () => interop(import('./_modules/removeOptionalTags')),
135+
removeRedundantAttributes: () => interop(import('./_modules/removeRedundantAttributes')),
136+
removeUnusedCss: () => interop(import('./_modules/removeUnusedCss')),
137+
sortAttributes: () => interop(import('./_modules/sortAttributes')),
138+
sortAttributesWithLists: () => interop(import('./_modules/sortAttributesWithLists'))
83139
} satisfies Record<string, () => Promise<HtmlnanoModule<any>>>;
84140

85141
const htmlnano = Object.assign(function htmlnano(optionsRun: HtmlnanoOptions = {}, presetRun?: HtmlnanoPreset) {

0 commit comments

Comments
 (0)