Skip to content

Commit 608be2b

Browse files
committed
feat(compiler): implement module alias resolution and enhance CSS import handling (#15)
1 parent e2763bc commit 608be2b

7 files changed

Lines changed: 153 additions & 22 deletions

File tree

fe/packages/compiler/__tests__/require-path-resolution.spec.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,49 @@ describe('Require Path Resolution', () => {
115115
expect(compiledCode).not.toMatch(/require\(['"]\.\//)
116116
})
117117

118+
it('should resolve app alias require paths correctly', async () => {
119+
fs.mkdirSync('pages/alias-test', { recursive: true })
120+
fs.mkdirSync('utils', { recursive: true })
121+
122+
fs.writeFileSync('app.json', JSON.stringify({
123+
pages: ['pages/alias-test/index'],
124+
resolveAlias: {
125+
'~/*': '/*',
126+
},
127+
}))
128+
129+
fs.writeFileSync('utils/util.js', `
130+
module.exports = {
131+
formatTime: function(time) {
132+
return time.toISOString()
133+
}
134+
}
135+
`)
136+
137+
fs.writeFileSync('pages/alias-test/index.js', `
138+
const util = require('~/utils/util.js')
139+
Page({
140+
onLoad() {
141+
console.log(util.formatTime(new Date()))
142+
}
143+
})
144+
`)
145+
146+
fs.writeFileSync('pages/alias-test/index.json', JSON.stringify({
147+
navigationBarTitleText: 'Alias Test'
148+
}))
149+
150+
storeInfo(tempDir)
151+
152+
const progress = { completedTasks: 0 }
153+
const result = await compileJS([{ path: 'pages/alias-test/index' }], null, null, progress)
154+
155+
const pageModule = result.find(module => module.path === 'pages/alias-test/index')
156+
expect(pageModule).toBeDefined()
157+
expect(pageModule.code).toContain('require("/utils/util")')
158+
expect(pageModule.code).not.toContain('require("~/utils/util.js")')
159+
})
160+
118161
it('should handle npm package requires correctly', async () => {
119162
// 创建测试文件结构
120163
fs.mkdirSync('pages/npm-test', { recursive: true })

fe/packages/compiler/__tests__/style-compiler.spec.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { ensureImportSemicolons, removeBaseComponentScope } from '../src/core/style-compiler'
2+
import { ensureImportSemicolons, normalizeRootStyleImports, removeBaseComponentScope, resolveStyleImportPath } from '../src/core/style-compiler'
33

44
describe('ensureImportSemicolons', () => {
55
it('should add semicolons to @import statements that do not have them', () => {
@@ -220,3 +220,17 @@ describe('removeBaseComponentScope', () => {
220220
expect(result).toEqual(expected)
221221
})
222222
})
223+
224+
describe('style import path helpers', () => {
225+
it('应该将根路径 import 解析到小程序项目根目录', () => {
226+
const result = resolveStyleImportPath('/tmp/app/pages/home/index.less', '/variable.less', '/tmp/app')
227+
expect(result.endsWith('/variable.less')).toBe(true)
228+
expect(result.includes('/pages/home/')).toBe(false)
229+
})
230+
231+
it('应该将根路径 less import 重写为绝对路径', () => {
232+
const result = normalizeRootStyleImports('@import "/variable.less";', '/tmp/app')
233+
expect(result).toContain('/variable.less')
234+
expect(result).not.toContain('@import "/variable.less";')
235+
})
236+
})

fe/packages/compiler/__tests__/view-compiler.spec.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { generateVModelTemplate, generateSlotDirective, parseBraceExp, parseClassRules, parseKeyExpression, processWxsContent, splitWithBraces } from '../src/core/view-compiler'
2+
import { generateVModelTemplate, generateSlotDirective, parseBraceExp, parseClassRules, parseKeyExpression, parseTemplateDataExp, processWxsContent, splitWithBraces } from '../src/core/view-compiler'
33

44
describe('parseKeyExpression - 解析 key 表达式', () => {
55
it('默认索引名 index - 应该直接返回 index', () => {
@@ -127,6 +127,18 @@ describe('parseBraceExp - 解析双花括号表达式', () => {
127127
})
128128
})
129129

130+
describe('parseTemplateDataExp - 解析 template data 对象表达式', () => {
131+
it('应该保留对象字面量中的三元表达式和展开语法', () => {
132+
expect(
133+
parseTemplateDataExp('{{tClass: foo ? bar : baz, src: item, ...imageProps, bindload: \'onImageLoad\'}}'),
134+
).toEqual('{tClass: foo ? bar : baz, src: item, ...imageProps, bindload: \'onImageLoad\'}')
135+
})
136+
137+
it('应该保留对象简写属性', () => {
138+
expect(parseTemplateDataExp('{{prefix, classPrefix, style}}')).toEqual('{prefix, classPrefix, style}')
139+
})
140+
})
141+
130142
describe('splitWithBraces - 按空格分割字符串(保留花括号)', () => {
131143
it('简单空格分割 - 应该返回数组', () => {
132144
expect(splitWithBraces('a b')).toEqual(['a', 'b'])

fe/packages/compiler/src/core/logic-compiler.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import MagicString from 'magic-string'
88
import { transform } from 'esbuild'
99
import ts from 'typescript'
1010
import { hasCompileInfo } from '../common/utils.js'
11-
import { getAppConfigInfo, getComponent, getContentByPath, getNpmResolver, getTargetPath, getWorkPath, resetStoreInfo } from '../env.js'
11+
import { getAppConfigInfo, getComponent, getContentByPath, getNpmResolver, getTargetPath, getWorkPath, resetStoreInfo, resolveAppAlias } from '../env.js'
1212

1313
// 用于缓存已处理的模块
1414
const processedModules = new Set()
@@ -447,14 +447,6 @@ function resolveDependencyId(specifier, modulePath, allowAbsolute) {
447447
}
448448
}
449449

450-
if (specifier.startsWith('@') || isBareModuleSpecifier(specifier)) {
451-
const npmModuleId = resolveNpmModuleId(specifier, modulePath)
452-
return {
453-
id: npmModuleId || specifier,
454-
shouldProcess: Boolean(npmModuleId),
455-
}
456-
}
457-
458450
if (specifier.startsWith('./') || specifier.startsWith('../')) {
459451
return {
460452
id: resolveRelativeModuleId(specifier, modulePath),
@@ -469,6 +461,22 @@ function resolveDependencyId(specifier, modulePath, allowAbsolute) {
469461
}
470462
}
471463

464+
const aliasResolved = resolveAppAlias(specifier)
465+
if (aliasResolved) {
466+
return {
467+
id: normalizeModuleId(aliasResolved),
468+
shouldProcess: true,
469+
}
470+
}
471+
472+
if (specifier.startsWith('@') || isBareModuleSpecifier(specifier)) {
473+
const npmModuleId = resolveNpmModuleId(specifier, modulePath)
474+
return {
475+
id: npmModuleId || specifier,
476+
shouldProcess: Boolean(npmModuleId),
477+
}
478+
}
479+
472480
return { id: specifier, shouldProcess: false }
473481
}
474482

fe/packages/compiler/src/core/style-compiler.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,19 @@ async function enhanceCSS(module) {
136136
}
137137

138138
// 预处理器编译
139-
let processedCSS = inputCSS
139+
let processedCSS = normalizeRootStyleImports(inputCSS)
140140
const ext = path.extname(absolutePath).toLowerCase()
141141

142142
try {
143143
if (ext === '.less') {
144-
const result = await less.render(inputCSS, {
144+
const result = await less.render(processedCSS, {
145145
filename: absolutePath,
146-
paths: [path.dirname(absolutePath)],
146+
paths: [path.dirname(absolutePath), getWorkPath()],
147147
})
148148
processedCSS = result.css
149149
} else if (ext === '.scss' || ext === '.sass') {
150-
const result = sass.compileString(inputCSS, {
151-
loadPaths: [path.dirname(absolutePath)],
150+
const result = sass.compileString(processedCSS, {
151+
loadPaths: [path.dirname(absolutePath), getWorkPath()],
152152
syntax: ext === '.sass' ? 'indented' : 'scss',
153153
})
154154
processedCSS = result.css
@@ -175,7 +175,7 @@ async function enhanceCSS(module) {
175175
// @import 样式导入
176176
// 替换字符串首尾的引号
177177
const str = node.params.replace(/^['"]|['"]$/g, '')
178-
const importFullPath = path.resolve(absolutePath, `../${str}`)
178+
const importFullPath = resolveStyleImportPath(absolutePath, str)
179179

180180
node.remove()
181181

@@ -267,6 +267,19 @@ function getAbsolutePath(modulePath) {
267267
}
268268
}
269269

270+
function resolveStyleImportPath(absolutePath, importPath, workPath = getWorkPath()) {
271+
if (importPath.startsWith('/')) {
272+
return path.join(workPath, importPath)
273+
}
274+
return path.resolve(path.dirname(absolutePath), importPath)
275+
}
276+
277+
function normalizeRootStyleImports(source, workPath = getWorkPath()) {
278+
return source.replace(/(@import\s+(?:\(.*?\)\s*)?(?:url\()?['"])(\/[^'")]+)(['"]\)?)/g, (_, prefix, importPath, suffix) => {
279+
return `${prefix}${path.join(workPath, importPath)}${suffix}`
280+
})
281+
}
282+
270283
/**
271284
* 移除基础组件选择器的 scoped 属性
272285
* @param {string} css - 包含 scoped 属性的 CSS
@@ -332,4 +345,4 @@ function processHostSelector(selector, moduleId) {
332345
.replace(/:host(?=\.|#|:)/g, `[data-v-${moduleId}]`)
333346
}
334347

335-
export { compileSS, ensureImportSemicolons, processHostSelector, removeBaseComponentScope }
348+
export { compileSS, ensureImportSemicolons, normalizeRootStyleImports, processHostSelector, removeBaseComponentScope, resolveStyleImportPath }

fe/packages/compiler/src/core/view-compiler.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,10 +1291,9 @@ function getProps(attrs, tag, components) {
12911291
}
12921292
}
12931293
else if (isWrappedByBraces(value)) {
1294-
let pVal = parseBraceExp(value)
1295-
if (tag === 'template' && name === 'data') {
1296-
pVal = `{${pVal}}`
1297-
}
1294+
const pVal = tag === 'template' && name === 'data'
1295+
? parseTemplateDataExp(value)
1296+
: parseBraceExp(value)
12981297

12991298
// 如果是自定义组件的属性绑定,记录绑定关系
13001299
if (components && components[tag]) {
@@ -1616,6 +1615,19 @@ function parseBraceExp(exp) {
16161615
return group.join('').replace(/^\+|\+$/g, '')
16171616
}
16181617

1618+
/**
1619+
* 解析 template 的 data 对象表达式,保留对象字面量和内部三元表达式
1620+
* @param {string} exp
1621+
* @returns {string} 解析后的对象表达式字符串
1622+
*/
1623+
function parseTemplateDataExp(exp) {
1624+
const matchResult = exp.trim().match(/^\{\{([\s\S]*)\}\}$/)
1625+
if (matchResult) {
1626+
return `{${matchResult[1].trim()}}`
1627+
}
1628+
return `{${parseBraceExp(exp)}}`
1629+
}
1630+
16191631
function transTagWxs($, scriptModule, filePath) {
16201632
let wxsNodes = $('wxs')
16211633
if (wxsNodes.length === 0) {
@@ -1861,6 +1873,7 @@ export {
18611873
parseBraceExp,
18621874
parseClassRules,
18631875
parseKeyExpression,
1876+
parseTemplateDataExp,
18641877
processIncludeConditionalAttrs,
18651878
processWxsContent,
18661879
splitWithBraces,

fe/packages/compiler/src/env.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ function storeComponentConfig(pageJsonContent, pageFilePath) {
244244
* @param {string} src
245245
*/
246246
function getModuleId(src, pageFilePath) {
247+
const resolvedAlias = resolveAppAlias(src)
248+
if (resolvedAlias) {
249+
return resolvedAlias
250+
}
251+
247252
if (!npmResolver) {
248253
// 如果 npm 解析器未初始化,使用原有逻辑
249254
const lastIndex = pageFilePath.lastIndexOf('/')
@@ -257,6 +262,28 @@ function getModuleId(src, pageFilePath) {
257262
return npmResolver.resolveComponentPath(src, pageFilePath)
258263
}
259264

265+
function resolveAppAlias(src) {
266+
const resolveAlias = configInfo.appInfo?.resolveAlias
267+
if (!resolveAlias || typeof src !== 'string') {
268+
return null
269+
}
270+
271+
for (const [alias, target] of Object.entries(resolveAlias)) {
272+
if (alias.endsWith('/*') && target.endsWith('/*')) {
273+
const aliasPrefix = alias.slice(0, -1)
274+
const targetPrefix = target.slice(0, -1)
275+
if (src.startsWith(aliasPrefix)) {
276+
return src.replace(aliasPrefix, targetPrefix)
277+
}
278+
}
279+
else if (src === alias) {
280+
return target
281+
}
282+
}
283+
284+
return null
285+
}
286+
260287
function getTargetPath() {
261288
return pathInfo.targetPath
262289
}
@@ -356,6 +383,7 @@ export {
356383
getTargetPath,
357384
getWorkPath,
358385
resetStoreInfo,
386+
resolveAppAlias,
359387
storeInfo,
360388
storeProjectConfig,
361389
}

0 commit comments

Comments
 (0)