@tarojs/cli
Version:
cli tool for taro
293 lines (258 loc) • 12.1 kB
text/typescript
import * as os from 'node:os'
import * as path from 'node:path'
import { babelKit, fs } from '@tarojs/helper'
import Page, { ConfigModificationState } from '../create/page'
import { modifyPagesOrSubPackages } from '../util/createPage'
// Config updates do not use the native template generator.
jest.mock('@tarojs/binding', () => ({
CompilerType: { Webpack5: 'webpack5' },
CSSType: { None: 'none' },
FrameworkType: { React: 'react' }
}))
function updateConfig (source: string, subPkgRootPath?: string) {
const ast = babelKit.parse(source, { sourceType: 'module' })!
const originalCode = babelKit.generate(ast, { compact: true }).code
const callback = jest.fn()
babelKit.traverse(ast, {
ExportDefaultDeclaration (path) {
modifyPagesOrSubPackages({
path,
fullPagePath: subPkgRootPath ? `${subPkgRootPath}/new/index` : 'pages/new/index',
subPkgRootPath,
callback
})
}
})
expect(callback).toHaveBeenCalledTimes(1)
return {
state: callback.mock.calls[0][0],
code: babelKit.generate(ast, { compact: true }).code,
originalCode
}
}
describe.each(['object', 'defineAppConfig'])('create page config: %s', (format) => {
const wrap = (config: string) => `export default ${format === 'object' ? config : `defineAppConfig(${config})`}`
it.each([
'pages: ["pages/home/index"]',
'"pages": ["pages/home/index"]',
'...baseConfig, pages: ["pages/home/index"]',
'...baseConfig, "pages": ["pages/home/index"]',
'get pages() { return [] }, "pages": ["pages/home/index"]',
'[pages]: [], "pages": ["pages/home/index"]'
])('appends to explicit pages: %s', (properties) => {
const result = updateConfig(wrap(`{ ${properties} }`))
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('["pages/home/index","pages/new/index"]')
if (properties.includes('...baseConfig')) expect(result.code).toContain('...baseConfig')
if (properties.includes('[pages]')) expect(result.code).toContain('[pages]:[]')
})
it.each(['pages', '"pages"'])('does not duplicate an existing page: %s', (key) => {
const result = updateConfig(wrap(`{ ${key}: ["pages/new/index"] }`))
expect(result.state).toBe(ConfigModificationState.NeedLess)
expect(result.code).toBe(result.originalCode)
})
it.each([
['"pages"', 'pages'],
['pages', '"pages"'],
['pages', 'pages'],
['"pages"', '"pages"']
])('updates the last pages property: %s followed by %s', (firstKey, lastKey) => {
const result = updateConfig(wrap(`{ ${firstKey}: ["ignored"], ${lastKey}: ["pages/home/index"] }`))
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain(`${firstKey}:["ignored"]`)
expect(result.code).toContain(`${lastKey}:["pages/home/index","pages/new/index"]`)
})
it('checks duplicates in the last pages property', () => {
const result = updateConfig(wrap('{ "pages": [], pages: ["pages/new/index"] }'))
expect(result.state).toBe(ConfigModificationState.NeedLess)
expect(result.code).toBe(result.originalCode)
})
it('leaves a nonliteral last pages property unchanged', () => {
const result = updateConfig(wrap('{ "pages": [], pages: pageList }'))
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
it.each([
'{}',
'{ ...baseConfig }',
'{ pages: pageList }',
'{ [pages]: [] }',
'{ ["pages"]: [] }',
'{ pages() { return [] } }'
])('leaves unsupported pages config unchanged: %s', (config) => {
const result = updateConfig(wrap(config))
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
it.each([
'subPackages: [{ root: "pkg/", pages: ["home/index"] }]',
'"subPackages": [{ "root": "pkg/", "pages": ["home/index"] }]',
'...baseConfig, "subPackages": [{ ...subConfig, "root": "pkg/", "pages": ["home/index"] }]',
'subPackages: [{ root() {}, root: "pkg/", pages() {}, pages: ["home/index"] }]'
])('updates an existing subpackage: %s', (properties) => {
const result = updateConfig(wrap(`{ ${properties} }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('["home/index","new/index"]')
expect(result.code.match(/"pkg\/"/g)).toHaveLength(1)
})
it('does not duplicate a page in a quoted subpackage config', () => {
const result = updateConfig(wrap('{ "subPackages": [{ "root": "pkg/", "pages": ["new/index"] }] }'), 'pkg')
expect(result.state).toBe(ConfigModificationState.NeedLess)
expect(result.code).toBe(result.originalCode)
})
it.each([
'{ "subPackages": [{ root: "ignored/", pages: [] }], subPackages: [{ root: "pkg/", pages: ["home/index"] }] }',
'{ subPackages: [{ "root": "ignored/", root: "pkg/", pages: ["home/index"] }] }',
'{ subPackages: [{ root: "pkg/", "pages": ["ignored"], pages: ["home/index"] }] }'
])('updates the effective subpackage properties: %s', (config) => {
const result = updateConfig(wrap(config), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('pages:["home/index","new/index"]')
expect(result.code).toContain('"ignored')
expect(result.code.match(/"pkg\/"/g)).toHaveLength(1)
})
it('matches the root property instead of an unrelated string value', () => {
const result = updateConfig(wrap('{ subPackages: [{ name: "pkg/", root: "other/", pages: [] }, { "root": "pkg/", "pages": ["home/index"] }] }'), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('root:"other/",pages:[]')
expect(result.code).toContain('"root":"pkg/","pages":["home/index","new/index"]')
})
it('adds a missing subPackages property', () => {
const result = updateConfig(wrap('{ pages: [] }'), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('subPackages:[{root:"pkg/",pages:["new/index"]}]')
})
it.each([
'pages() {}',
'get pages() { return [] }',
'set pages(value) {}',
'[key]: []',
'...baseConfig'
])('does not update pages shadowed by %s', (member) => {
const result = updateConfig(wrap(`{ pages: [], ${member} }`))
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
it.each([
'subPackages() {}',
'get subPackages() { return [] }',
'set subPackages(value) {}',
'[key]: []',
'...baseConfig'
])('does not update or add subPackages shadowed by %s', (member) => {
for (const properties of [member, `subPackages: [], ${member}`]) {
const result = updateConfig(wrap(`{ ${properties} }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
}
})
it.each([
'root() {}',
'get root() { return "pkg/" }',
'set root(value) {}',
'[key]: "pkg/"',
'...subConfig'
])('does not update or add a subpackage with root shadowed by %s', (member) => {
for (const properties of [`${member}, pages: []`, `root: "pkg/", ${member}, pages: []`]) {
const result = updateConfig(wrap(`{ subPackages: [{ ${properties} }] }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
}
})
it.each([
'pages() {}',
'get pages() { return [] }',
'set pages(value) {}',
'[key]: []',
'...subConfig'
])('does not update subpackage pages shadowed by %s', (member) => {
const result = updateConfig(wrap(`{ subPackages: [{ root: "pkg/", pages: [], ${member} }] }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
it.each([
'{ pages() {}, pages: [] }',
'{ get pages() { return [] }, pages: [] }',
'{ set pages(value) {}, pages: [] }',
'{ [key]: [], pages: [] }',
'{ ...baseConfig, pages: [] }'
])('updates pages when an explicit array overrides earlier members: %s', (config) => {
const result = updateConfig(wrap(config))
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('pages:["pages/new/index"]')
})
it('does not add a subpackage when an existing root is dynamic', () => {
const result = updateConfig(wrap('{ subPackages: [{ root: packageRoot, pages: [] }] }'), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
const unresolvedSubPackages = [
'{ root: packageRoot, pages: [] }',
'{ get root() { return "pkg/" }, pages: [] }',
'{ [key]: "pkg/", pages: [] }',
'{ ...subConfig, pages: [] }',
'...otherPackages'
]
it.each(unresolvedSubPackages)('updates an explicit subpackage alongside an unresolved entry: %s', (entry) => {
const target = '{ root: "pkg/", pages: ["home/index"] }'
for (const entries of [`${entry}, ${target}`, `${target}, ${entry}`]) {
const result = updateConfig(wrap(`{ subPackages: [${entries}] }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('root:"pkg/",pages:["home/index","new/index"]')
expect(result.code.replace(',"new/index"', '')).toBe(result.originalCode)
}
})
it.each(unresolvedSubPackages)('does not add a subpackage when no root matches and an entry is unresolved: %s', (entry) => {
const result = updateConfig(wrap(`{ subPackages: [${entry}, { root: "other/", pages: [] }] }`), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
it('adds a subpackage when all existing roots are known and different', () => {
const result = updateConfig(wrap('{ subPackages: [{ root: "other/", pages: [] }] }'), 'pkg')
expect(result.state).toBe(ConfigModificationState.Success)
expect(result.code).toContain('subPackages:[{root:"other/",pages:[]},{root:"pkg/",pages:["new/index"]}]')
})
it.each([
'{ "subPackages": subPackages }',
'{ "subPackages": [{ "root": "pkg/", "pages": pageList }] }'
])('leaves nonliteral subpackage configs unchanged: %s', (config) => {
const result = updateConfig(wrap(config), 'pkg')
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
})
})
it.each(['defineAppConfig()', 'defineAppConfig(config)', 'defineAppConfig(null)'])(
'leaves a non-object config unchanged: %s', (config) => {
for (const subPkgRootPath of [undefined, 'pkg']) {
const result = updateConfig(`export default ${config}`, subPkgRootPath)
expect(result.state).toBe(ConfigModificationState.Fail)
expect(result.code).toBe(result.originalCode)
}
}
)
describe('create page config file updates', () => {
let projectDir: string
beforeEach(() => {
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'taro-create-page-'))
jest.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
fs.removeSync(projectDir)
jest.restoreAllMocks()
})
it.each(['js', 'ts'])('writes the new page to app.config.%s', (extension) => {
const configPath = path.join(projectDir, 'src', `app.config.${extension}`)
const baseConfig = extension === 'ts' ? 'const baseConfig: { window?: object } = {}' : 'const baseConfig = {}'
fs.outputFileSync(configPath, `${baseConfig}\nexport default defineAppConfig({ ...baseConfig, "pages": ["pages/home/index"] })`)
const page = Object.create(Page.prototype)
page.conf = { projectDir, typescript: extension === 'ts' }
page.pageEntryPath = '/src/pages/new/index'
page.updateAppConfig()
const updated = fs.readFileSync(configPath, 'utf8')
expect(updated).toContain('pages/home/index')
expect(updated).toContain('pages/new/index')
expect(updated).toContain('...baseConfig')
page.updateAppConfig()
expect(fs.readFileSync(configPath, 'utf8')).toBe(updated)
})
})