Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
registry=https://registry.npmjs.org/
ignore-workspace-root-check=true
shamefully-hoist=true
4 changes: 4 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"recommendations": ["dbaeumer.vscode-eslint"],
"unwantedRecommendations": ["esbenp.prettier-vscode"]
}
36 changes: 35 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
{
"eslint.experimental.useFlatConfig": true
// Use ESLint for formatting (matches @antfu/eslint-config), not Prettier/TS formatter
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Flat config (eslint.config.js)
"eslint.useFlatConfig": true,
"eslint.experimental.useFlatConfig": true,
// Avoid duplicate squiggles; ESLint still auto-fixes style on save
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
},
"dependencies": {
"mlly": "^1.7.4",
"typescript": "^5.8.3",
"unplugin": "^2.3.4"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/build-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { CompilerBuildResults } from '@stencil/core/internal'
import { EventEmitter } from 'node:events'

export const stencilBuildEvents = new EventEmitter()

export interface StencilBuildEvents {
buildFinished: (results: CompilerBuildResults) => void
buildError: (err: unknown) => void
}
11 changes: 8 additions & 3 deletions src/build-queue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Compiler } from '@stencil/core/compiler'
import type { CompilerBuildResults } from '@stencil/core/internal'
import { EventEmitter } from 'node:events'
import { stencilBuildEvents } from './build-events.js'

export class BuildQueue extends EventEmitter {
#compiler: Compiler
Expand Down Expand Up @@ -29,11 +31,13 @@ export class BuildQueue extends EventEmitter {
async #runBuild() {
this.#isBuilding = true
this.emit('buildStart')
let results: CompilerBuildResults | undefined
try {
await this.#compiler.build()
results = await this.#compiler.build()
}
catch (err) {
this.emit('buildError', err)
stencilBuildEvents.emit('buildError', err)
throw err
}
finally {
Expand All @@ -42,8 +46,9 @@ export class BuildQueue extends EventEmitter {
this.#pending = false
await this.#runBuild()
}
else {
this.emit('buildFinished')
else if (results) {
this.emit('buildFinished', results)
stencilBuildEvents.emit('buildFinished', results)
}
}
}
Expand Down
88 changes: 74 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type * as CoreCompiler from '@stencil/core/compiler'
import type { OutputTargetDistCustomElements } from '@stencil/core/internal'
import type {
Config,
OutputTargetDistCustomElements,
} from '@stencil/core/internal'

import type { UnpluginFactory } from 'unplugin'
import type { Options } from './types.js'
Expand All @@ -13,16 +16,31 @@ import { findStaticImports, parseStaticImport } from 'mlly'
import { createUnplugin } from 'unplugin'
import { BuildQueue } from './build-queue'
import { STENCIL_IMPORT } from './constants.js'
import { getRootDir, getStencilConfigFile, parseTagConfig, transformCompiledCode } from './utils.js'
import {
clearStyleDependencies,
rebuildStyleMap,
setGlobalStyleFromConfig,
} from './style-dependencies.js'
import {
getRootDir,
getStencilConfigFile,
parseTagConfig,
transformCompiledCode,
} from './utils.js'

const DCE_OUTPUT_TARGET_NAME = 'dist-custom-elements'

export const unpluginFactory: UnpluginFactory<Options | undefined> = (options = {}) => {
export const unpluginFactory: UnpluginFactory<Options | undefined> = (
options = {},
) => {
const nodeLogger = nodeApi.createNodeLogger()
let distCustomElementsOptions: OutputTargetDistCustomElements | undefined
let compiler: CoreCompiler.Compiler | undefined
let buildQueue: BuildQueue | undefined
const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'
let stencilConfig: Config | undefined
let onBuildFinished: (() => void) | undefined
const isTest
= process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'

return {
name: 'unplugin-stencil',
Expand All @@ -34,7 +52,9 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
const configPath = await getStencilConfigFile(options)
const nodeSys = nodeApi.createNodeSys({ process, logger: nodeLogger })
nodeApi.setupNodeProcess({ process, logger: nodeLogger })
const coreCompiler = await nodeSys.dynamicImport!(nodeSys.getCompilerExecutingPath()) as { loadConfig: typeof CoreCompiler.loadConfig }
const coreCompiler = (await nodeSys.dynamicImport!(
nodeSys.getCompilerExecutingPath(),
)) as { loadConfig: typeof CoreCompiler.loadConfig }
const validated = await coreCompiler.loadConfig({
config: {
rootDir: getRootDir(options),
Expand All @@ -53,18 +73,39 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
sys: nodeSys,
})

distCustomElementsOptions = validated.config.outputTargets.find(o => o.type === DCE_OUTPUT_TARGET_NAME) as OutputTargetDistCustomElements
if (!distCustomElementsOptions)
throw new Error(`Could not find "${DCE_OUTPUT_TARGET_NAME}" output target`)
distCustomElementsOptions = validated.config.outputTargets.find(
o => o.type === DCE_OUTPUT_TARGET_NAME,
) as OutputTargetDistCustomElements
if (!distCustomElementsOptions) {
throw new Error(
`Could not find "${DCE_OUTPUT_TARGET_NAME}" output target`,
)
}

stencilConfig = validated.config
setGlobalStyleFromConfig(validated.config)

compiler = await createCompiler(validated.config)
buildQueue = new BuildQueue(compiler)

onBuildFinished = () => {
if (compiler && stencilConfig)
void rebuildStyleMap(compiler.sys, stencilConfig)
}
buildQueue.on('buildFinished', onBuildFinished)
},
async buildEnd() {
if (buildQueue && onBuildFinished)
buildQueue.off('buildFinished', onBuildFinished)

clearStyleDependencies()

// Clean up compiler resources when build ends
await compiler?.destroy()
compiler = undefined
buildQueue = undefined
stencilConfig = undefined
onBuildFinished = undefined

// In test mode, force exit after a short delay to allow cleanup
// This works around Stencil compiler not fully releasing file handles
Expand Down Expand Up @@ -116,7 +157,9 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
const imports = staticImports
.filter(imp => imp.specifier === STENCIL_IMPORT)
.map(imp => parseStaticImport(imp))
const isStencilComponent = imports.some(imp => 'Component' in (imp.namedImports || {}))
const isStencilComponent = imports.some(
imp => 'Component' in (imp.namedImports || {}),
)

/**
* don't compile the file if:
Expand All @@ -126,11 +169,13 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
* something with the setup failed and some of the primitives we need
* to compile the file are missing
*/
!compiler || !buildQueue
!compiler
|| !buildQueue
/**
* the output directory is not set
*/
|| !distCustomElementsOptions || !distCustomElementsOptions.dir
|| !distCustomElementsOptions
|| !distCustomElementsOptions.dir
/**
* the file is not a Stencil component and not a CSS file
*/
Expand All @@ -140,7 +185,10 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
}

const componentTag = parseTagConfig(code)
const compilerFilePath = path.resolve(distCustomElementsOptions.dir, `${componentTag}.js`)
const compilerFilePath = path.resolve(
distCustomElementsOptions.dir,
`${componentTag}.js`,
)

const raw = await buildQueue.getLatestBuild(id, compilerFilePath)

Expand All @@ -153,8 +201,14 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =
compilerFilePath,
)

const sourcemapFilePath = path.resolve(distCustomElementsOptions.dir, `${componentTag}.js.map`)
const rawSourcemap = await buildQueue.getLatestBuild(id, sourcemapFilePath)
const sourcemapFilePath = path.resolve(
distCustomElementsOptions.dir,
`${componentTag}.js.map`,
)
const rawSourcemap = await buildQueue.getLatestBuild(
id,
sourcemapFilePath,
)
const sourcemapExists = await compiler.sys.access(sourcemapFilePath)

const sourcemap = sourcemapExists
Expand All @@ -172,4 +226,10 @@ export const unpluginFactory: UnpluginFactory<Options | undefined> = (options =

export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory)

export { stencilBuildEvents } from './build-events.js'
export {
type ComponentStyleDependencies,
getComponentStyleDependencies,
} from './style-dependencies.js'

export default unplugin
Loading
Loading